diff --git a/.config/nextest.toml b/.config/nextest.toml index 7caa4a35e28..cba96681ec8 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -53,7 +53,7 @@ filter = 'package(solana-local-cluster) & test(/^test_fork_choice_refresh_old_vo slow-timeout = { period = "60s", terminate-after = 5 } [[profile.ci.overrides]] -filter = 'package(solana-local-cluster) & test(/^test_cluster_partition$/)' +filter = 'package(solana-local-cluster) & test(/^test_cluster_partition/)' slow-timeout = { period = "60s", terminate-after = 4 } [[profile.ci.overrides]] @@ -65,7 +65,7 @@ filter = 'package(solana-local-cluster) & test(/^test_slot_hash_expiry$/)' slow-timeout = { period = "60s", terminate-after = 4 } [[profile.ci.overrides]] -filter = 'package(solana-local-cluster) & test(/^test_kill_heaviest_partition$/)' +filter = 'package(solana-local-cluster) & test(/^test_kill_heaviest_partition/)' slow-timeout = { period = "60s", terminate-after = 4 } [[profile.ci.overrides]] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1524ca26c66..9b06b97eba2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,8 +1,8 @@ # Please keep this file sorted /bloom/ @anza-xyz/networking /bls-cert-verify/ @anza-xyz/consensus +/bls-sigverify/ @anza-xyz/consensus /compute-budget-instruction/ @anza-xyz/fees -/core/src/bls_sigverify/ @anza-xyz/consensus /core/src/consensus.rs @anza-xyz/consensus /core/src/consensus/ @anza-xyz/consensus /core/src/repair/ @anza-xyz/networking @anza-xyz/consensus diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index cd44ff2b883..28e8c24f3dc 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,4 +4,7 @@ #### Summary of Changes +#### Testing + + Fixes # diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 42f7002869a..cebba555c88 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,6 +8,16 @@ updates: time: "08:00" timezone: Etc/UTC open-pull-requests-limit: 6 + cooldown: + default-days: 7 + # crates published by anza-team bypass the cooldown + exclude: + - "solana-*" + - "agave-*" + - "spl-*" + - "rts-alloc" + - "shaq" + - "wincode" # split from root directory schedule to avoid duplicate checks - package-ecosystem: cargo @@ -20,20 +30,16 @@ updates: time: "14:00" timezone: Etc/UTC open-pull-requests-limit: 6 - - - package-ecosystem: "npm" - directory: "/docs" - schedule: - interval: weekly - time: "08:00" - timezone: Etc/UTC - open-pull-requests-limit: 3 cooldown: default-days: 7 - groups: - docs-dependencies: - patterns: - - "*" + # crates published by anza-team bypass the cooldown + exclude: + - "solana-*" + - "agave-*" + - "spl-*" + - "rts-alloc" + - "shaq" + - "wincode" - package-ecosystem: "github-actions" directory: "/" @@ -42,3 +48,5 @@ updates: time: "08:00" timezone: Etc/UTC open-pull-requests-limit: 3 + cooldown: + default-days: 7 diff --git a/.github/label-actions.yml b/.github/label-actions.yml deleted file mode 100644 index 8a67d20c3de..00000000000 --- a/.github/label-actions.yml +++ /dev/null @@ -1,27 +0,0 @@ -question: - issues: - # Post a comment, `{issue-author}` is an optional placeholder - comment: > - Hi @{issue-author}, - - - Thanks for your question! - - - We want to make sure to keep signal strong in the GitHub issue tracker – to make sure - that it remains the best place to track issues that affect the development of Solana itself. - - - Questions like yours deserve a purpose-built Q&A forum. Unless there exists evidence that - this is a bug with Solana itself, please post your question to the Solana Stack Exchange - using this link: https://solana.stackexchange.com/questions/ask - - - --- - - _This - [automated message](https://github.com/anza-xyz/agave/blob/master/.github/label-actions.yml) - is a result of having added the ‘question’ tag_. - - # Close the issue - close: true diff --git a/.github/scripts/downstream-project-spl-common.sh b/.github/scripts/downstream-project-spl-common.sh deleted file mode 100644 index 6db72e70dd3..00000000000 --- a/.github/scripts/downstream-project-spl-common.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env bash -set -e - -here="$(dirname "${BASH_SOURCE[0]}")" -program="$1" - -#shellcheck source=ci/downstream-projects/common.sh -source "$here"/../../ci/downstream-projects/common.sh - -set -x -rm -rf "${program}" -git clone https://github.com/solana-program/"${program}".git - -# copy toolchain file to use solana's rust version -cp "$SOLANA_DIR"/rust-toolchain.toml "${program}"/ -cd "${program}" || exit 1 -echo "HEAD: $(git rev-parse HEAD)" - -project_used_solana_version=$(sed -nE 's/solana = \"(.*)\"/\1/p' <"Cargo.toml") -echo "used solana version: $project_used_solana_version" -if semverGT "$project_used_solana_version" "$SOLANA_VER"; then - echo "skip" - export SKIP_SPL_DOWNSTREAM_PROJECT_TEST=1 - return -fi - -update_solana_dependencies . "$SOLANA_VER" -patch_crates_io_solana Cargo.toml "$SOLANA_DIR" diff --git a/.github/scripts/downstream-project-spl-install-deps.sh b/.github/scripts/downstream-project-spl-install-deps.sh deleted file mode 100755 index defb64fab23..00000000000 --- a/.github/scripts/downstream-project-spl-install-deps.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -e - -sudo apt-get update -sudo apt-get install libudev-dev libclang-dev protobuf-compiler -y diff --git a/.github/scripts/purge-ubuntu-runner.sh b/.github/scripts/purge-ubuntu-runner.sh deleted file mode 100755 index e8008202867..00000000000 --- a/.github/scripts/purge-ubuntu-runner.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -sudo rm -rf /usr/share/dotnet -sudo rm -rf /usr/share/swift -sudo rm -rf /usr/share/mysql -sudo rm -rf /usr/share/az_* -sudo rm -rf /usr/share/postgresql-common - -sudo rm -rf /opt/ghc -sudo rm -rf /opt/az -sudo rm -rf /opt/pipx -sudo rm -rf /opt/microsoft -sudo rm -rf /opt/google -sudo rm -rf /opt/hostedtoolcache - -sudo rm -rf /usr/local/lib/android -sudo rm -rf /usr/local/lib/heroku diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 269f4090a44..8f44a4be4a1 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -6,7 +6,7 @@ on: jobs: benchmark: - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' name: benchmark runs-on: benchmark strategy: @@ -61,7 +61,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Before Command if: ${{ matrix.test.before_command != '' }} diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index a494b72e612..d29938f42e0 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -17,14 +17,33 @@ on: jobs: version-bump: - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' runs-on: ubuntu-24.04 + permissions: + pull-requests: write + environment: "Version Bump" steps: + - name: Create github app token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + id: app-token + with: + client-id: ${{ vars.CLIENT_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + + - name: Set git config + run: | + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global url."https://x-access-token:${GITHUB_TOKEN}@github.com/".insteadOf https://github.com/ + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - token: ${{ secrets.VERSION_BUMP_PAT }} + token: ${{ steps.app-token.outputs.token }} + persist-credentials: false - name: Setup run: | @@ -37,10 +56,6 @@ jobs: --rev 930e3baec27a17cb4491b5585515b33f94dbd039 \ --bin xtask - # git config - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config user.name "github-actions[bot]" - - name: Process id: process env: @@ -71,6 +86,7 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, title: `Bump version to ${process.env.NEW_VERSION}`, + draft: true, head: `version-bump-${process.env.NEW_VERSION}`, base: process.env.TARGET_BRANCH }) diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index 2bc5d32447c..16fcb037447 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -30,7 +30,7 @@ env: jobs: clippy-nightly: - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' strategy: matrix: os: @@ -53,7 +53,7 @@ jobs: apk update apk add bash git - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 with: @@ -69,10 +69,10 @@ jobs: clippy-nightly-windows-2025: name: clippy-nightly (windows-2025) - if: github.repository == 'anza-xyz/agave' && github.event_name == 'push' + if: github.repository_owner == 'anza-xyz' && github.event_name == 'push' runs-on: windows-2025 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 with: diff --git a/.github/workflows/changelog-label.yml b/.github/workflows/changelog-label.yml index cbf79d12615..c0d03ebce9e 100644 --- a/.github/workflows/changelog-label.yml +++ b/.github/workflows/changelog-label.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 - name: Check if changes to CHANGELOG.md diff --git a/.github/workflows/crate-check.yml b/.github/workflows/crate-check.yml index 33ea169a70d..caf39ef0a37 100644 --- a/.github/workflows/crate-check.yml +++ b/.github/workflows/crate-check.yml @@ -1,4 +1,4 @@ -name: crate-check +name: Crate Check on: push: @@ -15,10 +15,10 @@ on: jobs: check: name: crate check - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 diff --git a/.github/workflows/dependabot-pr.yml b/.github/workflows/dependabot-pr.yml index 7280cbe302e..acd045e9b61 100644 --- a/.github/workflows/dependabot-pr.yml +++ b/.github/workflows/dependabot-pr.yml @@ -9,13 +9,29 @@ jobs: dependabot: timeout-minutes: 10 runs-on: ubuntu-24.04 - if: github.event.pull_request.user.login == 'dependabot[bot]' + if: github.actor == 'dependabot[bot]' steps: + - name: Create github app token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + id: app-token + with: + client-id: ${{ secrets.CLIENT_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + + - name: Set git config + run: | + git config --global user.email "49699333+dependabot[bot]@users.noreply.github.com" + git config --global user.name "dependabot[bot]" + git config --global url."https://x-access-token:${GITHUB_TOKEN}@github.com/".insteadOf https://github.com/ + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + - name: checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.ref }} - token: ${{ secrets.PAT }} + token: ${{ steps.app-token.outputs.token }} + persist-credentials: false - name: update code env: @@ -45,8 +61,6 @@ jobs: if [ -z "$(git status --porcelain)" ]; then echo "✅ Nothing to update" else - git config user.email "49699333+dependabot[bot]@users.noreply.github.com" - git config user.name "dependabot[bot]" git add **/Cargo.toml git add **/Cargo.lock git commit -am "Update all workspaces" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 9f2c2da9ef3..00000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,133 +0,0 @@ -name: docs - -on: - push: - branches: - - master - - v[0-9]+.[0-9]+ - tags: - - v[0-9]+.[0-9]+.[0-9]+ - pull_request: - branches: - - master - - v[0-9]+.[0-9]+ - -jobs: - check: - if: github.repository == 'anza-xyz/agave' - outputs: - continue: ${{ steps.check.outputs.need_to_build }} - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: Get commit range (push) - if: ${{ github.event_name == 'push' }} - run: | - echo "COMMIT_RANGE=${{ github.event.before }}..$GITHUB_SHA" >> $GITHUB_ENV - - - name: Get commit range (pull_request) - if: ${{ github.event_name == 'pull_request' }} - run: | - echo "COMMIT_RANGE=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" >> $GITHUB_ENV - - - name: Get file status - run: | - set +e - git diff --name-only $COMMIT_RANGE | grep \ - -e '.github/workflows/docs.yml' \ - -e 'docs/**' - echo "FILE_CHANGED=$?" >> $GITHUB_ENV - - - name: Check - id: check - shell: bash - run: | - source ci/env.sh - eval "$(ci/channel-info.sh)" - TAG=$CI_TAG - - echo "TAG: $TAG" - echo "CHANNEL: $CHANNEL" - echo "FILE_CHANGED: $FILE_CHANGED" - - echo need_to_build="$( - if [ "$TAG" != '' ] - then - echo 1 - elif [ $FILE_CHANGED = 0 ] && ( [ "$CHANNEL" = "beta" ] || [ "$CHANNEL" = "edge" ] ) - then - echo 1 - else - echo 0 - fi - )" >> $GITHUB_OUTPUT - - build: - needs: - - check - if: > - github.repository == 'anza-xyz/agave' && - needs.check.outputs.continue == 1 - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Setup pnpm - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - with: - version: 10 - - - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - package-manager-cache: false - - - name: Build - working-directory: docs - run: | - pnpm install --frozen-lockfile - ./build.sh - - - name: Upload artifacts - if: ${{ github.event_name == 'push' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: docs-build - path: ./docs/build - retention-days: 1 - - deploy: - needs: - - build - if: > - github.repository == 'anza-xyz/agave' && - github.event_name == 'push' - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: 24 - package-manager-cache: false - - - name: Download artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: docs-build - path: ./docs/build - - - name: Deploy - working-directory: docs - run: ./deploy.sh - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - VERCEL_SCOPE: ${{ secrets.VERCEL_SCOPE }} diff --git a/.github/workflows/downstream-project-anchor.yml b/.github/workflows/downstream-project-anchor.yml deleted file mode 100644 index 6dbc469abd7..00000000000 --- a/.github/workflows/downstream-project-anchor.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Downstream Project - Anchor - -on: - push: - branches: - - master - - v[0-9]+.[0-9]+ - pull_request: - branches: - - master - - v[0-9]+.[0-9]+ - paths: - - "**.rs" - - "Cargo.toml" - - "Cargo.lock" - - "scripts/build-downstream-anchor-projects.sh" - - "scripts/patch-spl-crates-for-anchor.sh" - - ".github/scripts/purge-ubuntu-runner.sh" - - ".github/scripts/downstream-project-spl-install-deps.sh" - - ".github/workflows/downstream-project-anchor.yml" - workflow_call: - inputs: - branch: - required: false - type: string - default: "master" - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -env: - SHELL: /bin/bash - SCCACHE_GHA_ENABLED: "true" - RUSTC_WRAPPER: "sccache" - -jobs: - test: - #if: github.repository == 'anza-xyz/agave' - if: false # restore once all program-specific repos are ready - runs-on: ubuntu-24.04 - strategy: - matrix: - version: ["master"] - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - shell: bash - run: | - .github/scripts/purge-ubuntu-runner.sh - - - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - with: - version: "v0.10.0" - - - shell: bash - run: | - source .github/scripts/downstream-project-spl-install-deps.sh - ./scripts/build-downstream-anchor-projects.sh ${{ matrix.version }} diff --git a/.github/workflows/downstream-project-spl-nightly.yml b/.github/workflows/downstream-project-spl-nightly.yml deleted file mode 100644 index ace52d430f3..00000000000 --- a/.github/workflows/downstream-project-spl-nightly.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Downstream Project - SPL (Nightly) - -on: - schedule: - - cron: "0 3 * * *" - -jobs: - main: - if: github.repository == 'anza-xyz/agave' - strategy: - fail-fast: false - matrix: - branch: - - master - uses: ./.github/workflows/downstream-project-spl.yml - with: - branch: ${{ matrix.branch }} diff --git a/.github/workflows/downstream-project-spl.yml b/.github/workflows/downstream-project-spl.yml deleted file mode 100644 index 36551cf7118..00000000000 --- a/.github/workflows/downstream-project-spl.yml +++ /dev/null @@ -1,171 +0,0 @@ -name: Downstream Project - SPL - -on: - push: - branches: - - master - - v[0-9]+.[0-9]+ - pull_request: - branches: - - master - - v[0-9]+.[0-9]+ - paths: - - "**.rs" - - "rust-toolchain.toml" - - "Cargo.toml" - - "Cargo.lock" - - "ci/downstream-projects/run-spl.sh" - - ".github/workflows/downstream-project-spl.yml" - - ".github/scripts/downstream-project-spl-common.sh" - - ".github/scripts/downstream-project-spl-install-deps.sh" - workflow_call: - inputs: - branch: - required: false - type: string - default: "master" - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -env: - SHELL: /bin/bash - SCCACHE_GHA_ENABLED: "true" - RUSTC_WRAPPER: "sccache" - -jobs: - check: - #if: github.repository == 'anza-xyz/agave' - if: false - runs-on: ubuntu-24.04 - timeout-minutes: 60 - strategy: - matrix: - programs: - - associated-token-account - - feature-proposal - - instruction-padding - - memo - - record - - single-pool - - slashing - - stake-pool - - token-2022 - # re-enable with https://github.com/buffalojoec/mollusk/pull/74 - # - token - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - shell: bash - run: | - .github/scripts/purge-ubuntu-runner.sh - - - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - with: - version: "v0.10.0" - - - shell: bash - run: | - source .github/scripts/downstream-project-spl-install-deps.sh - source .github/scripts/downstream-project-spl-common.sh "${{ matrix.programs }}" - if [ -n "$SKIP_SPL_DOWNSTREAM_PROJECT_TEST" ]; then - exit 0 - fi - cargo check - - test_cli: - #if: github.repository == 'anza-xyz/agave' - if: false - runs-on: ubuntu-24.04 - timeout-minutes: 60 - strategy: - matrix: - programs: - - single-pool - - token-2022 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - shell: bash - run: | - .github/scripts/purge-ubuntu-runner.sh - - - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - with: - version: "v0.10.0" - - - shell: bash - run: | - source .github/scripts/downstream-project-spl-install-deps.sh - source .github/scripts/downstream-project-spl-common.sh "${{ matrix.programs }}" - if [ -n "$SKIP_SPL_DOWNSTREAM_PROJECT_TEST" ]; then - exit 0 - fi - $CARGO_BUILD_SBF --manifest-path program/Cargo.toml - cargo test --manifest-path clients/cli/Cargo.toml - - cargo-test-sbf: - #if: github.repository == 'anza-xyz/agave' - if: false - runs-on: ubuntu-24.04 - timeout-minutes: 60 - strategy: - matrix: - programs: - - associated-token-account - - feature-proposal - - instruction-padding - - memo - - record - - single-pool - - slashing - - stake-pool - - token-2022 - # re-enable with https://github.com/buffalojoec/mollusk/pull/74 - # - token - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - shell: bash - run: | - .github/scripts/purge-ubuntu-runner.sh - - - uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - with: - version: "v0.10.0" - - - name: Install dependencies - shell: bash - run: | - source .github/scripts/downstream-project-spl-install-deps.sh - source .github/scripts/downstream-project-spl-common.sh "${{ matrix.programs }}" - if [ -n "$SKIP_SPL_DOWNSTREAM_PROJECT_TEST" ]; then - exit 0 - fi - - - name: Test SBPFv0 - shell: bash - run: | - source ci/downstream-projects/common.sh - cd "${{ matrix.programs }}" - $CARGO_BUILD_SBF --arch v0 --manifest-path program/Cargo.toml - RUSTFLAGS="-Awarnings" SBF_OUT_DIR="../target/deploy" cargo test --features test-sbf --manifest-path program/Cargo.toml - - - name: Test SBPFv1 - shell: bash - run: | - source ci/downstream-projects/common.sh - cd "${{ matrix.programs }}" - rm -rf target/deploy target/sbpf* - $CARGO_BUILD_SBF --arch v1 --manifest-path program/Cargo.toml - RUSTFLAGS="-Awarnings" SBF_OUT_DIR="../target/deploy" cargo test --features test-sbf --manifest-path program/Cargo.toml - - - name: Test SBPFv2 - shell: bash - run: | - source ci/downstream-projects/common.sh - cd "${{ matrix.programs }}" - rm -rf target/deploy target/sbpf* - $CARGO_BUILD_SBF --arch v2 --manifest-path program/Cargo.toml - RUSTFLAGS="-Awarnings" SBF_OUT_DIR="../target/deploy" cargo test --features test-sbf --manifest-path program/Cargo.toml diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml deleted file mode 100644 index 49e13945d12..00000000000 --- a/.github/workflows/label-actions.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: "Issue Label Actions" - -on: - issues: - types: [labeled, unlabeled] - -permissions: - contents: read - issues: write - -jobs: - action: - runs-on: ubuntu-24.04 - steps: - - uses: dessant/label-actions@809e0f9bc11ce08744e7ccac7cd10621d00aba2f # v5.0.2 diff --git a/.github/workflows/manage-stale-issues.yml b/.github/workflows/manage-stale-issues.yml index 09b5505c299..bd1381b90f0 100644 --- a/.github/workflows/manage-stale-issues.yml +++ b/.github/workflows/manage-stale-issues.yml @@ -21,11 +21,11 @@ jobs: # Forks do not need to run this, especially on cron schedule. if: > github.event_name != 'schedule' - || github.repository == 'anza-xyz/agave' + || github.repository_owner == 'anza-xyz' runs-on: ubuntu-24.04 steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: ascending: true # Spend API operations budget on older, more-likely-to-get-closed issues first close-issue-message: 'This issue was closed because it has been stale for 7 days with no activity.' diff --git a/.github/workflows/publish-windows-tarball.yml b/.github/workflows/publish-windows-tarball.yml index 2f847e4d187..add365ec79c 100644 --- a/.github/workflows/publish-windows-tarball.yml +++ b/.github/workflows/publish-windows-tarball.yml @@ -16,7 +16,7 @@ jobs: channel: ${{ steps.build.outputs.channel }} steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.inputs.commit }} fetch-depth: 0 @@ -89,6 +89,8 @@ jobs: if: ${{ needs.windows-build.outputs.tag != '' }} needs: [windows-build] runs-on: ubuntu-22.04 + permissions: + contents: write steps: - name: Download uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -97,7 +99,7 @@ jobs: path: ./windows-release/ - name: Release - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 with: tag_name: ${{ needs.windows-build.outputs.tag }} files: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af6cf27929e..a13fc62f43b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,11 +7,11 @@ on: jobs: trigger-buildkite-pipeline: - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' runs-on: ubuntu-24.04 steps: - name: Trigger a Buildkite Build - uses: "buildkite/trigger-pipeline-action@909fed762c73d5ae2b5d555ab910d66b3fae2670" # v2.4.1 + uses: "buildkite/trigger-pipeline-action@41fd38b69189bf186cf69cf10ec807a850cae593" # v2.5.0 with: buildkite_api_access_token: ${{ secrets.TRIGGER_BK_BUILD_TOKEN }} pipeline: "anza/agave-secondary" @@ -21,8 +21,10 @@ jobs: message: ":github: Triggered from a GitHub Action" draft-release: - if: github.repository == 'anza-xyz/agave' + if: github.repository_owner == 'anza-xyz' runs-on: ubuntu-24.04 + permissions: + contents: write steps: - name: Create Release uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.github/workflows/trigger-buildkite-pipeline.yml b/.github/workflows/trigger-buildkite-pipeline.yml new file mode 100644 index 00000000000..ba77f6c8354 --- /dev/null +++ b/.github/workflows/trigger-buildkite-pipeline.yml @@ -0,0 +1,134 @@ +name: Trigger Buildkite Pipeline (Pull Request) + +on: + pull_request_target: + branches: + - master + types: + - opened + - synchronize + - reopened + - labeled + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + member_check: + runs-on: ubuntu-24.04 + if: github.repository_owner == 'anza-xyz' && github.event.action != 'labeled' + outputs: + should_trigger: ${{ steps.check.outputs.should_trigger }} + permissions: + contents: read + steps: + - name: Check permission + id: check + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + USERNAME: ${{ github.event.sender.login }} + run: | + ROLE_NAME=$(curl -sS -L \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "X-GitHub-Api-Version: 2026-03-10" \ + https://api.github.com/repos/$REPO/collaborators/$USERNAME/permission | jq -r ".role_name") + + case "$ROLE_NAME" in + admin|maintain|write|triage) + echo "should_trigger=true" >> $GITHUB_OUTPUT + ;; + *) + echo "should_trigger=false" >> $GITHUB_OUTPUT + exit 1 + ;; + esac + + label_check: + runs-on: ubuntu-24.04 + if: github.repository_owner == 'anza-xyz' && github.event.action == 'labeled' && github.event.label.name == 'CI' + outputs: + should_trigger: ${{ steps.check.outputs.should_trigger }} + permissions: + contents: read + pull-requests: write + steps: + - name: Remove CI label + id: check + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + echo "should_trigger=true" >> $GITHUB_OUTPUT + + curl -sS -X DELETE \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GH_TOKEN" \ + "https://api.github.com/repos/$REPO/issues/$PR_NUMBER/labels/CI" \ + || echo "CI label was already removed." + + trigger: + runs-on: ubuntu-24.04 + needs: + - member_check + - label_check + if: >- + always() && + (needs.member_check.outputs.should_trigger == 'true' || needs.label_check.outputs.should_trigger == 'true') + permissions: {} + steps: + - name: Prepare Buildkite pipeline info + id: prepare + env: + BUILDKITE_PIPELINE: ${{ vars.BUILDKITE_PIPELINE }} + COMMIT: ${{ github.event.pull_request.head.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + COMMIT_SHORT="${COMMIT:0:8}" + BRANCH="pull/${PR_NUMBER}/head" + MESSAGE="PR#${PR_NUMBER} - ${COMMIT_SHORT}" + BUILD_META_DATA="{\"pr_number\":\"${PR_NUMBER}\"}" + + echo "branch=$BRANCH" | tee -a $GITHUB_OUTPUT + echo "build_meta_data=$BUILD_META_DATA" | tee -a $GITHUB_OUTPUT + echo "commit=$COMMIT" | tee -a $GITHUB_OUTPUT + echo "message=$MESSAGE" | tee -a $GITHUB_OUTPUT + echo "pipeline=$BUILDKITE_PIPELINE" | tee -a $GITHUB_OUTPUT + echo "pr_number=$PR_NUMBER" | tee -a $GITHUB_OUTPUT + + - name: Trigger a Buildkite Build + uses: "buildkite/trigger-pipeline-action@41fd38b69189bf186cf69cf10ec807a850cae593" # v2.5.0 + with: + pipeline: ${{ steps.prepare.outputs.pipeline }} + buildkite_api_access_token: ${{ secrets.BUILDKITE_API_ACCESS_TOKEN }} + branch: "${{ steps.prepare.outputs.branch }}" + build_meta_data: "${{ steps.prepare.outputs.build_meta_data }}" + commit: "${{ steps.prepare.outputs.commit }}" + message: "${{ steps.prepare.outputs.message }}" + pull_request_base_branch: "${{ github.event.pull_request.base.ref }}" + + - name: Summary + env: + BRANCH: ${{ steps.prepare.outputs.branch }} + BUILDKITE_PIPELINE: ${{ vars.BUILDKITE_PIPELINE }} + COMMIT: ${{ github.event.pull_request.head.sha }} + MESSAGE: ${{ steps.prepare.outputs.message }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + { + echo "## Buildkite Pipeline Info" + echo "" + echo "| Key | Value |" + echo "|-----|-------|" + echo "| Pipeline | \`$BUILDKITE_PIPELINE\` |" + echo "| Branch | \`$BRANCH\` |" + echo "| Commit | \`$COMMIT\` |" + echo "| PR Number | #$PR_NUMBER |" + echo "| Message | $MESSAGE |" + } >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/verify-packets.yml b/.github/workflows/verify-packets.yml deleted file mode 100644 index c348be3731a..00000000000 --- a/.github/workflows/verify-packets.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Verify Packets - -on: - push: - branches: - - master - pull_request: - branches: - - master - paths: - - .github/workflows/verify-packets.yml - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - gossip: - timeout-minutes: 30 - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Install required packages - run: | - sudo apt update - sudo apt install -y \ - libclang-dev \ - libprotobuf-dev \ - libssl-dev \ - libudev-dev \ - pkg-config \ - zlib1g-dev \ - llvm \ - clang \ - cmake \ - make \ - protobuf-compiler \ - git-lfs - - - name: Run packet verify - run: | - ./ci/test-verify-packets-gossip.sh diff --git a/.mergify.yml b/.mergify.yml index 6bf45241cdf..b6b1461947d 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,8 +1,10 @@ # https://doc.mergify.io/ +shared: + backport_assignee: &BackportAssignee + - "{{ merged_by|replace('mergify[bot]', label|select('equalto', 'community')|first|default(author)|replace('community', '@anza-xyz/community-pr-subscribers')) }}" pull_request_rules: - name: label changes from community conditions: - - author≠@agave-external-triage - author≠@anza - author≠mergify[bot] - author≠dependabot[bot] @@ -14,7 +16,6 @@ pull_request_rules: - need:merge-assist - name: request review for community changes conditions: - - author≠@agave-external-triage - author≠@anza - author≠mergify[bot] - author≠dependabot[bot] @@ -30,17 +31,6 @@ pull_request_rules: request_reviews: teams: - "@anza-xyz/community-pr-subscribers" - - name: label changes from agave-external-triage - conditions: - - author≠@anza - - author≠mergify[bot] - - author≠dependabot[bot] - - author≠github-actions[bot] - - author=@agave-external-triage - actions: - label: - add: - - need:merge-assist - name: automatic merge (squash) on CI success conditions: - and: @@ -48,38 +38,11 @@ pull_request_rules: - status-success=ci-gate - label=automerge - label!=no-automerge - - or: - # only require docs checks if docs files changed - - -files~=^docs/ - - status-success=build & deploy docs - or: - -files~=(\.rs|Cargo\.toml|Cargo\.lock|\.github/scripts/cargo-clippy-before-script\.sh|\.github/workflows/cargo\.yml)$ - or: - check-success=clippy-nightly (macos-latest) - check-success=clippy-nightly (macos-latest-large) - - or: - - -files~=(\.rs|Cargo\.toml|Cargo\.lock|cargo-build-sbf|cargo-test-sbf|ci/downstream-projects/run-spl\.sh|\.github/workflows/downstream-project-spl\.yml)$ - - and: - - status-success=check (associated-token-account) - - status-success=check (feature-proposal) - - status-success=check (instruction-padding) - - status-success=check (memo) - - status-success=check (record) - - status-success=check (single-pool) - - status-success=check (slashing) - - status-success=check (stake-pool) - - status-success=check (token-2022) - - status-success=test_cli (single-pool) - - status-success=test_cli (token-2022) - - status-success=cargo-test-sbf (associated-token-account) - - status-success=cargo-test-sbf (feature-proposal) - - status-success=cargo-test-sbf (instruction-padding) - - status-success=cargo-test-sbf (memo) - - status-success=cargo-test-sbf (record) - - status-success=cargo-test-sbf (single-pool) - - status-success=cargo-test-sbf (slashing) - - status-success=cargo-test-sbf (stake-pool) - - status-success=cargo-test-sbf (token-2022) - or: - -files~=(Cargo\.toml|.github/workflows/crate-check\.yml|ci/check-crates\.sh)$ - check-success=crate check @@ -98,23 +61,22 @@ pull_request_rules: - automerge comment: message: automerge label removed due to a CI failure - - name: v3.1 feature-gate backport + - name: v4.0 feature-gate backport conditions: - - label=v3.1 + - label=v4.0 - label=feature-gate actions: backport: - assignees: &BackportAssignee - - "{{ merged_by|replace('mergify[bot]', label|select('equalto', 'community')|first|default(author)|replace('community', '@anza-xyz/community-pr-subscribers')) }}" + assignees: *BackportAssignee title: "{{ destination_branch }}: {{ title }} (backport of #{{ number }})" ignore_conflicts: true labels: - feature-gate branches: - - v3.1 - - name: v3.1 non-feature-gate backport + - v4.0 + - name: v4.0 non-feature-gate backport conditions: - - label=v3.1 + - label=v4.0 - label!=feature-gate actions: backport: @@ -122,10 +84,10 @@ pull_request_rules: title: "{{ destination_branch }}: {{ title }} (backport of #{{ number }})" ignore_conflicts: true branches: - - v3.1 - - name: v4.0 feature-gate backport + - v4.0 + - name: v4.1 feature-gate backport conditions: - - label=v4.0 + - label=v4.1 - label=feature-gate actions: backport: @@ -135,10 +97,10 @@ pull_request_rules: labels: - feature-gate branches: - - v4.0 - - name: v4.0 non-feature-gate backport + - v4.1 + - name: v4.1 non-feature-gate backport conditions: - - label=v4.0 + - label=v4.1 - label!=feature-gate actions: backport: @@ -146,10 +108,10 @@ pull_request_rules: title: "{{ destination_branch }}: {{ title }} (backport of #{{ number }})" ignore_conflicts: true branches: - - v4.0 - - name: v4.1 feature-gate backport + - v4.1 + - name: v4.2 feature-gate backport conditions: - - label=v4.1 + - label=v4.2 - label=feature-gate actions: backport: @@ -159,10 +121,10 @@ pull_request_rules: labels: - feature-gate branches: - - v4.1 - - name: v4.1 non-feature-gate backport + - v4.2 + - name: v4.2 non-feature-gate backport conditions: - - label=v4.1 + - label=v4.2 - label!=feature-gate actions: backport: @@ -170,13 +132,13 @@ pull_request_rules: title: "{{ destination_branch }}: {{ title }} (backport of #{{ number }})" ignore_conflicts: true branches: - - v4.1 + - v4.2 - name: backport warning comment conditions: - or: - - label=v3.1 - label=v4.0 - label=v4.1 + - label=v4.2 actions: comment: message: > @@ -200,8 +162,8 @@ pull_request_rules: message: | If this PR represents a change to the public RPC API: - 1. Make sure it includes a complementary update to `rpc-client/` ([example](https://github.com/solana-labs/solana/pull/29558/files)) - 2. Open a follow-up PR to update the JavaScript client `@solana/kit` ([example](https://github.com/solana-labs/solana-web3.js/pull/2868/files)) + 1. Make sure it includes a complementary update to `rpc-client/` ([example](https://github.com/anza-xyz/agave/pull/2036/files)) + 2. Open a follow-up PR to update the JavaScript client `@solana/kit` ([example](https://github.com/anza-xyz/kit/pull/974/files)) Thank you for keeping the RPC clients in sync with the server API @{{author}}. - name: Reminder to add Firedancer team to changes in `programs/` diff --git a/CHANGELOG.md b/CHANGELOG.md index edcf8a1498e..914f819927b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,23 +8,52 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm and follows a [Backwards Compatibility Policy](https://docs.anza.xyz/backwards-compatibility) Release channels have their own copy of this changelog: -* [edge - v4.2](#edge-channel) -* [alpha - v4.1](https://github.com/anza-xyz/agave/blob/v4.1/CHANGELOG.md) -* [beta - v4.0](https://github.com/anza-xyz/agave/blob/v4.0/CHANGELOG.md) -* [stable - v3.1](https://github.com/anza-xyz/agave/blob/v3.1/CHANGELOG.md) +* [edge - v4.3](#edge-channel) +* [alpha - v4.2](https://github.com/anza-xyz/agave/blob/v4.2/CHANGELOG.md) +* [beta - v4.1](https://github.com/anza-xyz/agave/blob/v4.1/CHANGELOG.md) +* [stable - v4.0](https://github.com/anza-xyz/agave/blob/v4.0/CHANGELOG.md) -## 4.2.0-Unreleased +## 4.3.0-Unreleased ### RPC #### Breaking #### Changes ### Validator #### Breaking +#### Changes + +## 4.2.0 +### RPC +#### Breaking +* The `jsonParsed` output for confidential transfer `Deposit` (`depositConfidentialTransfer`) and `Withdraw` (`withdrawConfidentialTransfer`) instructions has been corrected. These instructions operate on a single token account, so the mislabeled `source` and `destination` fields have been replaced by a single `account` field (the `mint` field is unchanged). +* Blockstore reward column legacy format support removed. + * The `Rewards` column was updated in v1.5 (Solana Labs client) to switch from + storing bincode serialized values to protobuf encoded values. The old bincode + format will no longer be supported for fallback reads as of v4.2 +#### Changes +* Added `RpcClient::get_latest_blockhash_with_commitment_and_context`, which returns the + `getLatestBlockhash` response together with its context (notably `context.slot`). +### Validator +#### Breaking +* XDP transmit in SKB (copy) mode is now enabled by default on Linux. The validator requires + `CAP_NET_ADMIN` and `CAP_NET_RAW` capabilities (plus `CAP_BPF` and `CAP_PERFMON` for + `--xdp-zero-copy`). Pass `--no-xdp` to fall back to UDP sockets. The XDP CPU + core is auto-selected to avoid overlapping the PoH core; passing `--xdp-cpu-cores` + with a core that conflicts with the PoH core is an error. #### Deprecations * `--accounts-db-access-storages-method` is now deprecated and a no-op (the `mmap` value was deprecated in v4.0.0; mmap mode has now been removed entirely). The flag is still accepted for backward compatibility, but account storages are always accessed via file I/O. +* `--accounts-db-cache-limit-mb` is now deprecated. Use `--accounts-db-write-cache-limit` instead. +* `--experimental-poh-pinned-cpu-core` is now deprecated. Use `--poh-pinned-cpu-core` instead. +#### Changes +* Turbine shred ingestion now rejects shreds more than half an epoch in the future (previously up to 2 full epochs ahead was accepted). +* When XDP is enabled, gossip egress does not support private and loopback addresses. Operators running with `--allow-private-addr` must also pass `--no-xdp`. +### CLI +#### Breaking #### Changes +* `vote-account` supports Alpenglow and as such `vote-account --output json` breaks compatibility with older versions. +* Support Keystone hardware wallets using `usb://keystone` ## 4.1.0 ### RPC diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index badc9364006..a33880f52c1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,7 +71,7 @@ are not present, then write them. All changes should be benchmarked and evidence posted to the PR. Microbenchmark results along with mainnet/testnet validator timings or profiles, -bench-tps or other relevant integration benchmarks. Any code that adds complexity +and relevant integration benchmarks. Any code that adds complexity should be justified by a comisurate improvement in speed. All changes should be reviewed by subject matter experts. @@ -381,12 +381,6 @@ confused with 3-letter acronyms. ## Design Proposals -This Agave validator client's architecture is described by docs generated from markdown files in the `docs/src/` -directory and viewable on the official [Agave Validator Client](https://docs.anza.xyz) documentation website. - -Current design proposals may be viewed on the docs site: - -1. [Accepted Proposals](https://docs.anza.xyz/proposals/accepted-design-proposals) -2. [Implemented Proposals](https://docs.anza.xyz/implemented-proposals/implemented-proposals) - -New design proposals should follow this guide on [how to submit a design proposal](./docs/src/proposals.md#submit-a-design-proposal). +Design proposals are now tracked as Solana Improvement Documents (SIMDs) in the +[solana-foundation/solana-improvement-documents](https://github.com/solana-foundation/solana-improvement-documents) +repository. diff --git a/Cargo.lock b/Cargo.lock index 7956427ebf0..97f36a651cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "aead" version = "0.5.2" @@ -56,7 +62,7 @@ dependencies = [ [[package]] name = "agave-banking-stage-ingress-types" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "crossbeam-channel", "solana-perf", @@ -64,7 +70,7 @@ dependencies = [ [[package]] name = "agave-bls-cert-verify" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-bls-cert-verify", "agave-votor", @@ -72,23 +78,70 @@ dependencies = [ "bitvec", "criterion", "qualifier_attr", + "rand 0.9.4", "rayon", "solana-bls-signatures", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-signer-store", "thiserror 2.0.18", "wincode", ] +[[package]] +name = "agave-bls-sigverify" +version = "4.3.0-alpha.0" +dependencies = [ + "agave-bls-cert-verify", + "agave-bls-sigverify", + "agave-logger", + "agave-math-utils", + "agave-votor", + "agave-votor-messages", + "bincode", + "bitvec", + "criterion", + "crossbeam-channel", + "log", + "parking_lot 0.12.3", + "qualifier_attr", + "rayon", + "solana-bls-signatures", + "solana-clock", + "solana-epoch-schedule", + "solana-gossip", + "solana-hash 4.4.0", + "solana-keypair", + "solana-ledger", + "solana-measure", + "solana-metrics", + "solana-net-utils", + "solana-packet 4.1.0", + "solana-perf", + "solana-pubkey 4.2.0", + "solana-runtime", + "solana-signer", + "solana-signer-store", + "solana-streamer", + "thiserror 2.0.18", + "wincode", +] + +[[package]] +name = "agave-cpu-utils" +version = "4.3.0-alpha.0" +dependencies = [ + "libc", +] + [[package]] name = "agave-feature-set" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "ahash 0.8.12", "solana-epoch-schedule", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-pubkey 4.2.0", "solana-sha256-hasher", @@ -97,7 +150,7 @@ dependencies = [ [[package]] name = "agave-fs" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-io-uring", "io-uring", @@ -113,11 +166,11 @@ dependencies = [ [[package]] name = "agave-geyser-plugin-interface" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "log", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-signature", "solana-transaction", @@ -127,12 +180,10 @@ dependencies = [ [[package]] name = "agave-install" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", - "bincode", "bzip2", - "chrono", "clap 2.33.3", "console 0.16.3", "crossbeam-channel", @@ -145,17 +196,8 @@ dependencies = [ "semver 1.0.28", "serde", "serde_yaml", - "solana-clap-utils", - "solana-config-interface", - "solana-hash 4.3.0", - "solana-keypair", - "solana-message", - "solana-pubkey 4.2.0", - "solana-rpc-client", + "solana-hash 4.4.0", "solana-sha256-hasher", - "solana-signature", - "solana-signer", - "solana-transaction", "solana-version", "tar", "tempfile", @@ -166,7 +208,7 @@ dependencies = [ [[package]] name = "agave-io-uring" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "io-uring", "libc", @@ -177,7 +219,7 @@ dependencies = [ [[package]] name = "agave-logger" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "env_logger", "libc", @@ -187,7 +229,7 @@ dependencies = [ [[package]] name = "agave-math-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-math-utils", "num-traits", @@ -197,7 +239,7 @@ dependencies = [ [[package]] name = "agave-precompiles" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-logger", @@ -222,19 +264,19 @@ dependencies = [ [[package]] name = "agave-random" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "assert_matches", "rand 0.9.4", "rand_chacha 0.9.0", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", "test-case", ] [[package]] name = "agave-reserved-account-keys" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "solana-frozen-abi", @@ -247,11 +289,11 @@ dependencies = [ [[package]] name = "agave-scheduler-bindings" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "agave-scheduling-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-scheduler-bindings", @@ -264,6 +306,7 @@ dependencies = [ "slotmap", "solana-fee", "solana-pubkey 4.2.0", + "solana-runtime-transaction", "solana-transaction", "solana-transaction-error", "tempfile", @@ -273,7 +316,7 @@ dependencies = [ [[package]] name = "agave-snapshots" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-fs", "agave-logger", @@ -289,7 +332,7 @@ dependencies = [ "solana-accounts-db", "solana-clock", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-lattice-hash", "solana-measure", "solana-metrics", @@ -304,7 +347,7 @@ dependencies = [ [[package]] name = "agave-store-histogram" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "clap 2.33.3", "solana-version", @@ -312,17 +355,16 @@ dependencies = [ [[package]] name = "agave-transaction-view" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-transaction-view", "bincode", "criterion", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", "solana-packet 4.1.0", - "solana-program-runtime", "solana-pubkey 4.2.0", "solana-sdk-ids", "solana-short-vec", @@ -331,25 +373,26 @@ dependencies = [ "solana-svm-transaction", "solana-system-interface 3.2.0", "solana-transaction", - "solana-transaction-context", "wincode", ] [[package]] name = "agave-validator" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-cpu-utils", + "agave-feature-set", "agave-logger", "agave-snapshots", "agave-votor", "agave-xdp", "assert_cmd", "assert_matches", + "bytesize", "caps", "chrono", "clap 2.33.3", "console 0.16.3", - "core_affinity", "crossbeam-channel", "fd-lock", "indicatif", @@ -370,7 +413,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-accounts-db", "solana-clap-utils", @@ -386,7 +429,7 @@ dependencies = [ "solana-genesis-utils", "solana-geyser-plugin-manager", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-keypair", "solana-ledger", @@ -429,25 +472,21 @@ dependencies = [ [[package]] name = "agave-votor" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-bls-sigverify", "agave-logger", "agave-math-utils", - "agave-votor", "agave-votor-messages", - "bincode", "bitvec", "crossbeam-channel", "itertools 0.14.0", "lazy-lru", "log", "parking_lot 0.12.3", - "qualifier_attr", "rand 0.9.4", "serde", "serde_bytes", - "solana-accounts-db", - "solana-bloom", "solana-bls-signatures", "solana-client", "solana-clock", @@ -456,8 +495,9 @@ dependencies = [ "solana-frozen-abi", "solana-frozen-abi-macro", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-metrics", @@ -466,7 +506,6 @@ dependencies = [ "solana-pubkey 4.2.0", "solana-rpc", "solana-runtime", - "solana-sdk-ids", "solana-signature", "solana-signer", "solana-signer-store", @@ -474,8 +513,6 @@ dependencies = [ "solana-time-utils", "solana-transaction", "solana-transaction-error", - "solana-vote", - "solana-vote-program", "tempfile", "test-case", "thiserror 2.0.18", @@ -485,19 +522,20 @@ dependencies = [ [[package]] name = "agave-votor-messages" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-votor-messages", + "crossbeam-channel", "log", "serde", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-bls-signatures", "solana-clock", "solana-epoch-schedule", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-short-vec", "solana-signer-store", @@ -508,8 +546,9 @@ dependencies = [ [[package]] name = "agave-watchtower" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-feature-set", "agave-logger", "clap 2.33.3", "humantime", @@ -517,7 +556,7 @@ dependencies = [ "solana-clap-utils", "solana-cli-config", "solana-cli-output", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-metrics", "solana-native-token", "solana-notifier", @@ -525,28 +564,30 @@ dependencies = [ "solana-rpc-client", "solana-rpc-client-api", "solana-version", + "solana-vote-interface", ] [[package]] name = "agave-xdp" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-xdp-ebpf", "arc-swap", "arrayvec", "aya", "bytes", "caps", - "core_affinity", "crossbeam-channel", "libc", "log", + "nix", "thiserror 2.0.18", ] [[package]] name = "agave-xdp-ebpf" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "aya", "aya-ebpf", @@ -1005,9 +1046,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] name = "ascii" @@ -1131,7 +1172,7 @@ checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ "base64 0.22.1", "flate2", - "http 1.4.0", + "http 1.4.2", "log", "native-tls", "serde", @@ -1164,7 +1205,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "itoa", @@ -1188,7 +1229,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "mime", @@ -1200,19 +1241,22 @@ dependencies = [ [[package]] name = "aya" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18bc4e506fbb85ab7392ed993a7db4d1a452c71b75a246af4a80ab8c9d2dd50" +checksum = "66e644424fada9fff4fdc63848db1732fb69b626e8328202ef55c03df1f4d939" dependencies = [ + "anyhow", "assert_matches", "aya-obj", - "bitflags 2.11.1", - "bytes", + "bitflags 2.13.0", + "hashbrown 0.17.0", "libc", "log", + "nix", "object", "once_cell", - "thiserror 1.0.69", + "scopeguard", + "thiserror 2.0.18", ] [[package]] @@ -1256,16 +1300,14 @@ dependencies = [ [[package]] name = "aya-obj" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51b96c5a8ed8705b40d655273bc4212cbbf38d4e3be2788f36306f154523ec7" +checksum = "8c76b9c75d9cdc155ff8f6a06d61e873f67bf47be8cfa92a3b5aaea43f4b4077" dependencies = [ "bytes", - "core-error", - "hashbrown 0.15.1", "log", "object", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -1313,18 +1355,31 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", "syn 2.0.117", ] +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes 0.14.100", + "rand 0.8.6", + "rand_core 0.6.4", + "serde", + "unicode-normalization", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1340,6 +1395,30 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin_hashes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +dependencies = [ + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -1348,9 +1427,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -1372,9 +1451,9 @@ checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" [[package]] name = "bitvec" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" dependencies = [ "funty", "radium", @@ -1464,9 +1543,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" dependencies = [ "borsh-derive", "bytes", @@ -1475,9 +1554,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" dependencies = [ "once_cell", "proc-macro-crate 3.1.0", @@ -1519,6 +1598,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ + "sha2 0.10.9", "tinyvec", ] @@ -1602,18 +1682,18 @@ dependencies = [ [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" dependencies = [ "serde", ] [[package]] name = "bytesize" -version = "1.1.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" +checksum = "49e78e506b9d7633710dab98996f22f95f3d0f488e8f1aa162830556ed9fc14d" [[package]] name = "bzip2" @@ -1708,9 +1788,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -1830,7 +1910,7 @@ version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -1968,15 +2048,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "core-error" -version = "0.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efcdb2972eb64230b4c50646d8498ff73f5128d196a90c7236eec4cbe8619b8f" -dependencies = [ - "version_check", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -2003,17 +2074,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core_affinity" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a034b3a7b624016c6e13f5df875747cc25f884156aad2abd12b6c46797971342" -dependencies = [ - "libc", - "num_cpus", - "winapi", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -2032,6 +2092,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.2.1" @@ -2110,12 +2185,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" -dependencies = [ - "cfg-if 1.0.4", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -2257,38 +2329,14 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.117", + "darling_core", + "darling_macro", ] [[package]] @@ -2306,25 +2354,20 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core 0.21.3", + "darling_core", "quote", "syn 2.0.117", ] [[package]] -name = "darling_macro" -version = "0.23.0" +name = "dary_heap" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn 2.0.117", -] +checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe" [[package]] name = "dashmap" @@ -2549,7 +2592,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "libc", "objc2", @@ -2566,29 +2609,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "downcast" version = "0.11.0" @@ -2736,7 +2756,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -2969,6 +2989,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "fixedbitset" version = "0.5.7" @@ -3176,7 +3202,7 @@ dependencies = [ [[package]] name = "gen-syscall-list" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "regex", ] @@ -3314,7 +3340,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.4.2", "indexmap 2.14.0", "slab", "tokio", @@ -3372,10 +3398,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" dependencies = [ "allocator-api2", - "equivalent", "foldhash 0.1.5", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + [[package]] name = "hashbrown" version = "0.17.0" @@ -3387,6 +3423,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -3405,6 +3447,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + [[package]] name = "hidapi" version = "2.6.6" @@ -3454,6 +3505,15 @@ dependencies = [ "hmac 0.8.1", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.0", +] + [[package]] name = "http" version = "0.2.12" @@ -3467,9 +3527,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -3493,7 +3553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.2", ] [[package]] @@ -3504,7 +3564,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "pin-project-lite", ] @@ -3576,7 +3636,7 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "httparse", "httpdate", @@ -3595,7 +3655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", - "http 1.4.0", + "http 1.4.2", "hyper 1.8.1", "hyper-util", "rustls", @@ -3646,7 +3706,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "hyper 1.8.1", "ipnet", @@ -3953,7 +4013,7 @@ version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "libc", ] @@ -4240,6 +4300,19 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "keystone-ur" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a08a7e0ccd113dac19485c5c0fe87c70b663f896c48e4493814e446175078d4" +dependencies = [ + "bitcoin_hashes 0.12.0", + "crc", + "minicbor", + "phf", + "rand_xoshiro 0.6.0", +] + [[package]] name = "konst" version = "0.2.20" @@ -4282,6 +4355,30 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libflate" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85f7ef5c7e3c2ed51f0fbc40e016c66558b699f16593521f30b98713bbb99cb8" +dependencies = [ + "adler32", + "crc32fast", + "dary_heap", + "libflate_lz77", + "no_std_io2", +] + +[[package]] +name = "libflate_lz77" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7a10e427698aef6eef269482776debfef63384d30f13aad39a1a95e0e098fd" +dependencies = [ + "hashbrown 0.16.1", + "no_std_io2", + "rle-decode-fast", +] + [[package]] name = "libloading" version = "0.9.0" @@ -4437,9 +4534,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru" @@ -4519,9 +4616,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -4568,6 +4665,26 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2687e6cf9c00f48e9284cf9fd15f2ef341d03cc7743abf9df4c5f07fdee50b18" +[[package]] +name = "minicbor" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7005aaf257a59ff4de471a9d5538ec868a21586534fff7f85dd97d4043a6139" +dependencies = [ + "minicbor-derive", +] + +[[package]] +name = "minicbor-derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1154809406efdb7982841adb6311b3d095b46f78342dd646736122fe6b19e267" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "minimal-lexical" version = "0.1.4" @@ -4683,13 +4800,22 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "cfg_aliases", "libc", "memoffset 0.9.1", ] +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + [[package]] name = "nom" version = "7.0.0" @@ -4808,6 +4934,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4859,12 +4986,12 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "object" -version = "0.36.7" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "crc32fast", - "hashbrown 0.15.1", + "hashbrown 0.17.0", "indexmap 2.14.0", "memchr", ] @@ -4898,11 +5025,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.80" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "foreign-types", "libc", @@ -4938,9 +5065,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.116" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", @@ -5075,9 +5202,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pastey" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "pbkdf2" @@ -5088,16 +5215,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - [[package]] name = "pcap-file" version = "2.0.0" @@ -5148,17 +5265,69 @@ dependencies = [ "ucd-trie", ] +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset 0.4.2", + "indexmap 2.14.0", +] + [[package]] name = "petgraph" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ - "fixedbitset", + "fixedbitset 0.5.7", "hashbrown 0.15.1", "indexmap 2.14.0", ] +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.6", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + [[package]] name = "pickledb" version = "0.5.1" @@ -5330,6 +5499,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" +dependencies = [ + "proc-macro2", + "syn 1.0.109", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -5408,7 +5587,7 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.11.1", + "bitflags 2.13.0", "num-traits", "rand 0.9.4", "rand_chacha 0.9.0", @@ -5431,28 +5610,50 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", - "prost-derive 0.14.3", + "prost-derive 0.14.4", ] [[package]] name = "prost-build" -version = "0.14.3" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" dependencies = [ - "heck", + "bytes", + "heck 0.4.1", + "itertools 0.10.5", + "lazy_static", + "log", + "multimap", + "petgraph 0.6.5", + "prettyplease 0.1.25", + "prost 0.11.9", + "prost-types 0.11.9", + "regex", + "syn 1.0.109", + "tempfile", + "which", +] + +[[package]] +name = "prost-build" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" +dependencies = [ + "heck 0.5.0", "itertools 0.14.0", "log", "multimap", - "petgraph", - "prettyplease", - "prost 0.14.3", - "prost-types", + "petgraph 0.8.3", + "prettyplease 0.2.37", + "prost 0.14.4", + "prost-types 0.14.4", "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", @@ -5475,9 +5676,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools 0.14.0", @@ -5488,16 +5689,25 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.14.3" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" dependencies = [ - "prost 0.14.3", + "prost 0.11.9", +] + +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost 0.14.4", ] [[package]] name = "proto" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "protobuf-src", "tonic-prost-build", @@ -5534,9 +5744,9 @@ dependencies = [ [[package]] name = "protosol" -version = "6.1.0" +version = "9.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acef24e715f0ce9ac6491004da1c35c919c60517f976f61e0f05f34d22e873ec" +checksum = "e19fd23beba10243272008acfa690c6cda43efed0fd23e3fa9c9dc6e1a989848" dependencies = [ "prost 0.11.9", ] @@ -5547,7 +5757,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "memchr", "unicase", ] @@ -5589,16 +5799,16 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "socket2 0.6.3", "thiserror 2.0.18", @@ -5609,9 +5819,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "bytes", "fastbloom", @@ -5619,7 +5829,7 @@ dependencies = [ "lru-slab", "rand 0.9.4", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -5831,7 +6041,7 @@ dependencies = [ [[package]] name = "rbpf-cli" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "redox_syscall" @@ -5878,9 +6088,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick 1.0.1", "memchr", @@ -5907,9 +6117,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -5922,7 +6132,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -5963,7 +6173,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -5997,7 +6207,7 @@ checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" dependencies = [ "anyhow", "async-trait", - "http 1.4.0", + "http 1.4.2", "reqwest 0.12.28", "serde", "thiserror 1.0.69", @@ -6028,6 +6238,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + [[package]] name = "rocksdb" version = "0.24.0" @@ -6049,9 +6265,9 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.5.2" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" dependencies = [ "libc", "rtoolbox", @@ -6094,12 +6310,6 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -6139,7 +6349,7 @@ version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.4.14", @@ -6152,7 +6362,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.12.1", @@ -6161,9 +6371,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "log", "once_cell", @@ -6276,15 +6486,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" version = "0.1.19" @@ -6307,12 +6508,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "sec1" version = "0.7.3" @@ -6333,7 +6528,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -6346,7 +6541,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -6447,9 +6642,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -6472,9 +6667,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "serde_core", "serde_with_macros", @@ -6482,11 +6677,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -6507,24 +6702,23 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d" dependencies = [ "futures-executor", "futures-util", "log", "once_cell", "parking_lot 0.12.3", - "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" dependencies = [ "proc-macro2", "quote", @@ -6718,9 +6912,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" dependencies = [ "serde", ] @@ -6791,9 +6985,9 @@ dependencies = [ [[package]] name = "solana-account" -version = "4.3.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c1b95bd432efb92837bc38001365e5acb5c0175b199bc447b08a8c320ecf6c" +checksum = "385cad928d576683db3afef1b88d00a31a7126cc9f6f3f0c9f6b2d181437f53c" dependencies = [ "bincode", "qualifier_attr", @@ -6812,7 +7006,7 @@ dependencies = [ [[package]] name = "solana-account-decoder" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "Inflector", "assert_matches", @@ -6822,14 +7016,14 @@ dependencies = [ "bv", "serde", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder-client-types", "solana-address-lookup-table-interface", "solana-clock", "solana-config-interface", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-loader-v3-interface", "solana-nonce", @@ -6843,6 +7037,7 @@ dependencies = [ "solana-stake-interface", "solana-sysvar", "solana-vote-interface", + "solana-zk-sdk-pod", "spl-generic-token", "spl-pod", "spl-token-2022-interface", @@ -6850,18 +7045,19 @@ dependencies = [ "spl-token-interface", "spl-token-metadata-interface", "thiserror 2.0.18", + "wincode", "zstd", ] [[package]] name = "solana-account-decoder-client-types" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "bs58", "serde", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-pubkey 4.2.0", "zstd", ] @@ -6874,14 +7070,14 @@ checksum = "a9cf16495d9eb53e3d04e72366a33bb1c20c24e78c171d8b8f5978357b63ae95" dependencies = [ "bincode", "serde_core", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-program-error", "solana-program-memory", ] [[package]] name = "solana-accounts-cluster-bench" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "clap 2.33.3", @@ -6897,7 +7093,7 @@ dependencies = [ "solana-core", "solana-faucet", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-local-cluster", @@ -6925,7 +7121,7 @@ dependencies = [ [[package]] name = "solana-accounts-db" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-fs", "agave-logger", @@ -6950,7 +7146,7 @@ dependencies = [ "serde", "serde_bytes", "smallvec", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-accounts-db", "solana-address-lookup-table-interface", "solana-bucket-map", @@ -6959,7 +7155,7 @@ dependencies = [ "solana-fee-calculator", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-lattice-hash", @@ -7002,14 +7198,14 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] name = "solana-address" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1384b52c435a750cc9c538760fc7bb472fd78e65a9900a2d07312c5bb335b72" +checksum = "39c93e262f671bf402e1040e4a7e40b05d81da5956c7681948c975a0997517bb" dependencies = [ "arbitrary", "borsh", @@ -7049,6 +7245,7 @@ dependencies = [ "solana-pubkey 4.2.0", "solana-sdk-ids", "solana-slot-hashes", + "wincode", ] [[package]] @@ -7062,16 +7259,16 @@ dependencies = [ [[package]] name = "solana-banks-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "borsh", "futures 0.3.32", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-banks-interface", "solana-banks-server", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-program-pack", "solana-pubkey 4.2.0", @@ -7092,13 +7289,13 @@ dependencies = [ [[package]] name = "solana-banks-interface" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-pubkey 4.2.0", "solana-signature", @@ -7110,17 +7307,17 @@ dependencies = [ [[package]] name = "solana-banks-server" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "bincode", "crossbeam-channel", "futures 0.3.32", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-banks-interface", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-net-utils", "solana-pubkey 4.2.0", @@ -7166,14 +7363,15 @@ checksum = "7116e1d942a2432ca3f514625104757ab8a56233787e95144c93950029e31176" dependencies = [ "blake3", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] name = "solana-bloom" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bencher", + "bincode", "bv", "fnv", "rand 0.9.4", @@ -7182,11 +7380,12 @@ dependencies = [ "solana-bloom", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sanitize", "solana-sha256-hasher", "solana-signature", "solana-time-utils", + "wincode", ] [[package]] @@ -7258,7 +7457,7 @@ dependencies = [ [[package]] name = "solana-bpf-loader-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "assert_matches", "bincode", @@ -7266,7 +7465,7 @@ dependencies = [ "qualifier_attr", "rand 0.9.4", "shuttle", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bincode", "solana-bpf-loader-program", "solana-clock", @@ -7299,12 +7498,12 @@ dependencies = [ [[package]] name = "solana-bpf-loader-program-tests" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "assert_matches", "bincode", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bpf-loader-program", "solana-clock", "solana-instruction", @@ -7321,7 +7520,7 @@ dependencies = [ [[package]] name = "solana-bucket-map" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "ahash 0.8.12", @@ -7329,7 +7528,7 @@ dependencies = [ "bytemuck", "bytemuck_derive", "fs_extra", - "memmap2 0.9.10", + "memmap2 0.9.11", "modular-bitfield", "num_enum", "rand 0.9.4", @@ -7343,12 +7542,12 @@ dependencies = [ [[package]] name = "solana-builtins" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "solana-bpf-loader-program", "solana-compute-budget-program", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-program-runtime", "solana-pubkey 4.2.0", "solana-sdk-ids", @@ -7360,7 +7559,7 @@ dependencies = [ [[package]] name = "solana-builtins-default-costs" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "ahash 0.8.12", @@ -7372,9 +7571,11 @@ dependencies = [ [[package]] name = "solana-clap-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "assert_matches", + "bip39", + "bs58", "chrono", "clap 2.33.3", "rpassword", @@ -7383,7 +7584,7 @@ dependencies = [ "solana-cluster-type", "solana-commitment-config", "solana-derivation-path", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-native-token", @@ -7396,16 +7597,16 @@ dependencies = [ "solana-system-interface 3.2.0", "tempfile", "thiserror 2.0.18", - "tiny-bip39", "uriparse", "url 2.5.8", ] [[package]] name = "solana-clap-v3-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "assert_matches", + "bip39", "chrono", "clap 3.2.23", "five8", @@ -7415,7 +7616,7 @@ dependencies = [ "solana-cluster-type", "solana-commitment-config", "solana-derivation-path", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-native-token", @@ -7430,20 +7631,20 @@ dependencies = [ "solana-zk-sdk 5.0.1", "tempfile", "thiserror 2.0.18", - "tiny-bip39", "uriparse", "url 2.5.8", ] [[package]] name = "solana-cli" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-logger", "agave-votor-messages", "assert_matches", "bincode", + "bip39", "bs58", "clap 2.33.3", "console 0.16.3", @@ -7456,7 +7657,7 @@ dependencies = [ "reqwest 0.12.28", "serde", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-address-lookup-table-interface", "solana-bls-signatures", @@ -7475,7 +7676,7 @@ dependencies = [ "solana-feature-gate-interface", "solana-fee-calculator", "solana-fee-structure", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-loader-v3-interface", @@ -7501,6 +7702,7 @@ dependencies = [ "solana-sha256-hasher", "solana-signature", "solana-signer", + "solana-signer-store", "solana-slot-history", "solana-stake-interface", "solana-syscalls", @@ -7516,13 +7718,13 @@ dependencies = [ "tempfile", "test-case", "thiserror 2.0.18", - "tiny-bip39", "tokio", + "wincode", ] [[package]] name = "solana-cli-config" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "anyhow", "dirs-next", @@ -7535,11 +7737,13 @@ dependencies = [ [[package]] name = "solana-cli-output" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "Inflector", "agave-reserved-account-keys", + "agave-votor-messages", "base64 0.22.1", + "bitvec", "chrono", "clap 2.33.3", "console 0.16.3", @@ -7549,16 +7753,19 @@ dependencies = [ "semver 1.0.28", "serde", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-bincode", + "solana-bls-signatures", "solana-clap-utils", "solana-cli-config", "solana-clock", "solana-epoch-info", - "solana-hash 4.3.0", + "solana-epoch-schedule", + "solana-hash 4.4.0", "solana-keypair", "solana-message", + "solana-native-token", "solana-packet 4.1.0", "solana-pubkey 4.2.0", "solana-rpc-client-api", @@ -7578,7 +7785,7 @@ dependencies = [ [[package]] name = "solana-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-trait", "bincode", @@ -7589,7 +7796,7 @@ dependencies = [ "log", "solana-commitment-config", "solana-connection-cache", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-message", @@ -7614,7 +7821,7 @@ dependencies = [ [[package]] name = "solana-client-test" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "futures-util", @@ -7639,7 +7846,6 @@ dependencies = [ "solana-test-validator", "solana-transaction-status", "solana-version", - "systemstat", "tokio", "tungstenite", ] @@ -7650,10 +7856,10 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19635cc703678bcb26eb6faee4a363c71f4a0d622c445d9b70c048c42f4cbcd3" dependencies = [ - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-commitment-config", "solana-epoch-info", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -7667,12 +7873,13 @@ dependencies = [ [[package]] name = "solana-clock" -version = "3.0.1" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95cf11109c3b6115cc510f1e31f06fdd52f504271bc24ef5f1249fbbcae5f9f3" +checksum = "f0acdace90d96e2c9e70d681465b4fe888b6bcf27c354ae9774e9f8a3b72923d" dependencies = [ "serde", "serde_derive", + "solana-get-sysvar", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -7686,7 +7893,7 @@ checksum = "3a494cf8eda7d98d9f0144b288bb409c88308d2e86f15cc1045aa77b83304718" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -7701,7 +7908,7 @@ dependencies = [ [[package]] name = "solana-compute-budget" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-fee-structure", "solana-program-runtime", @@ -7709,7 +7916,7 @@ dependencies = [ [[package]] name = "solana-compute-budget-instruction" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "bincode", @@ -7720,7 +7927,7 @@ dependencies = [ "solana-compute-budget", "solana-compute-budget-instruction", "solana-compute-budget-interface", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -7748,14 +7955,14 @@ dependencies = [ [[package]] name = "solana-compute-budget-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-program-runtime", ] [[package]] name = "solana-compute-budget-program-bench" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "criterion", @@ -7786,7 +7993,7 @@ dependencies = [ [[package]] name = "solana-connection-cache" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "async-trait", @@ -7806,10 +8013,11 @@ dependencies = [ [[package]] name = "solana-core" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-banking-stage-ingress-types", "agave-bls-cert-verify", + "agave-bls-sigverify", "agave-feature-set", "agave-fs", "agave-logger", @@ -7858,7 +8066,8 @@ dependencies = [ "shaq", "signal-hook", "slab", - "solana-account 4.3.0", + "smallvec", + "solana-account 4.3.1", "solana-accounts-db", "solana-address-lookup-table-interface", "solana-bincode", @@ -7885,7 +8094,7 @@ dependencies = [ "solana-geyser-plugin-manager", "solana-gossip", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-leader-schedule", @@ -7960,7 +8169,7 @@ dependencies = [ [[package]] name = "solana-cost-model" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-logger", @@ -7978,7 +8187,7 @@ dependencies = [ "solana-cost-model", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -8017,23 +8226,9 @@ dependencies = [ [[package]] name = "solana-curve25519" -version = "3.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2cb3f53e40375fba75e908295c7723d16dad3fb54384140366fade2c7ef37bb" -dependencies = [ - "bytemuck", - "bytemuck_derive", - "curve25519-dalek 4.1.3", - "solana-define-syscall 3.0.0", - "subtle", - "thiserror 2.0.18", -] - -[[package]] -name = "solana-curve25519" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ee66a3e25ed8d20ed6fcd1ac71735415ff1edaf33ff1ec2118826eba927bcc" +checksum = "14b4d2a4bf0d0b0a86c22111917e86e8bd39a7b31420fb2c7d73eb83761fc7af" dependencies = [ "bytemuck", "bytemuck_derive", @@ -8074,7 +8269,7 @@ dependencies = [ [[package]] name = "solana-download-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-snapshots", "log", @@ -8098,7 +8293,7 @@ dependencies = [ [[package]] name = "solana-ed25519-program-tests" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "assert_matches", "rand 0.9.4", @@ -8114,7 +8309,7 @@ dependencies = [ [[package]] name = "solana-entry" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "agave-reserved-account-keys", @@ -8122,19 +8317,18 @@ dependencies = [ "assert_matches", "criterion", "crossbeam-channel", - "dlopen2", "log", "num_cpus", "proptest", "rand 0.9.4", "rayon", "smallvec", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-bls-signatures", "solana-clock", "solana-cost-model", "solana-entry", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-merkle-tree", @@ -8165,13 +8359,14 @@ dependencies = [ [[package]] name = "solana-epoch-rewards" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e7b0ba210593ba8ddd39d6d234d81795d1671cebf3026baa10d5dc23ac42f0" +checksum = "daf7eb4986b0b1d6f562b21f75a836f1a6df6e00c275efcef50aab5c144dc59e" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-get-sysvar", + "solana-hash 4.4.0", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -8184,20 +8379,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ee8beac9bff4db9225e57d532d169b0be5e447f1e6601a2f50f27a01bf5518f" dependencies = [ "siphasher 0.3.11", - "solana-address 2.6.0", - "solana-hash 4.3.0", + "solana-address 2.6.1", + "solana-hash 4.4.0", ] [[package]] name = "solana-epoch-schedule" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce264b7b42322325947c4136a09460bf5c73d9aa8262c9b0a2064be63ba8639" +checksum = "8116e6ffa6002237d5ab5edcbda17f9ba66b6742c45a89c9fb40a94dbacd4c1d" dependencies = [ "serde", "serde_derive", "solana-frozen-abi", "solana-frozen-abi-macro", + "solana-get-sysvar", + "solana-program-error", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -8221,7 +8418,7 @@ checksum = "0eb265ff95e28eceda117e2e3d2d2a611ecbbfe911dfeeeecd1521814540ffab" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-nonce", "solana-pubkey 4.2.0", @@ -8232,13 +8429,13 @@ dependencies = [ [[package]] name = "solana-faucet" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "crossbeam-channel", "log", "solana-cli-output", "solana-faucet", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -8258,7 +8455,7 @@ dependencies = [ [[package]] name = "solana-faucet-cli" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "clap 2.33.3", @@ -8281,7 +8478,7 @@ dependencies = [ "bincode", "serde", "serde_derive", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-instruction", "solana-program-error", @@ -8293,7 +8490,7 @@ dependencies = [ [[package]] name = "solana-fee" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "solana-fee-structure", @@ -8302,9 +8499,9 @@ dependencies = [ [[package]] name = "solana-fee-calculator" -version = "3.2.0" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e8add96b5741573e9f7529c4bb7719cfcfa999c3847a68cdfaef0cb6adf567" +checksum = "ef67f01cc6a0c72e99a08d0d484683f995de4c80e9568728fa77d1537f9b7e09" dependencies = [ "log", "serde", @@ -8338,9 +8535,9 @@ dependencies = [ [[package]] name = "solana-frozen-abi" -version = "3.4.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5b2792cc4cd4eaa419104dac4bed8e57b3189de001f9139b04f84b1a6f12d4a" +checksum = "038494fd91f75199a233ff12bc922b8e02da04bd6dd8fed26e710572733d39d5" dependencies = [ "bincode", "boxcar", @@ -8365,9 +8562,9 @@ dependencies = [ [[package]] name = "solana-frozen-abi-macro" -version = "3.4.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4efd54c3f198403955addf2160af0ccc30294a04cf34e155c775d69960e1f0f6" +checksum = "3ce2a1f17252391f44f620d4e47d1e3e0bec173f760a86ee7de2426db9ea7496" dependencies = [ "proc-macro2", "quote", @@ -8376,19 +8573,20 @@ dependencies = [ [[package]] name = "solana-genesis" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-logger", "agave-snapshots", "base64 0.22.1", "bincode", + "bs58", "clap 2.33.3", "itertools 0.14.0", "serde", "serde_json", "serde_yaml", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bls-signatures", "solana-borsh", "solana-clap-utils", @@ -8436,12 +8634,12 @@ dependencies = [ "memmap2 0.5.10", "serde", "serde_derive", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-clock", "solana-cluster-type", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-keypair", "solana-poh-config", @@ -8456,20 +8654,31 @@ dependencies = [ [[package]] name = "solana-genesis-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-snapshots", "log", "solana-download-utils", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-rpc-client", "thiserror 2.0.18", ] +[[package]] +name = "solana-get-sysvar" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef3bc859fc036ed490146793557386cbfae614ebba4adc704c37d94350824ed4" +dependencies = [ + "solana-address 2.6.1", + "solana-define-syscall 5.1.0", + "solana-program-error", +] + [[package]] name = "solana-geyser-plugin-manager" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-geyser-plugin-interface", "arc-swap", @@ -8481,16 +8690,15 @@ dependencies = [ "libloading", "log", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-accounts-db", "solana-clock", "solana-entry", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-ledger", "solana-measure", "solana-message", - "solana-metrics", "solana-pubkey 4.2.0", "solana-rpc", "solana-runtime", @@ -8503,19 +8711,20 @@ dependencies = [ [[package]] name = "solana-gossip" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "agave-random", "anyhow", "arc-swap", - "arrayvec", "assert_matches", "bincode", "bs58", "bv", + "bytes", "criterion", "crossbeam-channel", + "ed25519-dalek 2.2.0", "flate2", "indexmap 2.14.0", "itertools 0.14.0", @@ -8532,7 +8741,6 @@ dependencies = [ "serde-big-array", "serde_bytes", "serial_test", - "siphasher 1.0.2", "solana-bloom", "solana-client", "solana-clock", @@ -8541,7 +8749,7 @@ dependencies = [ "solana-frozen-abi", "solana-frozen-abi-macro", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-ledger", "solana-measure", @@ -8567,7 +8775,11 @@ dependencies = [ "solana-vote", "solana-vote-interface", "solana-vote-program", + "solana-wincode-varint", "static_assertions", + "strum", + "strum_macros", + "tempfile", "test-case", "thiserror 2.0.18", "wincode", @@ -8575,7 +8787,7 @@ dependencies = [ [[package]] name = "solana-gossip-cli" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "clap 2.33.3", @@ -8589,9 +8801,9 @@ dependencies = [ [[package]] name = "solana-hard-forks" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52fd9cc610fd0782f09482527cb7b4f41ec22071303742718b7b57fc43bb236b" +checksum = "45406eccad36220e52988b024d8daa93e691e38d5d71ad5fec55410cc9cf427d" dependencies = [ "serde", "serde_derive", @@ -8605,14 +8817,14 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "337c246447142f660f778cf6cb582beba8e28deb05b3b24bfb9ffd7c562e5f41" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] name = "solana-hash" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b113239362cee7093bfb250467138f079a2a03673181dc15bff6ccd677912d" +checksum = "fe51db00ac3aa9f950d1e6201a126acfa26e6d81bc4a183ba64ec02effcad883" dependencies = [ "borsh", "bytemuck", @@ -8635,9 +8847,9 @@ checksum = "7ce934b02bab639341f2fd62c3e7e3c39dcceb47f0196b7630ed1f82ecb704bd" [[package]] name = "solana-inflation" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f762559c5f962727efdcb03c61f5cf6c5364645695978fb145d25c88bbacdada" +checksum = "ccf104167e42e747602b88e02b25cacfc5de699c3b7cbba60d3250437e6a22ed" dependencies = [ "serde", "serde_derive", @@ -8663,9 +8875,9 @@ dependencies = [ [[package]] name = "solana-instruction-error" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b188842592fdf6cb96f55263ae1bf11713ab5114401d1d5a881ed7cc41bef6" +checksum = "3b7d34343838343a3755b7dfb1e438d94c6db2263b519cfe3c2257af932b6e93" dependencies = [ "num-traits", "serde", @@ -8673,21 +8885,37 @@ dependencies = [ "solana-frozen-abi", "solana-frozen-abi-macro", "solana-program-error", - "wincode", + "wincode", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0732294560e88ecdb2bbc656e67383e9f88c78ec09469cef172f0d28cd1bcd" +dependencies = [ + "bitflags 2.13.0", + "solana-account-info", + "solana-instruction", + "solana-instruction-error", + "solana-program-error", + "solana-sanitize", + "solana-sdk-ids", + "solana-serialize-utils", + "solana-sysvar-id", ] [[package]] name = "solana-instructions-sysvar" -version = "3.0.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddf67876c541aa1e21ee1acae35c95c6fbc61119814bfef70579317a5e26955" +checksum = "e38363a181313d607f7a118df2b401bb27b477a0104b09283cbf504f5f0f00cc" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "solana-account-info", "solana-instruction", "solana-instruction-error", "solana-program-error", - "solana-pubkey 3.0.0", "solana-sanitize", "solana-sdk-ids", "solana-serialize-utils", @@ -8702,14 +8930,15 @@ checksum = "ed1c0d16d6fdeba12291a1f068cdf0d479d9bff1141bf44afd7aa9d485f65ef8" dependencies = [ "sha3", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] name = "solana-keygen" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-votor-messages", + "bip39", "bs58", "clap 3.2.23", "dirs-next", @@ -8728,7 +8957,6 @@ dependencies = [ "solana-signer", "solana-version", "tempfile", - "tiny-bip39", ] [[package]] @@ -8742,7 +8970,7 @@ dependencies = [ "five8", "five8_core", "rand 0.9.4", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-derivation-path", "solana-seed-derivable", "solana-seed-phrase", @@ -8752,12 +8980,13 @@ dependencies = [ [[package]] name = "solana-last-restart-slot" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" +checksum = "c22474b83d3c7c318e1c3a725784fc2d1d03b728e36369e58ce48769a61ed85e" dependencies = [ "serde", "serde_derive", + "solana-get-sysvar", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -8765,7 +8994,7 @@ dependencies = [ [[package]] name = "solana-lattice-hash" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "blake3", @@ -8779,7 +9008,7 @@ dependencies = [ [[package]] name = "solana-leader-schedule" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-random", "itertools 0.14.0", @@ -8794,7 +9023,7 @@ dependencies = [ [[package]] name = "solana-ledger" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-logger", @@ -8803,8 +9032,7 @@ dependencies = [ "agave-votor-messages", "anyhow", "assert_matches", - "bincode", - "bitflags 2.11.1", + "bitflags 2.13.0", "bs58", "bytes", "bzip2", @@ -8824,7 +9052,7 @@ dependencies = [ "num_cpus", "num_enum", "proptest", - "prost 0.14.3", + "prost 0.14.4", "qualifier_attr", "rand 0.9.4", "rand_chacha 0.9.0", @@ -8833,7 +9061,7 @@ dependencies = [ "rocksdb", "scopeguard", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-accounts-db", "solana-address-lookup-table-interface", @@ -8846,7 +9074,7 @@ dependencies = [ "solana-frozen-abi-macro", "solana-genesis-config", "solana-genesis-utils", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-leader-schedule", @@ -8942,7 +9170,7 @@ dependencies = [ [[package]] name = "solana-local-cluster" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-logger", @@ -8958,7 +9186,7 @@ dependencies = [ "rand 0.9.4", "rayon", "serial_test", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-accounts-db", "solana-client", "solana-client-traits", @@ -8975,7 +9203,7 @@ dependencies = [ "solana-genesis-utils", "solana-gossip", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -8983,6 +9211,8 @@ dependencies = [ "solana-message", "solana-native-token", "solana-net-utils", + "solana-packet 4.1.0", + "solana-perf", "solana-poh-config", "solana-program-binaries", "solana-pubkey 4.2.0", @@ -9002,6 +9232,7 @@ dependencies = [ "solana-system-transaction", "solana-time-utils", "solana-tpu-client", + "solana-tpu-client-next", "solana-transaction", "solana-transaction-error", "solana-turbine", @@ -9012,35 +9243,38 @@ dependencies = [ "static_assertions", "strum", "tempfile", + "tokio", "tokio-util 0.7.18", + "wincode", ] [[package]] name = "solana-measure" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "solana-merkle-tree" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "fast-math", "hex", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", ] [[package]] name = "solana-message" -version = "4.1.1" +version = "4.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a634d1db65a393d4e87ec49ef97c8dfb12201e2ba9e5faf87ff34e632f29e509" +checksum = "d2666e0ff38557ad1239d0fc42f4688fb9df0817fb12c041dbae90c2b2e3fa55" dependencies = [ "blake3", - "lazy_static", "serde", "serde_derive", - "solana-address 2.6.0", - "solana-hash 4.3.0", + "solana-address 2.6.1", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-hash 4.4.0", "solana-instruction", "solana-sanitize", "solana-sdk-ids", @@ -9051,7 +9285,7 @@ dependencies = [ [[package]] name = "solana-metrics" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bencher", "crossbeam-channel", @@ -9085,13 +9319,15 @@ checksum = "ae8dd4c280dca9d046139eb5b7a5ac9ad10403fbd64964c7d7571214950d758f" [[package]] name = "solana-net-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", + "agave-xdp", "anyhow", "bincode", "bytes", "cfg-if 1.0.4", + "crossbeam-channel", "dashmap", "hxdmp", "itertools 0.14.0", @@ -9125,7 +9361,7 @@ dependencies = [ "serde", "serde_derive", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-sha256-hasher", "wincode", @@ -9137,8 +9373,8 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda945f4ccb317791c26379ca8ec410c5938aa7a5eff211fe5fe0bb428c3a75f" dependencies = [ - "solana-account 4.3.0", - "solana-hash 4.3.0", + "solana-account 4.3.1", + "solana-hash 4.4.0", "solana-nonce", "solana-sdk-ids", "wincode", @@ -9146,20 +9382,21 @@ dependencies = [ [[package]] name = "solana-notifier" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "log", "reqwest 0.12.28", "serde_json", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] name = "solana-nullable" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90429a75d69fdcb31952c3dea79f5f3c8157cfe88221e066103c9c237876073" +checksum = "a0f95d3028ef0f682bb174b77379c19d5dae2904a649f4a103fe29be7a139980" dependencies = [ + "borsh", "bytemuck", ] @@ -9185,7 +9422,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edf2f25743c95229ac0fdc32f8f5893ef738dbf332c669e9861d33ddb0f469d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -9195,7 +9432,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad62e1045c2347a0c0e219a6ceb0abfe904be622920996bfcac8d116fabe3c7" dependencies = [ "bincode", - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg_eval", "serde", "serde_derive", @@ -9207,7 +9444,7 @@ dependencies = [ [[package]] name = "solana-perf" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "agave-random", @@ -9230,13 +9467,14 @@ dependencies = [ "solana-clock", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-metrics", "solana-packet 4.1.0", "solana-perf", "solana-pubkey 4.2.0", + "solana-runtime-transaction", "solana-sdk-ids", "solana-short-vec", "solana-signature", @@ -9255,14 +9493,14 @@ dependencies = [ [[package]] name = "solana-poh" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-logger", "agave-votor-messages", "arc-swap", "assert_matches", "bincode", - "core_affinity", "criterion", "crossbeam-channel", "log", @@ -9271,7 +9509,7 @@ dependencies = [ "shuttle", "solana-clock", "solana-entry", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -9292,7 +9530,7 @@ dependencies = [ [[package]] name = "solana-poh-bench" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "clap 3.2.23", @@ -9366,10 +9604,10 @@ dependencies = [ "solana-epoch-stake", "solana-example-mocks", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", - "solana-instructions-sysvar", + "solana-instructions-sysvar 3.0.1", "solana-keccak-hasher", "solana-last-restart-slot", "solana-msg", @@ -9396,10 +9634,10 @@ dependencies = [ [[package]] name = "solana-program-binaries" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-loader-v3-interface", "solana-pubkey 4.2.0", "solana-rent", @@ -9456,7 +9694,7 @@ dependencies = [ [[package]] name = "solana-program-runtime" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "assert_matches", "base64 0.22.1", @@ -9468,7 +9706,7 @@ dependencies = [ "qualifier_attr", "rand 0.9.4", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-clock", "solana-epoch-rewards", @@ -9476,7 +9714,7 @@ dependencies = [ "solana-fee-structure", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-keypair", @@ -9507,11 +9745,12 @@ dependencies = [ "solana-transaction-context", "test-case", "thiserror 2.0.18", + "wincode", ] [[package]] name = "solana-program-test" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-logger", @@ -9522,10 +9761,10 @@ dependencies = [ "chrono-humanize", "log", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-accounts-db", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-banks-client", "solana-banks-interface", "solana-banks-server", @@ -9538,7 +9777,7 @@ dependencies = [ "solana-epoch-schedule", "solana-fee-calculator", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-loader-v3-interface", @@ -9569,6 +9808,7 @@ dependencies = [ "solana-transaction-error", "solana-vote-program", "spl-generic-token", + "spl-memo-interface", "test-case", "thiserror 2.0.18", "tokio", @@ -9590,12 +9830,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7db719574990de7e8b0f55a8593ac92a5ccb42c8ce67b3e4bf05b139d5d9ee71" dependencies = [ "rand 0.9.4", - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] name = "solana-pubsub-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "anyhow", "crossbeam-channel", @@ -9619,7 +9859,7 @@ dependencies = [ [[package]] name = "solana-quic-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "async-lock", @@ -9649,7 +9889,7 @@ dependencies = [ [[package]] name = "solana-rayon-threadlimit" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "log", "num_cpus", @@ -9658,17 +9898,20 @@ dependencies = [ [[package]] name = "solana-remote-wallet" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "assert_matches", "console 0.16.3", "dialoguer", + "hex", "hidapi", "log", "num-derive", "num-traits", "parking_lot 0.12.3", + "rusb", "semver 1.0.28", + "serde_json", "serial_test", "solana-derivation-path", "solana-offchain-message", @@ -9677,19 +9920,22 @@ dependencies = [ "solana-signer", "thiserror 2.0.18", "trezor-client", + "ur-parse-lib", + "ur-registry", "uriparse", ] [[package]] name = "solana-rent" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9809b081e99bc142ce803bcd7ee18306759ce3b30a96a9da3f6f41c45e50ef0" +checksum = "39f0d780bf8e8a1fe8b5b5fce1acad6b209485b86dec246e7523d5e4a8b7c7fc" dependencies = [ "serde", "serde_derive", "solana-frozen-abi", "solana-frozen-abi-macro", + "solana-get-sysvar", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -9708,11 +9954,12 @@ dependencies = [ [[package]] name = "solana-rpc" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-reserved-account-keys", "agave-snapshots", + "agave-votor-messages", "base64 0.22.1", "bs58", "crossbeam-channel", @@ -9731,7 +9978,7 @@ dependencies = [ "serde_json", "serial_test", "soketto", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-accounts-db", "solana-address-lookup-table-interface", @@ -9750,9 +9997,10 @@ dependencies = [ "solana-fee-structure", "solana-genesis-config", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-message", @@ -9811,8 +10059,9 @@ dependencies = [ [[package]] name = "solana-rpc-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-votor-messages", "assert_matches", "async-trait", "base64 0.22.1", @@ -9829,15 +10078,16 @@ dependencies = [ "semver 1.0.28", "serde", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-account-decoder-client-types", + "solana-bls-signatures", "solana-clock", "solana-commitment-config", "solana-epoch-info", "solana-epoch-schedule", "solana-feature-gate-interface", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -9845,6 +10095,7 @@ dependencies = [ "solana-rpc-client-api", "solana-signature", "solana-signer", + "solana-system-interface 3.2.0", "solana-system-transaction", "solana-transaction", "solana-transaction-error", @@ -9854,11 +10105,12 @@ dependencies = [ "static_assertions", "test-case", "tokio", + "wincode", ] [[package]] name = "solana-rpc-client-api" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "anyhow", "jsonrpc-core", @@ -9877,18 +10129,18 @@ dependencies = [ [[package]] name = "solana-rpc-client-nonce-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "anyhow", "clap 2.33.3", "futures 0.3.32", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-clap-utils", "solana-commitment-config", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-nonce", @@ -9906,7 +10158,7 @@ dependencies = [ [[package]] name = "solana-rpc-client-types" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "bs58", @@ -9915,7 +10167,7 @@ dependencies = [ "serde", "serde_json", "solana-account-decoder-client-types", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-clock", "solana-commitment-config", "solana-fee-calculator", @@ -9932,7 +10184,7 @@ dependencies = [ [[package]] name = "solana-rpc-test" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "async-trait", @@ -9948,7 +10200,7 @@ dependencies = [ "solana-clock", "solana-commitment-config", "solana-connection-cache", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-net-utils", "solana-pubkey 4.2.0", @@ -9969,7 +10221,7 @@ dependencies = [ [[package]] name = "solana-runtime" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-bls-cert-verify", "agave-feature-set", @@ -9991,6 +10243,7 @@ dependencies = [ "bytemuck", "criterion", "crossbeam-channel", + "crossbeam-utils", "dashmap", "imbl", "itertools 0.14.0", @@ -10001,7 +10254,6 @@ dependencies = [ "mockall", "num-derive", "num-traits", - "num_cpus", "percentage", "proptest", "qualifier_attr", @@ -10014,7 +10266,7 @@ dependencies = [ "serde_with", "shuttle", "smallvec", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-accounts-db", "solana-address-lookup-table-interface", @@ -10043,7 +10295,7 @@ dependencies = [ "solana-frozen-abi-macro", "solana-genesis-config", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-instruction", "solana-instruction-error", @@ -10103,7 +10355,6 @@ dependencies = [ "static_assertions", "strum", "strum_macros", - "symlink", "tempfile", "test-case", "thiserror 2.0.18", @@ -10113,7 +10364,7 @@ dependencies = [ [[package]] name = "solana-runtime-transaction" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-reserved-account-keys", @@ -10123,7 +10374,7 @@ dependencies = [ "solana-compute-budget", "solana-compute-budget-instruction", "solana-compute-budget-interface", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -10150,9 +10401,9 @@ checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" [[package]] name = "solana-sbpf" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b120fcbbc0d8562997183bef32c76a28400ecae7d8c9fff279d33002783a29" +checksum = "d777d7a89267dd133e985113c7e7f820fb7cfd9123a4a350cf8b39ebae1920bc" dependencies = [ "byteorder", "combine 3.8.1", @@ -10173,7 +10424,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] @@ -10244,23 +10495,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc905b200a95f2ea9146e43f2a7181e3aeb55de6bc12afb36462d00a3c7310de" dependencies = [ "hmac 0.12.1", - "pbkdf2 0.11.0", + "pbkdf2", "sha2 0.10.9", ] [[package]] name = "solana-send-transaction-service" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "async-trait", "crossbeam-channel", "itertools 0.14.0", "log", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-fee-calculator", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-metrics", @@ -10300,9 +10551,9 @@ dependencies = [ [[package]] name = "solana-serialize-utils" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cc401931d178472358e6b78dc72d031dc08f752d7410f0e8bd259dd6f02fa" +checksum = "761357b0853c9623bf12c1d2314b3d6160a85b087b84c45224fb85766d22616b" dependencies = [ "solana-instruction-error", "solana-pubkey 4.2.0", @@ -10317,7 +10568,7 @@ checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" dependencies = [ "sha2 0.10.9", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -10333,9 +10584,9 @@ dependencies = [ [[package]] name = "solana-short-vec" -version = "3.2.1" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb8cc883fc7b8ce4a7814cb1441b48c06437049ec11847005cf63bcfa85c546" +checksum = "7d8250a4495aad49ad20556a607da53bdcb20de78da10b65afbf918b7f1de647" dependencies = [ "serde_core", "solana-frozen-abi", @@ -10350,15 +10601,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6c79722e299d957958bf33695f7cd1ef6724ff55563c60fd9e3e24487cccde2" dependencies = [ "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", ] [[package]] name = "solana-signature" -version = "3.4.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a73c6e97cc2108be0adf6a6ea326434f8398df9d7eed81da2a4548b69e971c" +checksum = "b0364c7577c3c82a693ce28a1febc8d1b5d1b0a175fdc2114ae6186b69effe1e" dependencies = [ "ed25519-dalek 2.2.0", "five8", @@ -10374,11 +10625,11 @@ dependencies = [ [[package]] name = "solana-signer" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bfea97951fee8bae0d6038f39a5efcb6230ecdfe33425ac75196d1a1e3e3235" +checksum = "520bd6021163ee517f4bdc7ae03ded904f97e11320001ba0b3355f45eb14f558" dependencies = [ - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", "solana-signature", "solana-transaction-error", ] @@ -10396,15 +10647,17 @@ dependencies = [ [[package]] name = "solana-slot-hashes" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2585f70191623887329dfb5078da3a00e15e3980ea67f42c2e10b07028419f43" +checksum = "5c7ce2b4b8911bf2db3de7b6266e67bfc21a6a9f8c566fb096d9782ca2ad16ee" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-get-sysvar", + "solana-hash 4.4.0", "solana-sdk-ids", "solana-sysvar-id", + "wincode", ] [[package]] @@ -10418,6 +10671,7 @@ dependencies = [ "serde_derive", "solana-sdk-ids", "solana-sysvar-id", + "wincode", ] [[package]] @@ -10432,10 +10686,10 @@ dependencies = [ [[package]] name = "solana-stake-accounts" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "clap 2.33.3", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-clap-utils", "solana-cli-config", "solana-cli-output", @@ -10464,9 +10718,9 @@ dependencies = [ [[package]] name = "solana-stake-interface" -version = "3.1.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49eb5c77484214c3484921e2cdda79d185373118b5458c1b2df0f1a04c3bc30" +checksum = "252b4291eb25a5a356149ed4d4a940d5f655186210fa84b5d0d8d55c3dd42a81" dependencies = [ "borsh", "num-traits", @@ -10482,11 +10736,12 @@ dependencies = [ "solana-system-interface 3.2.0", "solana-sysvar", "solana-sysvar-id", + "wincode", ] [[package]] name = "solana-storage-bigtable" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-reserved-account-keys", "base64 0.22.1", @@ -10495,15 +10750,17 @@ dependencies = [ "flate2", "futures 0.3.32", "goauth", - "http 1.4.0", + "http 1.4.2", "hyper-util", "log", - "prost 0.14.3", - "prost-types", + "prost 0.14.4", + "prost-types 0.14.4", "serde", "smpl_jwt", "solana-clock", - "solana-hash 4.3.0", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-metrics", @@ -10526,15 +10783,15 @@ dependencies = [ [[package]] name = "solana-storage-proto" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bs58", "enum-iterator", - "prost 0.14.3", + "prost 0.14.4", "protobuf-src", "serde", "solana-account-decoder", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-message", "solana-pubkey 4.2.0", @@ -10552,7 +10809,7 @@ dependencies = [ [[package]] name = "solana-streamer" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "agave-xdp", @@ -10571,12 +10828,12 @@ dependencies = [ "nix", "num_cpus", "pem", - "percentage", "quinn", "rand 0.9.4", "rustls", "smallvec", "solana-keypair", + "solana-measure", "solana-message", "solana-metrics", "solana-net-utils", @@ -10595,8 +10852,10 @@ dependencies = [ [[package]] name = "solana-svm" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-feature-set", + "agave-precompiles", "ahash 0.8.12", "assert_matches", "bincode", @@ -10605,11 +10864,13 @@ dependencies = [ "log", "openssl", "percentage", + "prost 0.11.9", + "protosol", "qualifier_attr", "rand 0.9.4", "serde", "shuttle", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bpf-loader-program", "solana-clock", "solana-compute-budget", @@ -10621,9 +10882,10 @@ dependencies = [ "solana-fee-structure", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", - "solana-instructions-sysvar", + "solana-instruction-error", + "solana-instructions-sysvar 4.0.0", "solana-keypair", "solana-loader-v3-interface", "solana-loader-v4-interface", @@ -10631,6 +10893,7 @@ dependencies = [ "solana-native-token", "solana-nonce", "solana-nonce-account", + "solana-poseidon", "solana-precompile-error", "solana-program-binaries", "solana-program-entrypoint", @@ -10661,17 +10924,20 @@ dependencies = [ "solana-transaction", "solana-transaction-context", "solana-transaction-error", + "solana-vote-program", + "solana-zk-elgamal-proof-program", "spl-generic-token", "spl-token-interface", "test-case", "thiserror 2.0.18", + "xxhash-rust", ] [[package]] name = "solana-svm-callback" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-clock", "solana-precompile-error", "solana-pubkey 4.2.0", @@ -10679,22 +10945,22 @@ dependencies = [ [[package]] name = "solana-svm-feature-set" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "solana-svm-log-collector" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "log", ] [[package]] name = "solana-svm-measure" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "solana-svm-timings" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "eager", "enum-iterator", @@ -10703,23 +10969,21 @@ dependencies = [ [[package]] name = "solana-svm-transaction" -version = "4.2.0-alpha.0" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736d2cec944c6f8f298a7bd8ada5eb318cca9ab859281b1300842fa0e9a25afa" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", - "solana-nonce", "solana-pubkey 4.2.0", "solana-sdk-ids", "solana-signature", - "solana-system-interface 3.2.0", "solana-transaction", - "static_assertions", - "test-case", ] [[package]] name = "solana-svm-type-overrides" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "futures 0.3.32", "rand 0.9.4", @@ -10728,24 +10992,24 @@ dependencies = [ [[package]] name = "solana-syscalls" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "assert_matches", "bincode", "libsecp256k1", "num-traits", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-blake3-hasher", "solana-bls12-381-syscall", "solana-bn254", "solana-clock", "solana-cpi", - "solana-curve25519 4.0.0", + "solana-curve25519", "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-hash-512", "solana-instruction", "solana-keccak-hasher", @@ -10772,6 +11036,7 @@ dependencies = [ "solana-transaction-context", "test-case", "thiserror 2.0.18", + "wincode", ] [[package]] @@ -10798,7 +11063,7 @@ dependencies = [ "num-traits", "serde", "serde_derive", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-instruction", "solana-msg", "solana-program-error", @@ -10807,17 +11072,17 @@ dependencies = [ [[package]] name = "solana-system-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "assert_matches", "bincode", "criterion", "log", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bincode", "solana-compute-budget", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-nonce", "solana-nonce-account", @@ -10842,7 +11107,7 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a39522012be4127884bc611d65f58f1cf33a3afb7baee7e6774be90b48edc3c" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-pubkey 4.2.0", @@ -10872,7 +11137,7 @@ dependencies = [ "solana-fee-calculator", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-last-restart-slot", "solana-program-entrypoint", @@ -10893,24 +11158,26 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-sdk-ids", ] [[package]] name = "solana-test-validator" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-snapshots", + "agave-votor-messages", "arc-swap", "base64 0.22.1", "bincode", "crossbeam-channel", "log", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-accounts-db", + "solana-bls-signatures", "solana-cli-output", "solana-clock", "solana-cluster-type", @@ -10945,8 +11212,10 @@ dependencies = [ "solana-signer", "solana-streamer", "solana-syscalls", + "solana-test-validator", "solana-transaction", "solana-validator-exit", + "solana-vote-interface", "tokio", ] @@ -10958,8 +11227,9 @@ checksum = "0ced92c60aa76ec4780a9d93f3bd64dfa916e1b998eacc6f1c110f3f444f02c9" [[package]] name = "solana-tls-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "quinn", "rustls", "solana-keypair", "solana-pubkey 4.2.0", @@ -10969,7 +11239,7 @@ dependencies = [ [[package]] name = "solana-tokens" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "assert_matches", @@ -10991,7 +11261,7 @@ dependencies = [ "solana-cli-output", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -11018,40 +11288,9 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "solana-tps-client" -version = "4.2.0-alpha.0" -dependencies = [ - "log", - "solana-account 4.3.0", - "solana-client", - "solana-client-traits", - "solana-clock", - "solana-commitment-config", - "solana-connection-cache", - "solana-epoch-info", - "solana-hash 4.3.0", - "solana-keypair", - "solana-message", - "solana-net-utils", - "solana-pubkey 4.2.0", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-runtime", - "solana-signature", - "solana-signer", - "solana-streamer", - "solana-tpu-client", - "solana-transaction", - "solana-transaction-error", - "solana-transaction-status", - "tempfile", - "thiserror 2.0.18", -] - [[package]] name = "solana-tpu-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "futures-util", "indicatif", @@ -11062,6 +11301,7 @@ dependencies = [ "solana-commitment-config", "solana-connection-cache", "solana-epoch-schedule", + "solana-leader-schedule", "solana-message", "solana-pubkey 4.2.0", "solana-pubsub-client", @@ -11078,7 +11318,7 @@ dependencies = [ [[package]] name = "solana-tpu-client-next" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-trait", "crossbeam-channel", @@ -11093,6 +11333,7 @@ dependencies = [ "solana-clock", "solana-commitment-config", "solana-keypair", + "solana-leader-schedule", "solana-measure", "solana-metrics", "solana-net-utils", @@ -11115,14 +11356,16 @@ dependencies = [ [[package]] name = "solana-transaction" -version = "4.1.1" +version = "4.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a98253cefd62dea714b67926d3d918c3b9f7d66993f1f2322ac7549ea991dc3e" +checksum = "2509e70bdce879db3e0f56cf97e40edd53742e8f0e6f34d64c46e7900071b53f" dependencies = [ "serde", "serde_derive", - "solana-address 2.6.0", - "solana-hash 4.3.0", + "solana-address 2.6.1", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-message", @@ -11137,15 +11380,15 @@ dependencies = [ [[package]] name = "solana-transaction-context" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", "qualifier_attr", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-instruction", - "solana-instructions-sysvar", + "solana-instructions-sysvar 4.0.0", "solana-program-entrypoint", "solana-pubkey 4.2.0", "solana-rent", @@ -11160,9 +11403,9 @@ dependencies = [ [[package]] name = "solana-transaction-error" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a2165ad25b694c654d5395fc7a049452a192376e4c96a7fad05580f6ba5ba1c" +checksum = "a949797bc1ac31836d0070e791083028a8c2e0d493fa4cf51c0bed7a04c65c22" dependencies = [ "serde", "serde_derive", @@ -11175,7 +11418,7 @@ dependencies = [ [[package]] name = "solana-transaction-status" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "Inflector", "agave-reserved-account-keys", @@ -11190,7 +11433,7 @@ dependencies = [ "solana-account-decoder", "solana-address-lookup-table-interface", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-loader-v2-interface", "solana-loader-v3-interface", @@ -11207,6 +11450,7 @@ dependencies = [ "solana-transaction-status", "solana-transaction-status-client-types", "solana-vote-interface", + "solana-zk-sdk-pod", "spl-associated-token-account-interface", "spl-memo-interface", "spl-token-2022-interface", @@ -11215,11 +11459,12 @@ dependencies = [ "spl-token-interface", "spl-token-metadata-interface", "thiserror 2.0.18", + "wincode", ] [[package]] name = "solana-transaction-status-client-types" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "bincode", @@ -11242,13 +11487,12 @@ dependencies = [ [[package]] name = "solana-turbine" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-logger", "agave-votor", "agave-votor-messages", - "agave-xdp", "assert_matches", "bencher", "bs58", @@ -11267,7 +11511,7 @@ dependencies = [ "solana-entry", "solana-genesis-config", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-leader-schedule", "solana-ledger", @@ -11297,7 +11541,7 @@ dependencies = [ [[package]] name = "solana-udp-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-trait", "solana-connection-cache", @@ -11310,11 +11554,11 @@ dependencies = [ [[package]] name = "solana-unified-scheduler-logic" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "assert_matches", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-message", "solana-pubkey 4.2.0", @@ -11327,7 +11571,7 @@ dependencies = [ [[package]] name = "solana-unified-scheduler-pool" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-banking-stage-ingress-types", "agave-logger", @@ -11342,7 +11586,7 @@ dependencies = [ "solana-clock", "solana-cost-model", "solana-entry", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-ledger", "solana-metrics", @@ -11370,7 +11614,7 @@ checksum = "c5d2face763df5afeaa9509b9019968860e69cc1531ec8b4a2e6c7b702204d5a" [[package]] name = "solana-version" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "bincode", @@ -11381,11 +11625,13 @@ dependencies = [ "solana-frozen-abi-macro", "solana-sanitize", "solana-serde-varint", + "solana-wincode-varint", + "wincode", ] [[package]] name = "solana-vote" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "arbitrary", @@ -11395,13 +11641,13 @@ dependencies = [ "qualifier_attr", "rand 0.9.4", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bincode", "solana-bls-signatures", "solana-clock", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-packet 4.1.0", @@ -11422,22 +11668,23 @@ dependencies = [ [[package]] name = "solana-vote-interface" -version = "6.0.0" +version = "6.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d495cb001d3373229ba523625783e5168707f70a9c38ddc90fbc5b0470d600ea" +checksum = "61843d7be827cac5e025c3a16c1101a34fcdd8cf593f6a82eafdd253bd55a26b" dependencies = [ "arbitrary", "bincode", "cfg_eval", "num-derive", "num-traits", + "rand 0.9.4", "serde", "serde_derive", "serde_with", "solana-clock", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-pubkey 4.2.0", @@ -11451,7 +11698,7 @@ dependencies = [ [[package]] name = "solana-vote-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-logger", @@ -11460,7 +11707,7 @@ dependencies = [ "criterion", "log", "qualifier_attr", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bincode", "solana-bls-signatures", "solana-clock", @@ -11468,7 +11715,7 @@ dependencies = [ "solana-fee-calculator", "solana-frozen-abi", "solana-frozen-abi-macro", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-packet 4.1.0", "solana-program-runtime", @@ -11487,19 +11734,43 @@ dependencies = [ ] [[package]] -name = "solana-zero-copy" +name = "solana-wincode-varint" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f52dd8f733a13f6a18e55de83cf97c4c3f5fdf27ea3830bcff0b35313efcc2" +checksum = "6679e72a01dcfe7ff180f0e9d3a0d165e4e9664426930014f2702e7571c259c7" +dependencies = [ + "wincode", +] + +[[package]] +name = "solana-zero-copy" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea15126ebdc7e270c50d43884369af9f51d2308156d46a18e351522a164844d" +dependencies = [ + "bytemuck", + "bytemuck_derive", +] + +[[package]] +name = "solana-zk-elgamal-proof-interface" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8da7f01db2148a1dc16261ff1dc6f3930a1e255a33cece4f1b56658694f27f7" dependencies = [ - "borsh", "bytemuck", "bytemuck_derive", + "num-derive", + "num-traits", + "solana-address 2.6.1", + "solana-instruction", + "solana-sdk-ids", + "solana-zk-sdk-pod", ] [[package]] name = "solana-zk-elgamal-proof-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bytemuck", "criterion", @@ -11513,10 +11784,10 @@ dependencies = [ [[package]] name = "solana-zk-elgamal-proof-program-tests" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bytemuck", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-compute-budget", "solana-compute-budget-interface", "solana-instruction", @@ -11588,7 +11859,7 @@ dependencies = [ "serde_derive", "serde_json", "sha3", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-derivation-path", "solana-instruction", "solana-sdk-ids", @@ -11601,9 +11872,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "solana-zk-sdk-pod" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a800583b7a4cea3e851686af162cc6e4712eef97fa91dfa9aca2459b95c84777" +dependencies = [ + "base64 0.22.1", + "bytemuck", + "bytemuck_derive", + "solana-nullable", + "thiserror 2.0.18", +] + [[package]] name = "solana-zk-token-proof-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-program-runtime", ] @@ -11637,9 +11921,9 @@ dependencies = [ [[package]] name = "spl-discriminator" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48cc11459e265d5b501534144266620289720b4c44522a47bc6b63cd295d2f3" +checksum = "e597c5ff9ed7c74a54dbc47bae2f06e4db8c98f4356ad280200dc11878266db1" dependencies = [ "bytemuck", "solana-program-error", @@ -11683,12 +11967,12 @@ dependencies = [ [[package]] name = "spl-memo-interface" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4e2aedd58f858337fa609af5ad7100d4a243fdaf6a40d6eb4c28c5f19505d3" +checksum = "3745d384b0afee980d43d62b66c27bdcbbd03507732b8d3626d3413cb72084f2" dependencies = [ "solana-instruction", - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", ] [[package]] @@ -11697,7 +11981,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f9c6e142cdf1e7e77f480053ec9f0ce989890768ddf91f619b50f39d1b456f5" dependencies = [ - "borsh", "bytemuck", "bytemuck_derive", "num-derive", @@ -11713,26 +11996,28 @@ dependencies = [ [[package]] name = "spl-token-2022-interface" -version = "2.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fcd81188211f4b3c8a5eba7fd534c7142f9dd026123b3472492782cc72f4dc6" +checksum = "821d96d034ea31c4965d182c742153c491ae0abee531331b55771086c5030d86" dependencies = [ "arrayref", "bytemuck", + "getrandom 0.2.15", "num-derive", "num-traits", "num_enum", "solana-account-info", + "solana-address 2.6.1", "solana-instruction", + "solana-nullable", "solana-program-error", "solana-program-option", "solana-program-pack", - "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-zk-sdk 4.0.0", - "spl-pod", + "solana-zero-copy", + "solana-zk-elgamal-proof-interface", + "solana-zk-sdk-pod", "spl-token-confidential-transfer-proof-extraction", - "spl-token-confidential-transfer-proof-generation", "spl-token-group-interface", "spl-token-metadata-interface", "spl-type-length-value", @@ -11741,32 +12026,21 @@ dependencies = [ [[package]] name = "spl-token-confidential-transfer-proof-extraction" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879a9ebad0d77383d3ea71e7de50503554961ff0f4ef6cbca39ad126e6f6da3a" +checksum = "0c1a845ec724e807643a04f1d43439ee3571dd913848112ac76aa90b850eaf7c" dependencies = [ "bytemuck", "solana-account-info", - "solana-curve25519 3.1.4", + "solana-address 2.6.1", + "solana-curve25519", "solana-instruction", - "solana-instructions-sysvar", + "solana-instructions-sysvar 3.0.1", "solana-msg", "solana-program-error", - "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-zk-sdk 4.0.0", - "spl-pod", - "thiserror 2.0.18", -] - -[[package]] -name = "spl-token-confidential-transfer-proof-generation" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a2b41095945dc15274b924b21ccae9b3ec9dc2fdd43dbc08de8c33bbcd915" -dependencies = [ - "curve25519-dalek 4.1.3", - "solana-zk-sdk 4.0.0", + "solana-zk-elgamal-proof-interface", + "solana-zk-sdk-pod", "thiserror 2.0.18", ] @@ -11780,7 +12054,7 @@ dependencies = [ "num-derive", "num-traits", "num_enum", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-instruction", "solana-nullable", "solana-program-error", @@ -11791,9 +12065,9 @@ dependencies = [ [[package]] name = "spl-token-interface" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c564ac05a7c8d8b12e988a37d82695b5ba4db376d07ea98bc4882c81f96c7f3" +checksum = "7143c4e676984047a400e2fbd7ac29a1659f2b1b52b897cfde19316b562e4589" dependencies = [ "arrayref", "bytemuck", @@ -11811,38 +12085,38 @@ dependencies = [ [[package]] name = "spl-token-metadata-interface" -version = "0.8.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c467c7c3bd056f8fe60119e7ec34ddd6f23052c2fa8f1f51999098063b72676" +checksum = "3d3d96f175e7022ff200464dfa75a3708a4e9b70c83c4ecd04fe52ee479f4fef" dependencies = [ "borsh", "num-derive", "num-traits", + "num_enum", + "solana-address 2.6.1", "solana-borsh", "solana-instruction", + "solana-nullable", "solana-program-error", - "solana-pubkey 3.0.0", "spl-discriminator", - "spl-pod", "spl-type-length-value", "thiserror 2.0.18", ] [[package]] name = "spl-type-length-value" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca20a1a19f4507a98ca4b28ff5ed54cac9b9d34ed27863e2bde50a3238f9a6ac" +checksum = "2504631748c48d2a937414d64a12dcac4588d34bd07d355d648619c189d29435" dependencies = [ "bytemuck", "num-derive", "num-traits", "num_enum", "solana-account-info", - "solana-msg", "solana-program-error", + "solana-zero-copy", "spl-discriminator", - "spl-pod", "thiserror 2.0.18", ] @@ -11902,7 +12176,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -11978,7 +12252,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cca424247104946a59dacd27eaad296223b7feec3d168a6dd04585183091eb0b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "byteorder", "enum-as-inner", "libc", @@ -11986,20 +12260,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "systemstat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e89b75de097d0c52a1dc2114e19439d55f0e2e42d32168c6df44f139dfb66f" -dependencies = [ - "bytesize", - "lazy_static", - "libc", - "nom", - "time", - "winapi", -] - [[package]] name = "tap" version = "1.0.1" @@ -12239,23 +12499,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-bip39" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30fd743a02bf35236f6faf99adb03089bb77e91c998dac2c2ad76bb424f668c" -dependencies = [ - "once_cell", - "pbkdf2 0.12.2", - "rand 0.8.6", - "rustc-hash 1.1.0", - "sha2 0.10.9", - "thiserror 1.0.69", - "unicode-normalization", - "wasm-bindgen", - "zeroize", -] - [[package]] name = "tinystr" version = "0.7.6" @@ -12449,7 +12692,7 @@ dependencies = [ "base64 0.22.1", "bytes", "h2", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -12474,7 +12717,7 @@ version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" dependencies = [ - "prettyplease", + "prettyplease 0.2.37", "proc-macro2", "quote", "syn 2.0.117", @@ -12487,7 +12730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", - "prost 0.14.3", + "prost 0.14.4", "tonic", ] @@ -12497,10 +12740,10 @@ version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" dependencies = [ - "prettyplease", + "prettyplease 0.2.37", "proc-macro2", - "prost-build", - "prost-types", + "prost-build 0.14.4", + "prost-types 0.14.4", "quote", "syn 2.0.117", "tempfile", @@ -12533,11 +12776,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "iri-string", @@ -12651,7 +12894,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http 1.4.2", "httparse", "log", "rand 0.9.4", @@ -12778,6 +13021,37 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "461d0c5956fcc728ecc03a3a961e4adc9a7975d86f6f8371389a289517c02ca9" +[[package]] +name = "ur-parse-lib" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f88434c87d748dfb765bc5debbbab61d020392d23b568fe73eb44eaca46daa04" +dependencies = [ + "hex", + "keystone-ur", + "ur-registry", +] + +[[package]] +name = "ur-registry" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf91aff789fea1c05116a47b11fbe0bea32ef66e0c1f77b46550e1407efc4c6e" +dependencies = [ + "bs58", + "hex", + "keystone-ur", + "libflate", + "minicbor", + "no_std_io2", + "paste", + "prost 0.11.9", + "prost-build 0.11.9", + "prost-types 0.11.9", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "uriparse" version = "0.6.4" @@ -13014,6 +13288,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.39", +] + [[package]] name = "wide" version = "0.7.33" @@ -13057,10 +13343,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "wincode" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37095eb18dd6254c66217edc61a29d83d51f8818de8a2ffe88e4584ad73fb5f9" +checksum = "66d967db7705dc29120bb6e8ce5b5a2e27734ed5976d1c904e95bd238d1c3c5a" dependencies = [ + "bv", "bytes", "pastey", "proc-macro2", @@ -13071,11 +13358,11 @@ dependencies = [ [[package]] name = "wincode-derive" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e262d55d1261f31e2cfe49cc6385a421d14d99faa0526bbe3cc1bda0d3005c62" +checksum = "15ab90b719560d0fda79c74550ad1c948d17b118765942838055ebaf34d67071" dependencies = [ - "darling 0.21.3", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -13404,6 +13691,12 @@ dependencies = [ "rustix 0.38.39", ] +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index cd6cbc8c322..861b9bb68b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "banks-server", "bloom", "bls-cert-verify", + "bls-sigverify", "bucket_map", "builtins", "builtins-default-costs", @@ -26,6 +27,7 @@ members = [ "connection-cache", "core", "cost-model", + "cpu-utils", "download-utils", "entry", "faucet", @@ -100,14 +102,12 @@ members = [ "svm-log-collector", "svm-measure", "svm-timings", - "svm-transaction", "svm-type-overrides", "syscalls", "syscalls/gen-syscall-list", "test-validator", "tls-utils", "tokens", - "tps-client", "tpu-client", "tpu-client-next", "transaction-context", @@ -138,7 +138,7 @@ exclude = [ resolver = "2" [workspace.package] -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" authors = ["Anza Maintainers "] description = "Blockchain, Rebuilt for Scale" repository = "https://github.com/anza-xyz/agave" @@ -156,70 +156,79 @@ check-cfg = [ # Clippy lint configuration that can not be applied in clippy.toml [workspace.lints.clippy] +# Denied lints arithmetic_side_effects = "deny" default_trait_access = "deny" manual_let_else = "deny" +uninlined_format_args = "deny" used_underscore_binding = "deny" +# Allowed lints +new_without_default = "allow" + [workspace.dependencies] Inflector = "0.11.4" -agave-banking-stage-ingress-types = { path = "banking-stage-ingress-types", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-bls-cert-verify = { path = "bls-cert-verify", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-feature-set = { path = "feature-set", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-fs = { path = "fs", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-geyser-plugin-interface = { path = "geyser-plugin-interface", version = "=4.2.0-alpha.0" } -agave-io-uring = { path = "io-uring", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-logger = { path = "logger", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-math-utils = { path = "math-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-precompiles = { path = "precompiles", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-random = { path = "random", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-reserved-account-keys = { path = "reserved-account-keys", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-scheduler-bindings = { path = "scheduler-bindings", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-scheduling-utils = { path = "scheduling-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-snapshots = { path = "snapshots", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-transaction-view = { path = "transaction-view", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-votor = { path = "votor", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-votor-messages = { path = "votor-messages", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-xdp = { path = "xdp", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-xdp-ebpf = { path = "xdp-ebpf", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +agave-banking-stage-ingress-types = { path = "banking-stage-ingress-types", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-bls-cert-verify = { path = "bls-cert-verify", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-bls-sigverify = { path = "bls-sigverify", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-cpu-utils = { path = "cpu-utils", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-feature-set = { path = "feature-set", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-fs = { path = "fs", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-geyser-plugin-interface = { path = "geyser-plugin-interface", version = "=4.3.0-alpha.0" } +agave-io-uring = { path = "io-uring", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-logger = { path = "logger", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-math-utils = { path = "math-utils", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-precompiles = { path = "precompiles", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-random = { path = "random", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-reserved-account-keys = { path = "reserved-account-keys", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-scheduler-bindings = { path = "scheduler-bindings", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-scheduling-utils = { path = "scheduling-utils", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-snapshots = { path = "snapshots", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-transaction-view = { path = "transaction-view", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-votor = { path = "votor", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-votor-messages = { path = "votor-messages", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-xdp = { path = "xdp", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-xdp-ebpf = { path = "xdp-ebpf", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } ahash = "0.8.12" anyhow = "1.0.102" aquamarine = "0.6.0" arbitrary = "1.4.2" arc-swap = "1.9.1" arrayref = "0.3.9" -arrayvec = "0.7.6" +arrayvec = "0.7.7" assert_cmd = "2.2.2" assert_matches = "1.5.0" async-lock = "3.4.2" async-trait = "0.1.89" -aya = "0.13" +aya = "0.14" aya-ebpf = "0.1.1" base64 = "0.22.1" bencher = "0.1.5" bincode = "1.3.3" -bitflags = { version = "2.11.1" } -bitvec = { version = "1.0.1", features = ["serde"] } +bip39 = { version = "2.2.2", features = ["rand", "all-languages"] } +bitflags = { version = "2.13.0" } +bitvec = { version = "1.1.1", features = ["serde"] } blake3 = "1.8.5" blst = "0.3.16" blstrs = "0.7.1" -borsh = { version = "1.6.1", features = ["derive", "unstable__schema"] } +borsh = { version = "1.7.0", features = ["derive", "unstable__schema"] } bs58 = { version = "0.5.1", default-features = false } bv = "0.11.1" bytemuck = "1.25.0" bytemuck_derive = "1.10.2" -bytes = "1.11.1" +bytes = "1.12.0" +bytesize = "2.4.0" bzip2 = "0.6.1" caps = "0.5.6" cfg-if = "1.0.4" -chrono = { version = "0.4.44", default-features = false } +chrono = { version = "0.4.45", default-features = false } chrono-humanize = "0.2.3" clap = { version = "2.33.1", default-features = false, features = ["suggestions"] } console = "0.16.3" const_format = "0.2.36" -core_affinity = "0.8.3" criterion = "0.8.2" crossbeam-channel = "0.5.15" +crossbeam-utils = "0.8.21" csv = "1.4.0" ctrlc = "3.5.2" curve25519-dalek = { version = "4.1.3", features = ["digest", "rand_core"] } @@ -229,10 +238,9 @@ derive_more = { version = "2.1.1", features = ["full"] } dialoguer = "0.12.0" digest = "0.10.7" dirs-next = "2.0.0" -dlopen2 = "0.8.2" dyn-clone = "1.0.20" eager = "0.1.0" -ed25519-dalek = "=1.0.1" +ed25519-dalek = "2.2" enum-iterator = "2.3.0" env_logger = "0.11.10" fast-math = "0.1" @@ -251,7 +259,7 @@ hex = "0.4.3" hex-literal = "1.1.0" hidapi = { version = "2.6.6", default-features = false } histogram = "0.6.9" -http = "1.4.0" +http = "1.4.2" humantime = "2.3.0" hyper-util = "0.1.20" imbl = "7.0.0" @@ -280,10 +288,10 @@ libsecp256k1 = { version = "0.7.2", default-features = false, features = [ "std", "static-context", ] } -log = "0.4.29" +log = "0.4.33" lru = "0.18.0" lz4 = "1.28.1" -memmap2 = "0.9.10" +memmap2 = "0.9.11" memoffset = "0.9.1" min-max-heap = "1.3.0" mockall = "0.14.0" @@ -303,233 +311,235 @@ predicates = "3.1" pretty-hex = "0.4.2" pretty_assertions = "1.4.1" proptest = "1.11" -prost = "0.14.3" -prost-types = "0.14.3" +prost = "0.14.4" +prost-types = "0.14.4" protobuf-src = "1.1.0" -protosol = "=6.1.0" +protosol = "=9.0.1" qualifier_attr = { version = "0.2.2", default-features = false } -quinn = "0.11.9" +quinn = "0.11.11" rand = "0.9.4" rand_chacha = "0.9.0" rayon = "1.12.0" reed-solomon-erasure = "6.0.0" -regex = "1.12.3" +regex = "1.12.4" reqwest = { version = "0.12.28", default-features = false } reqwest-middleware = "0.4.2" rolling-file = "0.2.0" rpassword = "7.5" rts-alloc = { version = "4.0.0" } -rustls = { version = "0.23.40", features = ["std"], default-features = false } +rusb = "0.9" +rustls = { version = "0.23.41", features = ["std"], default-features = false } scopeguard = "1.2.0" semver = "1.0.28" seqlock = "0.2.0" serde = { version = "1.0.228", features = ["derive"] } serde-big-array = "0.5.1" serde_bytes = "0.11.19" -serde_json = "1.0.149" -serde_with = { version = "3.20.0", default-features = false } +serde_json = "1.0.150" +serde_with = { version = "3.21.0", default-features = false } serde_yaml = "0.9.34" -serial_test = "3.4.0" +serial_test = "3.5.0" shaq = { version = "3.0.0" } shuttle = "0.7.1" signal-hook = "0.4.4" -siphasher = "1.0.2" slab = "0.4.12" slotmap = "1.0.7" -smallvec = { version = "1.15.1", default-features = false, features = ["union"] } +smallvec = { version = "1.15.2", default-features = false, features = ["union"] } smpl_jwt = "0.9.0" socket2 = "0.6.3" soketto = "0.8" -solana-account = "4.3.0" -solana-account-decoder = { path = "account-decoder", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-account-decoder-client-types = { path = "account-decoder-client-types", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-account = "4.3.1" +solana-account-decoder = { path = "account-decoder", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-account-decoder-client-types = { path = "account-decoder-client-types", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-account-info = "3.1.1" -solana-accounts-db = { path = "accounts-db", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-address = "2.6.0" +solana-accounts-db = { path = "accounts-db", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-address = "2.6.1" solana-address-lookup-table-interface = "3.1.0" -solana-banks-client = { path = "banks-client", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-banks-interface = { path = "banks-interface", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-banks-server = { path = "banks-server", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-banks-client = { path = "banks-client", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-banks-interface = { path = "banks-interface", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-banks-server = { path = "banks-server", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-bincode = "3.1.0" solana-blake3-hasher = "3.1.0" -solana-bloom = { path = "bloom", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-bloom = { path = "bloom", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-bls-signatures = { version = "3.3.0", features = ["serde"] } solana-bls12-381-syscall = "0.1.0" solana-bn254 = "3.2.1" solana-borsh = "3.0.2" -solana-bpf-loader-program = { path = "programs/bpf_loader", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } -solana-bucket-map = { path = "bucket_map", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-builtins = { path = "builtins", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-builtins-default-costs = { path = "builtins-default-costs", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-clap-utils = { path = "clap-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-clap-v3-utils = { path = "clap-v3-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-cli-config = { path = "cli-config", version = "=4.2.0-alpha.0" } -solana-cli-output = { path = "cli-output", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-client = { path = "client", version = "=4.2.0-alpha.0" } +solana-bpf-loader-program = { path = "programs/bpf_loader", version = "=4.3.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } +solana-bucket-map = { path = "bucket_map", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-builtins = { path = "builtins", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-builtins-default-costs = { path = "builtins-default-costs", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-clap-utils = { path = "clap-utils", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-clap-v3-utils = { path = "clap-v3-utils", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-cli-config = { path = "cli-config", version = "=4.3.0-alpha.0" } +solana-cli-output = { path = "cli-output", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-client = { path = "client", version = "=4.3.0-alpha.0" } solana-client-traits = "4.0.0" solana-clock = "3.0.1" solana-cluster-type = "3.1.0" solana-commitment-config = "3.1.1" -solana-compute-budget = { path = "compute-budget", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-compute-budget-instruction = { path = "compute-budget-instruction", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-compute-budget = { path = "compute-budget", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-compute-budget-instruction = { path = "compute-budget-instruction", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-compute-budget-interface = "3.0.0" -solana-compute-budget-program = { path = "programs/compute-budget", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-compute-budget-program = { path = "programs/compute-budget", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-config-interface = "2.0.0" -solana-connection-cache = { path = "connection-cache", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } -solana-core = { path = "core", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-cost-model = { path = "cost-model", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-connection-cache = { path = "connection-cache", version = "=4.3.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } +solana-core = { path = "core", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-cost-model = { path = "cost-model", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-cpi = "3.1.0" -solana-curve25519 = "4.0.0" +solana-curve25519 = "4.0.1" solana-derivation-path = "3.0.0" -solana-download-utils = { path = "download-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-download-utils = { path = "download-utils", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-ed25519-program = "3.0.0" -solana-entry = { path = "entry", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-entry = { path = "entry", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-epoch-info = "3.1.0" -solana-epoch-rewards = "3.0.1" +solana-epoch-rewards = "3.1.0" solana-epoch-rewards-hasher = "3.1.0" -solana-epoch-schedule = "3.1.0" -solana-faucet = { path = "faucet", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-epoch-schedule = "3.2.0" +solana-faucet = { path = "faucet", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-feature-gate-interface = "4.0.0" -solana-fee = { path = "fee", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-fee-calculator = "3.2.0" +solana-fee = { path = "fee", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-fee-calculator = "3.2.2" solana-fee-structure = "3.0.0" solana-file-download = "3.1.4" -solana-frozen-abi = "3.4.0" -solana-frozen-abi-macro = "3.3.0" -solana-genesis = { path = "genesis", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-frozen-abi = "3.7.0" +solana-frozen-abi-macro = "3.6.0" +solana-genesis = { path = "genesis", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-genesis-config = "4.0.0" -solana-genesis-utils = { path = "genesis-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-geyser-plugin-manager = { path = "geyser-plugin-manager", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-gossip = { path = "gossip", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-hard-forks = "3.1.0" -solana-hash = "4.3.0" +solana-genesis-utils = { path = "genesis-utils", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-geyser-plugin-manager = { path = "geyser-plugin-manager", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-gossip = { path = "gossip", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-hard-forks = "3.1.1" +solana-hash = "4.4.0" solana-hash-512 = "1.1.0" -solana-inflation = "3.1.0" +solana-inflation = "3.1.1" solana-instruction = "3.4.0" -solana-instruction-error = "2.3.0" -solana-instructions-sysvar = "3.0.0" +solana-instruction-error = "2.4.0" +solana-instructions-sysvar = "4.0.0" solana-keccak-hasher = "3.1.0" solana-keypair = "3.1.2" -solana-last-restart-slot = "3.0.0" -solana-lattice-hash = { path = "lattice-hash", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-leader-schedule = { path = "leader-schedule", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-ledger = { path = "ledger", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-last-restart-slot = "3.1.0" +solana-lattice-hash = { path = "lattice-hash", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-leader-schedule = { path = "leader-schedule", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-ledger = { path = "ledger", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-loader-v2-interface = "3.0.0" solana-loader-v3-interface = "7.0.0" solana-loader-v4-interface = "3.1.0" -solana-local-cluster = { path = "local-cluster", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-measure = { path = "measure", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-merkle-tree = { path = "merkle-tree", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-message = "4.1.1" -solana-metrics = { path = "metrics", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-local-cluster = { path = "local-cluster", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-measure = { path = "measure", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-merkle-tree = { path = "merkle-tree", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-message = "4.2.4" +solana-metrics = { path = "metrics", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-msg = "3.1.0" solana-native-token = "3.0.0" -solana-net-utils = { path = "net-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-net-utils = { path = "net-utils", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-nohash-hasher = "0.2.1" solana-nonce = "3.2.0" solana-nonce-account = "4.1.0" -solana-notifier = { path = "notifier", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-notifier = { path = "notifier", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-offchain-message = "3.0.0" solana-packet = "4.1.0" -solana-perf = { path = "perf", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-poh = { path = "poh", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-perf = { path = "perf", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-poh = { path = "poh", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-poh-config = "3.0.0" solana-poseidon = "4.0.0" solana-precompile-error = "3.0.0" solana-presigner = "3.0.0" solana-program = { version = "4.0.0", default-features = false } -solana-program-binaries = { path = "program-binaries", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-program-binaries = { path = "program-binaries", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-program-entrypoint = "3.1.1" solana-program-error = "3.0.1" solana-program-option = "3.1.0" solana-program-pack = "3.1.0" -solana-program-runtime = { path = "program-runtime", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-program-test = { path = "program-test", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-program-runtime = { path = "program-runtime", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-program-test = { path = "program-test", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-pubkey = { version = "4.2.0", default-features = false } -solana-pubsub-client = { path = "pubsub-client", version = "=4.2.0-alpha.0" } -solana-quic-client = { path = "quic-client", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-rayon-threadlimit = { path = "rayon-threadlimit", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-remote-wallet = { path = "remote-wallet", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } -solana-rent = "4.2.0" +solana-pubsub-client = { path = "pubsub-client", version = "=4.3.0-alpha.0" } +solana-quic-client = { path = "quic-client", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-rayon-threadlimit = { path = "rayon-threadlimit", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-remote-wallet = { path = "remote-wallet", version = "=4.3.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } +solana-rent = "4.2.1" solana-reward-info = "6.2.0" -solana-rpc = { path = "rpc", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-rpc-client = { path = "rpc-client", version = "=4.2.0-alpha.0", default-features = false } -solana-rpc-client-api = { path = "rpc-client-api", version = "=4.2.0-alpha.0" } -solana-rpc-client-nonce-utils = { path = "rpc-client-nonce-utils", version = "=4.2.0-alpha.0" } -solana-rpc-client-types = { path = "rpc-client-types", version = "=4.2.0-alpha.0" } -solana-runtime = { path = "runtime", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-runtime-transaction = { path = "runtime-transaction", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-rpc = { path = "rpc", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-rpc-client = { path = "rpc-client", version = "=4.3.0-alpha.0", default-features = false } +solana-rpc-client-api = { path = "rpc-client-api", version = "=4.3.0-alpha.0" } +solana-rpc-client-nonce-utils = { path = "rpc-client-nonce-utils", version = "=4.3.0-alpha.0" } +solana-rpc-client-types = { path = "rpc-client-types", version = "=4.3.0-alpha.0" } +solana-runtime = { path = "runtime", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-runtime-transaction = { path = "runtime-transaction", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-sanitize = "3.0.1" -solana-sbpf = { version = "=0.20.0", default-features = false } +solana-sbpf = { version = "=0.21.1", default-features = false } solana-sdk-ids = "3.1.0" solana-secp256k1-program = "3.0.1" solana-secp256k1-recover = "3.1.1" solana-secp256r1-program = "3.0.0" solana-seed-derivable = "3.0.0" solana-seed-phrase = "3.0.0" -solana-send-transaction-service = { path = "send-transaction-service", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-send-transaction-service = { path = "send-transaction-service", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-serde = "3.0.0" solana-serde-varint = "3.0.1" -solana-serialize-utils = "3.1.1" +solana-serialize-utils = "3.1.2" solana-sha256-hasher = "3.1.0" solana-sha512-hasher = "1.0.1" -solana-short-vec = "3.2.1" +solana-short-vec = "3.2.2" solana-shred-version = "3.0.1" -solana-signature = { version = "3.4.0", default-features = false } -solana-signer = "3.0.0" +solana-signature = { version = "3.4.1", default-features = false } +solana-signer = "3.0.1" solana-signer-store = "0.1.0" -solana-slot-hashes = "3.0.1" +solana-slot-hashes = "3.1.0" solana-slot-history = "3.0.1" solana-stable-layout = "3.0.1" -solana-stake-interface = "3.1.0" -solana-storage-bigtable = { path = "storage-bigtable", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-storage-proto = { path = "storage-proto", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-streamer = { path = "streamer", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm = { path = "svm", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-callback = { path = "svm-callback", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-feature-set = { path = "svm-feature-set", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-log-collector = { path = "svm-log-collector", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-measure = { path = "svm-measure", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-timings = { path = "svm-timings", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-transaction = { path = "svm-transaction", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-type-overrides = { path = "svm-type-overrides", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-syscalls = { path = "syscalls", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } +solana-stake-interface = "4.3.0" +solana-storage-bigtable = { path = "storage-bigtable", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-storage-proto = { path = "storage-proto", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-streamer = { path = "streamer", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm = { path = "svm", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm-callback = { path = "svm-callback", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm-feature-set = { path = "svm-feature-set", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm-log-collector = { path = "svm-log-collector", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm-measure = { path = "svm-measure", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm-timings = { path = "svm-timings", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm-transaction = "4.2.0" +solana-svm-type-overrides = { path = "svm-type-overrides", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-syscalls = { path = "syscalls", version = "=4.3.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } solana-system-interface = { version = "3.2", features = ["alloc", "bincode", "serde", "wincode"] } -solana-system-program = { path = "programs/system", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-system-program = { path = "programs/system", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-system-transaction = "4.0.0" solana-sysvar = "4.0.0" solana-sysvar-id = "3.1.0" -solana-test-validator = { path = "test-validator", version = "=4.2.0-alpha.0" } +solana-test-validator = { path = "test-validator", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-time-utils = "3.0.0" -solana-tls-utils = { path = "tls-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-tpu-client = { path = "tpu-client", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } -solana-tpu-client-next = { path = "tpu-client-next", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-transaction = "4.1.1" -solana-transaction-context = { path = "transaction-context", version = "=4.2.0-alpha.0", features = ["agave-unstable-api", "bincode"] } -solana-transaction-error = "3.2.0" -solana-transaction-status = { path = "transaction-status", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-transaction-status-client-types = { path = "transaction-status-client-types", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-turbine = { path = "turbine", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-udp-client = { path = "udp-client", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-unified-scheduler-logic = { path = "unified-scheduler-logic", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-unified-scheduler-pool = { path = "unified-scheduler-pool", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-tls-utils = { path = "tls-utils", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-tpu-client = { path = "tpu-client", version = "=4.3.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } +solana-tpu-client-next = { path = "tpu-client-next", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-transaction = "4.1.4" +solana-transaction-context = { path = "transaction-context", version = "=4.3.0-alpha.0", features = ["agave-unstable-api", "bincode"] } +solana-transaction-error = "3.3.1" +solana-transaction-status = { path = "transaction-status", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-transaction-status-client-types = { path = "transaction-status-client-types", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-turbine = { path = "turbine", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-udp-client = { path = "udp-client", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-unified-scheduler-logic = { path = "unified-scheduler-logic", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-unified-scheduler-pool = { path = "unified-scheduler-pool", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-validator-exit = "3.0.0" -solana-version = { path = "version", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-vote = { path = "vote", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-vote-interface = "6.0.0" -solana-vote-program = { path = "programs/vote", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } -solana-zk-elgamal-proof-program = { path = "programs/zk-elgamal-proof", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-version = { path = "version", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-vote = { path = "vote", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-vote-interface = "6.0.2" +solana-vote-program = { path = "programs/vote", version = "=4.3.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } +solana-wincode-varint = "1.0.0" +solana-zk-elgamal-proof-program = { path = "programs/zk-elgamal-proof", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-zk-sdk = "5.0.1" -solana-zk-token-proof-program = { path = "programs/zk-token-proof", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-zk-sdk-pod = "0.1.2" +solana-zk-token-proof-program = { path = "programs/zk-token-proof", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } spl-associated-token-account-interface = "2.0.0" spl-generic-token = "2.0.0" -spl-memo-interface = "2.0.0" +spl-memo-interface = "2.1.0" spl-pod = "0.7.3" -spl-token-2022-interface = "2.1.0" -spl-token-confidential-transfer-proof-extraction = "0.5.1" +spl-token-2022-interface = "3.1.0" +spl-token-confidential-transfer-proof-extraction = "0.6.1" spl-token-group-interface = "0.7.2" -spl-token-interface = "2.0.0" -spl-token-metadata-interface = "0.8.0" +spl-token-interface = "3.0.0" +spl-token-metadata-interface = "1.0.1" static_assertions = "1.1.0" stream-cancel = "0.8.2" strum = "0.28.0" @@ -537,13 +547,11 @@ strum_macros = "0.28.0" symlink = "0.1.0" sys-info = "0.9.1" sysctl = "0.7.1" -systemstat = "0.2.6" tar = "0.4.46" tarpc = "0.29.0" tempfile = "3.27.0" test-case = "3.3.1" thiserror = "2.0.18" -tiny-bip39 = "2.0.0" tokio = "1.52.3" tokio-serde = "0.8" tokio-stream = "0.1.18" @@ -558,12 +566,15 @@ trees = "0.4.2" trezor-client = { version = "0.1.5", default-features = false, features = ["solana"] } tungstenite = "0.28.0" unwrap_none = "0.1.2" +ur-parse-lib = { version = "1.0.3", default-features = false, features = ["std"] } +ur-registry = { version = "1.0.3", default-features = false, features = ["std"] } uriparse = "0.6.4" url = "2.5.8" winapi = "0.3.8" -wincode = { version = "0.5.4", features = ["derive"] } +wincode = { version = "0.5.5", features = ["derive"] } winreg = "0.55" x509-parser = "0.18.1" +xxhash-rust = "0.8.5" zstd = "0.13.3" [profile.dev] diff --git a/README.md b/README.md index 779a55659bd..e9b66d5e4b7 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,21 @@ $ ./cargo build ``` > [!NOTE] -> Note that this builds a debug version that is **not suitable for running a testnet or mainnet validator**. Please read [`docs/src/cli/install.md`](docs/src/cli/install.md#build-from-source) for instructions to build a release version for test and production uses. +> Note that this builds a debug version that is **not suitable for running a testnet or mainnet validator**. Please read [the install guide](https://docs.anza.xyz/cli/install#build-from-source) for instructions to build a release version for test and production uses. + +## **4. Grant capabilities for XDP (Linux-only).** + +XDP transmit is enabled on Linux by default and requires extra capabilities. After building, grant them to the validator binary: + +```bash +$ sudo setcap 'cap_net_admin,cap_net_raw+eip' +``` + +For XDP zero-copy mode (`--xdp-zero-copy`), additional capabilities are needed: + +```bash +$ sudo setcap 'cap_net_admin,cap_net_raw,cap_bpf,cap_perfmon+eip' +``` # Testing diff --git a/RELEASE.md b/RELEASE.md index e22e5c1b7fb..10ab7ae56f3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -91,34 +91,16 @@ Then proceed with the usual "Miscellaneous Clean up" steps below. #### Steps -1. Check out the latest commit on `master` branch: - ``` - git fetch --all - git checkout upstream/master - ``` +1. Update master branch to the next release minor version: dispatch [Bump Version](https://github.com/anza-xyz/agave/actions/workflows/bump-version.yml) pipeline against `master` branch to create the version bump PR. `Version bump level` should be either `minor` or `major`. +1. Review version bump PR, get all required approvals and merge it. +1. Determine the last commit **right before the version bump commit**. 1. Determine the new branch name. The name should be "v" + the first 2 version fields - from Cargo.toml. For example, a Cargo.toml with version = "0.9.0" implies + from Cargo.toml **at the commit right before the version bump commit**. For example, a Cargo.toml with version = "0.9.0" implies the next branch name is "v0.9". 1. Create the new branch and push this branch to the `agave` repository: ``` - git checkout -b - git push -u origin - ``` - -Alternatively use the Github UI. - -### Update master branch to the next release minor version - -1. After the new branch has been created and pushed, update the Cargo.toml files on **master** to the next semantic version (e.g. 0.9.0 -> 0.10.0) with: - ``` - $ scripts/increment-cargo-version.sh minor - ``` -1. Push all the changed Cargo.toml and Cargo.lock files to the `master` branch with something like: - ``` - git co -b version_update - git ls-files -m | xargs git add - git commit -m 'Bump version to X.Y+1.0' - git push -u origin version_update + git checkout -b + git push -u upstream ``` 1. Confirm that your freshly cut release branch is shown as `BETA_CHANNEL` and the previous release branch as `STABLE_CHANNEL`: ``` diff --git a/SECURITY.md b/SECURITY.md index 2aa306418ce..27e1ec92a16 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,6 +14,10 @@ Provide a helpful title, detailed description of the vulnerability and an exploi proof-of-concept. Speculative submissions without proof-of-concept will be closed with no further consideration. +Create one GHSA per finding. GHSAs reporting multiple findings will be closed as +invalid. Such reports will not be eligible for bug bounties and will not hold a +position in duplicate report standings. + For vulnerabilities regarding SPL programs, please refer to the repositories and their associated security policy within the [solana-program organization](https://github.com/solana-program). @@ -196,6 +200,7 @@ one week in order to be eligible for a bounty * In the stable and beta branches, vulnerabilities are eligible for bounty upon merge * Submissions _MUST_ include an exploit proof-of-concept to be considered eligible +* Only reports describing a [single finding](#reporting) will be considered eligible * The participant submitting the bug report shall follow the process outlined within this document * Valid exploits can be eligible even if they are not successfully executed on a public cluster * Multiple submissions for the same class of exploit are still eligible for compensation, though may be compensated at a lower rate, however these will be assessed on a case-by-case basis diff --git a/account-decoder-client-types/Cargo.toml b/account-decoder-client-types/Cargo.toml index 5dc4e3c8f61..00520988ab3 100644 --- a/account-decoder-client-types/Cargo.toml +++ b/account-decoder-client-types/Cargo.toml @@ -26,3 +26,6 @@ serde_json = { workspace = true } solana-account = { workspace = true } solana-pubkey = { workspace = true } zstd = { workspace = true, optional = true } + +[lints] +workspace = true diff --git a/account-decoder-client-types/src/lib.rs b/account-decoder-client-types/src/lib.rs index 9d91da26d75..a41d40ba2d8 100644 --- a/account-decoder-client-types/src/lib.rs +++ b/account-decoder-client-types/src/lib.rs @@ -1,6 +1,6 @@ #![cfg(feature = "agave-unstable-api")] //! Core RPC client types for solana-account-decoder -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #[cfg(feature = "zstd")] use std::io::Read; use { diff --git a/account-decoder-client-types/src/token.rs b/account-decoder-client-types/src/token.rs index fa7162457b0..3b6809e79cc 100644 --- a/account-decoder-client-types/src/token.rs +++ b/account-decoder-client-types/src/token.rs @@ -124,6 +124,7 @@ pub enum UiExtension { ScaledUiAmountConfig(UiScaledUiAmountConfig), PausableConfig(UiPausableConfig), PausableAccount, + PermissionedBurnConfig(UiPermissionedBurnConfig), } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -337,3 +338,9 @@ pub struct UiPausableConfig { pub authority: Option, pub paused: bool, } + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct UiPermissionedBurnConfig { + pub authority: Option, +} diff --git a/account-decoder/Cargo.toml b/account-decoder/Cargo.toml index 578b5275a8c..effa6dbd07c 100644 --- a/account-decoder/Cargo.toml +++ b/account-decoder/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -41,17 +43,19 @@ solana-program-pack = { workspace = true } solana-pubkey = { workspace = true } solana-rent = { workspace = true } solana-sdk-ids = { workspace = true } -solana-slot-hashes = { workspace = true } -solana-slot-history = { workspace = true } +solana-slot-hashes = { workspace = true, features = ["wincode"] } +solana-slot-history = { workspace = true, features = ["wincode"] } solana-stake-interface = { workspace = true, features = ["bincode", "sysvar"] } solana-sysvar = { workspace = true } solana-vote-interface = { workspace = true, features = ["bincode"] } +solana-zk-sdk-pod = { workspace = true } spl-generic-token = { workspace = true } spl-token-2022-interface = { workspace = true } spl-token-group-interface = { workspace = true } spl-token-interface = { workspace = true } spl-token-metadata-interface = { workspace = true } thiserror = { workspace = true } +wincode = { workspace = true } zstd = { workspace = true } [dev-dependencies] diff --git a/account-decoder/src/parse_stake.rs b/account-decoder/src/parse_stake.rs index 433e5079a8f..32e603f9c97 100644 --- a/account-decoder/src/parse_stake.rs +++ b/account-decoder/src/parse_stake.rs @@ -194,7 +194,7 @@ mod test { stake: 20, activation_epoch: 2, deactivation_epoch: u64::MAX, - warmup_cooldown_rate: 0.25, + _reserved: [0; 8], }, credits_observed: 10, }; diff --git a/account-decoder/src/parse_sysvar.rs b/account-decoder/src/parse_sysvar.rs index af48808f51b..cdd4c0e0eb5 100644 --- a/account-decoder/src/parse_sysvar.rs +++ b/account-decoder/src/parse_sysvar.rs @@ -56,23 +56,27 @@ pub fn parse_sysvar(data: &[u8], pubkey: &Pubkey) -> Result(data).ok().map(|slot_hashes| { - let slot_hashes = slot_hashes - .iter() - .map(|slot_hash| UiSlotHashEntry { - slot: slot_hash.0, - hash: slot_hash.1.to_string(), - }) - .collect(); - SysvarAccountType::SlotHashes(slot_hashes) - }) + wincode::deserialize::(data) + .ok() + .map(|slot_hashes| { + let slot_hashes = slot_hashes + .iter() + .map(|slot_hash| UiSlotHashEntry { + slot: slot_hash.0, + hash: slot_hash.1.to_string(), + }) + .collect(); + SysvarAccountType::SlotHashes(slot_hashes) + }) } else if pubkey == &sysvar::slot_history::id() { - deserialize::(data).ok().map(|slot_history| { - SysvarAccountType::SlotHistory(UiSlotHistory { - next_slot: slot_history.next_slot, - bits: format!("{:?}", SlotHistoryBits(slot_history.bits)), + wincode::deserialize::(data) + .ok() + .map(|slot_history| { + SysvarAccountType::SlotHistory(UiSlotHistory { + next_slot: slot_history.next_slot, + bits: format!("{:?}", SlotHistoryBits(slot_history.bits)), + }) }) - }) } else if pubkey == &sysvar::stake_history::id() { deserialize::(data).ok().map(|stake_history| { let stake_history = stake_history @@ -266,8 +270,11 @@ mod test { #[allow(deprecated)] use solana_sysvar::recent_blockhashes::IterItem; use { - super::*, solana_account::create_account_for_test, solana_fee_calculator::FeeCalculator, + super::*, + solana_account::{Account, create_account_for_test}, + solana_fee_calculator::FeeCalculator, solana_hash::Hash, + solana_stake_interface::stake_history::SIZE, }; #[test] @@ -362,7 +369,8 @@ mod test { deactivating: 3, }; stake_history.add(1, stake_history_entry.clone()); - let stake_history_sysvar = create_account_for_test(&stake_history); + let stake_history_sysvar = + Account::new_data_with_space(1, &stake_history, SIZE, &sysvar::id()).unwrap(); assert_eq!( parse_sysvar(&stake_history_sysvar.data, &sysvar::stake_history::id()).unwrap(), SysvarAccountType::StakeHistory(vec![UiStakeHistoryEntry { diff --git a/account-decoder/src/parse_token.rs b/account-decoder/src/parse_token.rs index 4718b3b716b..3eb87ee1e75 100644 --- a/account-decoder/src/parse_token.rs +++ b/account-decoder/src/parse_token.rs @@ -173,14 +173,15 @@ pub fn get_token_account_mint(data: &[u8]) -> Option { mod test { use { super::*, - crate::parse_token_extension::{UiMemoTransfer, UiMintCloseAuthority}, + crate::parse_token_extension::{ + UiMemoTransfer, UiMintCloseAuthority, UiPermissionedBurnConfig, + }, solana_account_decoder_client_types::token::UiExtension, - spl_pod::optional_keys::OptionalNonZeroPubkey, spl_token_2022_interface::extension::{ BaseStateWithExtensionsMut, ExtensionType, StateWithExtensionsMut, immutable_owner::ImmutableOwner, interest_bearing_mint::InterestBearingConfig, memo_transfer::MemoTransfer, mint_close_authority::MintCloseAuthority, - scaled_ui_amount::ScaledUiAmountConfig, + permissioned_burn::PermissionedBurnConfig, scaled_ui_amount::ScaledUiAmountConfig, }, }; @@ -624,8 +625,7 @@ mod test { let mint_close_authority = mint_state .init_extension::(true) .unwrap(); - mint_close_authority.close_authority = - OptionalNonZeroPubkey::try_from(Some(owner_pubkey)).unwrap(); + mint_close_authority.close_authority = owner_pubkey.into(); mint_state.base = mint_base; mint_state.pack_base(); @@ -645,4 +645,48 @@ mod test { }), ); } + + #[test] + fn test_parse_token_mint_with_permissioned_burn() { + let owner_pubkey = Pubkey::new_from_array([3; 32]); + let authority_pubkey = Pubkey::new_from_array([4; 32]); + let mint_size = + ExtensionType::try_calculate_account_len::(&[ExtensionType::PermissionedBurn]) + .unwrap(); + let mint_base = Mint { + mint_authority: COption::Some(owner_pubkey), + supply: 42, + decimals: 3, + is_initialized: true, + freeze_authority: COption::Some(owner_pubkey), + }; + let mut mint_data = vec![0; mint_size]; + let mut mint_state = + StateWithExtensionsMut::::unpack_uninitialized(&mut mint_data).unwrap(); + + let permissioned_burn = mint_state + .init_extension::(true) + .unwrap(); + permissioned_burn.authority = authority_pubkey.into(); + + mint_state.base = mint_base; + mint_state.pack_base(); + mint_state.init_account_type().unwrap(); + + assert_eq!( + parse_token_v3(&mint_data, None).unwrap(), + TokenAccountType::Mint(UiMint { + mint_authority: Some(owner_pubkey.to_string()), + supply: 42.to_string(), + decimals: 3, + is_initialized: true, + freeze_authority: Some(owner_pubkey.to_string()), + extensions: vec![UiExtension::PermissionedBurnConfig( + UiPermissionedBurnConfig { + authority: Some(authority_pubkey.to_string()), + } + )], + }), + ); + } } diff --git a/account-decoder/src/parse_token_extension.rs b/account-decoder/src/parse_token_extension.rs index 958ff2afa6d..73de38b619c 100644 --- a/account-decoder/src/parse_token_extension.rs +++ b/account-decoder/src/parse_token_extension.rs @@ -3,17 +3,18 @@ pub use solana_account_decoder_client_types::token::{ UiConfidentialTransferFeeConfig, UiConfidentialTransferMint, UiCpiGuard, UiDefaultAccountState, UiExtension, UiGroupMemberPointer, UiGroupPointer, UiInterestBearingConfig, UiMemoTransfer, UiMetadataPointer, UiMintCloseAuthority, UiPausableConfig, UiPermanentDelegate, - UiScaledUiAmountConfig, UiTokenGroup, UiTokenGroupMember, UiTokenMetadata, UiTransferFee, - UiTransferFeeAmount, UiTransferFeeConfig, UiTransferHook, UiTransferHookAccount, + UiPermissionedBurnConfig, UiScaledUiAmountConfig, UiTokenGroup, UiTokenGroupMember, + UiTokenMetadata, UiTransferFee, UiTransferFeeAmount, UiTransferFeeConfig, UiTransferHook, + UiTransferHookAccount, }; use { crate::parse_token::convert_account_state, solana_clock::UnixTimestamp, solana_program_pack::Pack, solana_pubkey::Pubkey, - spl_token_2022_interface::{ - extension::{self, BaseState, BaseStateWithExtensions, ExtensionType, StateWithExtensions}, - solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, + solana_zk_sdk_pod::encryption::elgamal::PodElGamalPubkey, + spl_token_2022_interface::extension::{ + self, BaseState, BaseStateWithExtensions, ExtensionType, StateWithExtensions, }, spl_token_group_interface::state::{TokenGroup, TokenGroupMember}, spl_token_metadata_interface::state::TokenMetadata, @@ -153,6 +154,12 @@ pub fn parse_extension( .map(|&extension| UiExtension::PausableConfig(convert_pausable_config(extension))) .unwrap_or(UiExtension::UnparseableExtension), ExtensionType::PausableAccount => UiExtension::PausableAccount, + ExtensionType::PermissionedBurn => account + .get_extension::() + .map(|&extension| { + UiExtension::PermissionedBurnConfig(convert_permissioned_burn_config(extension)) + }) + .unwrap_or(UiExtension::UnparseableExtension), } } @@ -436,3 +443,12 @@ fn convert_pausable_config( paused: pausable_config.paused.into(), } } + +fn convert_permissioned_burn_config( + permissioned_burn_config: extension::permissioned_burn::PermissionedBurnConfig, +) -> UiPermissionedBurnConfig { + let authority: Option = permissioned_burn_config.authority.into(); + UiPermissionedBurnConfig { + authority: authority.map(|pubkey| pubkey.to_string()), + } +} diff --git a/accounts-cluster-bench/Cargo.toml b/accounts-cluster-bench/Cargo.toml index b71feeda172..b2035d11bf4 100644 --- a/accounts-cluster-bench/Cargo.toml +++ b/accounts-cluster-bench/Cargo.toml @@ -57,4 +57,7 @@ solana-local-cluster = { path = "../local-cluster", features = ["agave-unstable- solana-native-token = { workspace = true } solana-poh-config = { workspace = true } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } -solana-test-validator = { path = "../test-validator" } +solana-test-validator = { path = "../test-validator", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/accounts-cluster-bench/src/main.rs b/accounts-cluster-bench/src/main.rs index 052b68ea2ae..fcc9915f233 100644 --- a/accounts-cluster-bench/src/main.rs +++ b/accounts-cluster-bench/src/main.rs @@ -422,7 +422,7 @@ fn process_get_transaction( stats.errors += 1; stats.total_errors_time_us += measure.as_us(); if last_error.elapsed().as_secs() > 2 { - info!("get_transaction error: {:?}", &e); + info!("get_transaction error: {e:?}"); *last_error = Instant::now(); } } diff --git a/accounts-db/Cargo.toml b/accounts-db/Cargo.toml index 650be86d053..8a15a33a9e2 100644 --- a/accounts-db/Cargo.toml +++ b/accounts-db/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -117,6 +119,10 @@ test-case = { workspace = true } [target.'cfg(not(any(target_env = "msvc", target_os = "freebsd")))'.dev-dependencies] jemallocator = { workspace = true } +[[bench]] +name = "accounts_index" +harness = false + [[bench]] name = "bench_accounts_file" harness = false diff --git a/accounts-db/benches/accounts_index.rs b/accounts-db/benches/accounts_index.rs index 14a15fa8199..cde9802fca5 100644 --- a/accounts-db/benches/accounts_index.rs +++ b/accounts-db/benches/accounts_index.rs @@ -1,27 +1,20 @@ -#![feature(test)] - -extern crate test; - use { + criterion::{Criterion, criterion_group, criterion_main}, rand::{Rng, rng}, - solana_account::AccountSharedData, solana_accounts_db::{ account_info::AccountInfo, accounts_index::{ - ACCOUNTS_INDEX_CONFIG_FOR_BENCHMARKS, AccountSecondaryIndexes, AccountsIndex, - ReclaimsSlotList, UpsertReclaim, + ACCOUNTS_INDEX_CONFIG_FOR_BENCHMARKS, AccountsIndex, ReclaimsSlotList, UpsertReclaim, }, }, std::sync::Arc, - test::Bencher, }; #[cfg(not(any(target_env = "msvc", target_os = "freebsd")))] #[global_allocator] static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; -#[bench] -fn bench_accounts_index(bencher: &mut Bencher) { +fn bench_accounts_index(c: &mut Criterion) { const NUM_PUBKEYS: usize = 10_000; let pubkeys: Vec<_> = (0..NUM_PUBKEYS) .map(|_| solana_pubkey::new_rand()) @@ -40,8 +33,6 @@ fn bench_accounts_index(bencher: &mut Bencher) { f, f, pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), AccountInfo::default(), &mut reclaims, UpsertReclaim::PopulateReclaims, @@ -50,24 +41,24 @@ fn bench_accounts_index(bencher: &mut Bencher) { } let mut fork = NUM_FORKS; - let mut root = 0; - bencher.iter(|| { - for _p in 0..NUM_PUBKEYS { - let pubkey = rng().random_range(0..NUM_PUBKEYS); - index.upsert( - fork, - fork, - &pubkeys[pubkey], - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - AccountInfo::default(), - &mut reclaims, - UpsertReclaim::PopulateReclaims, - ); - reclaims.clear(); - } - index.add_root(root); - root += 1; - fork += 1; + c.bench_function("accounts_index", |b| { + b.iter(|| { + for _p in 0..NUM_PUBKEYS { + let pubkey = rng().random_range(0..NUM_PUBKEYS); + index.upsert( + fork, + fork, + &pubkeys[pubkey], + AccountInfo::default(), + &mut reclaims, + UpsertReclaim::PopulateReclaims, + ); + reclaims.clear(); + } + fork = fork.checked_add(1).expect("fork overflow"); + }); }); } + +criterion_group!(benches, bench_accounts_index); +criterion_main!(benches); diff --git a/accounts-db/src/account_info.rs b/accounts-db/src/account_info.rs index 3096f501a0b..4ddd42ae551 100644 --- a/accounts-db/src/account_info.rs +++ b/accounts-db/src/account_info.rs @@ -1,4 +1,4 @@ -//! AccountInfo represents a reference to AccountSharedData in either an AppendVec or the write cache. +//! AccountInfo represents a reference to AccountSharedData in an AppendVec //! AccountInfo is not persisted anywhere between program runs. //! AccountInfo is purely runtime state. //! Note that AccountInfo is saved to disk buckets during runtime, but disk buckets are recreated at startup. @@ -6,7 +6,7 @@ use { crate::{ accounts_db::AccountsFileId, accounts_file::ALIGN_BOUNDARY_OFFSET, - accounts_index::{DiskIndexValue, IndexValue, IsCached}, + accounts_index::{DiskIndexValue, IndexValue}, is_zero_lamport::IsZeroLamport, }, modular_bitfield::prelude::*, @@ -19,38 +19,21 @@ pub type Offset = usize; #[derive(Debug, PartialEq, Eq)] pub enum StorageLocation { AppendVec(AccountsFileId, Offset), - Cached, } impl StorageLocation { pub fn is_offset_equal(&self, other: &StorageLocation) -> bool { match self { - StorageLocation::Cached => { - matches!(other, StorageLocation::Cached) // technically, 2 cached entries match in offset - } - StorageLocation::AppendVec(_, offset) => { - match other { - StorageLocation::Cached => { - false // 1 cached, 1 not - } - StorageLocation::AppendVec(_, other_offset) => other_offset == offset, - } - } + StorageLocation::AppendVec(_, offset) => match other { + StorageLocation::AppendVec(_, other_offset) => other_offset == offset, + }, } } pub fn is_store_id_equal(&self, other: &StorageLocation) -> bool { match self { - StorageLocation::Cached => { - matches!(other, StorageLocation::Cached) // 2 cached entries are same store id - } - StorageLocation::AppendVec(store_id, _) => { - match other { - StorageLocation::Cached => { - false // 1 cached, 1 not - } - StorageLocation::AppendVec(other_store_id, _) => other_store_id == store_id, - } - } + StorageLocation::AppendVec(store_id, _) => match other { + StorageLocation::AppendVec(other_store_id, _) => other_store_id == store_id, + }, } } } @@ -60,15 +43,6 @@ impl StorageLocation { /// AppendVecs store accounts aligned to u64, so offset is always a multiple of 8 (sizeof(u64)) pub type OffsetReduced = u32; -/// This is an illegal value for 'offset'. -/// Account size on disk would have to be pointing to the very last 8 byte value in the max sized append vec. -/// That would mean there was a maximum size of 8 bytes for the last entry in the append vec. -/// A pubkey alone is 32 bytes, so there is no way for a valid offset to be this high of a value. -/// Realistically, a max offset is (1<<31 - 156) bytes or so for an account with zero data length. Of course, this -/// depends on the layout on disk, compression, etc. But, 8 bytes per account will never be possible. -/// So, we use this last value as a sentinel to say that the account info refers to an entry in the write cache. -const CACHED_OFFSET: OffsetReduced = (1 << (OffsetReduced::BITS - 1)) - 1; - #[bitfield(bits = 32)] #[repr(C)] #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] @@ -98,35 +72,15 @@ impl IsZeroLamport for AccountInfo { } } -impl IsCached for AccountInfo { - fn is_cached(&self) -> bool { - self.account_offset_and_flags.offset_reduced() == CACHED_OFFSET - } -} - impl IndexValue for AccountInfo {} impl DiskIndexValue for AccountInfo {} -impl IsCached for StorageLocation { - fn is_cached(&self) -> bool { - matches!(self, StorageLocation::Cached) - } -} - -/// We have to have SOME value for store_id when we are cached -const CACHE_VIRTUAL_STORAGE_ID: AccountsFileId = AccountsFileId::MAX; - impl AccountInfo { pub fn new(storage_location: StorageLocation, is_zero_lamport: bool) -> Self { let mut packed_offset_and_flags = PackedOffsetAndFlags::default(); let store_id = match storage_location { StorageLocation::AppendVec(store_id, offset) => { - let reduced_offset = Self::get_reduced_offset(offset); - assert_ne!( - CACHED_OFFSET, reduced_offset, - "illegal offset for non-cached item" - ); packed_offset_and_flags.set_offset_reduced(Self::get_reduced_offset(offset)); assert_eq!( Self::reduced_offset_to_offset(packed_offset_and_flags.offset_reduced()), @@ -135,10 +89,6 @@ impl AccountInfo { ); store_id } - StorageLocation::Cached => { - packed_offset_and_flags.set_offset_reduced(CACHED_OFFSET); - CACHE_VIRTUAL_STORAGE_ID - } }; packed_offset_and_flags.set_is_zero_lamport(is_zero_lamport); Self { @@ -152,8 +102,6 @@ impl AccountInfo { } pub fn store_id(&self) -> AccountsFileId { - // if the account is in a cached store, the store_id is meaningless - assert!(!self.is_cached()); self.store_id } @@ -166,11 +114,7 @@ impl AccountInfo { } pub fn storage_location(&self) -> StorageLocation { - if self.is_cached() { - StorageLocation::Cached - } else { - StorageLocation::AppendVec(self.store_id, self.offset()) - } + StorageLocation::AppendVec(self.store_id, self.offset()) } } @@ -195,13 +139,6 @@ mod test { } } - #[test] - #[should_panic(expected = "illegal offset")] - fn test_illegal_offset() { - let offset = (MAXIMUM_APPEND_VEC_FILE_SIZE - (ALIGN_BOUNDARY_OFFSET as u64)) as Offset; - AccountInfo::new(StorageLocation::AppendVec(0, offset), true); - } - #[test] #[should_panic(expected = "illegal offset")] fn test_alignment() { diff --git a/accounts-db/src/account_locks.rs b/accounts-db/src/account_locks.rs index 4d6b9e93976..558bbee054f 100644 --- a/accounts-db/src/account_locks.rs +++ b/accounts-db/src/account_locks.rs @@ -26,10 +26,10 @@ impl AccountLocks { >, ) -> Vec> { validated_batch_keys.iter_mut().for_each(|validated_keys| { - if let Ok(keys) = validated_keys.as_ref() { - if let Err(e) = self.can_lock_accounts(keys.clone()) { - *validated_keys = Err(e); - } + if let Ok(keys) = validated_keys.as_ref() + && let Err(e) = self.can_lock_accounts(keys.clone()) + { + *validated_keys = Err(e); } }); diff --git a/accounts-db/src/account_storage.rs b/accounts-db/src/account_storage.rs index f567e7e2e70..45b89461710 100644 --- a/accounts-db/src/account_storage.rs +++ b/accounts-db/src/account_storage.rs @@ -128,6 +128,20 @@ impl AccountStorage { self.map.iter().map(|iter_item| *iter_item.key()).collect() } + /// All slots with a storage entry below `max_slot_exclusive`. + pub(crate) fn slots_less_than(&self, max_slot_exclusive: Slot) -> Vec { + assert!( + self.no_shrink_in_progress(), + "shrink is in progress! slots: {:?}", + self.shrink_in_progress_map.read().unwrap().keys(), + ); + self.map + .iter() + .map(|iter_item| *iter_item.key()) + .filter(|slot| *slot < max_slot_exclusive) + .collect() + } + /// returns true if there is no entry for 'slot' #[cfg(test)] pub(crate) fn is_empty_entry(&self, slot: Slot) -> bool { diff --git a/accounts-db/src/account_storage_entry.rs b/accounts-db/src/account_storage_entry.rs index d317f696cd1..e4efdda4dec 100644 --- a/accounts-db/src/account_storage_entry.rs +++ b/accounts-db/src/account_storage_entry.rs @@ -31,7 +31,7 @@ pub struct AccountStorageEntry { pub(crate) num_alive_bytes: AtomicUsize, - /// offsets to accounts that are zero lamport single ref stored in this + /// offsets to accounts that are zero lamport single ref (ZLSR) stored in this /// storage. These are still alive. But, shrink will be able to remove them. /// /// NOTE: It's possible that one of these zero lamport single ref accounts @@ -211,6 +211,12 @@ impl AccountStorageEntry { self.accounts.flush() } + /// Detach the on-disk file from this storage's lifetime; see + /// [`AccountsFile::disable_remove_on_drop`]. + pub fn disable_remove_on_drop(&self) { + self.accounts.disable_remove_on_drop(); + } + pub(crate) fn add_accounts(&self, num_accounts: usize, num_bytes: usize) { self.num_alive_accounts .fetch_add(num_accounts, Ordering::Release); @@ -252,4 +258,8 @@ impl AccountStorageEntry { pub(crate) fn obsolete_accounts(&self) -> &RwLock { &self.obsolete_accounts } + + pub(crate) fn zero_lamport_single_ref_offsets(&self) -> &RwLock> { + &self.zero_lamport_single_ref_offsets + } } diff --git a/accounts-db/src/account_storage_reader.rs b/accounts-db/src/account_storage_reader.rs index c6eb8a04c2f..5f41cc555a5 100644 --- a/accounts-db/src/account_storage_reader.rs +++ b/accounts-db/src/account_storage_reader.rs @@ -16,6 +16,18 @@ use { // file fits entirely within the buffer. pub const ACCOUNT_STORAGE_MAX_BUFFER_SIZE: usize = 10 * 1024 * 1024; +#[cfg(not(target_os = "linux"))] +const READER_STACK_BUFFER_SIZE: usize = 64 * 1024; + +/// Concrete reader type returned by [`storage_file_buf_reader`]. +/// +/// The concrete type is exposed (rather than `impl FileBufRead<'a>`) so callers +/// can use inherent methods like `rebind`. +#[cfg(target_os = "linux")] +type StorageFileBufReader<'a> = buffered_reader::SequentialFileReader<'a>; +#[cfg(not(target_os = "linux"))] +type StorageFileBufReader<'a> = buffered_reader::BufferedReader<'a, READER_STACK_BUFFER_SIZE>; + /// When `use_page_cache` is `true`, direct I/O is forced off regardless of /// `io_setup.use_direct_io` so that reads can hit the kernel's page cache. /// Otherwise, the `io_setup.use_direct_io` setting is honored. @@ -23,35 +35,31 @@ pub fn storage_file_buf_reader<'a>( max_buf_size: usize, use_page_cache: bool, io_setup: &IoSetupState, -) -> io::Result + use<'a>> { +) -> io::Result> { #[cfg(target_os = "linux")] - let reader = buffered_reader::SequentialFileReaderBuilder::new() - .shared_sqpoll(io_setup.shared_sqpoll_fd()) - .use_direct_io(io_setup.use_direct_io && !use_page_cache) - .use_registered_buffers(io_setup.use_registered_io_uring_buffers) - .build(max_buf_size)?; + { + buffered_reader::SequentialFileReaderBuilder::new() + .shared_sqpoll(io_setup.shared_sqpoll_fd()) + .use_direct_io(io_setup.use_direct_io && !use_page_cache) + .use_registered_buffers(io_setup.use_registered_io_uring_buffers) + .build(max_buf_size) + } #[cfg(not(target_os = "linux"))] - let reader = { + { let _ = (max_buf_size, use_page_cache, io_setup); - const READER_STACK_BUFFER_SIZE: usize = 64 * 1024; - buffered_reader::BufferedReader::::new() - }; - Ok(reader) + Ok(StorageFileBufReader::new()) + } } -/// Returns a file handle for each storage suitable for archive-style reads -/// matching `use_direct_io` (see [`OpenFileForArchive`]). -/// -/// Returned as a `Vec` so the handles outlive the buffered reader they'll be -/// fed into. +/// Lazy iterator yielding a file handle for each storage suitable for +/// archive-style reads matching `use_direct_io` (see [`OpenFileForArchive`]). pub fn open_storage_files<'s>( - storages: impl IntoIterator, + storages: impl IntoIterator + 's, use_direct_io: bool, -) -> io::Result>> { +) -> impl Iterator>> + 's { storages .into_iter() - .map(|storage| storage.accounts.open_file_for_archive(use_direct_io)) - .collect() + .map(move |storage| storage.accounts.open_file_for_archive(use_direct_io)) } /// A wrapper type around `AccountStorageEntry` that implements the `Read` trait. @@ -122,13 +130,13 @@ impl<'a, R: FileBufRead<'a>> Read for AccountStorageReader<'_, R> { while total_read < buf_len { let next_obsolete_account = self.sorted_obsolete_accounts.last(); let file_offset = self.reader.get_file_offset() as usize; - if let Some(&(obsolete_start, obsolete_size)) = next_obsolete_account { - if file_offset == obsolete_start { - let skip_len = obsolete_size.min(self.num_total_bytes - obsolete_start); - self.reader.consume_or_skip(skip_len); - self.sorted_obsolete_accounts.pop(); - continue; - } + if let Some(&(obsolete_start, obsolete_size)) = next_obsolete_account + && file_offset == obsolete_start + { + let skip_len = obsolete_size.min(self.num_total_bytes - obsolete_start); + self.reader.consume_or_skip(skip_len); + self.sorted_obsolete_accounts.pop(); + continue; } // Cannot read beyond the end of the buffer @@ -207,7 +215,9 @@ mod tests { storage.accounts.write_accounts(&(slot, &accounts[..]), 0); - let files = open_storage_files(iter::once(&storage), false).unwrap(); + let files = open_storage_files(iter::once(&storage), false) + .collect::>>() + .unwrap(); let mut buf_reader = storage_file_buf_reader( ACCOUNT_STORAGE_MAX_BUFFER_SIZE, false, @@ -284,7 +294,9 @@ mod tests { let storage = storage.reopen_as_readonly().unwrap_or(storage); // Create the reader and check the length - let files = open_storage_files(iter::once(&storage), false).unwrap(); + let files = open_storage_files(iter::once(&storage), false) + .collect::>>() + .unwrap(); let mut file_reader = storage_file_buf_reader( ACCOUNT_STORAGE_MAX_BUFFER_SIZE, false, @@ -406,7 +418,9 @@ mod tests { let temp_dir = tempfile::tempdir().unwrap(); // Now iterate through all the possible snapshot slots and verify correctness - let files = open_storage_files(iter::once(&storage), false).unwrap(); + let files = open_storage_files(iter::once(&storage), false) + .collect::>>() + .unwrap(); let mut file_reader = storage_file_buf_reader( ACCOUNT_STORAGE_MAX_BUFFER_SIZE, false, diff --git a/accounts-db/src/accounts_cache.rs b/accounts-db/src/accounts_cache.rs index 50fbdeefac7..6b521884a33 100644 --- a/accounts-db/src/accounts_cache.rs +++ b/accounts-db/src/accounts_cache.rs @@ -36,26 +36,6 @@ impl MaxFlushedRoot { } } -/// Indicates whether a slot is an ancestor, an unflushed root, or a root being flushed -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SlotStatus { - /// Slot is in the ancestors list and not a root being flushed. Slot is guaranteed to be - /// the newest version of the account on this fork so the cached copy can be used without - /// checking storage - Ancestor, - /// Slot is in the ancestors list but is also a root being flushed. The version found in the - /// accounts cache may not be the newest version of the account, and the storage should be - /// checked for newer versions - AncestorBeingFlushed, - /// Slot is a root that has not yet been claimed for flushing. Slot is guaranteed to be the - /// newest version of the account so the cached copy can be used without checking storage - UnflushedRoot, - /// Slot is a root that is currently being flushed. The version found in the accounts cache - /// may not be the newest version of the account, and the storage should be checked for newer - /// versions - RootBeingFlushed, -} - #[derive(Debug)] pub struct SlotCache { cache: DashMap, PubkeyHasherBuilder>, @@ -225,8 +205,9 @@ impl AccountsCacheIndex { /// Decrement the reference count for each pubkey in `pubkeys`. Removes an entry entirely if /// the count reaches zero. `max_slot` is not updated; it will become stale if the removed slot - /// is the highest slot - fn remove(&self, pubkeys: impl IntoIterator) { + /// is the highest slot. Returns a vec of pubkeys removed from the index. + fn remove(&self, pubkeys: impl IntoIterator) -> Vec { + let mut removed_pubkeys = Vec::new(); for pubkey in pubkeys { let Entry::Occupied(mut occupied_entry) = self.entries.entry(pubkey) else { // If this has happened the index is corrupted @@ -237,8 +218,10 @@ impl AccountsCacheIndex { if *ref_count == 0 { occupied_entry.remove_entry(); self.num_unique_pubkeys.fetch_sub(1, Ordering::Relaxed); + removed_pubkeys.push(pubkey); } } + removed_pubkeys } /// Returns the recorded max slot for `pubkey`, or `None` if the pubkey is not present in the @@ -254,12 +237,10 @@ pub struct AccountsCache { cache: DashMap, BuildNoHashHasher>, // Index to find which slots a pubkey is stored in, to speed up lookups in load_latest index: AccountsCacheIndex, - // Queue of potentially unflushed roots. Random eviction + cache too large - // could have triggered a flush of this slot already - maybe_unflushed_roots: RwLock>, - // Roots that have been claimed for flushing by `begin_flush_roots` but not yet - // cleared by `end_flush_roots` - roots_being_flushed: RwLock>, + // Rooted slots that may still have accounts in the write cache. A slot enters via `add_root` + // and is dropped either by `remove_slot` when its cache is flushed, or by + // `remove_unflushed_root` when a flush finds it had no cache. + unflushed_roots: RwLock>, max_flushed_root: MaxFlushedRoot, /// The size of account data stored in the whole AccountsCache, in bytes total_size: Arc, @@ -287,11 +268,7 @@ impl AccountsCache { pub fn report_size(&self) { datapoint_info!( "accounts_cache_size", - ( - "num_roots", - self.maybe_unflushed_roots.read().unwrap().len(), - i64 - ), + ("num_roots", self.unflushed_roots.read().unwrap().len(), i64), ("num_slots", self.cache.len(), i64), ("total_size", self.size(), i64), ( @@ -339,48 +316,40 @@ impl AccountsCache { .and_then(|slot_cache| slot_cache.get_cloned(pubkey)) } - pub fn remove_slot(&self, slot: Slot) -> Option> { + /// Removes a slot from the accounts cache and returns the set of pubkeys removed from the index. + #[must_use] + pub fn remove_slot(&self, slot: Slot) -> Option> { let result = self.cache.remove(&slot).map(|(_, slot_cache)| slot_cache); - if let Some(slot_cache) = &result { - self.index.remove(slot_cache.iter().map(|item| *item.key())); - } + // If this slot was a root, it has now left the cache, so stop tracking it as unflushed. + self.unflushed_roots.write().unwrap().remove(&slot); + result + .as_ref() + .map(|slot_cache| self.index.remove(slot_cache.iter().map(|item| *item.key()))) } - /// Finds the best write-cache entry for `pubkey` visible from `ancestors`. Searches - /// ancestors first (highest to lowest), then unflushed roots, then roots being flushed. - /// Ancestors are checked exhaustively before roots, so a lower-slot ancestor wins over a - /// higher-slot root. Returns the account, slot, and status, or `None` if not found. + /// Finds the newest write-cache entry for `pubkey` visible from `ancestors`. Searches + /// ancestors first (highest to lowest), then roots (highest to lowest). Ancestors are + /// checked exhaustively before roots, so a lower-slot ancestor wins over a higher-slot + /// root. Returns the account and its slot, or `None` if not found. pub fn load_latest( &self, pubkey: &Pubkey, ancestors: &Ancestors, - ) -> Option<(Arc, Slot, SlotStatus)> { + ) -> Option<(Arc, Slot)> { // Exit early if the pubkey isn't in the cache let index_max_slot = self.index.max_slot_for_pubkey(pubkey)?; + // Ancestors take priority over roots regardless of slot. Iterate every slot in the + // range in descending order and return the first (highest) ancestor that has it. if let Some(ancestors_min_slot) = ancestors.min_slot() { - // Iterate every slot in the range in descending order - // Grab a read lock on flushing roots once before the loop to avoid locking/unlocking - // on every iteration. This lock ensures that the load call in the loop correctly - // identifies slots with status AncestorBeingFlushed. - let r_roots_being_flushed = self.roots_being_flushed.read().unwrap(); for slot in (ancestors_min_slot..=index_max_slot).rev() { - if ancestors.contains_key(&slot) { - if let Some(account) = self.load(slot, pubkey) { - // Need to check flush status of the slot even for ancestors, because - // there could be newer version of the account that has already been - // flushed - let slot_status = if r_roots_being_flushed.contains(&slot) { - SlotStatus::AncestorBeingFlushed - } else { - SlotStatus::Ancestor - }; - return Some((account, slot, slot_status)); - } + if ancestors.contains_key(&slot) + && let Some(account) = self.load(slot, pubkey) + { + return Some((account, slot)); } } - drop(r_roots_being_flushed); } // If the slot is not found in the ancestors fall back to searching roots. @@ -393,21 +362,13 @@ impl AccountsCache { .unwrap_or(index_max_slot) .min(index_max_slot); - let r_maybe_unflushed_roots = self.maybe_unflushed_roots.read().unwrap(); - for &slot in r_maybe_unflushed_roots.range(..=max_root_slot).rev() { + let r_unflushed_roots = self.unflushed_roots.read().unwrap(); + for &slot in r_unflushed_roots.range(..=max_root_slot).rev() { if let Some(account) = self.load(slot, pubkey) { - return Some((account, slot, SlotStatus::UnflushedRoot)); + return Some((account, slot)); } } - drop(r_maybe_unflushed_roots); - - let r_roots_being_flushed = self.roots_being_flushed.read().unwrap(); - for &slot in r_roots_being_flushed.range(..=max_root_slot).rev() { - if let Some(account) = self.load(slot, pubkey) { - return Some((account, slot, SlotStatus::RootBeingFlushed)); - } - } - drop(r_roots_being_flushed); + drop(r_unflushed_roots); // Found nothing, the version of the account in the cache must be on a different fork None @@ -418,42 +379,30 @@ impl AccountsCache { } pub fn add_root(&self, root: Slot) { - self.maybe_unflushed_roots.write().unwrap().insert(root); + self.unflushed_roots.write().unwrap().insert(root); } pub fn num_unflushed_roots(&self) -> usize { - self.maybe_unflushed_roots.read().unwrap().len() + self.unflushed_roots.read().unwrap().len() } - /// Atomically moves roots up to and including `max_root` (or all roots if `None`) out of - /// `maybe_unflushed_roots` and into `roots_being_flushed`, then returns them - /// - /// Panics if there are already roots being flushed (i.e. `end_flush_roots` was not called after - /// a previous `begin_flush_roots`) - pub fn begin_flush_roots(&self, max_root: Option) -> BTreeSet { - let mut w_maybe_unflushed_roots = self.maybe_unflushed_roots.write().unwrap(); - let mut w_roots_being_flushed = self.roots_being_flushed.write().unwrap(); - - assert!( - w_roots_being_flushed.is_empty(), - "begin_flush_roots called while roots are already being flushed; end_flush_roots must \ - be called first" - ); - - let roots_to_flush = if let Some(max_root) = max_root { - let remaining = w_maybe_unflushed_roots.split_off(&(max_root + 1)); - std::mem::replace(&mut *w_maybe_unflushed_roots, remaining) - } else { - std::mem::take(&mut *w_maybe_unflushed_roots) - }; - - *w_roots_being_flushed = roots_to_flush.clone(); - roots_to_flush + /// Returns whether `slot` is a root that has been added but not yet flushed to storage. + pub fn contains_unflushed_root(&self, slot: Slot) -> bool { + self.unflushed_roots.read().unwrap().contains(&slot) } - /// Signals that flushing is complete. Clears the roots that were staged by `begin_flush_roots` - pub fn end_flush_roots(&self) { - self.roots_being_flushed.write().unwrap().clear(); + /// Returns the unflushed roots up to and including `max_root` (or all roots if `None`). The + /// returned roots remain tracked as unflushed until `remove_slot` drops each one when its cache + /// is flushed. + pub fn roots_to_flush(&self, max_root: Option) -> BTreeSet { + // `None` means no upper bound, i.e. every root. + let max_root = max_root.unwrap_or(Slot::MAX); + self.unflushed_roots + .read() + .unwrap() + .range(..=max_root) + .copied() + .collect() } pub fn cached_frozen_slots(&self) -> Vec { @@ -493,6 +442,12 @@ impl AccountsCache { self.max_flushed_root.get() } + /// Stop tracking `slot` as an unflushed root without touching its cache. Used when a flushed + /// root had no cache for `remove_slot` to drop (e.g. genesis), so it would otherwise linger. + pub fn remove_unflushed_root(&self, slot: Slot) { + self.unflushed_roots.write().unwrap().remove(&slot); + } + pub fn set_max_flush_root(&self, root: Slot) { self.max_flushed_root.fetch_max(root); } @@ -683,43 +638,39 @@ mod tests { assert!(cache.index.max_slot_for_pubkey(&pk2).is_some()); // Remove slot 1 — pk2 should disappear, pk1 still present (in slot 3) - cache.remove_slot(1); + let pubkeys_removed = cache.remove_slot(1); + assert_eq!(pubkeys_removed, Some(vec![pk2])); assert!(cache.index.max_slot_for_pubkey(&pk1).is_some()); assert!(cache.index.max_slot_for_pubkey(&pk2).is_none()); // Remove slot 3 — pk1 should also disappear - cache.remove_slot(3); + let pubkeys_removed = cache.remove_slot(3); + assert_eq!(pubkeys_removed, Some(vec![pk1])); assert!(cache.index.max_slot_for_pubkey(&pk1).is_none()); } - /// Tests that `load_latest` returns the correct slot, status, and account value - /// given various combinations of ancestor slots, root slots, and flushing state. + /// Tests that `load_latest` returns the correct slot and account value + /// given various combinations of ancestor slots and root slots. /// /// Ancestors always take priority over roots regardless of slot // None case // `uncached_ancestors` are slots added to the Ancestors set but with no account // data stored in the cache. This lets us test root bounding by min_slot // without the ancestor path short-circuiting the lookup. - #[test_case(&[], &[], &[], &[], None; "not ancestor not root")] - #[test_case(&[10], &[], &[], &[], Some((10, SlotStatus::Ancestor)); "ancestor only")] - #[test_case(&[5, 10, 15], &[], &[], &[], Some((15, SlotStatus::Ancestor)); "highest ancestor returned")] - #[test_case(&[], &[10, 20], &[], &[], Some((20, SlotStatus::UnflushedRoot)); "rooted, with no ancestors")] - #[test_case(&[], &[], &[10], &[], Some((10, SlotStatus::RootBeingFlushed)); "root being flushed")] - #[test_case(&[10], &[], &[10], &[], Some((10, SlotStatus::AncestorBeingFlushed)); "ancestor being flushed")] - #[test_case(&[5], &[20], &[], &[], Some((5, SlotStatus::Ancestor)); "ancestor wins over higher root")] - #[test_case(&[], &[20], &[10], &[], Some((20, SlotStatus::UnflushedRoot));"unflushed root over flushing root")] - #[test_case(&[5], &[20], &[10], &[], Some((5, SlotStatus::Ancestor));"ancestor over unflushed and flushing roots")] + #[test_case(&[], &[], &[], None; "not ancestor not root")] + #[test_case(&[10], &[], &[], Some(10); "ancestor only")] + #[test_case(&[5, 10, 15], &[], &[], Some(15); "highest ancestor returned")] + #[test_case(&[], &[10, 20], &[], Some(20); "rooted, with no ancestors")] + #[test_case(&[5], &[20], &[], Some(5); "ancestor wins over higher root")] // Root beyond ancestors.min_slot() is excluded; older root still found - #[test_case(&[], &[5, 11], &[], &[10], Some((5, SlotStatus::UnflushedRoot)); "unflushed root beyond min ancestor excluded")] - #[test_case(&[], &[], &[5, 11], &[10], Some((5, SlotStatus::RootBeingFlushed)); "flushing root beyond min ancestor excluded")] + #[test_case(&[], &[5, 11], &[10], Some(5); "root beyond min ancestor excluded")] // Root within min_slot bound is still returned - #[test_case(&[], &[10], &[], &[15], Some((10, SlotStatus::UnflushedRoot)); "unflushed root below min ancestor returned")] + #[test_case(&[], &[10], &[15], Some(10); "root below min ancestor returned")] fn test_load_latest_slot_priority( ancestor_slots: &[Slot], - unflushed_root_slots: &[Slot], - flushing_root_slots: &[Slot], + root_slots: &[Slot], uncached_ancestors: &[Slot], - expected: Option<(Slot, SlotStatus)>, + expected: Option, ) { let cache = AccountsCache::default(); let pk = Pubkey::new_unique(); @@ -731,7 +682,7 @@ mod tests { AccountSharedData::new(slot, 0, &Pubkey::default()), ); } - for &slot in unflushed_root_slots.iter().chain(flushing_root_slots) { + for &slot in root_slots { cache.store( slot, &pk, @@ -739,19 +690,14 @@ mod tests { ); cache.add_root(slot); } - if let Some(&max) = flushing_root_slots.iter().max() { - cache.begin_flush_roots(Some(max)); - } let mut all_ancestors: Vec = ancestor_slots.to_vec(); all_ancestors.extend_from_slice(uncached_ancestors); let ancestors = Ancestors::from(all_ancestors); - let result = cache - .load_latest(&pk, &ancestors) - .map(|(account, slot, status)| { - assert_eq!(account.account.lamports(), slot); - (slot, status) - }); + let result = cache.load_latest(&pk, &ancestors).map(|(account, slot)| { + assert_eq!(account.account.lamports(), slot); + slot + }); assert_eq!(result, expected); } @@ -775,115 +721,63 @@ mod tests { cache.store(10, &pk, AccountSharedData::new(100, 0, &Pubkey::default())); cache.add_root(10); - cache.begin_flush_roots(None); - cache.end_flush_roots(); + // A flush finishes a slot with `remove_slot`, which drops both its cache and its + // unflushed-root tracking; call it directly here to stand in for that flush. + let _ = cache.remove_slot(10); - // After clearing flushed roots, slot is no longer visible + // With the slot gone from the cache and untracked as a root, it is not visible. let empty = Ancestors::default(); assert!(cache.load_latest(&pk, &empty).is_none()); + assert!(cache.unflushed_roots.read().unwrap().is_empty()); } #[test] - fn test_begin_flush_roots_with_max_root() { + fn test_roots_to_flush_with_max_root() { let cache = AccountsCache::default(); cache.add_root(1); cache.add_root(3); cache.add_root(5); cache.add_root(7); - // begin_flush_roots(Some(5)) should return {1, 3, 5} and leave {7} - let taken = cache.begin_flush_roots(Some(5)); - assert_eq!(taken, BTreeSet::from([1, 3, 5])); - - // Remaining unflushed roots should only contain 7 - assert_eq!(cache.maybe_unflushed_roots.read().unwrap().len(), 1); - assert!(cache.maybe_unflushed_roots.read().unwrap().contains(&7)); + // roots_to_flush(Some(5)) returns {1, 3, 5}, excluding 7. + let to_flush = cache.roots_to_flush(Some(5)); + assert_eq!(to_flush, BTreeSet::from([1, 3, 5])); - // Taken roots should now be in roots_being_flushed - assert!(cache.roots_being_flushed.read().unwrap().contains(&1)); - assert!(cache.roots_being_flushed.read().unwrap().contains(&3)); - assert!(cache.roots_being_flushed.read().unwrap().contains(&5)); - assert!(!cache.roots_being_flushed.read().unwrap().contains(&7)); - - cache.end_flush_roots(); + // roots_to_flush only reads: the tracked roots are unchanged. + assert_eq!( + *cache.unflushed_roots.read().unwrap(), + BTreeSet::from([1, 3, 5, 7]) + ); } #[test] - fn test_begin_flush_roots_none_takes_all() { + fn test_roots_to_flush_none_takes_all() { let cache = AccountsCache::default(); cache.add_root(2); cache.add_root(4); cache.add_root(6); - let taken = cache.begin_flush_roots(None); - assert_eq!(taken, BTreeSet::from([2, 4, 6])); + let to_flush = cache.roots_to_flush(None); + assert_eq!(to_flush, BTreeSet::from([2, 4, 6])); - // All unflushed roots should be drained - assert!(cache.maybe_unflushed_roots.read().unwrap().is_empty()); - - // All should be in roots_being_flushed + // roots_to_flush only reads: the tracked roots are unchanged. assert_eq!( - *cache.roots_being_flushed.read().unwrap(), + *cache.unflushed_roots.read().unwrap(), BTreeSet::from([2, 4, 6]) ); - - cache.end_flush_roots(); - } - - #[test] - #[should_panic(expected = "begin_flush_roots called while roots are already being flushed")] - fn test_begin_flush_roots_panics_if_already_flushing() { - let cache = AccountsCache::default(); - cache.add_root(1); - cache.begin_flush_roots(None); - // Calling again without end_flush_roots should panic - cache.add_root(2); - cache.begin_flush_roots(None); } #[test] - fn test_end_flush_roots_allows_next_flush() { + fn test_remove_slot_drops_unflushed_root() { let cache = AccountsCache::default(); + let pk = Pubkey::new_unique(); + cache.store(1, &pk, AccountSharedData::new(1, 0, &Pubkey::default())); cache.add_root(1); + cache.store(2, &pk, AccountSharedData::new(2, 0, &Pubkey::default())); cache.add_root(2); - // First cycle - let taken = cache.begin_flush_roots(None); - assert_eq!(taken, BTreeSet::from([1, 2])); - - cache.end_flush_roots(); - - // After clear, roots_being_flushed is empty - assert!(cache.roots_being_flushed.read().unwrap().is_empty()); - - // Second cycle works without panic - cache.add_root(3); - let taken = cache.begin_flush_roots(None); - assert_eq!(taken, BTreeSet::from([3])); - cache.end_flush_roots(); - } - - #[test] - fn test_load_latest_multiple_ancestors_one_being_flushed() { - let cache = AccountsCache::default(); - let pk = Pubkey::new_unique(); - - // Store at slots 5 and 10, both will be ancestors - cache.store(5, &pk, AccountSharedData::new(5, 0, &Pubkey::default())); - cache.store(10, &pk, AccountSharedData::new(10, 0, &Pubkey::default())); - - // Slot 10 is also a root that gets claimed for flushing - cache.add_root(10); - cache.begin_flush_roots(None); - - let ancestors = Ancestors::from(vec![5, 10]); - let (account, slot, status) = cache.load_latest(&pk, &ancestors).unwrap(); - - // Slot 10 is highest ancestor but is being flushed, so should return AncestorBeingFlushed - assert_eq!(slot, 10); - assert_eq!(status, SlotStatus::AncestorBeingFlushed); - assert_eq!(account.account.lamports(), 10); - - cache.end_flush_roots(); + // remove_slot drops slot 1 from both the cache and the tracked roots, leaving slot 2. + let _ = cache.remove_slot(1); + assert_eq!(*cache.unflushed_roots.read().unwrap(), BTreeSet::from([2])); } } diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index a344c4fbd97..a8fb9283f0e 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -36,19 +36,19 @@ use { stored_account_info::{StoredAccountInfo, StoredAccountInfoWithoutData}, }, account_storage_entry::AccountStorageEntry, - accounts_cache::{AccountsCache, CachedAccount, SlotCache, SlotStatus}, + accounts_cache::{AccountsCache, CachedAccount, SlotCache}, accounts_db::stats::{ AccountsStats, CleanAccountsStats, FlushStats, LoadAccountsStats, ObsoleteAccountsStats, PurgeStats, ShrinkAncientStats, ShrinkStats, ShrinkStatsSub, - StoreAccountsFrozenStats, StoreAccountsTiming, StoreAccountsUnfrozenStats, - WriteAccountsToCacheStats, + StoreAccountsForFlushStats, StoreAccountsForShrinkStats, StoreAccountsForSquashStats, + StoreAccountsUnfrozenStats, WriteAccountsToCacheStats, }, accounts_file::{AccountsFile, AccountsFileProvider}, accounts_hash::{AccountLtHash, AccountsLtHash, ZERO_LAMPORT_ACCOUNT_LT_HASH}, accounts_index::{ - AccountSecondaryIndexes, AccountsIndex, AccountsIndexRootsStats, - AccountsIndexScanResult, IndexKey, IsCached, ReclaimsSlotList, RefCount, ScanFilter, - SlotList, Startup, UpsertReclaim, in_mem_accounts_index::StartupStats, + AccountSecondaryIndexes, AccountsIndex, AccountsIndexScanResult, IndexKey, + ReclaimsSlotList, RefCount, ScanFilter, SlotList, Startup, UpsertReclaim, + in_mem_accounts_index::StartupStats, }, accounts_scan::{ScanConfig, ScanError, ScanGuard, ScanResult, ScanTracker}, accounts_update_notifier_interface::{AccountForGeyser, AccountsUpdateNotifier}, @@ -64,6 +64,7 @@ use { utils::{self, create_account_shared_data}, }, agave_fs::buffered_reader::RequiredLenBufFileRead, + ahash::HashMapExt, bv::BitVec, dashmap::{DashMap, DashSet}, log::*, @@ -98,7 +99,7 @@ use { }; // when the accounts write cache exceeds this many bytes, we will flush it -// this can be specified on the command line, too (--accounts-db-cache-limit-mb) +// this can be specified on the command line, too (--accounts-db-write-cache-limit) const WRITE_CACHE_LIMIT_BYTES_DEFAULT: u64 = 15_000_000_000; const SCAN_SLOT_PAR_ITER_THRESHOLD: usize = 4000; @@ -107,7 +108,7 @@ const UNREF_ACCOUNTS_BATCH_SIZE: usize = 10_000; const DEFAULT_NUM_DIRS: u32 = 4; // This value reflects recommended memory lock limit documented in the validator's -// setup instructions at docs/src/operations/guides/validator-start.md allowing use of +// setup instructions at https://docs.anza.xyz/operations/guides/validator-start allowing use of // several io_uring instances with fixed buffers for large disk IO operations. pub const TOTAL_IO_URING_BUFFERS_SIZE_LIMIT: usize = 2_000_000_000; @@ -324,7 +325,6 @@ pub struct GetUniqueAccountsResult { } pub struct AccountsAddRootTiming { - pub index_us: u64, pub cache_us: u64, } @@ -650,15 +650,13 @@ pub enum PopulateReadCache { } #[derive(Debug)] -pub enum LoadedAccountAccessor<'a> { +pub enum LoadedAccountAccessor { // StoredAccountInfo can't be held directly here due to its lifetime dependency on // AccountStorageEntry Stored(Option<(Arc, usize)>), - // None value in Cached variant means the cache was flushed - Cached(Option>>), } -impl LoadedAccountAccessor<'_> { +impl LoadedAccountAccessor { fn check_and_get_loaded_account_shared_data(&mut self) -> AccountSharedData { // all of these following .expect() and .unwrap() are like serious logic errors, // ideal for representing this as rust type system.... @@ -690,17 +688,12 @@ impl LoadedAccountAccessor<'_> { // ideal for representing this as rust type system.... match self { - LoadedAccountAccessor::Cached(None) | LoadedAccountAccessor::Stored(None) => { + LoadedAccountAccessor::Stored(None) => { panic!( "Should have already been taken care of when creating this \ LoadedAccountAccessor" ); } - LoadedAccountAccessor::Cached(Some(_cached_account)) => { - // Cached(Some(x)) variant always produces `Some` for get_loaded_account() since - // it just returns the inner `x` without additional fetches - self.get_loaded_account(callback).unwrap() - } LoadedAccountAccessor::Stored(Some(_maybe_storage_entry)) => { // If we do find the storage entry, we can guarantee that the storage entry is // safe to read from because we grabbed a reference to the storage entry while it @@ -720,12 +713,6 @@ impl LoadedAccountAccessor<'_> { mut callback: impl for<'local> FnMut(LoadedAccount<'local>) -> T, ) -> Option { match self { - LoadedAccountAccessor::Cached(cached_account) => { - let cached_account = cached_account.take().expect( - "Cache flushed/purged should be handled before trying to fetch account", - ); - Some(callback(LoadedAccount::Cached(cached_account))) - } LoadedAccountAccessor::Stored(maybe_storage_entry) => { // storage entry may not be present if slot was cleaned up in // between reading the accounts index and calling this function to @@ -821,6 +808,8 @@ struct CleanKeyTimings { delta_key_count: u64, dirty_pubkeys_count: u64, oldest_dirty_slot: Slot, + zero_lamport_single_ref_slots_added_to_shrink_count: u64, + zero_lamport_sweep_us: u64, } pub fn get_temp_accounts_paths(count: u32) -> io::Result<(Vec, Vec)> { @@ -902,8 +891,6 @@ pub struct AccountsDb { /// directory for bank hash details files bank_hash_details_dir: PathBuf, - shrink_paths: Vec, - /// Directory of paths this accounts_db needs to hold/remove #[allow(dead_code)] pub temp_paths: Option>, @@ -921,9 +908,6 @@ pub struct AccountsDb { /// Stats from storing accounts unfrozen store_accounts_unfrozen_stats: StoreAccountsUnfrozenStats, - /// Stats from storing accounts frozen - store_accounts_frozen_stats: StoreAccountsFrozenStats, - clean_accounts_stats: CleanAccountsStats, // Stats for purges called outside of clean_accounts() @@ -962,6 +946,10 @@ pub struct AccountsDb { /// for incremental snapshot support. zero_lamport_accounts_to_purge_after_full_snapshot: DashSet<(Slot, Pubkey)>, + /// Set by `set_latest_full_snapshot_slot` when the snapshot advances. Read and cleared by + /// clean + latest_full_snapshot_slot_advanced_since_clean: AtomicBool, + /// GeyserPlugin accounts update notifier accounts_update_notifier: Option, @@ -984,12 +972,19 @@ pub struct AccountsDb { /// Note, this is None if we're told to *not* take snapshots latest_full_snapshot_slot: SeqLock>, + /// The full snapshot slot we last swept for zero-lamport-single-ref shrink + /// eligibility. + last_swept_full_snapshot_slot: AtomicU64, + /// These are the ancient storages that could be valuable to /// shrink, sorted by amount of dead bytes. The elements /// are sorted from the largest dead bytes to the smallest. /// Members are Slot and capacity. If capacity is smaller, then /// that means the storage was already shrunk. pub(crate) best_ancient_slots_to_shrink: RwLock>, + + /// The largest slot that has been added as a root via `add_root`. + max_root: AtomicU64, } pub fn quarter_thread_count() -> usize { @@ -1057,11 +1052,6 @@ impl AccountsDb { (paths, None) }; - let shrink_paths = accounts_db_config - .shrink_paths - .clone() - .unwrap_or_else(|| paths.clone()); - let read_cache_size = accounts_db_config.read_cache_limit_bytes.unwrap_or(( Self::DEFAULT_MAX_READ_ONLY_CACHE_DATA_SIZE_LO, Self::DEFAULT_MAX_READ_ONLY_CACHE_DATA_SIZE_HI, @@ -1102,7 +1092,6 @@ impl AccountsDb { paths, bank_hash_details_dir: accounts_db_config.bank_hash_details_dir, temp_paths, - shrink_paths, skip_initial_hash_calc: accounts_db_config.skip_initial_hash_calc, ancient_append_vec_offset: accounts_db_config .ancient_append_vec_offset @@ -1143,7 +1132,6 @@ impl AccountsDb { stats: AccountsStats::default(), load_account_stats: LoadAccountsStats::default(), store_accounts_unfrozen_stats: StoreAccountsUnfrozenStats::default(), - store_accounts_frozen_stats: StoreAccountsFrozenStats::default(), #[cfg(test)] load_delay: u64::default(), #[cfg(test)] @@ -1151,9 +1139,12 @@ impl AccountsDb { is_bank_drop_callback_enabled: AtomicBool::default(), dirty_stores: DashMap::default(), zero_lamport_accounts_to_purge_after_full_snapshot: DashSet::default(), + latest_full_snapshot_slot_advanced_since_clean: AtomicBool::default(), accounts_file_provider: AccountsFileProvider::default(), latest_full_snapshot_slot: SeqLock::new(None), + last_swept_full_snapshot_slot: AtomicU64::new(0), best_ancient_slots_to_shrink: RwLock::default(), + max_root: AtomicU64::new(0), }; { @@ -1409,20 +1400,17 @@ impl AccountsDb { /// get the oldest slot that is within one epoch of the highest known root. /// The slot will have been offset by `self.ancient_append_vec_offset` fn get_oldest_non_ancient_slot(&self, epoch_schedule: &EpochSchedule) -> Slot { - self.get_oldest_non_ancient_slot_from_slot( - epoch_schedule, - self.accounts_index.max_root_inclusive(), - ) + self.get_oldest_non_ancient_slot_from_slot(epoch_schedule, self.max_root()) } - /// get the oldest slot that is within one epoch of `max_root_inclusive`. + /// get the oldest slot that is within one epoch of `max_root`. /// The slot will have been offset by `self.ancient_append_vec_offset` fn get_oldest_non_ancient_slot_from_slot( &self, epoch_schedule: &EpochSchedule, - max_root_inclusive: Slot, + max_root: Slot, ) -> Slot { - let mut result = max_root_inclusive; + let mut result = max_root; if let Some(offset) = self.ancient_append_vec_offset { result = Self::apply_offset_to_slot(result, offset); } @@ -1430,18 +1418,20 @@ impl AccountsDb { result, -((epoch_schedule.slots_per_epoch as i64).saturating_sub(1)), ); - result.min(max_root_inclusive) + result.min(max_root) } /// Collect all the uncleaned slots, up to a max slot /// /// Search through the uncleaned Pubkeys and return all the slots, up to a maximum slot. - fn collect_uncleaned_slots_up_to_slot(&self, max_slot_inclusive: Slot) -> Vec { + fn collect_uncleaned_slots_up_to_slot(&self, max_slot_inclusive: Option) -> Vec { self.uncleaned_pubkeys .iter() .filter_map(|entry| { let slot = *entry.key(); - (slot <= max_slot_inclusive).then_some(slot) + max_slot_inclusive + .is_none_or(|max_slot_inclusive| slot <= max_slot_inclusive) + .then_some(slot) }) .collect() } @@ -1451,7 +1441,7 @@ impl AccountsDb { /// pubkeys to `candidates` for cleaning. fn remove_uncleaned_slots_up_to_slot_and_move_pubkeys( &self, - max_slot_inclusive: Slot, + max_slot_inclusive: Option, candidates: &[RwLock>], ) { let uncleaned_slots = self.collect_uncleaned_slots_up_to_slot(max_slot_inclusive); @@ -1516,14 +1506,14 @@ impl AccountsDb { timings: &mut CleanKeyTimings, ) -> CleaningCandidates { let mut dirty_store_processing_time = Measure::start("dirty_store_processing"); - let max_root_inclusive = self.accounts_index.max_root_inclusive(); - let max_slot_inclusive = max_clean_root_inclusive.unwrap_or(max_root_inclusive); let mut dirty_stores = Vec::with_capacity(self.dirty_stores.len()); // find the oldest dirty slot // we'll add logging if that append vec cannot be marked dead let mut min_dirty_slot = None::; self.dirty_stores.retain(|slot, store| { - if *slot > max_slot_inclusive { + if max_clean_root_inclusive + .is_some_and(|max_clean_root_inclusive| *slot > max_clean_root_inclusive) + { true } else { min_dirty_slot = min_dirty_slot.map(|min| min.min(*slot)).or(Some(*slot)); @@ -1547,15 +1537,14 @@ impl AccountsDb { .might_contain_zero_lamport_entry |= is_zero_lamport; }; - let mut dirty_store_routine = || { + // `min_dirty_slot` (computed above) already holds the oldest dirty slot over this same set. + timings.oldest_dirty_slot = min_dirty_slot.unwrap_or_default(); + let dirty_store_routine = || { let chunk_size = 1.max(dirty_stores_len.saturating_div(rayon::current_num_threads())); - let oldest_dirty_slots: Vec = dirty_stores + dirty_stores .par_chunks(chunk_size) - .map(|dirty_store_chunk| { - let mut oldest_dirty_slot = max_slot_inclusive.saturating_add(1); - dirty_store_chunk.iter().for_each(|(slot, store)| { - oldest_dirty_slot = oldest_dirty_slot.min(*slot); - + .for_each(|dirty_store_chunk| { + dirty_store_chunk.iter().for_each(|(_slot, store)| { store .accounts .scan_accounts_without_data(|_offset, account| { @@ -1565,13 +1554,7 @@ impl AccountsDb { }) .expect("must scan accounts storage"); }); - oldest_dirty_slot - }) - .collect(); - timings.oldest_dirty_slot = *oldest_dirty_slots - .iter() - .min() - .unwrap_or(&max_slot_inclusive.saturating_add(1)); + }); }; if is_startup { @@ -1591,58 +1574,123 @@ impl AccountsDb { timings.dirty_store_processing_us += dirty_store_processing_time.as_us(); let mut collect_delta_keys = Measure::start("key_create"); - self.remove_uncleaned_slots_up_to_slot_and_move_pubkeys(max_slot_inclusive, &candidates); + self.remove_uncleaned_slots_up_to_slot_and_move_pubkeys( + max_clean_root_inclusive, + &candidates, + ); collect_delta_keys.stop(); timings.collect_delta_keys_us += collect_delta_keys.as_us(); timings.delta_key_count = Self::count_pubkeys(&candidates); - // Check if we should purge any of the - // zero_lamport_accounts_to_purge_later, based on the - // latest_full_snapshot_slot. - let latest_full_snapshot_slot = self.latest_full_snapshot_slot(); - assert!( - latest_full_snapshot_slot.is_some() + debug_assert!( + self.latest_full_snapshot_slot().is_some() || self .zero_lamport_accounts_to_purge_after_full_snapshot .is_empty(), "if snapshots are disabled, then zero_lamport_accounts_to_purge_later should always \ be empty" ); - if let Some(latest_full_snapshot_slot) = latest_full_snapshot_slot { + + // Cleaning up zero lamport accounts is gated by a full snapshot because they need to be + // retained for incremental snapshots. Once a full snapshot occurs, drain the list and + // search for newly shrinkable storages. + if self + .latest_full_snapshot_slot_advanced_since_clean + .swap(false, Ordering::Acquire) + && let Some(latest_full_snapshot_slot) = self.latest_full_snapshot_slot() + { self.zero_lamport_accounts_to_purge_after_full_snapshot .retain(|(slot, pubkey)| { - let is_candidate_for_clean = - max_slot_inclusive >= *slot && latest_full_snapshot_slot >= *slot; + let is_candidate_for_clean = max_clean_root_inclusive + .is_none_or(|max_clean_root_inclusive| max_clean_root_inclusive >= *slot) + && latest_full_snapshot_slot >= *slot; if is_candidate_for_clean { insert_candidate(*pubkey, true); } !is_candidate_for_clean }); + + let last_swept_full_snapshot_slot = + self.last_swept_full_snapshot_slot.load(Ordering::Relaxed); + let (added_to_shrink_count, sweep_us) = + measure_us!(self.check_shrink_eligibility_after_snapshot( + last_swept_full_snapshot_slot, + latest_full_snapshot_slot + )); + timings.zero_lamport_single_ref_slots_added_to_shrink_count += added_to_shrink_count; + timings.zero_lamport_sweep_us += sweep_us; } (candidates, min_dirty_slot) } + /// Loop through slots in `[last_swept_full_snapshot_slot + 1, latest_full_snapshot_slot]` and + /// check each storage to determine if it is eligible for shrink, since zero-lamport single-ref + /// accounts become shrinkable after a full snapshot advances past their slot. Advances the + /// `last_swept_full_snapshot_slot` to `latest_full_snapshot_slot` on completion. + /// + /// Returns the count of storages that were added to the shrink candidates set + fn check_shrink_eligibility_after_snapshot( + &self, + last_swept_full_snapshot_slot: Slot, + latest_full_snapshot_slot: Slot, + ) -> u64 { + let start = last_swept_full_snapshot_slot.saturating_add(1); + + let mut added_to_shrink_count = 0; + // Held for the full scan. Safe because the only paths that take this lock in production + // validator code run in earlier/later phases of the same AccountsBackgroundService + // iteration, never concurrently with clean_accounts. + let mut shrink_candidates = self.shrink_candidate_slots.lock().unwrap(); + for slot in start..=latest_full_snapshot_slot { + if let Some(store) = self.storage.get_slot_storage_entry(slot) + && self.is_shrinking_productive(&store) + && self.is_candidate_for_shrink(&store) + && shrink_candidates.insert(slot) + { + added_to_shrink_count += 1; + } + } + drop(shrink_candidates); + + self.last_swept_full_snapshot_slot + .store(latest_full_snapshot_slot, Ordering::Relaxed); + added_to_shrink_count + } + /// called with cli argument to verify refcounts are correct on all accounts /// this is very slow /// this function will call Rayon par_iter, so you will want to have thread pool installed if /// you want to call this without consuming all the cores on the CPU. fn exhaustively_verify_refcounts(&self, max_slot_inclusive: Option) { - let max_slot_inclusive = - max_slot_inclusive.unwrap_or_else(|| self.accounts_index.max_root_inclusive()); - info!("exhaustively verifying refcounts as of slot: {max_slot_inclusive}"); + info!("exhaustively verifying refcounts as of slot: {max_slot_inclusive:?}"); let pubkey_refcount = DashMap::>::default(); let mut storages = self.storage.all_storages(); - storages.retain(|s| s.slot() <= max_slot_inclusive); + // Flush is not running while we verify, so storages are stable. With no slot bound we + // verify every storage; otherwise we drop storages newer than the bound. + if let Some(max_slot_inclusive) = max_slot_inclusive { + storages.retain(|s| s.slot() <= max_slot_inclusive); + } // populate storages.par_iter().for_each_init( || Box::new(append_vec::new_scan_accounts_reader()), |reader, storage| { let slot = storage.slot(); + // Obsolete accounts are skipped during index generation, so they do not + // contribute to the index refcount. Skip them here too, otherwise we would count + // a physical copy the index never tracked and report a spurious mismatch. + let obsolete_accounts: IntSet<_> = storage + .obsolete_accounts_read_lock() + .filter_obsolete_accounts(None) + .map(|(offset, _)| offset) + .collect(); storage .accounts - .scan_accounts(reader.as_mut(), |_offset, account| { + .scan_accounts(reader.as_mut(), |offset, account| { + if obsolete_accounts.contains(&offset) { + return; + } let pk = account.pubkey(); match pubkey_refcount.entry(*pk) { dashmap::mapref::entry::Entry::Occupied(mut occupied_entry) => { @@ -1686,7 +1734,11 @@ impl AccountsDb { let slot_list = index_entry.slot_list_read_lock(); let num_too_new = slot_list .iter() - .filter(|(slot, _)| slot > &max_slot_inclusive) + .filter(|(slot, _)| { + max_slot_inclusive.is_some_and( + |max_slot_inclusive| *slot > max_slot_inclusive, + ) + }) .count(); if ((index_entry.ref_count() as usize) - num_too_new) @@ -1811,7 +1863,7 @@ impl AccountsDb { // Add all the rooted entries that contain this pubkey. // We know the highest rooted entry is zero lamports. candidate_info.slot_list = - self.accounts_index.get_rooted_entries( + self.accounts_index.get_entries_up_to_inclusive( slot_list, max_clean_root_inclusive, ); @@ -1948,13 +2000,6 @@ impl AccountsDb { } else { let mut key_set = HashSet::new(); key_set.insert(*pubkey); - assert!( - !account_info.is_cached(), - "The Accounts Cache must be flushed first for this account info. \ - pubkey: {}, slot: {}", - *pubkey, - *slot - ); let count = self .storage .get_account_storage_entry(*slot, account_info.store_id()) @@ -2067,6 +2112,12 @@ impl AccountsDb { ("delta_insert_us", key_timings.delta_insert_us, i64), ("delta_key_count", key_timings.delta_key_count, i64), ("dirty_pubkeys_count", key_timings.dirty_pubkeys_count, i64), + ( + "zero_lamport_single_ref_slots_added_to_shrink_count", + key_timings.zero_lamport_single_ref_slots_added_to_shrink_count, + i64 + ), + ("zero_lamport_sweep_us", key_timings.zero_lamport_sweep_us, i64), ("useful_keys", useful_accum.load(Ordering::Relaxed), i64), ("total_keys_count", num_candidates, i64), ("retained_keys_count", retained_keys_count, i64), @@ -2131,8 +2182,10 @@ impl AccountsDb { i64 ), ( - "roots_added", - self.accounts_index.roots_added.swap(0, Ordering::Relaxed), + "clean_unref_from_storage_us", + self.clean_accounts_stats + .clean_unref_from_storage_us + .swap(0, Ordering::Relaxed), i64 ), ( @@ -2142,11 +2195,6 @@ impl AccountsDb { .swap(0, Ordering::Relaxed), i64 ), - ( - "roots_removed", - self.accounts_index.roots_removed.swap(0, Ordering::Relaxed), - i64 - ), ( "active_scans", self.scan_tracker.active_scans.load(Ordering::Relaxed), @@ -2344,9 +2392,6 @@ impl AccountsDb { ); } - // Remove dead slots from the accounts index root tracker - self.remove_dead_slots_metadata(dead_slots.iter()); - clean_dead_slots.stop(); let mut purge_removed_slots = Measure::start("reclaims::purge_removed_slots"); @@ -2381,6 +2426,8 @@ impl AccountsDb { stats: &ShrinkStats, slot_to_shrink: Slot, ) -> LoadAccountsIndexForShrink<'a, T> { + let can_purge_zero_lamport_single_ref = + self.can_purge_zero_lamport_single_ref_after_shrink(slot_to_shrink); let count = accounts.len(); let mut alive_accounts = T::with_capacity(count, slot_to_shrink); let mut pubkeys_to_unref = Vec::with_capacity(count); @@ -2392,7 +2439,6 @@ impl AccountsDb { let mut index_scan_returned_some_count = 0; let mut index_scan_returned_none_count = 0; let mut all_are_zero_lamports = true; - let latest_full_snapshot_slot = self.latest_full_snapshot_slot(); self.accounts_index.scan( accounts.iter().map(|account| account.pubkey()), |pubkey, slots_refs| { @@ -2400,11 +2446,7 @@ impl AccountsDb { let mut do_populate_accounts_for_shrink = |ref_count, slot_list| { if stored_account.is_zero_lamport() && ref_count == 1 - && latest_full_snapshot_slot - .map(|latest_full_snapshot_slot| { - latest_full_snapshot_slot >= slot_to_shrink - }) - .unwrap_or(true) + && can_purge_zero_lamport_single_ref { // only do this if our slot is prior to the latest full snapshot // we found a zero lamport account that is the only instance of this account. We can delete it completely. @@ -2622,6 +2664,7 @@ impl AccountsDb { /// These accounts were found during shrink of `slot` to be slot_list=[slot] and ref_count == 1 and lamports = 0. /// This means this slot contained the only account data for this pubkey and it is zero lamport. + /// And also `slot` is <= the latest full snapshot slot, and can purge zero lamport accounts. /// Thus, we did NOT treat this as an alive account, so we did NOT copy the zero lamport account to the new /// storage. So, the account will no longer be alive or exist at `slot`. /// So, first, remove the ref count since this newly shrunk storage will no longer access it. @@ -2727,15 +2770,12 @@ impl AccountsDb { match slot_refs { Some((slot_list, ref_count)) => { // Let's handle the special case - after unref, the result is a single ref zero lamport account. - if slot_list.len() == 1 && ref_count == 2 { - if let Some((slot_alive, acct_info)) = slot_list.first() { - if acct_info.is_zero_lamport() && !acct_info.is_cached() { - self.zero_lamport_single_ref_found( - *slot_alive, - acct_info.offset(), - ); - } - } + if slot_list.len() == 1 + && ref_count == 2 + && let Some((slot_alive, acct_info)) = slot_list.first() + && acct_info.is_zero_lamport() + { + self.zero_lamport_single_ref_found(*slot_alive, acct_info.offset()); } } None => { @@ -2777,34 +2817,31 @@ impl AccountsDb { if let Some(store) = self .storage .get_slot_storage_entry_shrinking_in_progress_ok(slot) + && store.insert_zero_lamport_single_ref_account_offset(offset) { - if store.insert_zero_lamport_single_ref_account_offset(offset) { - // this wasn't previously marked as zero lamport single ref + // this wasn't previously marked as zero lamport single ref + self.shrink_stats + .num_zero_lamport_single_ref_accounts_found + .fetch_add(1, Ordering::Relaxed); + + if store.num_zero_lamport_single_ref_accounts() == store.count() { + // all accounts in this storage can be dead + self.dirty_stores.entry(slot).or_insert(store); self.shrink_stats - .num_zero_lamport_single_ref_accounts_found + .num_dead_slots_added_to_clean .fetch_add(1, Ordering::Relaxed); - - if store.num_zero_lamport_single_ref_accounts() == store.count() { - // all accounts in this storage can be dead - self.dirty_stores.entry(slot).or_insert(store); - self.shrink_stats - .num_dead_slots_added_to_clean - .fetch_add(1, Ordering::Relaxed); - } else if Self::is_shrinking_productive(&store) - && self.is_candidate_for_shrink(&store) - { - // this store might be eligible for shrinking now - let is_new = self.shrink_candidate_slots.lock().unwrap().insert(slot); - if is_new { - self.shrink_stats - .num_slots_with_zero_lamport_accounts_added_to_shrink - .fetch_add(1, Ordering::Relaxed); - } - } else { + } else if self.is_shrinking_productive(&store) && self.is_candidate_for_shrink(&store) { + // this store might be eligible for shrinking now + let is_new = self.shrink_candidate_slots.lock().unwrap().insert(slot); + if is_new { self.shrink_stats - .marking_zero_dead_accounts_in_non_shrinkable_store + .num_slots_with_zero_lamport_accounts_added_to_shrink .fetch_add(1, Ordering::Relaxed); } + } else { + self.shrink_stats + .marking_zero_dead_accounts_in_non_shrinkable_store + .fetch_add(1, Ordering::Relaxed); } } } @@ -2888,7 +2925,7 @@ impl AccountsDb { // mutating rooted slots; There should be no writers to them. let accounts = [(slot, &shrink_collect.alive_accounts.alive_accounts()[..])]; let storable_accounts = StorableAccountsBySlot::new(slot, &accounts, self); - stats_sub.store_accounts_timing = self.store_accounts_frozen( + stats_sub.store_accounts_stats = self.store_accounts_for_shrink( storable_accounts, shrink_in_progress.new_storage(), UpdateIndexThreadSelection::PoolWithThreshold, @@ -2897,7 +2934,7 @@ impl AccountsDb { rewrite_elapsed.stop(); stats_sub.rewrite_elapsed_us = Saturating(rewrite_elapsed.as_us()); - // `store_accounts_frozen()` above may have purged accounts from some + // `store_accounts_for_shrink()` above may have purged accounts from some // other storage entries (the ones that were just overwritten by this // new storage entry). This means some of those stores might have caused // this slot to be read to `self.shrink_candidate_slots`, so delete @@ -2913,48 +2950,10 @@ impl AccountsDb { self.reopen_storage_as_readonly_shrinking_in_progress_ok(slot); - Self::update_shrink_stats(&self.shrink_stats, stats_sub, true); + self.shrink_stats.accumulate_sub_stats(stats_sub, true); self.shrink_stats.report(); } - pub(crate) fn update_shrink_stats( - shrink_stats: &ShrinkStats, - stats_sub: ShrinkStatsSub, - increment_count: bool, - ) { - if increment_count { - shrink_stats - .num_slots_shrunk - .fetch_add(1, Ordering::Relaxed); - } - shrink_stats.create_and_insert_store_elapsed.fetch_add( - stats_sub.create_and_insert_store_elapsed_us.0, - Ordering::Relaxed, - ); - shrink_stats.store_accounts_elapsed.fetch_add( - stats_sub.store_accounts_timing.store_accounts_elapsed, - Ordering::Relaxed, - ); - shrink_stats.update_index_elapsed.fetch_add( - stats_sub.store_accounts_timing.update_index_elapsed, - Ordering::Relaxed, - ); - shrink_stats.handle_reclaims_elapsed.fetch_add( - stats_sub.store_accounts_timing.handle_reclaims_elapsed, - Ordering::Relaxed, - ); - shrink_stats - .rewrite_elapsed - .fetch_add(stats_sub.rewrite_elapsed_us.0, Ordering::Relaxed); - shrink_stats - .unpackable_slots_count - .fetch_add(stats_sub.unpackable_slots_count.0 as u64, Ordering::Relaxed); - shrink_stats.newest_alive_packed_count.fetch_add( - stats_sub.newest_alive_packed_count.0 as u64, - Ordering::Relaxed, - ); - } - /// get stores for 'slot' /// Drop 'shrink_in_progress', which will cause the old store to be removed from the storage map. /// For 'shrink_in_progress'.'old_storage' which is not retained, insert in 'dead_storages' and optionally 'dirty_stores' @@ -2993,17 +2992,16 @@ impl AccountsDb { if let Some(storage) = self .storage .get_slot_storage_entry_shrinking_in_progress_ok(slot) + && let Some(new_storage) = storage.reopen_as_readonly() { - if let Some(new_storage) = storage.reopen_as_readonly() { - // consider here the race condition of tx processing having looked up something in the index, - // which could return (slot, append vec id). We want the lookup for the storage to get a storage - // that works whether the lookup occurs before or after the replace call here. - // So, the two storages have to be exactly equivalent wrt offsets, counts, len, id, etc. - assert_eq!(storage.id(), new_storage.id()); - assert_eq!(storage.accounts.len(), new_storage.accounts.len()); - self.storage - .replace_storage_with_equivalent(slot, Arc::new(new_storage)); - } + // consider here the race condition of tx processing having looked up something in the index, + // which could return (slot, append vec id). We want the lookup for the storage to get a storage + // that works whether the lookup occurs before or after the replace call here. + // So, the two storages have to be exactly equivalent wrt offsets, counts, len, id, etc. + assert_eq!(storage.id(), new_storage.id()); + assert_eq!(storage.accounts.len(), new_storage.accounts.len()); + self.storage + .replace_storage_with_equivalent(slot, Arc::new(new_storage)); } } @@ -3014,7 +3012,7 @@ impl AccountsDb { old_store: Arc, size: u64, ) -> ShrinkInProgress<'_> { - let shrunken_store = self.create_store(slot, size, "shrink", self.shrink_paths.as_slice()); + let shrunken_store = self.create_store(slot, size, "shrink"); self.storage .shrinking_in_progress(slot, old_store, shrunken_store) } @@ -3027,10 +3025,9 @@ impl AccountsDb { if let Some(store) = self .storage .get_slot_storage_entry_shrinking_in_progress_ok(slot) + && self.is_shrinking_productive(&store) { - if Self::is_shrinking_productive(&store) { - self.shrink_storage(store) - } + self.shrink_storage(store) } } @@ -3053,26 +3050,28 @@ impl AccountsDb { struct StoreUsageInfo { slot: Slot, alive_ratio: f64, + alive_bytes_after_shrink: u64, store: Arc, } - let mut store_usage: Vec = Vec::with_capacity(shrink_slots.len()); + let mut store_usages = Vec::with_capacity(shrink_slots.len()); let mut total_alive_bytes: u64 = 0; let mut total_bytes: u64 = 0; for slot in shrink_slots { let Some(store) = self.storage.get_slot_storage_entry(*slot) else { continue; }; - let alive_bytes = store.alive_bytes(); - total_alive_bytes += alive_bytes as u64; + let alive_bytes_after_shrink = self.alive_bytes_after_shrink(&store) as u64; + total_alive_bytes += alive_bytes_after_shrink; total_bytes += store.capacity(); - let alive_ratio = alive_bytes as f64 / store.capacity() as f64; - store_usage.push(StoreUsageInfo { + let alive_ratio = alive_bytes_after_shrink as f64 / store.capacity() as f64; + store_usages.push(StoreUsageInfo { slot: *slot, alive_ratio, + alive_bytes_after_shrink, store: store.clone(), }); } - store_usage.sort_by(|a, b| { + store_usages.sort_by(|a, b| { a.alive_ratio .partial_cmp(&b.alive_ratio) .unwrap_or(std::cmp::Ordering::Equal) @@ -3082,15 +3081,15 @@ impl AccountsDb { // shrinking while still achieving the overall goals. let mut shrink_slots = IntMap::default(); let mut shrink_slots_next_batch = ShrinkCandidates::default(); - for usage in &store_usage { - let store = &usage.store; + for store_usage in &store_usages { + let store = &store_usage.store; let alive_ratio = (total_alive_bytes as f64) / (total_bytes as f64); debug!( "alive_ratio: {:?} store_id: {:?}, store_ratio: {:?} requirement: {:?}, \ total_bytes: {:?} total_alive_bytes: {:?}", alive_ratio, - usage.store.id(), - usage.alive_ratio, + store_usage.store.id(), + store_usage.alive_ratio, shrink_ratio, total_bytes, total_alive_bytes @@ -3100,38 +3099,31 @@ impl AccountsDb { debug!( "Shrinking goal can be achieved at slot {:?}, total_alive_bytes: {:?} \ total_bytes: {:?}, alive_ratio: {:}, shrink_ratio: {:?}", - usage.slot, total_alive_bytes, total_bytes, alive_ratio, shrink_ratio + store_usage.slot, total_alive_bytes, total_bytes, alive_ratio, shrink_ratio ); - if usage.alive_ratio < shrink_ratio { - shrink_slots_next_batch.insert(usage.slot); + if store_usage.alive_ratio < shrink_ratio { + shrink_slots_next_batch.insert(store_usage.slot); } else { break; } } else { let current_store_size = store.capacity(); - let after_shrink_size = store.alive_bytes() as u64; + let after_shrink_size = store_usage.alive_bytes_after_shrink; let bytes_saved = current_store_size.saturating_sub(after_shrink_size); total_bytes -= bytes_saved; - shrink_slots.insert(usage.slot, Arc::clone(store)); + shrink_slots.insert(store_usage.slot, Arc::clone(store)); } } (shrink_slots, shrink_slots_next_batch) } - fn get_roots_less_than(&self, slot: Slot) -> Vec { - self.accounts_index - .roots_tracker - .read() - .unwrap() - .alive_roots - .get_all_less_than(slot) - } - /// return all slots that are more than one epoch old and thus could already be an ancient append vec /// or which could need to be combined into a new or existing ancient append vec /// offset is used to combine newer slots than we normally would. This is designed to be used for testing. fn get_sorted_potential_ancient_slots(&self, oldest_non_ancient_slot: Slot) -> Vec { - let mut ancient_slots = self.get_roots_less_than(oldest_non_ancient_slot); + // Only storages can be combined into ancient append vecs, so the storage map is the + // source of truth here. + let mut ancient_slots = self.storage.slots_less_than(oldest_non_ancient_slot); ancient_slots.sort_unstable(); ancient_slots } @@ -3145,33 +3137,14 @@ impl AccountsDb { let oldest_non_ancient_slot = self.get_oldest_non_ancient_slot(epoch_schedule); let can_randomly_shrink = true; - let sorted_slots = self.get_sorted_potential_ancient_slots(oldest_non_ancient_slot); + let (sorted_slots, select_slots_us) = + measure_us!(self.get_sorted_potential_ancient_slots(oldest_non_ancient_slot)); + self.shrink_ancient_stats + .select_slots_us + .fetch_add(select_slots_us, Ordering::Relaxed); self.combine_ancient_slots_packed(sorted_slots, can_randomly_shrink); } - /// each slot in 'dropped_roots' has been combined into an ancient append vec. - /// We are done with the slot now forever. - pub(crate) fn handle_dropped_roots_for_ancient( - &self, - dropped_roots: impl Iterator, - ) { - dropped_roots.for_each(|slot| { - self.accounts_index.clean_dead_slot(slot); - // the storage has been removed from this slot and recycled or dropped - assert!(self.storage.remove(&slot, false).is_none()); - debug_assert!( - !self - .accounts_index - .roots_tracker - .read() - .unwrap() - .alive_roots - .contains(&slot), - "slot: {slot}" - ); - }); - } - /// add all 'pubkeys' into the set of pubkeys that are 'uncleaned', associated with 'slot' /// clean will visit these pubkeys next time it runs fn add_uncleaned_pubkeys_after_shrink( @@ -3238,21 +3211,21 @@ impl AccountsDb { if shrink_slots.len() < SHRINK_INSERT_ANCIENT_THRESHOLD { let mut ancients = self.best_ancient_slots_to_shrink.write().unwrap(); while let Some((slot, capacity)) = ancients.pop_front() { - if let Some(store) = self.storage.get_slot_storage_entry(slot) { - if !shrink_slots.contains(&slot) - && capacity == store.capacity() - && Self::is_candidate_for_shrink(self, &store) - { - let ancient_bytes_added_to_shrink = store.alive_bytes() as u64; - shrink_slots.insert(slot, store); - self.shrink_stats - .ancient_bytes_added_to_shrink - .fetch_add(ancient_bytes_added_to_shrink, Ordering::Relaxed); - self.shrink_stats - .ancient_slots_added_to_shrink - .fetch_add(1, Ordering::Relaxed); - break; - } + if let Some(store) = self.storage.get_slot_storage_entry(slot) + && !shrink_slots.contains(&slot) + && capacity == store.capacity() + && self.is_candidate_for_shrink(&store) + { + let ancient_bytes_added_to_shrink = + self.alive_bytes_after_shrink(&store) as u64; + shrink_slots.insert(slot, store); + self.shrink_stats + .ancient_bytes_added_to_shrink + .fetch_add(ancient_bytes_added_to_shrink, Ordering::Relaxed); + self.shrink_stats + .ancient_slots_added_to_shrink + .fetch_add(1, Ordering::Relaxed); + break; } } } @@ -3375,13 +3348,11 @@ impl AccountsDb { F: FnMut(Option<(&Pubkey, AccountSharedData, Slot)>), { // Register this scan so that slots needed by the scan are not cleaned out from under us. - let scan_guard = ScanGuard::try_new(&self.scan_tracker, bank_id, || { - self.accounts_index.max_root_inclusive() - }) - .ok_or(ScanError::SlotRemoved { - slot: ancestors.max_slot(), - bank_id, - })?; + let scan_guard = ScanGuard::try_new(&self.scan_tracker, bank_id, || self.max_root()) + .ok_or(ScanError::SlotRemoved { + slot: ancestors.max_slot(), + bank_id, + })?; // If the scan's ancestors are all rooted, drop them and scan roots only // Scan Guard max root must be used as the scan guard guarantees that @@ -3404,7 +3375,7 @@ impl AccountsDb { break; } - if let Some((cached_account, slot, _)) = + if let Some((cached_account, slot)) = self.accounts_cache.load_latest(&pubkey, ancestors) { cached_versions.insert(pubkey, (cached_account, slot)); @@ -3424,22 +3395,19 @@ impl AccountsDb { ancestors, max_root, |pubkey, (account_info, slot)| { - if let Some((cached_account, cache_slot)) = cached_versions.remove(pubkey) { - if cache_slot >= slot { - scan_func(Some((pubkey, cached_account.account.clone(), cache_slot))); - return; - } + if let Some((cached_account, cache_slot)) = cached_versions.remove(pubkey) + && cache_slot >= slot + { + scan_func(Some((pubkey, cached_account.account.clone(), cache_slot))); + return; } let mut account_accessor = - self.get_account_accessor(slot, pubkey, &account_info.storage_location()); + self.get_account_accessor(slot, &account_info.storage_location()); - let account_slot = match account_accessor { - LoadedAccountAccessor::Cached(None) => None, - _ => account_accessor.get_loaded_account(|loaded_account| { - (pubkey, loaded_account.take_account(), slot) - }), - }; + let account_slot = account_accessor.get_loaded_account(|loaded_account| { + (pubkey, loaded_account.take_account(), slot) + }); scan_func(account_slot) }, config, @@ -3488,13 +3456,11 @@ impl AccountsDb { } // Register this scan so that slots needed by the scan are not cleaned out from under us. - let scan_guard = ScanGuard::try_new(&self.scan_tracker, bank_id, || { - self.accounts_index.max_root_inclusive() - }) - .ok_or(ScanError::SlotRemoved { - slot: ancestors.max_slot(), - bank_id, - })?; + let scan_guard = ScanGuard::try_new(&self.scan_tracker, bank_id, || self.max_root()) + .ok_or(ScanError::SlotRemoved { + slot: ancestors.max_slot(), + bank_id, + })?; // If the scan's ancestors are all rooted, drop them and scan roots only // Scan Guard max root must be used as the scan guard guarantees that @@ -3645,12 +3611,12 @@ impl AccountsDb { ancestors: &Ancestors, pubkey: &'a Pubkey, clone_in_lock: bool, - ) -> Option<(Slot, StorageLocation, Option>)> { + ) -> Option<(Slot, StorageLocation, Option)> { self.accounts_index .get_with_and_then(pubkey, ancestors, true, |(slot, account_info)| { let storage_location = account_info.storage_location(); - let account_accessor = clone_in_lock - .then(|| self.get_account_accessor(slot, pubkey, &storage_location)); + let account_accessor = + clone_in_lock.then(|| self.get_account_accessor(slot, &storage_location)); (slot, storage_location, account_accessor) }) } @@ -3662,26 +3628,26 @@ impl AccountsDb { ancestors: &'a Ancestors, pubkey: &'a Pubkey, load_hint: LoadHint, - ) -> Option<(LoadedAccountAccessor<'a>, Slot)> { + ) -> Option<(LoadedAccountAccessor, Slot)> { // Happy drawing time! :) // - // Reader | Accessed data source for cached/stored + // Reader | Accessed data source for stored // -------------------------------------+---------------------------------- - // R1 read_index_for_accessor_or_load_slow()| cached/stored: index + // R1 read_index_for_accessor_or_load_slow()| stored: index // | | // <(store_id, offset, ..)> | // V | - // R2 retry_to_get_account_accessor()/ | cached: map of caches & entry for (slot, pubkey) - // get_account_accessor() | stored: map of stores + // R2 retry_to_get_account_accessor()/ | stored: map of stores + // get_account_accessor() | // | | // | // V | - // R3 check_and_get_loaded_account()/ | cached: N/A (note: basically noop unwrap) - // get_loaded_account() | stored: store's entry for slot + // R3 check_and_get_loaded_account()/ | stored: store's entry for slot + // get_loaded_account() | // | | // | // V | - // R4 take_account() | cached/stored: entry of cache/storage for (slot, pubkey) + // R4 take_account() | stored: entry of storage for (slot, pubkey) // | | // | // V | @@ -3692,11 +3658,11 @@ impl AccountsDb { // F1 flush_slot_cache() | N/A // | | // V | - // F2 store_accounts_frozen()/ | map of stores (creates new entry) + // F2 store_accounts_for_flush()/ | map of stores (creates new entry) // write_accounts_to_storage() | // | | // V | - // F3 store_accounts_frozen()/ | index + // F3 store_accounts_for_flush()/ | index // update_index_stored_accounts()| (replaces existing store_id, offset in caches) // | | // V | @@ -3712,12 +3678,12 @@ impl AccountsDb { // S1 do_shrink_slot_store() | N/A // | | // V | - // S2 store_accounts_frozen()/ | map of stores (creates new entry) + // S2 store_accounts_for_shrink()/ | map of stores (creates new entry) // write_accounts_to_storage() | // | | // V | - // S3 store_accounts_frozen()/ | index - // update_index_stored_accounts()| (replaces existing store_id, offset in stores) + // S3 store_accounts_for_shrink()/ | index + // update_index_for_shrink() | (replaces existing store_id, offset in stores) // | | // V | // S4 do_shrink_slot_store()/ | map of stores (removes old entry) @@ -3748,12 +3714,11 @@ impl AccountsDb { // P1 purge_slot() | N/A // | | // V | - // P2 purge_slots_from_cache_and_store() | map of caches/stores (removes old entry) + // P2 purge_slots_from_cache() | map of caches/stores (removes old entry) // | | // V | - // P3 purge_slots_from_cache_and_store()/ | index - // purge_slot_cache()/ | - // purge_slot_cache_pubkeys() | (removes existing store_id, offset for cache) + // P3 purge_slots_from_cache()/ | index + // remove_dead_slots_metadata() | (removes index roots metadata for cached slot) // purge_slot_storage()/ | // purge_keys_exact() | (removes accounts index entries) // handle_reclaims() | (removes storage entries) @@ -3765,42 +3730,6 @@ impl AccountsDb { // Remarks for purger: So, for any reading operations, it's a race condition // where P2 happens between R1 and R2. In that case, retrying from R1 is safu. // In that case, we may bail at index read retry when P3 hasn't been run - // - // Remover | Accessed data source for cached - // ---------------------------------------+---------------------------------- - // M1 purge_slots_from_cache_and_store() | index - // (via remove_unrooted_slots()) | (removes old entries for slot) - // | | - // V | - // M2 purge_slots_from_cache_and_store() | map of caches - // (via remove_unrooted_slots()) | (removes old slot cache) - // | | - // V | - // M3 store_accounts_cached() | map of caches (creates new Cached entry) - // | | - // V | - // M4 update index | index (writes fresh Cached entry) - // V - // - // Remarks for remover: This scenario arises when remove_unrooted_slots() - // purges a cached slot (e.g. duplicate-block detection abandoning a fork) - // and the same slot is subsequently re-stored (e.g. re-processed by banking - // stage on a fresh fork). - // - // M1 removes the index entries before M2 removes the cache (see - // purge_slots_from_cache_and_store). M3 writes the fresh cache entry - // before M4 writes the fresh index entry, so any reader who observes M4's - // index entry is guaranteed to find M3's cache entry too. - // - // The observable race is M2 happening between R1 and R2, with M3+M4 - // completing before the subsequent index re-read: - // R1 → (slot, Cached) [old index entry, before M1] - // M1/M2 remove old index and cache entries - // M3/M4 write fresh cache and index entries - // get_account_accessor() → Cached(None) [old entry removed by M2] - // re-read index → (slot, Cached) [fresh M4 entry, same store_id] - // In that case, retrying from R1 is safe: the next get_account_accessor() - // on the fresh (slot, Cached) entry is guaranteed to return Cached(Some(_)). #[cfg(test)] { @@ -3811,49 +3740,12 @@ impl AccountsDb { // Failsafe for potential race conditions with other subsystems let mut num_acceptable_failed_iterations = 0; loop { - let account_accessor = self.get_account_accessor(slot, pubkey, &storage_location); + let account_accessor = self.get_account_accessor(slot, &storage_location); match account_accessor { - LoadedAccountAccessor::Cached(Some(_)) | LoadedAccountAccessor::Stored(Some(_)) => { + LoadedAccountAccessor::Stored(Some(_)) => { // Great! There was no race, just return :) This is the most usual situation return Some((account_accessor, slot)); } - LoadedAccountAccessor::Cached(None) => { - num_acceptable_failed_iterations += 1; - // Cache was flushed in between checking the index and retrieving from the cache, - // so retry. This works because in accounts cache flush, an account is written to - // storage *before* it is removed from the cache - match load_hint { - LoadHint::FixedMaxRoot => { - // Under FixedMaxRoot, Cached(None) can occur at most once per load. - // There are two ways the initial cache miss can happen: - // - // 1) The write-cache entry for the located slot was being concurrently - // flushed to storage. After re-reading the index the entry will - // have moved to Stored, so the next get_account_accessor() call - // succeeds via Stored(Some(_)). With a fixed root the index - // references a single ancestor slot for a given account; that - // slot is flushed exactly once, so this race cannot repeat. - // - // 2) A parent slot was removed by remove_unrooted_slots() and - // concurrently re-stored (the M1-M4 "Remover" sequence above). - // We read the stale index entry (R1, before M1 ran), found the - // old cache entry already removed (M2), and got Cached(None). - // After re-reading the index we see the fresh M4 entry, and the - // next get_account_accessor() call is guaranteed to find M3's - // cache entry. For a second Cached(None) to occur here the - // same parent slot would have to complete the full - // remove-replay-confirm-duplicate cycle a second time between - // consecutive loop iterations — a far stricter requirement than - // the first miss, which only needed a single remove + re-store. - assert!(num_acceptable_failed_iterations <= 1); - } - LoadHint::Unspecified => { - // Because newer root can be added to the index (= not fixed), - // multiple flush race conditions can be observed under very rare - // condition, at least theoretically - } - } - } LoadedAccountAccessor::Stored(None) => { match load_hint { LoadHint::FixedMaxRoot => { @@ -3926,50 +3818,37 @@ impl AccountsDb { // Notice the subtle `?` at previous line, we bail out pretty early if missing. if new_slot == slot && new_storage_location.is_store_id_equal(&storage_location) { - if !new_storage_location.is_cached() { - self.accounts_index - .get_and_then(pubkey, |entry| -> (_, ()) { - let message = format!( - "Bad index entry detected ({pubkey}, {slot}, \ - {storage_location:?}, {load_hint:?}, {new_storage_location:?}, \ - {entry:?})" - ); - // Considering that we've failed to get accessor above and further that - // the index still returned the same (slot, store_id) tuple, offset must be same - // too. - assert!( - new_storage_location.is_offset_equal(&storage_location), - "{message}" - ); + self.accounts_index + .get_and_then(pubkey, |entry| -> (_, ()) { + let message = format!( + "Bad index entry detected ({pubkey}, {slot}, {storage_location:?}, \ + {load_hint:?}, {new_storage_location:?}, {entry:?})" + ); + // Considering that we've failed to get accessor above and further that + // the index still returned the same (slot, store_id) tuple, offset must be same + // too. + assert!( + new_storage_location.is_offset_equal(&storage_location), + "{message}" + ); - // If this is not a cache entry, then this was a minor fork slot - // that had its storage entries cleaned up by purge_slots() but hasn't been - // cleaned yet. That means this must be rpc access and not replay/banking at the - // very least. Note that purge shouldn't occur even for RPC as caller must hold all - // of ancestor slots.. - assert_eq!(load_hint, LoadHint::Unspecified, "{message}"); - - // Everything being assert!()-ed, let's panic!() here as it's an error condition - // after all.... - // That reasoning is based on the fact all of code-path reaching this fn - // retry_to_get_account_accessor() must outlive the Arc (and its all - // ancestors) over this fn invocation, guaranteeing the prevention of being purged, - // first of all. - // For details, see the comment in ScanGuard::should_use_ancestors(), - // which is referring back here. - panic!("{message}"); - }); - } else { - // For the Cached variant: remove_unrooted_slots() removes the index entry and - // the cache entry, then a subsequent re-store writes a fresh Cached entry for - // the same slot. This produces the observable sequence: - // get_account_accessor() -> Cached(None) [old entry gone] - // read_index_for_accessor_or_load_slow() -> (slot, Cached) [new entry] - // That is not an index corruption -- the next get_account_accessor() call on - // the fresh (slot, Cached) will succeed. Fall through to retry. - // - // Also no code in this arm! - } + // If this is not a cache entry, then this was a minor fork slot + // that had its storage entries cleaned up by purge_slots() but hasn't been + // cleaned yet. That means this must be rpc access and not replay/banking at the + // very least. Note that purge shouldn't occur even for RPC as caller must hold all + // of ancestor slots.. + assert_eq!(load_hint, LoadHint::Unspecified, "{message}"); + + // Everything being assert!()-ed, let's panic!() here as it's an error condition + // after all.... + // That reasoning is based on the fact all of code-path reaching this fn + // retry_to_get_account_accessor() must outlive the Arc (and its all + // ancestors) over this fn invocation, guaranteeing the prevention of being purged, + // first of all. + // For details, see the comment in ScanGuard::should_use_ancestors(), + // which is referring back here. + panic!("{message}"); + }); } else if fallback_to_slow_path { // the above bad-index-entry check must had been checked first to retain the same // behavior @@ -3991,50 +3870,29 @@ impl AccountsDb { load_hint: LoadHint, populate_read_cache: PopulateReadCache, ) -> Option<(AccountSharedData, Slot)> { - let starting_max_root = self.accounts_index.max_root_inclusive(); + let starting_max_root = self.max_root(); - // First check the write cache - let cache_result = self.accounts_cache.load_latest(pubkey, ancestors); - - if let Some((cached_account, cached_slot, slot_status)) = &cache_result { - // If the slot is an ancestor or an unflushed root, it hasn't been flushed to - // storage yet, so the write cache is authoritative — return it directly. - if matches!( - slot_status, - SlotStatus::Ancestor | SlotStatus::UnflushedRoot - ) { - self.load_account_stats - .num_loaded_from_write_cache - .fetch_add(1, Ordering::Relaxed); - return Some((cached_account.account.clone(), *cached_slot)); - } + // Check the write cache first; a hit is the freshest version visible on this fork, + // so return it + if let Some((cached_account, cached_slot)) = + self.accounts_cache.load_latest(pubkey, ancestors) + { + self.load_account_stats + .num_loaded_from_write_cache + .fetch_add(1, Ordering::Relaxed); + return Some((cached_account.account.clone(), cached_slot)); } let (slot, storage_location, _maybe_account_accessor) = self.read_index_for_accessor_or_load_slow(ancestors, pubkey, false)?; // Notice the subtle `?` at previous line, we bail out pretty early if missing. - if let Some((cached_account, cached_slot, _)) = &cache_result { - if *cached_slot >= slot { - // The write cache holds a version at a slot at least as new as what the - // index returned, so it represents the freshest visible value and can be - // returned directly without going through the storage accessor. - self.load_account_stats - .num_loaded_from_write_cache - .fetch_add(1, Ordering::Relaxed); - return Some((cached_account.account.clone(), *cached_slot)); - } - } - - let in_write_cache = storage_location.is_cached(); - if !in_write_cache { - let result = self.read_only_accounts_cache.load(*pubkey, slot); - if let Some(account) = result { - self.load_account_stats - .num_loaded_from_read_cache - .fetch_add(1, Ordering::Relaxed); - return Some((account, slot)); - } + let result = self.read_only_accounts_cache.load(*pubkey, slot); + if let Some(account) = result { + self.load_account_stats + .num_loaded_from_read_cache + .fetch_add(1, Ordering::Relaxed); + return Some((account, slot)); } let (mut account_accessor, slot) = self.retry_to_get_account_accessor( @@ -4044,29 +3902,13 @@ impl AccountsDb { pubkey, load_hint, )?; - // note that the account being in the cache could be different now than it was previously - // since the cache could be flushed in between the 2 calls. - let in_write_cache = matches!(account_accessor, LoadedAccountAccessor::Cached(_)); - if in_write_cache { - // While the account wasn't loaded directly from the write cache, the write cache - // provided the correct slot, so count this as a load from the write cache - if cache_result.is_some_and(|(_, cached_slot, _)| cached_slot == slot) { - self.load_account_stats - .num_loaded_from_write_cache - .fetch_add(1, Ordering::Relaxed); - } else { - self.load_account_stats - .num_loaded_from_index_cache - .fetch_add(1, Ordering::Relaxed); - } - } else { - self.load_account_stats - .num_loaded_from_index_storage - .fetch_add(1, Ordering::Relaxed); - } + self.load_account_stats + .num_loaded_from_index_storage + .fetch_add(1, Ordering::Relaxed); + let account = account_accessor.check_and_get_loaded_account_shared_data(); - if !in_write_cache && populate_read_cache == PopulateReadCache::True { + if populate_read_cache == PopulateReadCache::True { /* We show this store into the read-only cache for account 'A' and future loads of 'A' from the read-only cache are safe/reflect 'A''s latest state on this fork. @@ -4084,7 +3926,7 @@ impl AccountsDb { } if load_hint == LoadHint::FixedMaxRoot { // If the load hint is that the max root is fixed, the max root should be fixed. - let ending_max_root = self.accounts_index.max_root_inclusive(); + let ending_max_root = self.max_root(); if starting_max_root != ending_max_root { warn!( "do_load_with_populate_read_cache() scanning pubkey {pubkey} called with \ @@ -4097,17 +3939,12 @@ impl AccountsDb { } #[cfg_attr(test, qualifiers(pub(crate)))] - fn get_account_accessor<'a>( - &'a self, + fn get_account_accessor( + &self, slot: Slot, - pubkey: &'a Pubkey, storage_location: &StorageLocation, - ) -> LoadedAccountAccessor<'a> { + ) -> LoadedAccountAccessor { match storage_location { - StorageLocation::Cached => { - let maybe_cached_account = self.accounts_cache.load(slot, pubkey).map(Cow::Owned); - LoadedAccountAccessor::Cached(maybe_cached_account) - } StorageLocation::AppendVec(store_id, offset) => { let maybe_storage_entry = self .storage @@ -4118,16 +3955,11 @@ impl AccountsDb { } } - fn create_store( - &self, - slot: Slot, - size: u64, - from: &str, - paths: &[PathBuf], - ) -> Arc { + fn create_store(&self, slot: Slot, size: u64, from: &str) -> Arc { self.stats .create_store_count .fetch_add(1, Ordering::Relaxed); + let paths = &self.paths; let path_index = rng().random_range(0..paths.len()); let store = Arc::new(self.new_storage_entry(slot, Path::new(&paths[path_index]), size)); @@ -4144,23 +3976,14 @@ impl AccountsDb { store } + #[cfg(test)] fn create_and_insert_store( &self, slot: Slot, size: u64, from: &str, ) -> Arc { - self.create_and_insert_store_with_paths(slot, size, from, &self.paths) - } - - fn create_and_insert_store_with_paths( - &self, - slot: Slot, - size: u64, - from: &str, - paths: &[PathBuf], - ) -> Arc { - let store = self.create_store(slot, size, from, paths); + let store = self.create_store(slot, size, from); self.storage.insert(store.clone()); store } @@ -4201,13 +4024,16 @@ impl AccountsDb { self.purge_slots(std::iter::once(&slot)); } - /// Purges every slot in `removed_slots` from both the cache and storage. This includes - /// entries in the accounts index, cache entries, and any backing storage entries. - fn purge_slots_from_cache_and_store<'a>( + /// Purges each slot in `removed_slots` from the write cache (and the accounts index). Slots + /// no longer present in the cache are skipped. This never touches backing storage, so it + /// cannot delete a flushed (rooted) slot's data. Returns whether any slot was actually + /// removed from the cache. This allows the snapshot minimizer to determine whether + /// it should purge the storage as well + fn purge_slots_from_cache<'a>( &self, - removed_slots: impl Iterator + Clone, + removed_slots: impl Iterator, purge_stats: &PurgeStats, - ) { + ) -> bool { let mut remove_cache_elapsed_across_slots = 0; let mut num_cached_slots_removed = 0; let mut total_removed_cached_bytes = 0; @@ -4220,21 +4046,33 @@ impl AccountsDb { // holding the index lock, finding the index entry, and then looking up the entry // in the cache. If it fails to find that entry, it will panic in `get_loaded_account()` if let Some(slot_cache) = self.accounts_cache.slot_cache(*remove_slot) { - // If the slot is still in the cache, remove the backing storages for - // the slot and from the Accounts Index num_cached_slots_removed += 1; total_removed_cached_bytes += slot_cache.total_bytes(); - self.purge_slot_cache(*remove_slot, &slot_cache); remove_cache_elapsed.stop(); remove_cache_elapsed_across_slots += remove_cache_elapsed.as_us(); // Nobody else should have removed the slot cache entry yet - assert!(self.accounts_cache.remove_slot(*remove_slot).is_some()); - } else { - self.purge_slot_storage(*remove_slot, purge_stats); + let pubkeys_removed = self + .accounts_cache + .remove_slot(*remove_slot) + .expect("slot cache entry must still be present"); + // Cache writes populate the secondary indexes but not the primary index, so a + // cache-only account dropped here without ever being flushed would leak its + // secondary entry. + // Explicitly ignore the return value as there is no need to purge the account + // from the primary index as well + // + // Narrow race: If a write re-adds the same pubkey key between `remove_slot` and + // `handle_dead_keys`, the new secondary entry will be dropped. Only + // secondary-index scans observe it (no consensus impact), and it self-heals: the + // entry is re-added when that slot flushes, or stays correctly removed if the slot + // is later purged. + if !self.account_indexes.is_empty() { + let _ = self + .accounts_index + .handle_dead_keys(&pubkeys_removed, &self.account_indexes); + } + self.accounts_index.write_through_pubkeys(pubkeys_removed); } - // It should not be possible that a slot is neither in the cache or storage. Even in - // a slot with all ticks, `Bank::new_from_parent()` immediately stores some sysvars - // on bank creation. } purge_stats @@ -4246,6 +4084,8 @@ impl AccountsDb { purge_stats .total_removed_cached_bytes .fetch_add(total_removed_cached_bytes, Ordering::Relaxed); + + num_cached_slots_removed > 0 } /// Purges every slot in `removed_slots` from both the cache and storage. This includes @@ -4255,10 +4095,17 @@ impl AccountsDb { #[cfg(feature = "dev-context-only-utils")] pub fn purge_slots_for_snapshot_minimizer<'a>( &self, - removed_slots: impl Iterator + Clone, + removed_slots: impl Iterator, ) { - let stats = PurgeStats::default(); - self.purge_slots_from_cache_and_store(removed_slots, &stats); + let purge_stats = PurgeStats::default(); + for remove_slot in removed_slots { + // Unlike the consensus purge paths, minimization may purge slots that have already + // been flushed to storage, so fall back to purging storage for any slot that is no + // longer in the cache. + if !self.purge_slots_from_cache(iter::once(remove_slot), &purge_stats) { + self.purge_slot_storage(*remove_slot, &purge_stats); + } + } } /// Purge the backing storage entries for the given slot, does not purge from @@ -4268,23 +4115,6 @@ impl AccountsDb { removed_slots: impl Iterator + Clone, purge_stats: &PurgeStats, ) { - // Check all slots `removed_slots` are no longer "relevant" roots. - // Note that the slots here could have been rooted slots, but if they're passed here - // for removal it means: - // 1) All updates in that old root have been outdated by updates in newer roots - // 2) Those slots/roots should have already been purged from the accounts index root - // tracking metadata via `accounts_index.clean_dead_slot()`. - let mut safety_checks_elapsed = Measure::start("safety_checks_elapsed"); - assert!( - self.accounts_index - .get_rooted_from_list(removed_slots.clone()) - .is_empty() - ); - safety_checks_elapsed.stop(); - purge_stats - .safety_checks_elapsed - .fetch_add(safety_checks_elapsed.as_us(), Ordering::Relaxed); - let mut total_removed_stored_bytes = 0; let mut all_removed_slot_storages = vec![]; @@ -4314,9 +4144,6 @@ impl AccountsDb { purge_stats .num_stored_slots_removed .fetch_add(num_stored_slots_removed, Ordering::Relaxed); - purge_stats - .total_removed_storage_entries - .fetch_add(num_stored_slots_removed, Ordering::Relaxed); purge_stats .total_removed_stored_bytes .fetch_add(total_removed_stored_bytes, Ordering::Relaxed); @@ -4325,34 +4152,7 @@ impl AccountsDb { .fetch_add(num_stored_slots_removed as u64, Ordering::Relaxed); } - fn purge_slot_cache(&self, purged_slot: Slot, slot_cache: &SlotCache) { - let pubkeys = slot_cache.iter().map(|account| *account.key()); - self.purge_slot_cache_pubkeys(purged_slot, pubkeys, true); - } - - fn purge_slot_cache_pubkeys( - &self, - purged_slot: Slot, - pubkeys: impl IntoIterator, - is_dead: bool, - ) { - // Slot purged from cache should not exist in the backing store - assert!( - self.storage - .get_slot_storage_entry_shrinking_in_progress_ok(purged_slot) - .is_none() - ); - let mut num_purged_keys = 0; - let (reclaims, _) = self.purge_keys_exact(pubkeys.into_iter().map(|key| { - num_purged_keys += 1; - (key, purged_slot) - })); - assert_eq!(reclaims.len(), num_purged_keys); - if is_dead { - self.remove_dead_slots_metadata(std::iter::once(&purged_slot)); - } - } - + #[cfg(feature = "dev-context-only-utils")] fn purge_slot_storage(&self, remove_slot: Slot, purge_stats: &PurgeStats) { // Because AccountsBackgroundService synchronously flushes from the accounts cache // and handles all Bank::drop() (the cleanup function that leads to this @@ -4417,6 +4217,11 @@ impl AccountsDb { // `add_root()` should be called first let mut safety_checks_elapsed = Measure::start("safety_checks_elapsed"); let non_roots = slots + // Only purge slots that are still in the write cache and are not + // unflushed roots. Flushed roots have already been removed from the + // cache, so the `contains` check excludes them; the + // `contains_unflushed_root` check excludes roots still pending flush. + // // Only safe to check when there are duplicate versions of a slot // because ReplayStage will not make new roots before dumping the // duplicate slots first. Thus we will not be in a case where we @@ -4426,24 +4231,30 @@ impl AccountsDb { // Also note roots are never removed via `remove_unrooted_slot()`, so // it's safe to filter them out here as they won't need deletion from // self.scan_tracker.removed_bank_ids in - // `purge_slots_from_cache_and_store()`. - .filter(|slot| !self.accounts_index.is_alive_root(**slot)); + // `purge_slots_from_cache()`. + .filter(|slot| { + self.accounts_cache.contains(**slot) + && !self.accounts_cache.contains_unflushed_root(**slot) + }); safety_checks_elapsed.stop(); self.external_purge_slots_stats .safety_checks_elapsed .fetch_add(safety_checks_elapsed.as_us(), Ordering::Relaxed); - self.purge_slots_from_cache_and_store(non_roots, &self.external_purge_slots_stats); + self.purge_slots_from_cache(non_roots, &self.external_purge_slots_stats); self.external_purge_slots_stats .report("external_purge_slots_stats", Some(1000)); } pub fn remove_unrooted_slots(&self, remove_slots: &[(Slot, BankId)]) { - let rooted_slots = self - .accounts_index - .get_rooted_from_list(remove_slots.iter().map(|(slot, _)| slot)); assert!( - rooted_slots.is_empty(), - "Trying to remove accounts for rooted slots {rooted_slots:?}" + remove_slots.iter().all(|(slot, _)| { + debug_assert!( + self.accounts_cache.contains(*slot), + "Trying to remove slot not in cache {slot}" + ); + !self.accounts_cache.contains_unflushed_root(*slot) + }), + "Trying to remove accounts for rooted slots {remove_slots:?}" ); // Mark down these slots are about to be purged so that new attempts to scan these @@ -4457,7 +4268,7 @@ impl AccountsDb { } let remove_unrooted_purge_stats = PurgeStats::default(); - self.purge_slots_from_cache_and_store( + self.purge_slots_from_cache( remove_slots.iter().map(|(slot, _)| slot), &remove_unrooted_purge_stats, ); @@ -4592,19 +4403,34 @@ impl AccountsDb { flush_stats.store_accounts_total_us.0, i64 ), + ("write_accounts_us", flush_stats.write_accounts_us.0, i64), + ("update_index_us", flush_stats.update_index_us.0, i64), + ("handle_reclaims_us", flush_stats.handle_reclaims_us.0, i64), + ( + "mark_zero_lamport_single_ref_accounts_us", + flush_stats.mark_zero_lamport_single_ref_accounts_us.0, + i64 + ), ( - "update_index_us", - flush_stats.store_accounts_timing.update_index_elapsed, + "num_zero_lamport_single_ref_accounts_marked", + flush_stats.num_zero_lamport_single_ref_accounts_marked.0, i64 ), + ("num_reclaims", flush_stats.num_reclaims.0, i64), ( - "store_accounts_elapsed_us", - flush_stats.store_accounts_timing.store_accounts_elapsed, + "num_obsolete_slots_removed", + flush_stats.num_obsolete_slots_removed.0, i64 ), ( - "handle_reclaims_elapsed_us", - flush_stats.store_accounts_timing.handle_reclaims_elapsed, + "num_obsolete_bytes_removed", + flush_stats.num_obsolete_bytes_removed.0, + i64 + ), + ("select_pubkeys_us", flush_stats.select_pubkeys_us.0, i64), + ( + "disk_index_write_through_us", + flush_stats.disk_index_write_through_us.0, i64 ), ); @@ -4622,20 +4448,9 @@ impl AccountsDb { // If there is a long running scan going on, this could prevent any cleaning // based on updates from slots > `max_clean_root`. let max_clean_root = self.max_clean_root(requested_flush_root); - - let mut written_accounts = HashSet::new(); - - let mut should_flush_f = { - Some(move |&pubkey: &Pubkey| { - // if not in hashset, then not flushed previously, so flush it - written_accounts.insert(pubkey) - }) - }; - self.flush_rooted_accounts_cache( requested_flush_root, - max_clean_root, - should_flush_f.as_mut(), + FlushShouldClean::Yes { max_clean_root }, ) } @@ -4643,83 +4458,133 @@ impl AccountsDb { // to storage but older versions of the accounts are still required. For example, the // older versions may be needed for snapshotting. fn flush_rooted_accounts_cache_without_clean(&self) -> (usize, usize, FlushStats) { - self.flush_rooted_accounts_cache(None, None, None::<&mut fn(&_) -> bool>) + self.flush_rooted_accounts_cache(None, FlushShouldClean::No) } /// Flush all rooted slots up to `requested_flush_root`. + /// + /// When `should_clean` is `Yes`, only the newest version of each account across the + /// flushed roots (at or below its `max_clean_root`) is written to storage; older + /// versions are purged from the index instead. When `No`, every account in + /// every flushed root is written. fn flush_rooted_accounts_cache( &self, requested_flush_root: Option, - max_clean_root: Option, - mut should_flush_f: Option<&mut impl FnMut(&Pubkey) -> bool>, + should_clean: FlushShouldClean, ) -> (usize, usize, FlushStats) { // Always flush up to `requested_flush_root`, which is necessary for things like snapshotting. let flushed_roots: BTreeSet = - self.accounts_cache.begin_flush_roots(requested_flush_root); + self.accounts_cache.roots_to_flush(requested_flush_root); let max_flush_root = flushed_roots.last().copied(); let num_new_roots = flushed_roots.len(); - // Iterate from highest to lowest so that we don't need to flush earlier - // outdated updates in earlier roots + // For each root being flushed, which of its cached accounts to write to storage. + let (pubkeys_to_flush, select_pubkeys_us) = match should_clean { + FlushShouldClean::Yes { max_clean_root } => { + measure_us!(self.select_pubkeys_to_flush(&flushed_roots, max_clean_root)) + } + // Not cleaning: every root writes all of its accounts. + FlushShouldClean::No => ( + flushed_roots + .iter() + .map(|&root| (root, PubkeysToFlush::All)) + .collect(), + 0, + ), + }; + let mut num_roots_flushed = 0; - let mut flush_stats = FlushStats::default(); - for root in flushed_roots.into_iter().rev() { - if let Some(stats) = - self.flush_slot_cache(root, should_flush_f.as_mut(), max_clean_root) - { + let mut flush_stats = FlushStats { + select_pubkeys_us: Saturating(select_pubkeys_us), + ..FlushStats::default() + }; + for root in flushed_roots { + if let Some(stats) = self.flush_slot_cache(root, &pubkeys_to_flush[&root]) { num_roots_flushed += 1; flush_stats.accumulate(&stats); + } else { + // A root with no cache to flush (e.g. genesis, whose accounts load straight to + // storage) was still tracked by `add_root`. `flush_slot_cache` couldn't drop it + // via `remove_slot`, so untrack it here to keep it from lingering as an unflushed + // root at or below max_flushed_root. + self.accounts_cache.remove_unflushed_root(root); } } max_flush_root.inspect(|&root| self.accounts_cache.set_max_flush_root(root)); - // Now that all the rooted slots are flushed, we can clear the roots from the cache - self.accounts_cache.end_flush_roots(); - (num_new_roots, num_roots_flushed, flush_stats) } + /// Determines which of each flushed root's accounts to write to storage when cleaning: each + /// root keeps `Only` the newest version of each account, deduped newest-first. A root above + /// `max_clean_root` instead flushes `All`, since an in-flight scan may still need those + /// versions; `None` means there is no bound and every flushed root is cleaned. + fn select_pubkeys_to_flush( + &self, + flushed_roots: &BTreeSet, + max_clean_root: Option, + ) -> IntMap { + let mut pubkeys_to_flush = IntMap::with_capacity(flushed_roots.len()); + + // Presize the dedup set from the newest root (flushed first), doubled to leave room + // for unique accounts contributed by older roots. + let dedup_capacity = flushed_roots + .last() + .and_then(|&root| self.accounts_cache.slot_cache(root)) + .map_or(0, |slot_cache| slot_cache.len() * 2); + let mut written_accounts = + HashSet::with_capacity_and_hasher(dedup_capacity, PubkeyHasherBuilder::default()); + + // Iterate from newest root to oldest root being flushed in this batch + for &root in flushed_roots.iter().rev() { + let cleaned = max_clean_root.is_none_or(|max_clean_root| root <= max_clean_root); + let to_flush = if !cleaned { + PubkeysToFlush::All + } else { + let mut flush_keys = HashSet::default(); + if let Some(slot_cache) = self.accounts_cache.slot_cache(root) { + for entry in slot_cache.iter() { + let pubkey = *entry.key(); + // If not seen in a newer root, this is the newest version, so flush it. + if written_accounts.insert(pubkey) { + flush_keys.insert(pubkey); + } + } + } + PubkeysToFlush::Only(flush_keys) + }; + pubkeys_to_flush.insert(root, to_flush); + } + pubkeys_to_flush + } + fn do_flush_slot_cache( &self, slot: Slot, slot_cache: &SlotCache, - mut should_flush_f: Option<&mut impl FnMut(&Pubkey) -> bool>, - max_clean_root: Option, + pubkeys_to_flush: &PubkeysToFlush, ) -> FlushStats { - debug_assert!(self.accounts_index.is_alive_root(slot)); + debug_assert!(self.accounts_cache.contains_unflushed_root(slot)); let mut flush_stats = FlushStats::default(); let iter_items: Vec<_> = slot_cache.iter().collect(); - let mut pubkeys: Vec = vec![]; - if should_flush_f.is_some() { - if let Some(max_clean_root) = max_clean_root { - if slot > max_clean_root { - // Only if the root is greater than the `max_clean_root` do we - // have to prevent cleaning, otherwise, just default to `should_flush_f` - // for any slots <= `max_clean_root` - should_flush_f = None; - } - } - } let accounts: Vec<(&Pubkey, &AccountSharedData)> = iter_items .iter() .filter_map(|iter_item| { let key = iter_item.key(); let account = &iter_item.value().account; - let should_flush = should_flush_f - .as_mut() - .map(|should_flush_f| should_flush_f(key)) - .unwrap_or(true); + let should_flush = match pubkeys_to_flush { + PubkeysToFlush::All => true, + PubkeysToFlush::Only(flush_keys) => flush_keys.contains(key), + }; if should_flush { flush_stats.num_bytes_flushed += AppendVec::calculate_stored_size(account.data().len()) as u64; flush_stats.num_accounts_flushed += 1; Some((key, account)) } else { - // If we don't flush, we have to remove the entry from the - // index, since it's equivalent to purging - pubkeys.push(*key); + // A newer version wins, so this one isn't written flush_stats.num_bytes_purged += AppendVec::calculate_stored_size(account.data().len()) as u64; flush_stats.num_accounts_purged += 1; @@ -4728,41 +4593,33 @@ impl AccountsDb { }) .collect(); - let is_dead_slot = accounts.is_empty(); - // Remove the account index entries from earlier roots that are outdated by later roots. - // Safe because queries to the index will be reading updates from later roots. - self.purge_slot_cache_pubkeys(slot, pubkeys, is_dead_slot); - - // Use ReclaimOldSlots to reclaim old slots if marking obsolete accounts and cleaning - // Cleaning is enabled if `should_flush_f` is Some. - // should_flush_f is set to None when + // Use ReclaimOldSlots to reclaim old slots if marking obsolete accounts and cleaning. + // Cleaning is enabled if pubkeys_to_flush is PubkeysToFlush::Only + // pubkeys_to_flush is PubkeysToFlush::All when // 1) There's an ongoing scan to avoid reclaiming accounts being scanned. // 2) The slot is > max_clean_root to prevent unrooted slots from reclaiming rooted versions. - let reclaim_method = if should_flush_f.is_some() { - UpsertReclaim::ReclaimOldSlots - } else { - UpsertReclaim::IgnoreReclaims + let reclaim_method = match pubkeys_to_flush { + PubkeysToFlush::Only(_) => UpsertReclaim::ReclaimOldSlots, + PubkeysToFlush::All => UpsertReclaim::IgnoreReclaims, }; - if !is_dead_slot { + if !accounts.is_empty() { // This ensures that all updates are written to an AppendVec, before any // updates to the index happen, so anybody that sees a real entry in the index, // will be able to find the account in storage - let flushed_store = self.create_and_insert_store( - slot, - flush_stats.num_bytes_flushed.0, - "flush_slot_cache", - ); + let flushed_store = + self.create_store(slot, flush_stats.num_bytes_flushed.0, "flush_slot_cache"); + self.storage.insert(Arc::clone(&flushed_store)); - let (store_accounts_timing_inner, store_accounts_total_inner_us) = - measure_us!(self._store_accounts_frozen( + let (store_accounts_for_flush_stats, store_accounts_for_flush_us) = + measure_us!(self.store_accounts_for_flush( (slot, &accounts[..]), &flushed_store, reclaim_method, UpdateIndexThreadSelection::PoolWithThreshold, )); - flush_stats.store_accounts_timing = store_accounts_timing_inner; - flush_stats.store_accounts_total_us = Saturating(store_accounts_total_inner_us); + flush_stats.accumulate_store_accounts_for_flush(store_accounts_for_flush_stats); + flush_stats.store_accounts_total_us += Saturating(store_accounts_for_flush_us); // If the above sizing function is correct, just one AppendVec is enough to hold // all the data for the slot @@ -4774,8 +4631,17 @@ impl AccountsDb { // atomic switch from the cache to storage. // There is some racy condition for existing readers who just has read exactly while // flushing. That case is handled by retry_to_get_account_accessor() - assert!(self.accounts_cache.remove_slot(slot).is_some()); - + let pubkeys_removed = self + .accounts_cache + .remove_slot(slot) + .expect("slot must be in the cache when flushing"); + + // Now that this slot has left the cache, any pubkey that no longer appears + // in any cached slot is eligible to be written through so its in-mem entry + // becomes clean and can be evicted. + let (_, disk_index_write_through_us) = + measure_us!(self.accounts_index.write_through_pubkeys(pubkeys_removed)); + flush_stats.disk_index_write_through_us = Saturating(disk_index_write_through_us); // Add `accounts` to uncleaned_pubkeys since they were written to storage // and should be visited by `clean`. // If old slots were reclaimed, accounts were already cleaned, @@ -4797,19 +4663,18 @@ impl AccountsDb { flush_stats } - /// `should_flush_f` is an optional closure that determines whether a given - /// account should be flushed. Passing `None` will by default flush all - /// accounts + /// `pubkeys_to_flush` selects which accounts are written to storage: `Only(set)` flushes + /// just the pubkeys in the set, dropping the rest with the cache, while `All` flushes every + /// account in the slot. fn flush_slot_cache( &self, slot: Slot, - should_flush_f: Option<&mut impl FnMut(&Pubkey) -> bool>, - max_clean_root: Option, + pubkeys_to_flush: &PubkeysToFlush, ) -> Option { // If a slot cache exists for this slot, flush it. - self.accounts_cache.slot_cache(slot).map(|slot_cache| { - self.do_flush_slot_cache(slot, &slot_cache, should_flush_f, max_clean_root) - }) + self.accounts_cache + .slot_cache(slot) + .map(|slot_cache| self.do_flush_slot_cache(slot, &slot_cache, pubkeys_to_flush)) } fn report_store_stats(&self) { @@ -4843,6 +4708,20 @@ impl AccountsDb { ("total_bytes", total_bytes, i64), ("total_alive_bytes", total_alive_bytes, i64), ("total_alive_ratio", total_alive_ratio, f64), + ( + "append_vecs_open", + append_vec::APPEND_VEC_STATS + .files_open + .load(Ordering::Relaxed), + i64 + ), + ( + "append_vecs_dirty", + append_vec::APPEND_VEC_STATS + .files_dirty + .load(Ordering::Relaxed), + i64 + ), ); } @@ -4850,6 +4729,7 @@ impl AccountsDb { /// /// Only intended to be called at startup (or by tests). /// Only intended to be used while testing the experimental accumulator hash. + /// NOT safe to call concurrently with flush operations pub fn calculate_accounts_lt_hash_at_startup_from_index( &self, ancestors: &Ancestors, @@ -4861,7 +4741,7 @@ impl AccountsDb { // A different parallel implementation that iterated over the bins *sequentially* and then // hashed the accounts *within* a bin in parallel took about 600 seconds. That impl uses // less memory, as only a single index bin is loaded into mem at a time. - let lt_hash = self + let mut lt_hash = self .accounts_index .account_maps .par_iter() @@ -4875,7 +4755,6 @@ impl AccountsDb { (!account_info.is_zero_lamport()).then(|| { self.get_account_accessor( slot, - &pubkey, &account_info.storage_location(), ) .get_loaded_account(|loaded_account| { @@ -4899,6 +4778,36 @@ impl AccountsDb { accum }); + let cache_lt_hash = { + let mut cache_lt_hash = LtHash::identity(); + for pubkey in self.accounts_cache.cached_pubkeys().iter() { + // mix out whatever older version the index walk produced (if any) + self.accounts_index.get_with_and_then( + pubkey, + ancestors, + false, + |(slot, account_info)| { + self.get_account_accessor(slot, &account_info.storage_location()) + .get_loaded_account(|loaded_account| { + cache_lt_hash + .mix_out(&Self::lt_hash_account(&loaded_account, pubkey).0); + }); + }, + ); + // mix in the cache version + if let Some((account, _slot)) = self.load( + ancestors, + pubkey, + LoadHint::FixedMaxRoot, + PopulateReadCache::False, + ) { + cache_lt_hash.mix_in(&Self::lt_hash_account(&account, pubkey).0); + } + } + cache_lt_hash + }; + lt_hash.mix_in(&cache_lt_hash); + AccountsLtHash(lt_hash) } @@ -4911,34 +4820,66 @@ impl AccountsDb { /// /// Only intended to be called at startup by ledger-tool or tests. pub fn calculate_capitalization_at_startup_from_index(&self, ancestors: &Ancestors) -> u64 { - self.accounts_index + let stored_lamports = |pubkey: &Pubkey| { + self.accounts_index + .get_with_and_then(pubkey, ancestors, false, |(slot, account_info)| { + (!account_info.is_zero_lamport()).then(|| { + self.get_account_accessor(slot, &account_info.storage_location()) + .get_loaded_account(|loaded_account| loaded_account.lamports()) + // SAFETY: The index said this pubkey exists, so + // there must be an account to load. + .unwrap() + }) + }) + .flatten() + .unwrap_or(0) + }; + + let storage_capitialization = self + .accounts_index .account_maps .par_iter() .map(|accounts_index_bin| { accounts_index_bin .keys() .into_iter() - .map(|pubkey| { - self.accounts_index - .get_with_and_then(&pubkey, ancestors, false, |(slot, account_info)| { - (!account_info.is_zero_lamport()).then(|| { - self.get_account_accessor( - slot, - &pubkey, - &account_info.storage_location(), - ) - .get_loaded_account(|loaded_account| loaded_account.lamports()) - // SAFETY: The index said this pubkey exists, so - // there must be an account to load. - .unwrap() - }) - }) - .flatten() - .unwrap_or(0) - }) + .map(|pubkey| stored_lamports(&pubkey)) .try_fold(0, u64::checked_add) }) .try_reduce(|| 0, u64::checked_add) + .expect("capitalization cannot overflow"); + + // Sum as i128 because there is potential (although unlikely) for the cache updates to + // overflow i64::MAX. For example, if the cache has multiple transactions that transfer a + // large amount of lamports from one account to another, it could sum all of the transfers + // from accounts first, overflow i128. Wrapping logic could also handle this properly (ie. + // come to the correct answer), but then detection of overflow would be broken. + let cached_update = self + .accounts_cache + .cached_pubkeys() + .iter() + .map(|pubkey| { + // subtract out whatever older version the index walk produced (if any) + let stored_lamports = stored_lamports(pubkey); + + // add in the cached amount of lamports + let cached_lamports = self + .load( + ancestors, + pubkey, + LoadHint::FixedMaxRoot, + PopulateReadCache::False, + ) + .map(|(account, _slot)| account.lamports()) + .unwrap_or(0); + + cached_lamports as i128 - stored_lamports as i128 + }) + .sum::(); + + i128::from(storage_capitialization) + .checked_add(cached_update) + .and_then(|result| u64::try_from(result).ok()) .expect("capitalization cannot overflow") } @@ -4996,65 +4937,62 @@ impl AccountsDb { } } - /// Updates the accounts index with the given accounts. If store_account is false, skip storing - /// the account in the index as the account was not stored in the cache + /// Updates the secondary index with the given accounts. If store_account is false, skip storing + /// the account in the secondary index as the account was not stored in the cache /// Used for cached accounts only. - fn update_index_cached_accounts<'a>( + fn update_secondary_index_cached_accounts<'a>( &self, accounts: &impl StorableAccounts<'a>, store_account: &BitVec, update_index_thread_selection: UpdateIndexThreadSelection, ) { - let target_slot = accounts.target_slot(); - let len = accounts.len(); - assert_eq!(accounts.len() as u64, store_account.len()); - - let update = |start, end| { - (start..end).for_each(|i| { - if store_account[i as u64] { - accounts.account(i, |account| { - let info = - AccountInfo::new(StorageLocation::Cached, account.is_zero_lamport()); - self.accounts_index.upsert( - target_slot, - target_slot, - account.pubkey(), - &account, - &self.account_indexes, - info, - ReclaimsSlotList::default().as_mut(), - UpsertReclaim::PreviousSlotEntryWasCached, - ); - }); - } - }); - }; + if !self.account_indexes.is_empty() { + let len = accounts.len(); + assert_eq!(accounts.len() as u64, store_account.len()); + + // Cache writes do not upsert the accounts index; it only ever holds storage entries, + // populated on flush. Readers find cache-only accounts through the write cache. Only + // the secondary indexes are updated here. + let update = |start, end| { + (start..end).for_each(|i| { + if store_account[i as u64] { + accounts.account(i, |account| { + self.accounts_index.update_secondary_indexes( + account.pubkey(), + &account, + &self.account_indexes, + ); + }); + } + }); + }; - let threshold = 1; - if matches!( - update_index_thread_selection, - UpdateIndexThreadSelection::PoolWithThreshold, - ) && len > threshold - { - let chunk_size = len.div_ceil(self.thread_pool_foreground.current_num_threads()); - let batches = 1 + len / chunk_size; - self.thread_pool_foreground.install(|| { - (0..batches).into_par_iter().for_each(|batch| { - let start = batch * chunk_size; - let end = std::cmp::min(start + chunk_size, len); - update(start, end) - }) - }); - } else { - update(0, len); + let threshold = 1; + if matches!( + update_index_thread_selection, + UpdateIndexThreadSelection::PoolWithThreshold, + ) && len > threshold + { + let chunk_size = len.div_ceil(self.thread_pool_foreground.current_num_threads()); + let batches = 1 + len / chunk_size; + self.thread_pool_foreground.install(|| { + (0..batches).into_par_iter().for_each(|batch| { + let start = batch * chunk_size; + let end = std::cmp::min(start + chunk_size, len); + update(start, end) + }) + }); + } else { + update(0, len); + } } } /// Updates the accounts index with the given `infos` and `accounts`. - /// Used when storing accounts to storage. + /// Used when storing accounts to storage for flush. /// Returns a vector of `SlotList` containing the reclaims for each batch processed. /// The element of the returned vector is guaranteed to be non-empty. - fn update_index_stored_accounts<'a>( + fn update_index_for_flush<'a>( &self, infos: Vec, accounts: &impl StorableAccounts<'a>, @@ -5065,33 +5003,33 @@ impl AccountsDb { let target_slot = accounts.target_slot(); let len = std::cmp::min(accounts.len(), infos.len()); - // If reclaiming old slots, ensure the target slot is a root - // Having an unrooted slot reclaim a rooted version of a slot - // could lead to index corruption if the unrooted version is - // discarded - if reclaim == UpsertReclaim::ReclaimOldSlots { - assert!(target_slot <= self.accounts_index.max_root_inclusive()); - } - let update = |start, end| { let mut reclaims = ReclaimsSlotList::with_capacity((end - start) / 2); (start..end).for_each(|i| { let info: AccountInfo = infos[i]; - debug_assert!(!info.is_cached()); - accounts.account(i, |account| { - let old_slot = accounts.slot(i); - self.accounts_index.upsert( - target_slot, - old_slot, - account.pubkey(), - &account, - &self.account_indexes, - info, - &mut reclaims, - reclaim, - ); - }); + let old_slot = accounts.slot(i); + let pubkey = accounts.pubkey(i); + self.accounts_index.upsert( + target_slot, + old_slot, + pubkey, + info, + &mut reclaims, + reclaim, + ); + + if !self.account_indexes.is_empty() { + // Since StorableAccounts::account() may read the account from disk, + // avoid calling it unless secondary indexes are enabled. + accounts.account(i, |account| { + self.accounts_index.update_secondary_indexes( + pubkey, + &account, + &self.account_indexes, + ); + }); + } }); reclaims }; @@ -5126,14 +5064,78 @@ impl AccountsDb { } } + /// Updates the accounts index for the shrink path: each account at `accounts.slot(i)` has + /// its existing index entry replaced to point at the rewritten storage at `target_slot`. + /// + /// Unlike `update_index_stored_accounts` this does not collect reclaims — the caller is + /// responsible for the source storage's alive-bytes accounting. Secondary indexes are also + /// not touched, since shrink only changes `(store_id, offset)` and they index by pubkey. + fn update_index_for_shrink<'a>( + &self, + infos: &[AccountInfo], + accounts: &impl StorableAccounts<'a>, + update_index_thread_selection: UpdateIndexThreadSelection, + thread_pool: &ThreadPool, + ) { + let target_slot = accounts.target_slot(); + let len = std::cmp::min(accounts.len(), infos.len()); + + let update = |start, end| { + (start..end).for_each(|i| { + let info: AccountInfo = infos[i]; + let old_slot = accounts.slot(i); + let pubkey = accounts.pubkey(i); + self.accounts_index + .replace(target_slot, old_slot, pubkey, info); + }); + }; + + let threshold = 1; + if matches!( + update_index_thread_selection, + UpdateIndexThreadSelection::PoolWithThreshold, + ) && len > threshold + { + let chunk_size = len.div_ceil(thread_pool.current_num_threads()); + let batches = 1 + len / chunk_size; + thread_pool.install(|| { + (0..batches).into_par_iter().for_each(|batch| { + let start = batch * chunk_size; + let end = std::cmp::min(start + chunk_size, len); + update(start, end) + }) + }); + } else { + update(0, len); + } + } + fn should_not_shrink(alive_bytes: u64, total_bytes: u64) -> bool { alive_bytes >= total_bytes } - fn is_shrinking_productive(store: &AccountStorageEntry) -> bool { + /// Can zero lamport single ref accounts in `slot` be purged? + fn can_purge_zero_lamport_single_ref_after_shrink(&self, slot: Slot) -> bool { + self.latest_full_snapshot_slot() + .is_none_or(|latest_full_snapshot_slot| slot <= latest_full_snapshot_slot) + } + + /// Returns the expected alive bytes after shrinking `store`. + pub(crate) fn alive_bytes_after_shrink(&self, store: &AccountStorageEntry) -> usize { + // Obsolete accounts are already excluded from `store.alive_bytes()`. + // Zero-lamport single-ref accounts are counted as alive until shrink can purge them, + // which is gated by the latest full snapshot slot. + if self.can_purge_zero_lamport_single_ref_after_shrink(store.slot()) { + store.alive_bytes_exclude_zero_lamport_single_ref_accounts() + } else { + store.alive_bytes() + } + } + + fn is_shrinking_productive(&self, store: &AccountStorageEntry) -> bool { let alive_count = store.count(); let total_bytes = store.capacity(); - let alive_bytes = store.alive_bytes_exclude_zero_lamport_single_ref_accounts() as u64; + let alive_bytes = self.alive_bytes_after_shrink(store) as u64; if Self::should_not_shrink(alive_bytes, total_bytes) { trace!( "shrink_slot_forced ({}): not able to shrink at all: num alive: {}, bytes alive: \ @@ -5157,7 +5159,7 @@ impl AccountsDb { // It is not possible to identify ancient append vecs when we pack, so no check for ancient when we are not appending. let total_bytes = store.capacity(); - let alive_bytes = store.alive_bytes_exclude_zero_lamport_single_ref_accounts() as u64; + let alive_bytes = self.alive_bytes_after_shrink(store) as u64; match self.shrink_ratio { AccountShrinkThreshold::TotalSpace { shrink_ratio: _ } => alive_bytes < total_bytes, AccountShrinkThreshold::IndividualStore { shrink_ratio } => { @@ -5184,8 +5186,6 @@ impl AccountsDb { let mut new_shrink_candidates = ShrinkCandidates::default(); let mut measure = Measure::start("remove"); for (slot, account_info) in reclaims { - // No cached accounts should make it here - assert!(!account_info.is_cached()); reclaimed_offsets .entry(*slot) .or_default() @@ -5253,7 +5253,7 @@ impl AccountsDb { if remaining_accounts == 0 { self.dirty_stores.insert(*slot, store); dead_slots.insert(*slot); - } else if Self::is_shrinking_productive(&store) + } else if self.is_shrinking_productive(&store) && self.is_candidate_for_shrink(&store) { // Checking that this single storage entry is ready for shrinking, @@ -5281,10 +5281,10 @@ impl AccountsDb { .fetch_add(measure.as_us(), Ordering::Relaxed); dead_slots.retain(|slot| { - if let Some(slot_store) = self.storage.get_slot_storage_entry(*slot) { - if slot_store.count() != 0 { - return false; - } + if let Some(slot_store) = self.storage.get_slot_storage_entry(*slot) + && slot_store.count() != 0 + { + return false; } true }); @@ -5292,14 +5292,6 @@ impl AccountsDb { (dead_slots, reclaimed_offsets) } - fn remove_dead_slots_metadata<'a>(&'a self, dead_slots_iter: impl Iterator) { - let accounts_index_root_stats = self.accounts_index.clean_dead_slots(dead_slots_iter); - - self.clean_accounts_stats - .latest_accounts_index_roots_stats - .update(&accounts_index_root_stats); - } - /// lookup each pubkey in 'pubkeys' and unref it in the accounts index /// skip pubkeys that are in 'pubkeys_removed_from_accounts_index' fn unref_pubkeys<'a>( @@ -5326,15 +5318,12 @@ impl AccountsDb { |_pubkey, slots_refs| { if let Some((slot_list, ref_count)) = slots_refs { // Let's handle the special case - after unref, the result is a single ref zero lamport account. - if slot_list.len() == 1 && ref_count == 2 { - if let Some((slot_alive, acct_info)) = slot_list.first() { - if acct_info.is_zero_lamport() && !acct_info.is_cached() { - self.zero_lamport_single_ref_found( - *slot_alive, - acct_info.offset(), - ); - } - } + if slot_list.len() == 1 + && ref_count == 2 + && let Some((slot_alive, acct_info)) = slot_list.first() + && acct_info.is_zero_lamport() + { + self.zero_lamport_single_ref_found(*slot_alive, acct_info.offset()); } } AccountsIndexScanResult::Unref @@ -5415,7 +5404,6 @@ impl AccountsDb { }; //Unref the accounts from storage - let mut accounts_index_root_stats = AccountsIndexRootsStats::default(); let mut measure_unref = Measure::start("unref_from_storage"); if let Some(purged_account_slots) = purged_account_slots { @@ -5426,11 +5414,9 @@ impl AccountsDb { ); } measure_unref.stop(); - accounts_index_root_stats.clean_unref_from_storage_us += measure_unref.as_us(); - self.clean_accounts_stats - .latest_accounts_index_roots_stats - .update(&accounts_index_root_stats); + .clean_unref_from_storage_us + .fetch_add(measure_unref.as_us(), Ordering::Relaxed); measure.stop(); self.clean_accounts_stats @@ -5458,18 +5444,22 @@ impl AccountsDb { self.write_accounts_to_cache(accounts.target_slot(), &accounts, ancestors); let write_accounts_us = write_accounts_time.end_as_us(); - // Update the index - let update_index_time = Measure::start("update_index"); - self.update_index_cached_accounts(&accounts, &store_account, update_index_thread_selection); - let update_index_us = update_index_time.end_as_us(); + // Update the secondary index + let update_secondary_index_time = Measure::start("update_secondary_index"); + self.update_secondary_index_cached_accounts( + &accounts, + &store_account, + update_index_thread_selection, + ); + let update_secondary_index_us = update_secondary_index_time.end_as_us(); let stats = &self.store_accounts_unfrozen_stats; stats .write_to_cache_us .fetch_add(write_accounts_us, Ordering::Relaxed); stats - .update_index_us - .fetch_add(update_index_us, Ordering::Relaxed); + .update_secondary_index_us + .fetch_add(update_secondary_index_us, Ordering::Relaxed); stats .num_initial_accounts_to_store .fetch_add(write_stats.num_initial_accounts_to_store, Ordering::Relaxed); @@ -5495,52 +5485,99 @@ impl AccountsDb { self.report_store_timings(); } + /// Store `accounts` into `storage`. + /// + /// This fn is to only be called by ancient squash. + pub(crate) fn store_accounts_for_squash<'a>( + &self, + accounts: impl StorableAccounts<'a>, + storage: &AccountStorageEntry, + ) -> StoreAccountsForSquashStats { + let slot = accounts.target_slot(); + + // Flush the read cache if necessary + let flush_read_cache_us = if self.read_only_accounts_cache.can_slot_be_in_cache(slot) { + let flush_read_cache_time = Measure::start("flush_read_cache"); + (0..accounts.len()).for_each(|index| { + // Based on the patterns of how a validator writes accounts, it is almost always + // the case that there is no read only cache entry for this pubkey and slot. + // So, we can give that hint to the `remove` for performance. + self.read_only_accounts_cache + .remove_assume_not_present(accounts.pubkey(index)); + }); + flush_read_cache_time.end_as_us() + } else { + 0 + }; + + let store_accounts_for_shrink_stats = self.store_accounts_for_shrink( + accounts, + storage, + UpdateIndexThreadSelection::PoolWithThreshold, + ); + StoreAccountsForSquashStats { + store_accounts_for_shrink_stats, + flush_read_cache_us, + } + } + /// Stores accounts in the storage and updates the index. - /// This function is intended for accounts that are rooted (frozen). + /// This function is intended for accounts that are being shrunk (moving from one store to another) /// - `UpsertReclaims` is set to `IgnoreReclaims`. If the slot in `accounts` differs from the new slot, /// accounts may be removed from the account index. In such cases, the caller must ensure that alive /// accounts are decremented for the older storage or that the old storage is removed entirely - pub fn store_accounts_frozen<'a>( + pub fn store_accounts_for_shrink<'a>( &self, accounts: impl StorableAccounts<'a>, storage: &AccountStorageEntry, update_index_thread_selection: UpdateIndexThreadSelection, - ) -> StoreAccountsTiming { - self._store_accounts_frozen( - accounts, - storage, - UpsertReclaim::IgnoreReclaims, + ) -> StoreAccountsForShrinkStats { + let slot = accounts.target_slot(); + let num_accounts_stored = accounts.len(); + + // Write the accounts to storage + let write_accounts_time = Measure::start("write_accounts"); + let infos = self.write_accounts_to_storage(slot, storage, &accounts); + let write_accounts_us = write_accounts_time.end_as_us(); + + let mark_zero_lamport_single_ref_time = Measure::start("mark_zero_lamport_single_ref"); + let num_zero_lamport_single_ref_accounts_marked = + self.mark_zero_lamport_single_ref_accounts_for_shrink(&infos, &accounts, storage); + let mark_zero_lamport_single_ref_us = mark_zero_lamport_single_ref_time.end_as_us(); + + let update_index_time = Measure::start("update_index"); + self.update_index_for_shrink( + &infos, + &accounts, update_index_thread_selection, - ) + &self.thread_pool_background, + ); + let update_index_us = update_index_time.end_as_us(); + + StoreAccountsForShrinkStats { + write_accounts_us, + mark_zero_lamport_single_ref_accounts_us: mark_zero_lamport_single_ref_us, + update_index_us, + num_accounts_stored: num_accounts_stored as u64, + num_zero_lamport_single_ref_accounts_marked, + } } /// Stores accounts in the storage and updates the index. - /// This function is intended for accounts that are rooted (frozen). - /// - `UpsertReclaims` must be set to `IgnoreReclaims` at this time - fn _store_accounts_frozen<'a>( + /// This function is intended for accounts that are being flushed (moving from the cache to storage) + /// - `UpsertReclaims` determines whether to reclaim old slots. If `ReclaimOldSlots` is used, all + /// old versions of the account are reclaimed. If `IgnoreReclaims` is used, old versions of the + /// account are not reclaimed and must be cleaned later. + fn store_accounts_for_flush<'a>( &self, accounts: impl StorableAccounts<'a>, storage: &AccountStorageEntry, reclaim_handling: UpsertReclaim, update_index_thread_selection: UpdateIndexThreadSelection, - ) -> StoreAccountsTiming { + ) -> StoreAccountsForFlushStats { let slot = accounts.target_slot(); - let num_accounts_stored = accounts.len(); - let stats = &self.store_accounts_frozen_stats; - debug_assert!(self.accounts_index.is_alive_root(slot)); - - // Flush the read cache if necessary. This will occur during shrink or clean - let flush_read_cache_time = Measure::start("flush_read_cache"); - if self.read_only_accounts_cache.can_slot_be_in_cache(slot) { - (0..accounts.len()).for_each(|index| { - // based on the patterns of how a validator writes accounts, it is almost always the case that there is no read only cache entry - // for this pubkey and slot. So, we can give that hint to the `remove` for performance. - self.read_only_accounts_cache - .remove_assume_not_present(accounts.pubkey(index)); - }); - } - let flush_read_cache_us = flush_read_cache_time.end_as_us(); + debug_assert!(self.accounts_cache.contains_unflushed_root(slot)); // Write the accounts to storage let write_accounts_time = Measure::start("write_accounts"); @@ -5549,15 +5586,11 @@ impl AccountsDb { let mark_zero_lamport_time = Measure::start("mark_zero_lamport"); let num_zero_lamport_single_ref_accounts_marked = - self.mark_zero_lamport_single_ref_accounts(&infos, storage, reclaim_handling); + self.mark_zero_lamport_single_ref_accounts_for_flush(&infos, storage, reclaim_handling); let mark_zero_lamport_us = mark_zero_lamport_time.end_as_us(); - // If the cache was flushed, then because `update_index` occurs - // after the account are stored by the above `store_accounts_to` - // call and all the accounts are stored, all reads after this point - // will know to not check the cache anymore let update_index_time = Measure::start("update_index"); - let reclaims = self.update_index_stored_accounts( + let reclaims = self.update_index_for_flush( infos, &accounts, reclaim_handling, @@ -5572,8 +5605,11 @@ impl AccountsDb { // should skip handle_reclaims only when reclaims is empty. No need to // check the elements of reclaims are empty. let handle_reclaims_time = Measure::start("handle_reclaims"); + let mut num_reclaims = 0; + let mut num_obsolete_slots_removed = 0; + let mut num_obsolete_bytes_removed = 0; if !reclaims.is_empty() { - let reclaims_len = reclaims.iter().map(|r| r.len()).sum::(); + num_reclaims = reclaims.iter().map(|r| r.len() as u64).sum(); let purge_stats = PurgeStats::default(); self.handle_reclaims( reclaims.iter().flatten(), @@ -5582,79 +5618,35 @@ impl AccountsDb { &purge_stats, MarkAccountsObsolete::Yes(slot), ); - stats.num_obsolete_slots_removed.fetch_add( - purge_stats.num_stored_slots_removed.load(Ordering::Relaxed), - Ordering::Relaxed, - ); - stats.num_obsolete_bytes_removed.fetch_add( - purge_stats - .total_removed_stored_bytes - .load(Ordering::Relaxed), - Ordering::Relaxed, - ); - stats - .num_reclaims - .fetch_add(reclaims_len as u64, Ordering::Relaxed); + num_obsolete_slots_removed = + purge_stats.num_stored_slots_removed.load(Ordering::Relaxed) as u64; + num_obsolete_bytes_removed = purge_stats + .total_removed_stored_bytes + .load(Ordering::Relaxed); } let handle_reclaims_us = handle_reclaims_time.end_as_us(); - stats - .flush_read_cache_us - .fetch_add(flush_read_cache_us, Ordering::Relaxed); - stats - .write_to_storage_us - .fetch_add(write_accounts_us, Ordering::Relaxed); - stats - .update_index_us - .fetch_add(update_index_us, Ordering::Relaxed); - stats - .mark_zero_lamport_single_ref_accounts_us - .fetch_add(mark_zero_lamport_us, Ordering::Relaxed); - stats - .handle_reclaims_us - .fetch_add(handle_reclaims_us, Ordering::Relaxed); - stats - .num_accounts_stored - .fetch_add(num_accounts_stored as u64, Ordering::Relaxed); - stats.num_zero_lamport_single_ref_accounts_marked.fetch_add( + StoreAccountsForFlushStats { + write_accounts_us, + update_index_us, + handle_reclaims_us, + mark_zero_lamport_single_ref_accounts_us: mark_zero_lamport_us, num_zero_lamport_single_ref_accounts_marked, - Ordering::Relaxed, - ); - stats.report(); - - StoreAccountsTiming { - store_accounts_elapsed: write_accounts_us, - update_index_elapsed: update_index_us, - handle_reclaims_elapsed: handle_reclaims_us, + num_reclaims, + num_obsolete_slots_removed, + num_obsolete_bytes_removed, } } /// Returns whether the latest version of pubkey from ancestors is zero-lamport /// Returns `None` if the account doesn't exist fn is_ancestor_zero_lamport(&self, pubkey: &Pubkey, ancestors: &Ancestors) -> Option { - if let Some((cached_account, cache_slot, status)) = + if let Some((cached_account, _cache_slot)) = self.accounts_cache.load_latest(pubkey, ancestors) { - let cache_is_zero_lamport = cached_account.account.lamports() == 0; - // When the slot isn't being flushed, the cache is definitely the newest - if matches!(status, SlotStatus::Ancestor | SlotStatus::UnflushedRoot) { - return Some(cache_is_zero_lamport); - } - - // Slot is being flushed; a newer version may already exist in storage. - if let Some((index_slot, index_is_zero_lamport)) = self - .accounts_index - .get_with_and_then(pubkey, ancestors, true, |(slot, account)| { - (slot, account.is_zero_lamport()) - }) - { - // Return the zero lamport status of the newest version between the cache and index - // If slots are the same it doesn't matter which is returned (They are identical) - if index_slot > cache_slot { - return Some(index_is_zero_lamport); - } - } - Some(cache_is_zero_lamport) + // Check the write cache first; a hit is the freshest version visible on this fork, + // so return it + Some(cached_account.account.lamports() == 0) } else { self.accounts_index .get_with_and_then(pubkey, ancestors, true, |(_, account)| { @@ -5742,7 +5734,8 @@ impl AccountsDb { infos.len(), accounts_and_meta_to_store.len() ); - self.create_and_insert_store(slot, data_len * 2, "large create"); + let store = self.create_store(slot, data_len * 2, "large create"); + self.storage.insert(store); } continue; }; @@ -5763,10 +5756,10 @@ impl AccountsDb { infos } - /// Marks zero lamport single reference accounts in the storage during store_accounts_frozen + /// Marks zero lamport single reference accounts in the storage during store_accounts_for_flush /// /// Returns the number of accounts marked. - fn mark_zero_lamport_single_ref_accounts( + fn mark_zero_lamport_single_ref_accounts_for_flush( &self, account_infos: &[AccountInfo], storage: &AccountStorageEntry, @@ -5789,7 +5782,7 @@ impl AccountsDb { // If any zero lamport accounts were marked, the storage may be valid for shrinking if num_marked > 0 && self.is_candidate_for_shrink(storage) - && Self::is_shrinking_productive(storage) + && self.is_shrinking_productive(storage) { self.shrink_candidate_slots .lock() @@ -5801,6 +5794,38 @@ impl AccountsDb { num_marked } + /// Marks zero lamport single reference accounts in the storage during store_accounts_for_shrink + /// + /// Returns the number of accounts marked. + fn mark_zero_lamport_single_ref_accounts_for_shrink<'a>( + &self, + account_infos: &[AccountInfo], + accounts: &impl StorableAccounts<'a>, + storage: &AccountStorageEntry, + ) -> u64 { + debug_assert_eq!(account_infos.len(), accounts.len()); + let mut num_marked = 0; + for (i, account_info) in account_infos.iter().enumerate() { + if account_info.is_zero_lamport() { + let pubkey = accounts.pubkey(i); + let is_single_ref = self.accounts_index.get_and_then(pubkey, |entry| { + let is_single_ref = entry.is_some_and(|entry| { + entry.ref_count() == 1 && entry.slot_list_lock_read_len() == 1 + }); + (false, is_single_ref) + }); + if is_single_ref { + debug_assert_eq!(account_info.store_id(), storage.id()); + if storage.insert_zero_lamport_single_ref_account_offset(account_info.offset()) + { + num_marked += 1; + } + } + } + } + num_marked + } + fn report_store_timings(&self) { if self.stats.last_store_report.should_update(1000) { let read_cache_stats = self.read_only_accounts_cache.get_and_reset_stats(); @@ -5850,13 +5875,8 @@ impl AccountsDb { i64 ), ( - "read_only_accounts_cache_evictor_wakeup_count_all", - read_cache_stats.evictor_wakeup_count_all, - i64 - ), - ( - "read_only_accounts_cache_evictor_wakeup_count_productive", - read_cache_stats.evictor_wakeup_count_productive, + "read_only_accounts_cache_evict_run_count", + read_cache_stats.evict_run_count, i64 ), ( @@ -5895,19 +5915,22 @@ impl AccountsDb { } pub fn add_root(&self, slot: Slot) -> AccountsAddRootTiming { - let mut index_time = Measure::start("index_add_root"); - self.accounts_index.add_root(slot); - index_time.stop(); let mut cache_time = Measure::start("cache_add_root"); self.accounts_cache.add_root(slot); cache_time.stop(); + self.max_root.fetch_max(slot, Ordering::Relaxed); + AccountsAddRootTiming { - index_us: index_time.as_us(), cache_us: cache_time.as_us(), } } + /// Returns the largest slot that has been added as a root via `add_root`. + pub fn max_root(&self) -> Slot { + self.max_root.load(Ordering::Relaxed) + } + /// Returns storages for `requested_slots` pub fn get_storages( &self, @@ -5933,6 +5956,23 @@ impl AccountsDb { /// Sets the latest full snapshot slot to `slot` pub fn set_latest_full_snapshot_slot(&self, slot: Slot) { *self.latest_full_snapshot_slot.lock_write() = Some(slot); + self.latest_full_snapshot_slot_advanced_since_clean + .store(true, Ordering::Release); + } + + /// Marks slots <= slot as already swept for zero-lamport-single-ref shrink eligibility + pub fn set_last_swept_full_snapshot_slot(&self, slot: Slot) { + // Prior to setting this, the latest full snapshot slot must be set, and + // last_swept_full_snapshot_slot value must be less than or equal to it. + assert!( + self.latest_full_snapshot_slot() + .is_some_and(|snapshot_slot| slot <= snapshot_slot), + "last swept full snapshot slot {slot} cannot be greater than latest full snapshot \ + slot {:?}", + self.latest_full_snapshot_slot() + ); + self.last_swept_full_snapshot_slot + .store(slot, Ordering::Relaxed); } fn generate_index_for_slot<'a>( @@ -6108,6 +6148,11 @@ impl AccountsDb { } let num_storages = storages.len(); + // `storages` is sorted by slot, so the last one is the highest root. + if let Some(storage) = storages.last() { + self.max_root.fetch_max(storage.slot(), Ordering::Relaxed); + } + self.accounts_index.set_startup(Startup::Startup); let mut total_accum = IndexGenerationAccumulator::with_slots_capacity(num_storages); @@ -6377,11 +6422,6 @@ impl AccountsDb { .insert(slot, storages[storage_index].clone()); } - // Need to add these last, otherwise older updates will be cleaned - for storage in &storages { - self.accounts_index.add_root(storage.slot()); - } - self.set_storage_count_and_alive_bytes(total_accum.storage_info, &mut timings); let mut mark_obsolete_accounts_time = Measure::start("mark_obsolete_accounts_time"); @@ -6470,8 +6510,7 @@ impl AccountsDb { } ObsoleteAccountsStats { accounts_marked_obsolete: reclaims.len() as u64, - slots_removed: stats.total_removed_storage_entries.load(Ordering::Relaxed) - as u64, + slots_removed: stats.num_stored_slots_removed.load(Ordering::Relaxed) as u64, } }) .sum(); @@ -6501,41 +6540,40 @@ impl AccountsDb { self.accounts_index.scan( pubkeys.iter(), |pubkey, slots_refs| { - if let Some((slot_list, _ref_count)) = slots_refs { - if slot_list.len() > 1 { - // Only the account data len in the highest slot should be used, and the rest are - // duplicates. So find the max slot to keep. - // Then sum up the remaining data len, which are the duplicates. - // All of the slots need to go in the 'uncleaned_slots' list. For clean to work properly, - // the slot where duplicate accounts are found in the index need to be in 'uncleaned_slots' list, too. - let max = slot_list.iter().map(|(slot, _)| slot).max().unwrap(); - slot_list.iter().for_each(|(slot, account_info)| { - if slot == max { - // the info in 'max' is the most recent, current info for this pubkey - return; + if let Some((slot_list, _ref_count)) = slots_refs + && slot_list.len() > 1 + { + // Only the account data len in the highest slot should be used, and the rest are + // duplicates. So find the max slot to keep. + // Then sum up the remaining data len, which are the duplicates. + // All of the slots need to go in the 'uncleaned_slots' list. For clean to work properly, + // the slot where duplicate accounts are found in the index need to be in 'uncleaned_slots' list, too. + let max = slot_list.iter().map(|(slot, _)| slot).max().unwrap(); + slot_list.iter().for_each(|(slot, account_info)| { + if slot == max { + // the info in 'max' is the most recent, current info for this pubkey + return; + } + let maybe_storage_entry = self + .storage + .get_account_storage_entry(*slot, account_info.store_id()); + let mut accessor = LoadedAccountAccessor::Stored( + maybe_storage_entry.map(|entry| (entry, account_info.offset())), + ); + accessor.check_and_get_loaded_account(|loaded_account| { + let data_len = loaded_account.data_len(); + let lamports = loaded_account.lamports(); + if lamports > 0 { + accounts_data_len_from_duplicates += data_len; } - let maybe_storage_entry = self - .storage - .get_account_storage_entry(*slot, account_info.store_id()); - let mut accessor = LoadedAccountAccessor::Stored( - maybe_storage_entry.map(|entry| (entry, account_info.offset())), - ); - accessor.check_and_get_loaded_account(|loaded_account| { - let data_len = loaded_account.data_len(); - let lamports = loaded_account.lamports(); - if lamports > 0 { - accounts_data_len_from_duplicates += data_len; - } - num_duplicate_accounts += 1; - let account_lt_hash = - Self::lt_hash_account(&loaded_account, pubkey); - duplicates_lt_hash.0.mix_in(&account_lt_hash.0); - capitalization_from_duplicates = capitalization_from_duplicates - .checked_add(u128::from(lamports)) - .expect("capitalization cannot overflow"); - }); + num_duplicate_accounts += 1; + let account_lt_hash = Self::lt_hash_account(&loaded_account, pubkey); + duplicates_lt_hash.0.mix_in(&account_lt_hash.0); + capitalization_from_duplicates = capitalization_from_duplicates + .checked_add(u128::from(lamports)) + .expect("capitalization cannot overflow"); }); - } + }); } AccountsIndexScanResult::OnlyKeepInMemoryIfDirty }, @@ -6588,15 +6626,11 @@ impl AccountsDb { } pub fn print_accounts_stats(&self, label: &str) { - self.print_index(label); + self.print_index(); self.print_count_and_status(label); } - fn print_index(&self, label: &str) { - let mut alive_roots: Vec<_> = self.accounts_index.all_alive_roots(); - #[allow(clippy::stable_sort_primitive)] - alive_roots.sort(); - info!("{label}: accounts_index alive_roots: {alive_roots:?}"); + fn print_index(&self) { self.accounts_index.account_maps.iter().for_each(|map| { for pubkey in map.keys() { self.accounts_index.get_and_then(&pubkey, |account_entry| { @@ -6631,6 +6665,30 @@ impl AccountsDb { } } +/// Whether a rooted-cache flush should clean (dedup across roots, keeping only the newest +/// version of each account and reclaiming older ones) or write every account untouched. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FlushShouldClean { + /// Write every account in every flushed root. Older versions must be preserved (e.g. for + /// snapshotting). + No, + /// Clean roots at or below `max_clean_root`; roots above it still flush all their accounts + /// (`None` means clean every flushed root). + Yes { max_clean_root: Option }, +} + +/// Which of a slot's cached accounts to write to storage when flushing it. +#[derive(Debug, PartialEq, Eq)] +enum PubkeysToFlush { + /// Flush every account in the slot, reclaiming nothing. Used for roots above + /// `max_clean_root` and when not cleaning, since an in-flight scan may still need + /// those versions. + All, + /// Flush only these pubkeys (the newest version of each, per `select_pubkeys_to_flush`), + /// purging the rest from the index and reclaiming older versions. + Only(HashSet), +} + /// Specify whether obsolete accounts should be marked or not during reclaims /// They should only be marked if they are also getting unreffed in the index /// Temporarily allow dead code until the feature is implemented @@ -6713,16 +6771,8 @@ impl AccountsDb { } pub fn flush_accounts_cache_slot_for_tests(&self, slot: Slot) { - assert!( - self.accounts_index - .roots_tracker - .read() - .unwrap() - .alive_roots - .contains(&slot), - "slot: {slot}" - ); - self.flush_slot_cache(slot, None::<&mut fn(&_) -> bool>, None); + assert!(self.accounts_cache.contains_unflushed_root(slot)); + self.flush_slot_cache(slot, &PubkeysToFlush::All); } /// useful to adapt tests written prior to introduction of the write cache @@ -6736,7 +6786,7 @@ impl AccountsDb { /// Note that this is non-deterministic if clean is running asynchronously. /// If a zero lamport account exists in the index, then Some is returned. /// Once it is cleaned from the index, None is returned. - fn load_without_fixed_root( + fn do_load_for_tests( &self, ancestors: &Ancestors, pubkey: &Pubkey, @@ -6751,13 +6801,13 @@ impl AccountsDb { pub fn assert_load_account(&self, slot: Slot, pubkey: Pubkey, expected_lamports: u64) { let ancestors = Ancestors::from(vec![slot]); - let (account, slot) = self.load_without_fixed_root(&ancestors, &pubkey).unwrap(); + let (account, slot) = self.do_load_for_tests(&ancestors, &pubkey).unwrap(); assert_eq!((account.lamports(), slot), (expected_lamports, slot)); } pub fn assert_not_load_account(&self, slot: Slot, pubkey: Pubkey) { let ancestors = Ancestors::from(vec![slot]); - let load = self.load_without_fixed_root(&ancestors, &pubkey); + let load = self.do_load_for_tests(&ancestors, &pubkey); assert!(load.is_none(), "{load:?}"); } @@ -6771,7 +6821,7 @@ impl AccountsDb { let ancestors = Ancestors::from(vec![slot]); for _ in 0..num { let idx = rng().random_range(0..num); - let account = self.load_without_fixed_root(&ancestors, &pubkeys[idx]); + let account = self.do_load_for_tests(&ancestors, &pubkeys[idx]); let account1 = Some(( AccountSharedData::new( (idx + count) as u64, @@ -6855,7 +6905,7 @@ impl AccountsDb { let account = AccountSharedData::new((t + 1) as u64, space, AccountSharedData::default().owner()); pubkeys.push(pubkey); - assert!(self.load_without_fixed_root(&ancestors, &pubkey).is_none()); + assert!(self.do_load_for_tests(&ancestors, &pubkey).is_none()); self.store_for_tests((slot, [(&pubkey, &account)].as_slice())); } for t in 0..num_vote { @@ -6864,7 +6914,7 @@ impl AccountsDb { AccountSharedData::new((num + t + 1) as u64, space, &solana_vote_program::id()); pubkeys.push(pubkey); let ancestors = Ancestors::from(vec![slot]); - assert!(self.load_without_fixed_root(&ancestors, &pubkey).is_none()); + assert!(self.do_load_for_tests(&ancestors, &pubkey).is_none()); self.store_for_tests((slot, [(&pubkey, &account)].as_slice())); } } @@ -6885,15 +6935,7 @@ impl AccountsDb { /// useful to adapt tests written prior to introduction of the write cache /// to use the write cache pub fn flush_root_write_cache(&self, root: Slot) { - assert!( - self.accounts_index - .roots_tracker - .read() - .unwrap() - .alive_roots - .contains(&root), - "slot: {root}" - ); + assert!(self.accounts_cache.contains_unflushed_root(root)); self.flush_accounts_cache(true, Some(root)); } diff --git a/accounts-db/src/accounts_db/accounts_db_config.rs b/accounts-db/src/accounts_db/accounts_db_config.rs index a7e0a577162..32927228b69 100644 --- a/accounts-db/src/accounts_db/accounts_db_config.rs +++ b/accounts-db/src/accounts_db/accounts_db_config.rs @@ -17,7 +17,6 @@ pub struct AccountsDbConfig { pub index: Option, pub account_indexes: Option, pub bank_hash_details_dir: PathBuf, - pub shrink_paths: Option>, pub shrink_ratio: AccountShrinkThreshold, /// The low and high watermark sizes for the read cache, in bytes. /// If None, defaults will be used. @@ -48,7 +47,6 @@ pub const ACCOUNTS_DB_CONFIG_FOR_TESTING: AccountsDbConfig = AccountsDbConfig { index: Some(ACCOUNTS_INDEX_CONFIG_FOR_TESTING), account_indexes: None, bank_hash_details_dir: PathBuf::new(), // tests don't use bank hash details - shrink_paths: None, shrink_ratio: DEFAULT_ACCOUNTS_SHRINK_THRESHOLD_OPTION, read_cache_limit_bytes: None, read_cache_evict_sample_size: None, @@ -69,7 +67,6 @@ pub const ACCOUNTS_DB_CONFIG_FOR_BENCHMARKS: AccountsDbConfig = AccountsDbConfig index: Some(ACCOUNTS_INDEX_CONFIG_FOR_BENCHMARKS), account_indexes: None, bank_hash_details_dir: PathBuf::new(), // benches don't use bank hash details - shrink_paths: None, shrink_ratio: DEFAULT_ACCOUNTS_SHRINK_THRESHOLD_OPTION, read_cache_limit_bytes: None, read_cache_evict_sample_size: None, diff --git a/accounts-db/src/accounts_db/stats.rs b/accounts-db/src/accounts_db/stats.rs index 91d17c3840e..638007e9256 100644 --- a/accounts-db/src/accounts_db/stats.rs +++ b/accounts-db/src/accounts_db/stats.rs @@ -1,5 +1,4 @@ use { - crate::{accounts_index::AccountsIndexRootsStats, append_vec::APPEND_VEC_STATS}, solana_time_utils::AtomicInterval, std::{ iter::Sum, @@ -26,8 +25,8 @@ pub struct StoreAccountsUnfrozenStats { pub last_report: AtomicInterval, /// time spent writing accounts to the write cache pub write_to_cache_us: AtomicU64, - /// time spend updating the accounts index - pub update_index_us: AtomicU64, + /// time spent updating the secondary indexes + pub update_secondary_index_us: AtomicU64, /// initial number of accounts to be stored pub num_initial_accounts_to_store: AtomicU64, /// number of accounts actually stored @@ -59,8 +58,8 @@ impl StoreAccountsUnfrozenStats { i64 ), ( - "update_index_us", - self.update_index_us.swap(0, Ordering::Relaxed), + "update_secondary_index_us", + self.update_secondary_index_us.swap(0, Ordering::Relaxed), i64 ), ( @@ -101,89 +100,56 @@ impl StoreAccountsUnfrozenStats { } } -/// Stats from storing accounts frozen (i.e. to an account storage file) +/// Stats from storing accounts for shrink (i.e. moving accounts into new storage file) #[derive(Debug, Default)] -pub struct StoreAccountsFrozenStats { - pub last_report: AtomicInterval, - pub flush_read_cache_us: AtomicU64, - pub write_to_storage_us: AtomicU64, - pub update_index_us: AtomicU64, - pub mark_zero_lamport_single_ref_accounts_us: AtomicU64, - pub handle_reclaims_us: AtomicU64, - pub num_accounts_stored: AtomicU64, - pub num_zero_lamport_single_ref_accounts_marked: AtomicU64, - pub num_reclaims: AtomicU64, - pub num_obsolete_slots_removed: AtomicUsize, - pub num_obsolete_bytes_removed: AtomicU64, +pub struct StoreAccountsForShrinkStats { + pub write_accounts_us: u64, + pub mark_zero_lamport_single_ref_accounts_us: u64, + pub update_index_us: u64, + pub num_accounts_stored: u64, + pub num_zero_lamport_single_ref_accounts_marked: u64, } -impl StoreAccountsFrozenStats { - const REPORT_INTERVAL_MS: u64 = Duration::from_secs(1).as_millis() as u64; +impl StoreAccountsForShrinkStats { + pub fn accumulate(&mut self, other: &Self) { + self.write_accounts_us += other.write_accounts_us; + self.mark_zero_lamport_single_ref_accounts_us += + other.mark_zero_lamport_single_ref_accounts_us; + self.update_index_us += other.update_index_us; + self.num_accounts_stored += other.num_accounts_stored; + self.num_zero_lamport_single_ref_accounts_marked += + other.num_zero_lamport_single_ref_accounts_marked; + } +} - pub fn report(&self) { - let should_report = self.last_report.should_update(Self::REPORT_INTERVAL_MS); - if !should_report { - return; - } +/// Stats from storing accounts for squash (i.e. moving accounts between storage files) +#[derive(Debug, Default)] +pub struct StoreAccountsForSquashStats { + pub store_accounts_for_shrink_stats: StoreAccountsForShrinkStats, + pub flush_read_cache_us: u64, +} - datapoint_info!( - "accounts_db_store_accounts_frozen", - ( - "flush_read_cache_us", - self.flush_read_cache_us.swap(0, Ordering::Relaxed), - i64 - ), - ( - "write_to_storage_us", - self.write_to_storage_us.swap(0, Ordering::Relaxed), - i64 - ), - ( - "update_index_us", - self.update_index_us.swap(0, Ordering::Relaxed), - i64 - ), - ( - "mark_zero_lamport_single_ref_accounts_us", - self.mark_zero_lamport_single_ref_accounts_us - .swap(0, Ordering::Relaxed), - i64 - ), - ( - "handle_reclaims_us", - self.handle_reclaims_us.swap(0, Ordering::Relaxed), - i64 - ), - ( - "num_accounts_stored", - self.num_accounts_stored.swap(0, Ordering::Relaxed), - i64 - ), - ( - "num_zero_lamport_single_ref_accounts_marked", - self.num_zero_lamport_single_ref_accounts_marked - .swap(0, Ordering::Relaxed), - i64 - ), - ( - "num_reclaims", - self.num_reclaims.swap(0, Ordering::Relaxed), - i64 - ), - ( - "num_obsolete_slots_removed", - self.num_obsolete_slots_removed.swap(0, Ordering::Relaxed), - i64 - ), - ( - "num_obsolete_bytes_removed", - self.num_obsolete_bytes_removed.swap(0, Ordering::Relaxed), - i64 - ), - ); +impl StoreAccountsForSquashStats { + pub fn accumulate(&mut self, other: &Self) { + self.store_accounts_for_shrink_stats + .accumulate(&other.store_accounts_for_shrink_stats); + self.flush_read_cache_us += other.flush_read_cache_us; } } +/// Stats from storing accounts for flush (i.e. flushing the write cache to a storage file) +#[derive(Debug, Default)] +pub struct StoreAccountsForFlushStats { + pub write_accounts_us: u64, + pub update_index_us: u64, + pub handle_reclaims_us: u64, + pub mark_zero_lamport_single_ref_accounts_us: u64, + pub num_zero_lamport_single_ref_accounts_marked: u64, + pub num_reclaims: u64, + pub num_obsolete_slots_removed: u64, + pub num_obsolete_bytes_removed: u64, +} + #[derive(Debug, Default)] pub struct PurgeStats { pub last_report: AtomicInterval, @@ -193,7 +159,6 @@ pub struct PurgeStats { pub drop_storage_entries_elapsed: AtomicU64, pub num_cached_slots_removed: AtomicUsize, pub num_stored_slots_removed: AtomicUsize, - pub total_removed_storage_entries: AtomicUsize, pub total_removed_cached_bytes: AtomicU64, pub total_removed_stored_bytes: AtomicU64, pub scan_storages_elapsed: AtomicU64, @@ -241,12 +206,6 @@ impl PurgeStats { self.num_stored_slots_removed.swap(0, Ordering::Relaxed), i64 ), - ( - "total_removed_storage_entries", - self.total_removed_storage_entries - .swap(0, Ordering::Relaxed), - i64 - ), ( "total_removed_cached_bytes", self.total_removed_cached_bytes.swap(0, Ordering::Relaxed), @@ -277,137 +236,69 @@ impl PurgeStats { } } -#[derive(Default, Debug)] -pub struct StoreAccountsTiming { - pub store_accounts_elapsed: u64, - pub update_index_elapsed: u64, - pub handle_reclaims_elapsed: u64, -} - -impl StoreAccountsTiming { - pub fn accumulate(&mut self, other: &Self) { - self.store_accounts_elapsed += other.store_accounts_elapsed; - self.update_index_elapsed += other.update_index_elapsed; - self.handle_reclaims_elapsed += other.handle_reclaims_elapsed; - } -} - #[derive(Debug, Default)] pub struct FlushStats { pub num_accounts_flushed: Saturating, pub num_bytes_flushed: Saturating, pub num_accounts_purged: Saturating, pub num_bytes_purged: Saturating, - pub store_accounts_timing: StoreAccountsTiming, pub store_accounts_total_us: Saturating, + pub write_accounts_us: Saturating, + pub update_index_us: Saturating, + pub handle_reclaims_us: Saturating, + pub mark_zero_lamport_single_ref_accounts_us: Saturating, + pub num_zero_lamport_single_ref_accounts_marked: Saturating, + pub num_reclaims: Saturating, + pub num_obsolete_slots_removed: Saturating, + pub num_obsolete_bytes_removed: Saturating, + pub select_pubkeys_us: Saturating, + pub disk_index_write_through_us: Saturating, } impl FlushStats { + pub fn accumulate_store_accounts_for_flush( + &mut self, + store_accounts_stats: StoreAccountsForFlushStats, + ) { + self.write_accounts_us += Saturating(store_accounts_stats.write_accounts_us); + self.update_index_us += Saturating(store_accounts_stats.update_index_us); + self.handle_reclaims_us += Saturating(store_accounts_stats.handle_reclaims_us); + self.mark_zero_lamport_single_ref_accounts_us += + Saturating(store_accounts_stats.mark_zero_lamport_single_ref_accounts_us); + self.num_zero_lamport_single_ref_accounts_marked += + Saturating(store_accounts_stats.num_zero_lamport_single_ref_accounts_marked); + self.num_reclaims += Saturating(store_accounts_stats.num_reclaims); + self.num_obsolete_slots_removed += + Saturating(store_accounts_stats.num_obsolete_slots_removed); + self.num_obsolete_bytes_removed += + Saturating(store_accounts_stats.num_obsolete_bytes_removed); + } + pub fn accumulate(&mut self, other: &Self) { self.num_accounts_flushed += other.num_accounts_flushed; self.num_bytes_flushed += other.num_bytes_flushed; self.num_accounts_purged += other.num_accounts_purged; self.num_bytes_purged += other.num_bytes_purged; - self.store_accounts_timing - .accumulate(&other.store_accounts_timing); self.store_accounts_total_us += other.store_accounts_total_us; - } -} - -#[derive(Debug, Default)] -pub struct LatestAccountsIndexRootsStats { - pub roots_len: AtomicUsize, - pub uncleaned_roots_len: AtomicUsize, - pub roots_range: AtomicU64, - pub rooted_cleaned_count: AtomicUsize, - pub unrooted_cleaned_count: AtomicUsize, - pub clean_unref_from_storage_us: AtomicU64, - pub clean_dead_slot_us: AtomicU64, -} - -impl LatestAccountsIndexRootsStats { - pub fn update(&self, accounts_index_roots_stats: &AccountsIndexRootsStats) { - if let Some(value) = accounts_index_roots_stats.roots_len { - self.roots_len.store(value, Ordering::Relaxed); - } - if let Some(value) = accounts_index_roots_stats.uncleaned_roots_len { - self.uncleaned_roots_len.store(value, Ordering::Relaxed); - } - if let Some(value) = accounts_index_roots_stats.roots_range { - self.roots_range.store(value, Ordering::Relaxed); - } - self.rooted_cleaned_count.fetch_add( - accounts_index_roots_stats.rooted_cleaned_count, - Ordering::Relaxed, - ); - self.unrooted_cleaned_count.fetch_add( - accounts_index_roots_stats.unrooted_cleaned_count, - Ordering::Relaxed, - ); - self.clean_unref_from_storage_us.fetch_add( - accounts_index_roots_stats.clean_unref_from_storage_us, - Ordering::Relaxed, - ); - self.clean_dead_slot_us.fetch_add( - accounts_index_roots_stats.clean_dead_slot_us, - Ordering::Relaxed, - ); - } - - pub fn report(&self) { - datapoint_info!( - "accounts_index_roots_len", - ("roots_len", self.roots_len.load(Ordering::Relaxed), i64), - ( - "uncleaned_roots_len", - self.uncleaned_roots_len.load(Ordering::Relaxed), - i64 - ), - ( - "roots_range_width", - self.roots_range.load(Ordering::Relaxed), - i64 - ), - ( - "unrooted_cleaned_count", - self.unrooted_cleaned_count.swap(0, Ordering::Relaxed), - i64 - ), - ( - "rooted_cleaned_count", - self.rooted_cleaned_count.swap(0, Ordering::Relaxed), - i64 - ), - ( - "clean_unref_from_storage_us", - self.clean_unref_from_storage_us.swap(0, Ordering::Relaxed), - i64 - ), - ( - "clean_dead_slot_us", - self.clean_dead_slot_us.swap(0, Ordering::Relaxed), - i64 - ), - ( - "append_vecs_open", - APPEND_VEC_STATS.files_open.load(Ordering::Relaxed), - i64 - ), - ( - "append_vecs_dirty", - APPEND_VEC_STATS.files_dirty.load(Ordering::Relaxed), - i64 - ) - ); - - // Don't need to reset since this tracks the latest updates, not a cumulative total + self.write_accounts_us += other.write_accounts_us; + self.update_index_us += other.update_index_us; + self.handle_reclaims_us += other.handle_reclaims_us; + self.mark_zero_lamport_single_ref_accounts_us += + other.mark_zero_lamport_single_ref_accounts_us; + self.num_zero_lamport_single_ref_accounts_marked += + other.num_zero_lamport_single_ref_accounts_marked; + self.num_reclaims += other.num_reclaims; + self.num_obsolete_slots_removed += other.num_obsolete_slots_removed; + self.num_obsolete_bytes_removed += other.num_obsolete_bytes_removed; + self.select_pubkeys_us += other.select_pubkeys_us; + self.disk_index_write_through_us += other.disk_index_write_through_us; } } #[derive(Debug, Default)] pub struct CleanAccountsStats { pub purge_stats: PurgeStats, - pub latest_accounts_index_roots_stats: LatestAccountsIndexRootsStats, + pub clean_unref_from_storage_us: AtomicU64, // stats held here and reported by clean_accounts pub clean_old_root_us: AtomicU64, @@ -422,7 +313,6 @@ pub struct CleanAccountsStats { impl CleanAccountsStats { pub fn report(&self) { self.purge_stats.report("clean_purge_slots_stats", None); - self.latest_accounts_index_roots_stats.report(); } } @@ -431,9 +321,9 @@ pub struct ShrinkAncientStats { pub shrink_stats: ShrinkStats, pub ancient_append_vecs_shrunk: AtomicU64, pub total_us: AtomicU64, + pub select_slots_us: AtomicU64, pub random_shrink: AtomicU64, pub slots_considered: AtomicU64, - pub ancient_scanned: AtomicU64, pub bytes_ancient_created: AtomicU64, pub bytes_from_must_shrink: AtomicU64, pub bytes_from_smallest_storages: AtomicU64, @@ -446,27 +336,36 @@ pub struct ShrinkAncientStats { pub total_alive_bytes: AtomicU64, pub slot: AtomicU64, pub ideal_storage_size: AtomicU64, + pub flush_read_cache_us: AtomicU64, } #[derive(Debug, Default)] pub struct ShrinkStatsSub { - pub store_accounts_timing: StoreAccountsTiming, + pub store_accounts_stats: StoreAccountsForShrinkStats, + pub rewrite_elapsed_us: Saturating, + pub create_and_insert_store_elapsed_us: Saturating, +} + +#[derive(Debug, Default)] +pub struct SquashStatsSub { + pub store_accounts_stats: StoreAccountsForSquashStats, pub rewrite_elapsed_us: Saturating, pub create_and_insert_store_elapsed_us: Saturating, pub unpackable_slots_count: Saturating, pub newest_alive_packed_count: Saturating, } -impl ShrinkStatsSub { +impl SquashStatsSub { pub fn accumulate(&mut self, other: &Self) { - self.store_accounts_timing - .accumulate(&other.store_accounts_timing); + self.store_accounts_stats + .accumulate(&other.store_accounts_stats); self.rewrite_elapsed_us += other.rewrite_elapsed_us; self.create_and_insert_store_elapsed_us += other.create_and_insert_store_elapsed_us; self.unpackable_slots_count += other.unpackable_slots_count; self.newest_alive_packed_count += other.newest_alive_packed_count; } } + #[derive(Debug, Default)] pub struct ShrinkStats { pub last_report: AtomicInterval, @@ -474,9 +373,11 @@ pub struct ShrinkStats { pub storage_read_elapsed: AtomicU64, pub index_read_elapsed: AtomicU64, pub create_and_insert_store_elapsed: AtomicU64, - pub store_accounts_elapsed: AtomicU64, - pub update_index_elapsed: AtomicU64, - pub handle_reclaims_elapsed: AtomicU64, + pub write_accounts_us: AtomicU64, + pub mark_zero_lamport_single_ref_accounts_us: AtomicU64, + pub update_index_us: AtomicU64, + pub num_accounts_stored: AtomicU64, + pub num_zero_lamport_single_ref_accounts_marked: AtomicU64, pub remove_old_stores_shrink_us: AtomicU64, pub rewrite_elapsed: AtomicU64, pub unpackable_slots_count: AtomicU64, @@ -494,7 +395,6 @@ pub struct ShrinkStats { pub accounts_loaded: AtomicU64, pub initial_candidates_count: AtomicU64, pub purged_zero_lamports: AtomicU64, - pub accounts_not_found_in_index: AtomicU64, pub num_ancient_slots_shrunk: AtomicU64, pub ancient_slots_added_to_shrink: AtomicU64, pub ancient_bytes_added_to_shrink: AtomicU64, @@ -505,6 +405,39 @@ pub struct ShrinkStats { } impl ShrinkStats { + pub fn accumulate_sub_stats(&self, stats_sub: ShrinkStatsSub, increment_count: bool) { + if increment_count { + self.num_slots_shrunk.fetch_add(1, Ordering::Relaxed); + } + self.create_and_insert_store_elapsed.fetch_add( + stats_sub.create_and_insert_store_elapsed_us.0, + Ordering::Relaxed, + ); + self.rewrite_elapsed + .fetch_add(stats_sub.rewrite_elapsed_us.0, Ordering::Relaxed); + self.accumulate_store_accounts_for_shrink_stats(stats_sub.store_accounts_stats); + } + + pub fn accumulate_store_accounts_for_shrink_stats( + &self, + store_accounts_stats: StoreAccountsForShrinkStats, + ) { + self.write_accounts_us + .fetch_add(store_accounts_stats.write_accounts_us, Ordering::Relaxed); + self.mark_zero_lamport_single_ref_accounts_us.fetch_add( + store_accounts_stats.mark_zero_lamport_single_ref_accounts_us, + Ordering::Relaxed, + ); + self.update_index_us + .fetch_add(store_accounts_stats.update_index_us, Ordering::Relaxed); + self.num_accounts_stored + .fetch_add(store_accounts_stats.num_accounts_stored, Ordering::Relaxed); + self.num_zero_lamport_single_ref_accounts_marked.fetch_add( + store_accounts_stats.num_zero_lamport_single_ref_accounts_marked, + Ordering::Relaxed, + ); + } + pub fn report(&self) { if self.last_report.should_update(1000) { datapoint_info!( @@ -558,18 +491,30 @@ impl ShrinkStats { i64 ), ( - "store_accounts_elapsed", - self.store_accounts_elapsed.swap(0, Ordering::Relaxed), + "write_accounts_us", + self.write_accounts_us.swap(0, Ordering::Relaxed), i64 ), ( - "update_index_elapsed", - self.update_index_elapsed.swap(0, Ordering::Relaxed), + "mark_zero_lamport_single_ref_accounts_us", + self.mark_zero_lamport_single_ref_accounts_us + .swap(0, Ordering::Relaxed), i64 ), ( - "handle_reclaims_elapsed", - self.handle_reclaims_elapsed.swap(0, Ordering::Relaxed), + "update_index_us", + self.update_index_us.swap(0, Ordering::Relaxed), + i64 + ), + ( + "num_accounts_stored", + self.num_accounts_stored.swap(0, Ordering::Relaxed), + i64 + ), + ( + "num_zero_lamport_single_ref_accounts_marked", + self.num_zero_lamport_single_ref_accounts_marked + .swap(0, Ordering::Relaxed), i64 ), ( @@ -632,11 +577,6 @@ impl ShrinkStats { self.num_ancient_slots_shrunk.swap(0, Ordering::Relaxed), i64 ), - ( - "accounts_not_found_in_index", - self.accounts_not_found_in_index.swap(0, Ordering::Relaxed), - i64 - ), ( "initial_candidates_count", self.initial_candidates_count.swap(0, Ordering::Relaxed), @@ -672,6 +612,31 @@ impl ShrinkStats { } impl ShrinkAncientStats { + pub fn accumulate_sub_stats(&self, stats_sub: SquashStatsSub) { + let shrink_stats = &self.shrink_stats; + shrink_stats.create_and_insert_store_elapsed.fetch_add( + stats_sub.create_and_insert_store_elapsed_us.0, + Ordering::Relaxed, + ); + shrink_stats + .rewrite_elapsed + .fetch_add(stats_sub.rewrite_elapsed_us.0, Ordering::Relaxed); + shrink_stats + .unpackable_slots_count + .fetch_add(stats_sub.unpackable_slots_count.0 as u64, Ordering::Relaxed); + shrink_stats.newest_alive_packed_count.fetch_add( + stats_sub.newest_alive_packed_count.0 as u64, + Ordering::Relaxed, + ); + let StoreAccountsForSquashStats { + store_accounts_for_shrink_stats, + flush_read_cache_us, + } = stats_sub.store_accounts_stats; + self.flush_read_cache_us + .fetch_add(flush_read_cache_us, Ordering::Relaxed); + shrink_stats.accumulate_store_accounts_for_shrink_stats(store_accounts_for_shrink_stats); + } + pub fn report(&self) { datapoint_info!( "shrink_ancient_stats", @@ -718,23 +683,40 @@ impl ShrinkAncientStats { i64 ), ( - "store_accounts_elapsed", + "flush_read_cache_us", + self.flush_read_cache_us.swap(0, Ordering::Relaxed), + i64 + ), + ( + "write_accounts_us", + self.shrink_stats + .write_accounts_us + .swap(0, Ordering::Relaxed), + i64 + ), + ( + "mark_zero_lamport_single_ref_accounts_us", self.shrink_stats - .store_accounts_elapsed + .mark_zero_lamport_single_ref_accounts_us .swap(0, Ordering::Relaxed), i64 ), ( - "update_index_elapsed", + "update_index_us", + self.shrink_stats.update_index_us.swap(0, Ordering::Relaxed), + i64 + ), + ( + "num_accounts_stored", self.shrink_stats - .update_index_elapsed + .num_accounts_stored .swap(0, Ordering::Relaxed), i64 ), ( - "handle_reclaims_elapsed", + "num_zero_lamport_single_ref_accounts_marked", self.shrink_stats - .handle_reclaims_elapsed + .num_zero_lamport_single_ref_accounts_marked .swap(0, Ordering::Relaxed), i64 ), @@ -829,12 +811,12 @@ impl ShrinkAncientStats { self.slots_considered.swap(0, Ordering::Relaxed), i64 ), + ("total_us", self.total_us.swap(0, Ordering::Relaxed), i64), ( - "ancient_scanned", - self.ancient_scanned.swap(0, Ordering::Relaxed), + "select_slots_us", + self.select_slots_us.swap(0, Ordering::Relaxed), i64 ), - ("total_us", self.total_us.swap(0, Ordering::Relaxed), i64), ( "bytes_ancient_created", self.bytes_ancient_created.swap(0, Ordering::Relaxed), @@ -877,13 +859,6 @@ impl ShrinkAncientStats { .swap(0, Ordering::Relaxed), i64 ), - ( - "accounts_not_found_in_index", - self.shrink_stats - .accounts_not_found_in_index - .swap(0, Ordering::Relaxed), - i64 - ), ("slot", self.slot.load(Ordering::Relaxed), i64), ( "ideal_storage_size", @@ -930,7 +905,6 @@ pub struct WriteAccountsToCacheStats { pub struct LoadAccountsStats { pub num_loaded_from_write_cache: AtomicU64, pub num_loaded_from_read_cache: AtomicU64, - pub num_loaded_from_index_cache: AtomicU64, pub num_loaded_from_index_storage: AtomicU64, } @@ -948,11 +922,6 @@ impl LoadAccountsStats { self.num_loaded_from_read_cache.swap(0, Ordering::Relaxed), i64 ), - ( - "num_loaded_from_index_cache", - self.num_loaded_from_index_cache.swap(0, Ordering::Relaxed), - i64 - ), ( "num_loaded_from_index_storage", self.num_loaded_from_index_storage diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index 01f5556687f..c7dba966585 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -3,7 +3,10 @@ use { super::*, crate::{ accounts_file::AccountsFileProvider, - accounts_index::{AccountSecondaryIndexesIncludeExclude, test_utils::*}, + accounts_index::{ + ACCOUNTS_INDEX_CONFIG_FOR_TESTING, AccountIndex, AccountSecondaryIndexesIncludeExclude, + AccountsIndexConfig, IndexLimit, IndexLimitThreshold, test_utils::*, + }, append_vec::{AppendVec, test_utils::TempFile}, storable_accounts::AccountForStorage, }, @@ -107,6 +110,28 @@ impl AccountStorageEntry { } } +fn create_store_for_shrink_tests( + accounts_db: &AccountsDb, + slot: Slot, + file_size: u64, + alive_bytes: usize, + num_zero_lamport_single_ref_accounts: usize, +) -> Arc { + let store = Arc::new(AccountStorageEntry::new( + Path::new(""), + slot, + slot as AccountsFileId, + file_size, + AccountsFileProvider::AppendVec, + )); + accounts_db.storage.insert(Arc::clone(&store)); + store.add_accounts(num_zero_lamport_single_ref_accounts.max(1), alive_bytes); + for offset in 0..num_zero_lamport_single_ref_accounts { + store.insert_zero_lamport_single_ref_account_offset(offset); + } + store +} + /// Macro to define tests for all permutations of accounts-db configs /// /// E.g. account storage file format @@ -258,8 +283,6 @@ pub(crate) fn append_single_account_with_default_hash( slot, slot, pubkey, - account, - &AccountSecondaryIndexes::default(), account_info, &mut ReclaimsSlotList::new(), UpsertReclaim::IgnoreReclaims, @@ -338,10 +361,7 @@ define_accounts_db_test!(test_accountsdb_add_root, |db| { db.store_for_tests((0, [(&key, &account0)].as_slice())); db.add_root(0); let ancestors = Ancestors::from(vec![1]); - assert_eq!( - db.load_without_fixed_root(&ancestors, &key), - Some((account0, 0)) - ); + assert_eq!(db.do_load_for_tests(&ancestors, &key), Some((account0, 0))); }); define_accounts_db_test!(test_accountsdb_latest_ancestor, |db| { @@ -355,13 +375,13 @@ define_accounts_db_test!(test_accountsdb_latest_ancestor, |db| { let ancestors = Ancestors::from(vec![1]); assert_eq!( - &db.load_without_fixed_root(&ancestors, &key).unwrap().0, + &db.do_load_for_tests(&ancestors, &key).unwrap().0, &account1 ); let ancestors = Ancestors::from(vec![1, 0]); assert_eq!( - &db.load_without_fixed_root(&ancestors, &key).unwrap().0, + &db.do_load_for_tests(&ancestors, &key).unwrap().0, &account1 ); @@ -392,13 +412,13 @@ define_accounts_db_test!(test_accountsdb_latest_ancestor_with_root, |db| { let ancestors = Ancestors::from(vec![1]); assert_eq!( - &db.load_without_fixed_root(&ancestors, &key).unwrap().0, + &db.do_load_for_tests(&ancestors, &key).unwrap().0, &account1 ); let ancestors = Ancestors::from(vec![1, 0]); assert_eq!( - &db.load_without_fixed_root(&ancestors, &key).unwrap().0, + &db.do_load_for_tests(&ancestors, &key).unwrap().0, &account1 ); }); @@ -428,29 +448,23 @@ define_accounts_db_test!(test_accountsdb_root_one_slot, |db| { // at the Accounts level) let ancestors = Ancestors::from(vec![0, 1]); assert_eq!( - &db.load_without_fixed_root(&ancestors, &key).unwrap().0, + &db.do_load_for_tests(&ancestors, &key).unwrap().0, &account1 ); // we should see 1 token in slot 2 let ancestors = Ancestors::from(vec![0, 2]); assert_eq!( - &db.load_without_fixed_root(&ancestors, &key).unwrap().0, + &db.do_load_for_tests(&ancestors, &key).unwrap().0, &account0 ); db.add_root(0); let ancestors = Ancestors::from(vec![1]); - assert_eq!( - db.load_without_fixed_root(&ancestors, &key), - Some((account1, 1)) - ); + assert_eq!(db.do_load_for_tests(&ancestors, &key), Some((account1, 1))); let ancestors = Ancestors::from(vec![2]); - assert_eq!( - db.load_without_fixed_root(&ancestors, &key), - Some((account0, 0)) - ); // original value + assert_eq!(db.do_load_for_tests(&ancestors, &key), Some((account0, 0))); // original value }); define_accounts_db_test!(test_accountsdb_add_root_many, |db| { @@ -459,9 +473,7 @@ define_accounts_db_test!(test_accountsdb_add_root_many, |db| { for _ in 1..100 { let idx = rng().random_range(0..99); let ancestors = Ancestors::from(vec![0]); - let account = db - .load_without_fixed_root(&ancestors, &pubkeys[idx]) - .unwrap(); + let account = db.do_load_for_tests(&ancestors, &pubkeys[idx]).unwrap(); let default_account = AccountSharedData::from(Account { lamports: (idx + 1) as u64, ..Account::default() @@ -475,13 +487,9 @@ define_accounts_db_test!(test_accountsdb_add_root_many, |db| { for _ in 1..100 { let idx = rng().random_range(0..99); let ancestors = Ancestors::from(vec![0]); - let account0 = db - .load_without_fixed_root(&ancestors, &pubkeys[idx]) - .unwrap(); + let account0 = db.do_load_for_tests(&ancestors, &pubkeys[idx]).unwrap(); let ancestors = Ancestors::from(vec![1]); - let account1 = db - .load_without_fixed_root(&ancestors, &pubkeys[idx]) - .unwrap(); + let account1 = db.do_load_for_tests(&ancestors, &pubkeys[idx]).unwrap(); let default_account = AccountSharedData::from(Account { lamports: (idx + 1) as u64, ..Account::default() @@ -543,15 +551,9 @@ define_accounts_db_test!(test_accounts_unsquashed, |db0| { // masking accounts is done at the Accounts level, at accountsDB we see // original account let ancestors = Ancestors::from(vec![0, 1]); - assert_eq!( - db0.load_without_fixed_root(&ancestors, &key), - Some((account1, 1)) - ); + assert_eq!(db0.do_load_for_tests(&ancestors, &key), Some((account1, 1))); let ancestors = Ancestors::from(vec![0]); - assert_eq!( - db0.load_without_fixed_root(&ancestors, &key), - Some((account0, 0)) - ); + assert_eq!(db0.do_load_for_tests(&ancestors, &key), Some((account0, 0))); }); /// Test to verify that reclaiming old storages during flush works correctly. @@ -601,10 +603,11 @@ fn test_flush_slots_with_reclaim_old_slots() { let storage = accounts.create_and_insert_store(new_slot, 4096, "test_flush_slots"); - accounts.accounts_index.add_root(new_slot); + accounts.accounts_cache.add_root(new_slot); - // Flushing this storage directly using _store_accounts_frozen. This is done to pass in UpsertReclaim::ReclaimOldSlots - accounts._store_accounts_frozen( + // Flushing this storage directly using store_accounts_for_flush. This is done to pass in + // UpsertReclaim::ReclaimOldSlots + accounts.store_accounts_for_flush( (new_slot, &accounts_list[..]), &storage, UpsertReclaim::ReclaimOldSlots, @@ -632,6 +635,159 @@ fn test_flush_slots_with_reclaim_old_slots() { assert!(accounts.storage.get_slot_storage_entry(new_slot).is_some()); } +/// With write-through enabled, a pubkey that is hot-written across multiple cached +/// slots must not be written through to disk until the *last* cached slot leaves the +/// cache. +#[test] +fn test_flush_defers_write_through_until_all_cached_slots_drop() { + // Build an AccountsDb whose index has IndexLimit::Threshold so write-through is enabled. + let db = AccountsDb::new_single_for_tests_with_provider_and_config( + AccountsFileProvider::AppendVec, + AccountsDbConfig { + index: Some(AccountsIndexConfig { + index_limit: IndexLimit::Threshold(IndexLimitThreshold { + num_bytes: 25_000_000_000, + num_entries_overhead: 1, + num_entries_to_evict: 1, + }), + ..ACCOUNTS_INDEX_CONFIG_FOR_TESTING + }), + ..ACCOUNTS_DB_CONFIG_FOR_TESTING + }, + ); + + let pubkey = Pubkey::new_unique(); + let account = AccountSharedData::new(1, 0, &Pubkey::default()); + + // Cache the same pubkey at three consecutive slots without flushing in between. + db.store_for_tests((0, [(&pubkey, &account)].as_slice())); + db.store_for_tests((1, [(&pubkey, &account)].as_slice())); + db.store_for_tests((2, [(&pubkey, &account)].as_slice())); + + let immediate_disk_writes = || { + db.accounts_index + .stats() + .flush_entries_updated_on_disk_immediate + .load(Ordering::Relaxed) + }; + + let baseline_writes = immediate_disk_writes(); + + // Flush slot 0. Pubkey is still cached at slots 1 and 2, so remove_slot does + // not return it and try_write_through is never called, so no immediate disk + // write must fire. + db.add_root_and_flush_write_cache(0); + assert_eq!(immediate_disk_writes(), baseline_writes); + + // Flush slot 1. Pubkey is still cached at slot 2, same story. + db.add_root_and_flush_write_cache(1); + assert_eq!(immediate_disk_writes(), baseline_writes); + + // Flush slot 2. The pubkey is no longer in any cached slot, so the cache-drop + // loop in flush_slot_cache calls try_write_through. ReclaimOldSlots has + // collapsed the slot list to a single storage entry with ref_count == 1, so + // write-through fires and bumps the counter by exactly one. + db.add_root_and_flush_write_cache(2); + assert_eq!( + immediate_disk_writes(), + baseline_writes + 1, + "exactly one immediate disk write should have fired across all flushes", + ); +} + +/// With write-through disabled (the default, non-threshold index config) the cache-drop path +/// must be a pure no-op: flushing a slot leaves the in-mem entry dirty and writes nothing +/// through to disk. Guards the "disabled => old behavior preserved" contract for the most +/// common production configuration. +#[test] +fn test_flush_does_not_write_through_when_write_through_disabled() { + // new_single_for_tests uses IndexLimit::InMemOnly, so write-through is disabled. The + // behavioral assertions below (entry stays dirty, no disk write) would fail if that default + // ever changed to a threshold config. + let db = AccountsDb::new_single_for_tests(); + + let pubkey = Pubkey::new_unique(); + let account = AccountSharedData::new(1, 0, &Pubkey::default()); + db.store_for_tests((0, [(&pubkey, &account)].as_slice())); + + let immediate_disk_writes = || { + db.accounts_index + .stats() + .flush_entries_updated_on_disk_immediate + .load(Ordering::Relaxed) + }; + let baseline_writes = immediate_disk_writes(); + + // Flush slot 0. The pubkey leaves the cache, but write-through is disabled, so + // write_through_pubkeys short-circuits and no immediate disk write fires. + db.add_root_and_flush_write_cache(0); + assert_eq!(immediate_disk_writes(), baseline_writes); +} + +/// The dead-slot purge path removes only the purged slot's index entries, so a pubkey with +/// a surviving dirty single-ref entry at another slot must be written through when the purge +/// drops its last cached slot. +#[test] +fn test_purge_unrooted_slot_writes_through_surviving_entry() { + let db = AccountsDb::new_single_for_tests_with_provider_and_config( + AccountsFileProvider::AppendVec, + AccountsDbConfig { + index: Some(AccountsIndexConfig { + index_limit: IndexLimit::Threshold(IndexLimitThreshold { + num_bytes: 25_000_000_000, + num_entries_overhead: 1, + num_entries_to_evict: 1, + }), + ..ACCOUNTS_INDEX_CONFIG_FOR_TESTING + }), + ..ACCOUNTS_DB_CONFIG_FOR_TESTING + }, + ); + let pubkey = Pubkey::new_unique(); + let account = AccountSharedData::new(1, 0, &Pubkey::default()); + + // Cache the pubkey at slot 0 (will be rooted + flushed) and slot 1 (stays unrooted/cached). + db.store_for_tests((0, [(&pubkey, &account)].as_slice())); + db.store_for_tests((1, [(&pubkey, &account)].as_slice())); + + let is_dirty_in_mem = |pubkey: &Pubkey| -> bool { + db.accounts_index.get_and_then(pubkey, |entry| { + (false, entry.expect("entry should be in the index").dirty()) + }) + }; + let immediate_disk_writes = || { + db.accounts_index + .stats() + .flush_entries_updated_on_disk_immediate + .load(Ordering::Relaxed) + }; + + // Flush slot 0. The pubkey is still cached at slot 1, so it does not leave the cache and + // write-through does not fire; its slot-0 storage entry is left dirty (slot list now has the + // uncached slot-0 entry plus the cached slot-1 entry). + let baseline_writes = immediate_disk_writes(); + db.add_root_and_flush_write_cache(0); + assert!( + is_dirty_in_mem(&pubkey), + "dirty after flushing slot 0 while still cached at slot 1" + ); + assert_eq!(immediate_disk_writes(), baseline_writes); + + // Purge the unrooted slot 1 from the cache. `purge_slot_cache` removes only the slot-1 + // entry, leaving the slot-0 entry as a single-ref dirty entry; the pubkey leaves the cache + // entirely, so the purge path's write-through fires on that surviving entry exactly once. + db.remove_unrooted_slots(&[(1, 1)]); + assert!( + !is_dirty_in_mem(&pubkey), + "should be clean after purge writes through the surviving entry" + ); + assert_eq!( + immediate_disk_writes(), + baseline_writes + 1, + "purge path must write through the surviving single-ref entry exactly once" + ); +} + define_accounts_db_test!(test_remove_unrooted_slot_cached, |db| { let unrooted_slot = 9; let unrooted_bank_id = 9; @@ -641,13 +797,13 @@ define_accounts_db_test!(test_remove_unrooted_slot_cached, |db| { assert!(!db.contains(&key)); db.store_for_tests((unrooted_slot, &[(&key, &account0)][..])); assert!(db.accounts_cache.contains(unrooted_slot)); - assert!(!db.accounts_index.is_alive_root(unrooted_slot)); + assert!(!db.accounts_cache.contains_unflushed_root(unrooted_slot)); assert!(db.contains(&key)); db.assert_load_account(unrooted_slot, key, 1); // Purge the slot db.remove_unrooted_slots(&[(unrooted_slot, unrooted_bank_id)]); - assert!(db.load_without_fixed_root(&ancestors, &key).is_none()); + assert!(db.do_load_for_tests(&ancestors, &key).is_none()); assert!(db.accounts_cache.slot_cache(unrooted_slot).is_none()); assert!(db.storage.get_slot_storage_entry(unrooted_slot).is_none()); assert!(!db.contains(&key)); @@ -658,6 +814,48 @@ define_accounts_db_test!(test_remove_unrooted_slot_cached, |db| { db.assert_load_account(unrooted_slot, key, 2); }); +/// Cache writes populate the secondary indexes but not the primary index. When a cache-only +/// account is dropped via `remove_unrooted_slots` before it is ever flushed, its secondary-index +/// entry must be reclaimed; otherwise it leaks because there is no primary entry to drive the +/// usual `handle_dead_keys` purge. +#[test] +fn test_remove_unrooted_slot_purges_secondary_index_for_cache_only_account() { + let db = AccountsDb { + account_indexes: program_id_index_enabled(), + ..AccountsDb::new_with_config( + Vec::new(), + ACCOUNTS_DB_CONFIG_FOR_TESTING, + None, + Arc::default(), + ) + }; + + let owner = Pubkey::new_unique(); + let pubkey = Pubkey::new_unique(); + let slot = 1; + let bank_id = 0; + let account = AccountSharedData::new(1, 0, &owner); + + // Write only to the cache at an unrooted slot: this populates the secondary index keyed by + // the account owner, but leaves the primary index untouched. + db.store_for_tests((slot, &[(&pubkey, &account)][..])); + assert!(!db.accounts_index.contains(&pubkey)); + assert_eq!( + db.accounts_index + .get_index_key_size(&AccountIndex::ProgramId, &owner), + Some(1) + ); + + // Dropping the unrooted slot must reclaim the secondary entry, not leak it. + db.remove_unrooted_slots(&[(slot, bank_id)]); + assert!(!db.accounts_cache.contains_pubkey(&pubkey)); + assert_eq!( + db.accounts_index + .get_index_key_size(&AccountIndex::ProgramId, &owner), + None + ); +} + // Test that removing a rooted storage works correctly. This is behaviour specific to // the snapshot minimizer define_accounts_db_test!(test_remove_slot_snapshot_minimizer, |db| { @@ -674,7 +872,7 @@ define_accounts_db_test!(test_remove_slot_snapshot_minimizer, |db| { // Purge the slot db.purge_slots_for_snapshot_minimizer([(&rooted_slot)].into_iter()); - assert!(db.load_without_fixed_root(&ancestors, &key).is_none()); + assert!(db.do_load_for_tests(&ancestors, &key).is_none()); assert!(db.accounts_cache.slot_cache(rooted_slot).is_none()); assert!(db.storage.get_slot_storage_entry(rooted_slot).is_none()); assert!(!db.contains(&key)); @@ -684,15 +882,14 @@ fn update_accounts(accounts: &AccountsDb, pubkeys: &[Pubkey], slot: Slot, range: for _ in 1..1000 { let idx = rng().random_range(0..range); let ancestors = Ancestors::from(vec![slot]); - if let Some((mut account, _)) = accounts.load_without_fixed_root(&ancestors, &pubkeys[idx]) - { + if let Some((mut account, _)) = accounts.do_load_for_tests(&ancestors, &pubkeys[idx]) { account.checked_add_lamports(1).unwrap(); accounts.store_for_tests((slot, [(&pubkeys[idx], &account)].as_slice())); if account.is_zero_lamport() { let ancestors = Ancestors::from(vec![slot]); assert!( accounts - .load_without_fixed_root(&ancestors, &pubkeys[idx]) + .do_load_for_tests(&ancestors, &pubkeys[idx]) .is_none() ); } else { @@ -713,7 +910,7 @@ fn test_account_one() { let mut pubkeys: Vec = vec![]; db.create_account(&mut pubkeys, 0, 1, 0, 0); let ancestors = Ancestors::from(vec![0]); - let account = db.load_without_fixed_root(&ancestors, &pubkeys[0]).unwrap(); + let account = db.do_load_for_tests(&ancestors, &pubkeys[0]).unwrap(); let default_account = AccountSharedData::from(Account { lamports: 1, ..Account::default() @@ -756,7 +953,7 @@ fn test_account_grow_many() { for (i, key) in keys.iter().enumerate() { assert_eq!( accounts - .load_without_fixed_root(&ancestors, key) + .do_load_for_tests(&ancestors, key) .unwrap() .0 .lamports(), @@ -805,17 +1002,11 @@ fn test_account_grow() { } let ancestors = Ancestors::from(vec![0]); assert_eq!( - accounts - .load_without_fixed_root(&ancestors, &pubkey1) - .unwrap() - .0, + accounts.do_load_for_tests(&ancestors, &pubkey1).unwrap().0, account1 ); assert_eq!( - accounts - .load_without_fixed_root(&ancestors, &pubkey2) - .unwrap() - .0, + accounts.do_load_for_tests(&ancestors, &pubkey2).unwrap().0, account2 ); @@ -829,17 +1020,11 @@ fn test_account_grow() { } let ancestors = Ancestors::from(vec![0]); assert_eq!( - accounts - .load_without_fixed_root(&ancestors, &pubkey1) - .unwrap() - .0, + accounts.do_load_for_tests(&ancestors, &pubkey1).unwrap().0, account1 ); assert_eq!( - accounts - .load_without_fixed_root(&ancestors, &pubkey2) - .unwrap() - .0, + accounts.do_load_for_tests(&ancestors, &pubkey2).unwrap().0, account2 ); if flush { @@ -1019,7 +1204,9 @@ fn test_remove_zero_lamport_single_ref_accounts_after_shrink() { accounts.accounts_index.get_and_then(&pubkey_zero, |entry| { let expected_ref_count = if pass < 2 { 1 } else { 2 }; assert_eq!(entry.unwrap().ref_count(), expected_ref_count, "{pass}"); - let expected_slot_list = if pass < 1 { 1 } else { 2 }; + // The index holds only flushed writes: one entry at `slot` for passes 0 and 1, + // and a second at `slot + 1` for pass 2. + let expected_slot_list = if pass < 2 { 1 } else { 2 }; assert_eq!(entry.unwrap().slot_list_lock_read_len(), expected_slot_list); (false, ()) }); @@ -1043,23 +1230,14 @@ fn test_remove_zero_lamport_single_ref_accounts_after_shrink() { assert!(entry.is_none(), "{pass}"); } 1 => { - // alive only in slot + 1 - assert_eq!(entry.unwrap().slot_list_lock_read_len(), 1); - assert_eq!( - entry - .unwrap() - .slot_list_read_lock() - .first() - .map(|(s, _)| s) - .cloned() - .unwrap(), - slot + 1 - ); - let expected_ref_count = 0; - assert_eq!( - entry.map(|e| e.ref_count()), - Some(expected_ref_count), - "{pass}" + // The single-ref entry at `slot` is removed; pass 1's cache-only write at + // `slot + 1` stays in the write cache. + assert!(entry.is_none(), "{pass}"); + assert!( + accounts + .accounts_cache + .load(slot + 1, &pubkey_zero) + .is_some() ); } 2 => { @@ -1178,6 +1356,293 @@ fn test_shrink_zero_lamport_single_ref_account() { } } +/// Ensure that `shrink` marks zero lamport single ref accounts in the new storage. +#[test] +fn test_shrink_marks_zero_lamport_single_ref_account_in_new_storage() { + let accounts_db = AccountsDb::new_single_for_tests(); + let slot0 = 0; + let slot1 = slot0 + 1; + // latest full snapshot must be older than the slot(s) we plan to shrink, + // otherwise zero lamport single ref accounts will be purged + accounts_db.set_latest_full_snapshot_slot(slot0); + + let obsolete_pubkey = Pubkey::new_unique(); + let obsolete_zero_lamport_pubkey = Pubkey::new_unique(); + let zero_lamport_single_ref_pubkey = Pubkey::new_unique(); + let zero_lamport_multi_ref_pubkey = Pubkey::new_unique(); + let alive_pubkey = Pubkey::new_unique(); + let closed_account = AccountSharedData::new(0, 0, &Pubkey::default()); + let open_account = AccountSharedData::new(1, 0, &Pubkey::default()); + + let (_temp_dirs, paths) = get_temp_accounts_paths(1).unwrap(); + let storage1 = Arc::new(AccountStorageEntry::new( + &paths[0], + slot1, + 10, + DEFAULT_FILE_SIZE, + AccountsFileProvider::AppendVec, + )); + // store an obsolete account; it should not be marked ZLSR + append_single_account_with_default_hash( + &storage1, + &obsolete_pubkey, + &open_account, + true, + Some(&accounts_db.accounts_index), + ); + // store an obsolete zero lamport account; it should not be marked ZLSR + append_single_account_with_default_hash( + &storage1, + &obsolete_zero_lamport_pubkey, + &closed_account, + true, + Some(&accounts_db.accounts_index), + ); + // store a zero lamport single ref account; it *should* be marked ZLSR + append_single_account_with_default_hash( + &storage1, + &zero_lamport_single_ref_pubkey, + &closed_account, + true, + Some(&accounts_db.accounts_index), + ); + // store a zero lamport multi ref account; it should not be marked ZLSR + append_single_account_with_default_hash( + &storage1, + &zero_lamport_multi_ref_pubkey, + &closed_account, + true, + Some(&accounts_db.accounts_index), + ); + // store an alive account; it should not be marked ZLSR + append_single_account_with_default_hash( + &storage1, + &alive_pubkey, + &open_account, + true, + Some(&accounts_db.accounts_index), + ); + insert_store(&accounts_db, Arc::clone(&storage1)); + accounts_db.add_root(slot1); + + // we manually created the storage, so nothing got marked + assert_eq!(storage1.num_zero_lamport_single_ref_accounts(), 0); + + // store the multi ref account again, in slot 2, so it becomes multi ref + let slot2 = slot1 + 1; + accounts_db.store_for_tests(( + slot2, + [(&zero_lamport_multi_ref_pubkey, &closed_account)].as_slice(), + )); + accounts_db.add_root(slot2); + // flush without clean so the ZLMR account isn't marked obsolete in slot 1 + accounts_db.flush_rooted_accounts_cache_without_clean(); + + // mark the obsolete accounts as obsolete + let ancestors = Ancestors::from(vec![slot2]); + for pubkey in [obsolete_pubkey, obsolete_zero_lamport_pubkey] { + let account_info = accounts_db + .accounts_index + .get_with_and_then(&pubkey, &ancestors, false, |account_info| account_info) + .unwrap(); + accounts_db.remove_dead_accounts( + [account_info].iter(), + None, + MarkAccountsObsolete::Yes(slot1), + ); + } + + accounts_db.shrink_slot_forced(slot1); + + let new_storage1 = accounts_db.get_and_assert_single_storage(slot1); + let new_zero_lamport_single_ref_info = accounts_db + .accounts_index + .get_with_and_then( + &zero_lamport_single_ref_pubkey, + &ancestors, + false, + |(_slot, account_info)| account_info, + ) + .unwrap(); + + // ensure ids are different, to indicate shrink ran + assert_ne!(new_storage1.id(), storage1.id()); + // ensure there are three accounts in the storage now, removing the two obsolete ones + assert_eq!(new_storage1.count(), 3); + // ensure the zlsr account info has the correct id for the new shrunk storage + assert_eq!( + new_zero_lamport_single_ref_info.store_id(), + new_storage1.id(), + ); + // ensure the new shrunk storage does have a marked zlsr account + assert_eq!(new_storage1.num_zero_lamport_single_ref_accounts(), 1); + let new_storage_zlsr_offsets = new_storage1 + .zero_lamport_single_ref_offsets() + .read() + .unwrap(); + // ensure the storage's zlsr offset list contains the zlsr account info's + assert!(new_storage_zlsr_offsets.contains(&new_zero_lamport_single_ref_info.offset())); +} + +/// unit test for `alive_bytes_after_shrink()` +/// +/// Check all the permutations of latest full snapshot slot w.r.t. if/how +/// zero lamport single ref accounts are counted as alive bytes or not. +#[test] +fn test_alive_bytes_after_shrink() { + let accounts_db = AccountsDb::new_single_for_tests(); + let slot = 5; + // note the initial alive bytes should be big enough so that subtracting + // all the zero lamport single ref accounts does not saturate at zero. + let initial_alive_bytes = 123_456; + let store = create_store_for_shrink_tests( + &accounts_db, + slot, + 4096, // <-- file size + initial_alive_bytes, + 2, // <-- num zero lamport single ref accounts + ); + + // test case: latest full snapshot slot is None -- ZLSR accounts are dead + { + // latest full snapshot slot starts off as None + assert!(accounts_db.latest_full_snapshot_slot().is_none()); + + // ensure ZLSR accounts are dead bytes + let alive_bytes_after_shrink1 = accounts_db.alive_bytes_after_shrink(&store); + assert!(alive_bytes_after_shrink1 < initial_alive_bytes); + + // add a ZLSR account, and ensure alive bytes reduces + store.insert_zero_lamport_single_ref_account_offset(2); + let alive_bytes_after_shrink2 = accounts_db.alive_bytes_after_shrink(&store); + assert!(alive_bytes_after_shrink2 < alive_bytes_after_shrink1); + } + + // test case: slot > latest full snapshot -- ZLSR accounts are alive + { + accounts_db.set_latest_full_snapshot_slot(slot - 1); + + // ensure ZLSR accounts are *not* dead bytes + let alive_bytes_after_shrink1 = accounts_db.alive_bytes_after_shrink(&store); + assert_eq!(alive_bytes_after_shrink1, initial_alive_bytes); + + // add a ZLSR account, and ensure alive bytes is unchanged + store.insert_zero_lamport_single_ref_account_offset(3); + let alive_bytes_after_shrink2 = accounts_db.alive_bytes_after_shrink(&store); + assert_eq!(alive_bytes_after_shrink2, initial_alive_bytes); + } + + // test case: slot == latest full snapshot -- ZLSR accounts are dead + { + accounts_db.set_latest_full_snapshot_slot(slot); + + // ensure ZLSR accounts are dead bytes + let alive_bytes_after_shrink1 = accounts_db.alive_bytes_after_shrink(&store); + assert!(alive_bytes_after_shrink1 < initial_alive_bytes); + + // add a ZLSR account, and ensure alive bytes reduces + store.insert_zero_lamport_single_ref_account_offset(4); + let alive_bytes_after_shrink2 = accounts_db.alive_bytes_after_shrink(&store); + assert!(alive_bytes_after_shrink2 < alive_bytes_after_shrink1); + } + + // test case: slot < latest full snapshot -- ZLSR accounts are dead + { + accounts_db.set_latest_full_snapshot_slot(slot + 1); + + // ensure ZLSR accounts are dead bytes + let alive_bytes_after_shrink1 = accounts_db.alive_bytes_after_shrink(&store); + assert!(alive_bytes_after_shrink1 < initial_alive_bytes); + + // add a ZLSR account, and ensure alive bytes reduces + store.insert_zero_lamport_single_ref_account_offset(5); + let alive_bytes_after_shrink2 = accounts_db.alive_bytes_after_shrink(&store); + assert!(alive_bytes_after_shrink2 < alive_bytes_after_shrink1); + } +} + +/// Ensure that shrinking a storage... +/// * with zero lamport single ref accounts +/// * in a slot *older* than the latest full snapshot slot +/// +/// Results in... +/// * the zero lamport single ref accounts *not* in the shrunk storage +/// * the expected number of alive bytes +#[test] +fn test_alive_bytes_after_shrink_with_zero_lamport_single_ref_accounts() { + let accounts_db = AccountsDb::new_single_for_tests(); + let slot = 1; + let dead_account = AccountSharedData::new(0, 123, &Pubkey::default()); + let dead_pubkeys = [ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + let alive_account = AccountSharedData::new(11, 17, &Pubkey::default()); + let alive_pubkey = Pubkey::new_unique(); + + accounts_db.store_for_tests(( + slot, + [ + (&dead_pubkeys[0], &dead_account), + (&dead_pubkeys[1], &dead_account), + (&dead_pubkeys[2], &dead_account), + (&alive_pubkey, &alive_account), + ] + .as_slice(), + )); + accounts_db.add_root_and_flush_write_cache(slot); + + // We must set the latest full snapshot slot to `slot` or greater + // to ensure that ZLSR accounts are treated as dead for `shrink`. + accounts_db.set_latest_full_snapshot_slot(slot); + + let storage = accounts_db.get_storage_for_slot(slot).unwrap(); + accounts_db.accounts_index.scan( + dead_pubkeys.iter(), + |_pubkey, slots_refs| { + let (slot_list, ref_count) = slots_refs.unwrap(); + assert_eq!(slot_list.len(), 1); + assert_eq!(ref_count, 1); + + let (indexed_slot, acct_info) = slot_list.first().unwrap(); + assert_eq!(*indexed_slot, slot); + assert!(acct_info.is_zero_lamport()); + accounts_db.zero_lamport_single_ref_found(*indexed_slot, acct_info.offset()); + AccountsIndexScanResult::KeepInMemory + }, + None, + ScanFilter::All, + ); + + assert_eq!( + storage.num_zero_lamport_single_ref_accounts(), + dead_pubkeys.len(), + ); + + let alive_bytes_before_shrink = storage.alive_bytes(); + let expected_alive_bytes_after_shrink = accounts_db.alive_bytes_after_shrink(&storage); + assert_ne!(expected_alive_bytes_after_shrink, 0); + assert!(expected_alive_bytes_after_shrink < alive_bytes_before_shrink); + assert_eq!( + expected_alive_bytes_after_shrink, + AppendVec::calculate_stored_size(alive_account.data().len()), + ); + + accounts_db.shrink_slot_forced(slot); + + let storage_after_shrink = accounts_db.get_storage_for_slot(slot).unwrap(); + assert_eq!( + storage_after_shrink.alive_bytes(), + expected_alive_bytes_after_shrink, + ); + assert_eq!(storage_after_shrink.count(), 1); + assert!(accounts_db.contains(&alive_pubkey)); + for pubkey in &dead_pubkeys { + assert!(!accounts_db.contains(pubkey)); + } +} + #[test] fn test_clean_multiple_zero_lamport_decrements_index_ref_count() { agave_logger::setup(); @@ -1473,7 +1938,7 @@ fn test_accounts_db_purge_keep_live() { // since the store count will not be zero accounts.store_for_tests((current_slot, [(&pubkey2, &account2)].as_slice())); accounts.add_root_and_flush_write_cache(current_slot); - let ancestors = Ancestors::from(vec![accounts.accounts_index.max_root_inclusive()]); + let ancestors = Ancestors::from(vec![accounts.max_root()]); let (slot1, account_info1) = accounts .accounts_index .get_with_and_then(&pubkey, &ancestors, false, |(slot, account_info)| { @@ -1649,7 +2114,7 @@ fn test_cleanup_key_not_removed() { db.print_accounts_stats("post"); let ancestors = Ancestors::from(vec![2]); assert_eq!( - db.load_without_fixed_root(&ancestors, &key1) + db.do_load_for_tests(&ancestors, &key1) .unwrap() .0 .lamports(), @@ -1669,7 +2134,7 @@ fn test_store_large_account() { db.store_for_tests((0, [(&key, &account)].as_slice())); let ancestors = Ancestors::from(vec![0]); - let ret = db.load_without_fixed_root(&ancestors, &key).unwrap(); + let ret = db.do_load_for_tests(&ancestors, &key).unwrap(); assert_eq!(ret.0.data().len(), data_len); } @@ -2369,6 +2834,100 @@ fn test_select_candidates_by_total_usage_all_clean() { assert_eq!(0, next_candidates.len()); } +/// Ensure selecting shrink candidates respects zero-lamport single-ref accounts. +#[test] +fn test_select_candidates_by_total_usage_with_zero_lamport_single_ref_accounts() { + let accounts_db = AccountsDb::new_single_for_tests(); + let mut shrink_candidates = ShrinkCandidates::default(); + + let num_zero_lamport_single_ref_accounts = 4; + let file_size = AppendVec::calculate_stored_size(0) * num_zero_lamport_single_ref_accounts; + + let slot_with_zlsr = 11; + let store_with_zlsr = create_store_for_shrink_tests( + &accounts_db, + slot_with_zlsr, + file_size as u64, + file_size, + num_zero_lamport_single_ref_accounts, + ); + shrink_candidates.insert(slot_with_zlsr); + + let slot_no_zlsr = 22; + let store_no_zlsr = create_store_for_shrink_tests( + &accounts_db, + slot_no_zlsr, + file_size as u64, + file_size * 9 / 10, + 0, + ); + shrink_candidates.insert(slot_no_zlsr); + + // test case: The latest full snapshot slot is *older* than + // the store with zero lamport single ref accounts. + // Ensure shrink will see ZLSR accounts as *alive*. + { + accounts_db.set_latest_full_snapshot_slot(slot_with_zlsr - 1); + + // Bytes from ZLSR accounts are alive, and will stay alive after shrink. + assert_eq!( + accounts_db.alive_bytes_after_shrink(&store_with_zlsr), + store_with_zlsr.alive_bytes(), + ); + assert!(!accounts_db.is_candidate_for_shrink(&store_with_zlsr)); + assert!(!accounts_db.is_shrinking_productive(&store_with_zlsr)); + + // Stores without ZLSR accounts use the raw alive bytes. + assert_eq!( + accounts_db.alive_bytes_after_shrink(&store_no_zlsr), + store_no_zlsr.alive_bytes(), + ); + + let (selected_candidates, next_candidates) = accounts_db + .select_candidates_by_total_usage(&shrink_candidates, DEFAULT_ACCOUNTS_SHRINK_RATIO); + + // both slots are above the shrink ratio, so neither should be shrink candidates + assert!(selected_candidates.is_empty()); + assert!(next_candidates.is_empty()); + } + + // test case: The latest full snapshot slot is either: + // * newer than the store with ZLSR accounts + // * the same as the store with ZLSR accounts + // * unset + // Ensure shrink will see ZLSR accounts as *dead*. + { + for latest_full_snapshot_slot in [Some(slot_with_zlsr + 1), Some(slot_with_zlsr), None] { + *accounts_db.latest_full_snapshot_slot.lock_write() = latest_full_snapshot_slot; + + // Bytes from ZLSR accounts are alive, but would be dead after shrink. + assert_eq!(store_with_zlsr.alive_bytes(), file_size); + assert_eq!(accounts_db.alive_bytes_after_shrink(&store_with_zlsr), 0); + assert!(accounts_db.is_candidate_for_shrink(&store_with_zlsr)); + assert!(accounts_db.is_shrinking_productive(&store_with_zlsr)); + + // Stores without ZLSR accounts use the raw alive bytes. + assert_eq!( + accounts_db.alive_bytes_after_shrink(&store_no_zlsr), + store_no_zlsr.alive_bytes(), + ); + + let (selected_candidates, next_candidates) = accounts_db + .select_candidates_by_total_usage( + &shrink_candidates, + DEFAULT_ACCOUNTS_SHRINK_RATIO, + ); + + // slot 11 with the ZLSR accounts *is* selected for shrink + assert_eq!(1, selected_candidates.len()); + assert!(selected_candidates.contains_key(&slot_with_zlsr)); + + // slot 22 is above the shrink ratio, so ensure it is not a candidate + assert!(next_candidates.is_empty()); + } + } +} + #[test] fn test_delete_dependencies() { agave_logger::setup(); @@ -2386,8 +2945,6 @@ fn test_delete_dependencies() { 0, 0, &key0, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), info0, &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -2396,8 +2953,6 @@ fn test_delete_dependencies() { 1, 1, &key0, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), info1, &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -2406,8 +2961,6 @@ fn test_delete_dependencies() { 1, 1, &key1, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), info1, &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -2416,8 +2969,6 @@ fn test_delete_dependencies() { 2, 2, &key1, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), info2, &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -2426,8 +2977,6 @@ fn test_delete_dependencies() { 2, 2, &key2, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), info2, &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -2436,16 +2985,10 @@ fn test_delete_dependencies() { 3, 3, &key2, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), info3, &mut reclaims, UpsertReclaim::IgnoreReclaims, ); - accounts_index.add_root(0); - accounts_index.add_root(1); - accounts_index.add_root(2); - accounts_index.add_root(3); let num_bins = accounts_index.bins(); let mut candidates: Box<_> = std::iter::repeat_with(HashMap::::new) .take(num_bins) @@ -2453,7 +2996,7 @@ fn test_delete_dependencies() { for key in [&key0, &key1, &key2] { let (rooted_entries, ref_count) = accounts_index.get_and_then(key, |entry| { let slot_list_lock = entry.unwrap().slot_list_read_lock(); - let rooted = accounts_index.get_rooted_entries(slot_list_lock.as_ref(), None); + let rooted = accounts_index.get_entries_up_to_inclusive(slot_list_lock.as_ref(), None); (false, (rooted, entry.unwrap().ref_count())) }); let index = accounts_index.bin_calculator.bin_from_pubkey(key); @@ -2599,7 +3142,7 @@ fn test_wrapping_storage_id() { assert_eq!(slots - 1, db.next_id.load(Ordering::Acquire)); let ancestors = Ancestors::default(); keys.iter().for_each(|key| { - assert!(db.load_without_fixed_root(&ancestors, key).is_some()); + assert!(db.do_load_for_tests(&ancestors, key).is_some()); }); } @@ -2625,7 +3168,7 @@ fn test_reuse_storage_id() { }); let ancestors = Ancestors::default(); keys.iter().for_each(|key| { - assert!(db.load_without_fixed_root(&ancestors, key).is_some()); + assert!(db.do_load_for_tests(&ancestors, key).is_some()); }); } @@ -2646,7 +3189,7 @@ fn test_zero_lamport_new_root_not_cleaned() { // Should still be able to find zero lamport account in slot 1 assert_eq!( - db.load_without_fixed_root(&Ancestors::default(), &account_key), + db.do_load_for_tests(&Ancestors::default(), &account_key), Some((zero_lamport_account, 1)) ); } @@ -2660,26 +3203,23 @@ fn test_store_load_cached() { db.store_for_tests((slot, &[(&key, &account0)][..])); // Load with no ancestors and no root will return nothing - assert!( - db.load_without_fixed_root(&Ancestors::default(), &key) - .is_none() - ); + assert!(db.do_load_for_tests(&Ancestors::default(), &key).is_none()); // Load with ancestors not equal to `slot` will return nothing let ancestors = Ancestors::from(vec![slot + 1]); - assert!(db.load_without_fixed_root(&ancestors, &key).is_none()); + assert!(db.do_load_for_tests(&ancestors, &key).is_none()); // Load with ancestors equal to `slot` will return the account let ancestors = Ancestors::from(vec![slot]); assert_eq!( - db.load_without_fixed_root(&ancestors, &key), + db.do_load_for_tests(&ancestors, &key), Some((account0.clone(), slot)) ); // Adding root will return the account even without ancestors db.add_root(slot); assert_eq!( - db.load_without_fixed_root(&Ancestors::default(), &key), + db.do_load_for_tests(&Ancestors::default(), &key), Some((account0, slot)) ); } @@ -2698,7 +3238,7 @@ fn test_store_flush_load_cached() { db.flush_accounts_cache(true, None); let ancestors = Ancestors::from(vec![slot]); assert_eq!( - db.load_without_fixed_root(&ancestors, &key), + db.do_load_for_tests(&ancestors, &key), Some((account0.clone(), slot)) ); @@ -2706,7 +3246,7 @@ fn test_store_flush_load_cached() { db.add_root(slot); db.flush_accounts_cache(true, None); assert_eq!( - db.load_without_fixed_root(&Ancestors::default(), &key), + db.do_load_for_tests(&Ancestors::default(), &key), Some((account0, slot)) ); } @@ -2734,25 +3274,22 @@ fn test_flush_accounts_cache() { // Unrooted slot should be able to be fetched before the flush let ancestors = Ancestors::from(vec![unrooted_slot]); assert_eq!( - db.load_without_fixed_root(&ancestors, &unrooted_key), + db.do_load_for_tests(&ancestors, &unrooted_key), Some((account0.clone(), unrooted_slot)) ); db.flush_accounts_cache(true, None); // After the flush, the unrooted slot is still in the cache - assert!( - db.load_without_fixed_root(&ancestors, &unrooted_key) - .is_some() - ); + assert!(db.do_load_for_tests(&ancestors, &unrooted_key).is_some()); assert!(db.contains(&unrooted_key)); assert_eq!(db.accounts_cache.num_slots(), 1); assert!(db.accounts_cache.slot_cache(unrooted_slot).is_some()); assert_eq!( - db.load_without_fixed_root(&Ancestors::default(), &key5), + db.do_load_for_tests(&Ancestors::default(), &key5), Some((account0.clone(), root5)) ); assert_eq!( - db.load_without_fixed_root(&Ancestors::default(), &key6), + db.do_load_for_tests(&Ancestors::default(), &key6), Some((account0, root6)) ); } @@ -2827,7 +3364,7 @@ fn run_test_flush_accounts_cache_if_needed(num_roots: usize, num_unrooted: usize Ancestors::from(vec![slot]) }; assert_eq!( - db.load_without_fixed_root(&ancestors, &key), + db.do_load_for_tests(&ancestors, &key), Some((account0.clone(), slot)) ); } @@ -2980,6 +3517,44 @@ fn test_load_with_read_only_accounts_cache() { assert_eq!(slot, 2); } +/// `select_pubkeys_to_flush` keeps only the newest version of each account across the +/// cleaned roots and flushes roots above `max_clean_root` in full. +#[test] +fn test_select_pubkeys_to_flush() { + let db = AccountsDb::new_single_for_tests(); + let account = AccountSharedData::new(1, 0, &Pubkey::default()); + + // The same account is written in all three roots. + let roots = BTreeSet::from([5, 10, 15]); + let shared = Pubkey::new_unique(); + for &slot in &roots { + db.accounts_cache.store(slot, &shared, account.clone()); + } + + let shared_only = PubkeysToFlush::Only([shared].into_iter().collect()); + let deduped = PubkeysToFlush::Only(HashSet::default()); + + // No bound: every flushed root is cleaned, so `shared` is written only at its newest root + // (15), deduped from 5 and 10. + let plans = db.select_pubkeys_to_flush(&roots, None); + assert_eq!(plans[&15], shared_only); + assert_eq!(plans[&10], deduped); + assert_eq!(plans[&5], deduped); + + // max_clean_root = 15 (== newest root): same result, every root is at or below the bound. + let plans = db.select_pubkeys_to_flush(&roots, Some(15)); + assert_eq!(plans[&15], shared_only); + assert_eq!(plans[&10], deduped); + assert_eq!(plans[&5], deduped); + + // max_clean_root = 10: root 15 is above the boundary and flushes `All` (no dedup), so + // `shared` is not dropped from root 10 — a scan at root 10 may still need that version. + let plans = db.select_pubkeys_to_flush(&roots, Some(10)); + assert_eq!(plans[&15], PubkeysToFlush::All); + assert_eq!(plans[&10], shared_only); + assert_eq!(plans[&5], deduped); +} + #[test] fn test_flush_cache_clean() { let db = Arc::new(AccountsDb::new_single_for_tests()); @@ -3136,14 +3711,14 @@ fn setup_scan( bank_id, |maybe_account| { ready_.store(true, Ordering::Relaxed); - if let Some((pubkey, _, _)) = maybe_account { - if *pubkey == stall_key { - loop { - if exit_.load(Ordering::Relaxed) { - break; - } else { - sleep(Duration::from_millis(10)); - } + if let Some((pubkey, _, _)) = maybe_account + && *pubkey == stall_key + { + loop { + if exit_.load(Ordering::Relaxed) { + break; + } else { + sleep(Duration::from_millis(10)); } } } @@ -3243,6 +3818,11 @@ impl AccountsDb { } fn get_account_at_slot(&self, pubkey: &Pubkey, slot: Slot) -> Option { + // Check the cache for the pubkey first + if let Some(cached) = self.accounts_cache.load(slot, pubkey) { + return Some(cached.account.clone()); + } + // Add the slot to ancestors so unrooted slots will be selected let mut ancestors = Ancestors::default(); ancestors.insert(slot); @@ -3256,7 +3836,7 @@ impl AccountsDb { assert_eq!(slot_found, slot); let storage_location = account_info.storage_location(); - let mut accessor = self.get_account_accessor(slot, pubkey, &storage_location); + let mut accessor = self.get_account_accessor(slot, &storage_location); accessor.check_and_get_loaded_account_shared_data() }, @@ -3374,6 +3954,78 @@ define_accounts_db_test!( } ); +/// When the full snapshot advances past slots that still hold zero-lamport single-ref +/// accounts, the next clean's range sweep must re-queue those slots for shrink so the +/// zero lamport single ref accounts can be removed by shrink. +#[test_case(false; "without_last_swept_set_queues_both_slots")] +#[test_case(true; "with_last_swept_set_skips_only_at_last_swept")] +fn test_zero_lamport_single_ref_resweep_respects_last_swept(set_last_swept: bool) { + let db = AccountsDb::new_single_for_tests(); + + let one_lamport_account = AccountSharedData::new(1, 0, AccountSharedData::default().owner()); + let zero_lamport_account = AccountSharedData::new(0, 0, AccountSharedData::default().owner()); + let slot_at_last_swept = 2; + let slot_above_last_swept = 3; + let full_snapshot_slot = 4; + + // Seed the snapshot at slot 0 to avoid cleaning zero lamport single ref accounts + db.set_latest_full_snapshot_slot(0); + + // slot 1: older non-zero versions of both keys. Both entries are overwritten by the + // stores below, so slot1 is reclaimed + let key_zero_at_last_swept = Pubkey::new_unique(); + let key_zero_above_last_swept = Pubkey::new_unique(); + db.store_for_tests(( + 1, + &[ + (&key_zero_at_last_swept, &one_lamport_account), + (&key_zero_above_last_swept, &one_lamport_account), + ][..], + )); + db.add_root_and_flush_write_cache(1); + + // slot 2: zero-lamport account plus an unrelated live account so the storage still + // has alive bytes now that key_zero is a single-ref zero-lamport. + db.store_for_tests(( + slot_at_last_swept, + &[ + (&key_zero_at_last_swept, &zero_lamport_account), + (&Pubkey::new_unique(), &one_lamport_account), + ][..], + )); + db.add_root_and_flush_write_cache(slot_at_last_swept); + + // slot 3: same pattern, but for a key whose slot sits *above* the seeded + // last-swept slot, so it must be queued in both variants — proving the sweep + // walks past the last-swept slot. + db.store_for_tests(( + slot_above_last_swept, + &[ + (&key_zero_above_last_swept, &zero_lamport_account), + (&Pubkey::new_unique(), &one_lamport_account), + ][..], + )); + db.add_root_and_flush_write_cache(slot_above_last_swept); + + // Optionally mark slot 2 as already swept. `set_last_swept_full_snapshot_slot` + // requires `last_swept <= latest`, so advance latest to slot 2 first (it gets + // advanced again to `full_snapshot_slot` below). + if set_last_swept { + db.set_latest_full_snapshot_slot(slot_at_last_swept); + db.set_last_swept_full_snapshot_slot(slot_at_last_swept); + } + + // Advance the snapshot past both ZLSR slots and clean + // The sweep range is (0, 4] when not set, queueing both slot 2 and slot 3. + // The sweep range is (2, 4] when set, queueing only slot 3. + db.set_latest_full_snapshot_slot(full_snapshot_slot); + db.clean_accounts(Some(full_snapshot_slot), false); + + let queued = db.shrink_candidate_slots.lock().unwrap(); + assert_eq!(queued.contains(&slot_at_last_swept), !set_last_swept); + assert!(queued.contains(&slot_above_last_swept)); +} + fn setup_accounts_db_cache_clean( num_slots: usize, scan_slot: Option, @@ -3429,7 +4081,7 @@ fn setup_accounts_db_cache_clean( } } - accounts_db.accounts_cache.remove_slot(stall_slot); + let _ = accounts_db.accounts_cache.remove_slot(stall_slot); // If there's <= max_cache_slots(), no slots should be flushed if accounts_db.accounts_cache.num_slots() <= max_cache_slots() { @@ -3461,11 +4113,7 @@ fn test_accounts_db_cache_clean_dead_slots() { // an older ancestor set let ancestors = Ancestors::from(vec![last_dead_slot]); for key in &keys { - assert!( - accounts_db - .load_without_fixed_root(&ancestors, key) - .is_some() - ); + assert!(accounts_db.do_load_for_tests(&ancestors, key).is_some()); } // If no `max_clean_root` is specified, cleaning should purge all flushed slots @@ -3481,11 +4129,7 @@ fn test_accounts_db_cache_clean_dead_slots() { // Dead slots have been purged, so these keys should not be findable in the database. for key in &keys { - assert!( - accounts_db - .load_without_fixed_root(&ancestors, key) - .is_none() - ); + assert!(accounts_db.do_load_for_tests(&ancestors, key).is_none()); } // Each slot should only have one entry in the storage, since all other accounts were // cleaned due to later updates @@ -3746,6 +4390,32 @@ fn test_flush_rooted_accounts_cache_with_clean() { fn test_flush_rooted_accounts_cache_without_clean() { run_flush_rooted_accounts_cache(false); } + +/// A rooted slot with no write-cache entry (e.g. genesis, whose accounts load straight to storage) +/// is still tracked by `add_root`. Flushing must untrack it rather than leave it stranded in +/// `unflushed_roots` at or below `max_flushed_root`. +#[test] +fn test_flush_untracks_cacheless_root() { + let db = AccountsDb::new_single_for_tests(); + + // Slot 0: rooted but never written to the write cache. + db.accounts_cache.add_root(0); + + // Slot 10: a normal rooted slot with a cached account. + let pubkey = Pubkey::new_unique(); + db.accounts_cache.store( + 10, + &pubkey, + AccountSharedData::new(10, 0, &Pubkey::default()), + ); + db.add_root(10); + + // Flushing through slot 10 must drop the cacheless root 0 instead of stranding it below + // max_flushed_root (which would otherwise trip the unflushed-root invariant). + db.flush_accounts_cache(true, Some(10)); + + assert_eq!(db.accounts_cache.num_unflushed_roots(), 0); +} #[test] fn test_shrink_unref() { let db = AccountsDb::new_single_for_tests(); @@ -4231,9 +4901,9 @@ fn test_collect_uncleaned_slots_up_to_slot() { db.uncleaned_pubkeys.insert(slot2, vec![pubkey2]); db.uncleaned_pubkeys.insert(slot3, vec![pubkey3]); - let mut uncleaned_slots1 = db.collect_uncleaned_slots_up_to_slot(slot1); - let mut uncleaned_slots2 = db.collect_uncleaned_slots_up_to_slot(slot2); - let mut uncleaned_slots3 = db.collect_uncleaned_slots_up_to_slot(slot3); + let mut uncleaned_slots1 = db.collect_uncleaned_slots_up_to_slot(Some(slot1)); + let mut uncleaned_slots2 = db.collect_uncleaned_slots_up_to_slot(Some(slot2)); + let mut uncleaned_slots3 = db.collect_uncleaned_slots_up_to_slot(Some(slot3)); uncleaned_slots1.sort_unstable(); uncleaned_slots2.sort_unstable(); @@ -4278,7 +4948,7 @@ fn test_remove_uncleaned_slots_and_collect_pubkeys_up_to_slot() { std::iter::repeat_with(|| RwLock::new(HashMap::::new())) .take(num_bins) .collect(); - db.remove_uncleaned_slots_up_to_slot_and_move_pubkeys(slot3, &candidates); + db.remove_uncleaned_slots_up_to_slot_and_move_pubkeys(Some(slot3), &candidates); let candidates_contain = |pubkey: &Pubkey| { candidates @@ -4293,6 +4963,7 @@ fn test_remove_uncleaned_slots_and_collect_pubkeys_up_to_slot() { #[test] fn test_shrink_productive() { agave_logger::setup(); + let accounts = AccountsDb::new_single_for_tests(); let path = Path::new(""); let file_size = 100; let slot = 11; @@ -4305,7 +4976,7 @@ fn test_shrink_productive() { AccountsFileProvider::AppendVec, )); store.add_account(file_size as usize); - assert!(!AccountsDb::is_shrinking_productive(&store)); + assert!(!accounts.is_shrinking_productive(&store)); let store = Arc::new(AccountStorageEntry::new( path, @@ -4317,10 +4988,10 @@ fn test_shrink_productive() { store.add_account(file_size as usize / 2); store.add_account(file_size as usize / 4); store.remove_accounts(file_size as usize / 4, 1); - assert!(AccountsDb::is_shrinking_productive(&store)); + assert!(accounts.is_shrinking_productive(&store)); store.add_account(file_size as usize / 2); - assert!(!AccountsDb::is_shrinking_productive(&store)); + assert!(!accounts.is_shrinking_productive(&store)); } #[test] @@ -4848,8 +5519,6 @@ fn test_unref_pubkeys_removed_from_accounts_index() { slot1, slot1, &pk1, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), AccountInfo::default(), &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -4900,8 +5569,6 @@ fn test_unref_accounts() { slot1, slot1, &pk1, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), AccountInfo::default(), &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -4931,8 +5598,6 @@ fn test_unref_accounts() { slot, slot, &pk, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), AccountInfo::default(), &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -4972,8 +5637,6 @@ define_accounts_db_test!(test_many_unrefs, |db| { slot, slot, &pk1, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), AccountInfo::default(), &mut reclaims, UpsertReclaim::IgnoreReclaims, @@ -5204,15 +5867,17 @@ define_accounts_db_test!(test_get_sorted_potential_ancient_slots, |db| { ); let root1 = DEFAULT_MAX_ANCIENT_STORAGES as u64 + ancient_append_vec_offset as u64 + 1; db.add_root(root1); + db.create_and_insert_store(root1, 4096, "test"); let root2 = root1 + 1; db.add_root(root2); + db.create_and_insert_store(root2, 4096, "test"); let oldest_non_ancient_slot = db.get_oldest_non_ancient_slot(&epoch_schedule); assert!( db.get_sorted_potential_ancient_slots(oldest_non_ancient_slot) .is_empty() ); let completed_slot = epoch_schedule.slots_per_epoch; - db.accounts_index.add_root(AccountsDb::apply_offset_to_slot( + db.add_root(AccountsDb::apply_offset_to_slot( completed_slot, ancient_append_vec_offset, )); @@ -5224,7 +5889,7 @@ define_accounts_db_test!(test_get_sorted_potential_ancient_slots, |db| { .is_empty() ); let completed_slot = epoch_schedule.slots_per_epoch + root1; - db.accounts_index.add_root(AccountsDb::apply_offset_to_slot( + db.add_root(AccountsDb::apply_offset_to_slot( completed_slot, ancient_append_vec_offset, )); @@ -5234,7 +5899,7 @@ define_accounts_db_test!(test_get_sorted_potential_ancient_slots, |db| { vec![root1, root2] ); let completed_slot = epoch_schedule.slots_per_epoch + root2; - db.accounts_index.add_root(AccountsDb::apply_offset_to_slot( + db.add_root(AccountsDb::apply_offset_to_slot( completed_slot, ancient_append_vec_offset, )); @@ -5243,12 +5908,7 @@ define_accounts_db_test!(test_get_sorted_potential_ancient_slots, |db| { db.get_sorted_potential_ancient_slots(oldest_non_ancient_slot), vec![root1, root2] ); - db.accounts_index - .roots_tracker - .write() - .unwrap() - .alive_roots - .remove(&root1); + db.storage.remove(&root1, false); let oldest_non_ancient_slot = db.get_oldest_non_ancient_slot(&epoch_schedule); assert_eq!( db.get_sorted_potential_ancient_slots(oldest_non_ancient_slot), @@ -5496,7 +6156,7 @@ fn test_shrink_collect_with_obsolete_accounts() { db.add_root_and_flush_write_cache(slot); let storage = db.get_and_assert_single_storage(slot); - let ancestors = Ancestors::from(vec![db.accounts_index.max_root_inclusive()]); + let ancestors = Ancestors::from(vec![db.max_root()]); for (i, pubkey) in pubkeys.iter().enumerate() { // Mark Some accounts obsolete. These will include zero lamport and non zero lamport accounts @@ -5685,8 +6345,6 @@ fn populate_index(db: &AccountsDb, slots: Range) { slot, slot, account.pubkey(), - &account, - &AccountSecondaryIndexes::default(), info, &mut ReclaimsSlotList::new(), UpsertReclaim::IgnoreReclaims, @@ -5731,7 +6389,6 @@ pub(crate) fn create_storages_and_update_index( let storage = sample_storage_with_entries_id(tf, slot, &pubkey1, id, alive, account_data_size); insert_store(db, Arc::clone(&storage)); - db.accounts_index.add_root(slot); } let storage = db.get_storage_for_slot(starting_slot).unwrap(); @@ -5799,98 +6456,10 @@ fn get_one_ancient_append_vec_and_others(num_normal_slots: usize) -> (AccountsDb get_one_ancient_append_vec_and_others_with_account_size(num_normal_slots, None) } -#[test] -fn test_handle_dropped_roots_for_ancient() { - agave_logger::setup(); - let db = AccountsDb::new_single_for_tests(); - db.handle_dropped_roots_for_ancient(std::iter::empty::()); - let slot0 = 0; - let dropped_roots = vec![slot0]; - db.accounts_index.add_root(slot0); - assert!(db.accounts_index.is_alive_root(slot0)); - db.handle_dropped_roots_for_ancient(dropped_roots.into_iter()); - assert!(!db.accounts_index.is_alive_root(slot0)); -} - fn insert_store(db: &AccountsDb, append_vec: Arc) { db.storage.insert(append_vec); } -#[test] -#[should_panic(expected = "self.storage.remove")] -fn test_handle_dropped_roots_for_ancient_assert() { - agave_logger::setup(); - let common_store_path = Path::new(""); - let store_file_size = 10_000; - let entry = Arc::new(AccountStorageEntry::new( - common_store_path, - 0, - 1, - store_file_size, - AccountsFileProvider::AppendVec, - )); - let db = AccountsDb::new_single_for_tests(); - let slot0 = 0; - let dropped_roots = vec![slot0]; - insert_store(&db, entry); - db.handle_dropped_roots_for_ancient(dropped_roots.into_iter()); -} - -/// Test that `clean` respects rooted vs unrooted slots w.r.t. reclaims -/// -/// When an account is in multiple slots, and the latest is unrooted, `clean` should *not* reclaim -/// all the rooted versions. -#[test] -fn test_clean_old_storages_with_reclaims_unrooted() { - let accounts_db = AccountsDb::new_single_for_tests(); - let pubkey = Pubkey::new_unique(); - let old_slot = 11; - let new_slot = 22; - let slots = [old_slot, new_slot]; - for &slot in &slots { - let account = AccountSharedData::new(slot, 0, &Pubkey::new_unique()); - // store `pubkey` into multiple slots, and also store another unique pubkey - // to prevent the whole storage from being marked as dead by `clean`. - accounts_db.store_for_tests(( - slot, - [(&pubkey, &account), (&Pubkey::new_unique(), &account)].as_slice(), - )); - } - - // only `old_slot` should be rooted, not `new_slot` - accounts_db.add_root_and_flush_write_cache(old_slot); - assert!(accounts_db.accounts_index.is_alive_root(old_slot)); - assert!(!accounts_db.accounts_index.is_alive_root(new_slot)); - - // ensure `old_slot` is in uncleaned_pubkeys (but not dirty_stores) so it'll be cleaned - assert!(accounts_db.uncleaned_pubkeys.contains_key(&old_slot)); - assert!(!accounts_db.dirty_stores.contains_key(&old_slot)); - // and `new_slot` should be in neither - assert!(!accounts_db.uncleaned_pubkeys.contains_key(&new_slot)); - assert!(!accounts_db.dirty_stores.contains_key(&new_slot)); - - // ensure the slot list for `pubkey` has both the old and new slots - let slot_list = accounts_db - .accounts_index - .get_bin(&pubkey) - .slot_list_mut(&pubkey, |slot_list| slot_list.clone_list()) - .unwrap(); - assert_eq!(slot_list.len(), slots.len()); - assert!(slot_list.iter().map(|(slot, _)| slot).eq(slots.iter())); - - // `clean` should *not* reclaim the account in `old_slot` because `new_slot` is not a root - accounts_db.clean_accounts_for_tests(); - - // ensure we have NOT reclaimed the account in `old_slot` - let slot_list = accounts_db - .accounts_index - .get_bin(&pubkey) - .slot_list_mut(&pubkey, |slot_list| slot_list.clone_list()) - .unwrap(); - assert_eq!(slot_list.len(), slots.len()); - assert!(slot_list.iter().map(|(slot, _)| slot).eq(slots.iter())); -} - /// Ensure the calculating capitalization produces the correct value #[test] fn test_calculate_capitalization_simple() { @@ -6123,7 +6692,9 @@ fn test_new_zero_lamport_accounts_skipped() { .unwrap() .contains_key(&pubkey1) ); - assert!(accounts_db.accounts_index.contains(&pubkey2)); + // pubkey2 and pubkey3 are in the write cache but not the index; cache writes reach the + // index only on flush. + assert!(!accounts_db.accounts_index.contains(&pubkey2)); assert!( accounts_db .accounts_cache @@ -6131,7 +6702,7 @@ fn test_new_zero_lamport_accounts_skipped() { .unwrap() .contains_key(&pubkey2) ); - assert!(accounts_db.accounts_index.contains(&pubkey3)); + assert!(!accounts_db.accounts_index.contains(&pubkey3)); assert!( accounts_db .accounts_cache @@ -6140,18 +6711,26 @@ fn test_new_zero_lamport_accounts_skipped() { .contains_key(&pubkey3) ); - // 3. Insert a zero-lamport update for an already-indexed pubkey (pubkey2). - // Verify pubkey2 remains in the index and gets added to the slot cache. + // 3. Insert a zero-lamport update for pubkey2, which already has a non-zero entry in the + // write cache. The update is stored rather than skipped, overwriting the cache entry + // with zero lamports. accounts_db.store_accounts_unfrozen( (slot, [(&pubkey2, &zero_account)].as_slice()), UpdateIndexThreadSelection::Inline, &ancestors, ); - assert!(accounts_db.accounts_index.contains(&pubkey2)); - assert!(accounts_db.accounts_cache.contains_pubkey(&pubkey2)); + assert_eq!( + accounts_db + .accounts_cache + .load(slot, &pubkey2) + .unwrap() + .account + .lamports(), + 0 + ); - // 4. Flush the slot to simulate write-cache -> storage transition and verify - // pubkey1 is still not present while pubkey2 remains indexed but is now zero-lamport. + // 4. Flush the slot (write cache -> storage). pubkey1 (only ever written as zero) is + // absent; pubkey2 and pubkey3 are now in the index. accounts_db.add_root_and_flush_write_cache(slot); assert!(!accounts_db.contains(&pubkey1)); assert!(accounts_db.contains(&pubkey2)); @@ -6164,7 +6743,7 @@ fn test_new_zero_lamport_accounts_skipped() { })); // 5. Add a non-zero lamport account for a pubkey that was previously only written as zero - // (pubkey1) and verify the pubkey is added to the index. + // (pubkey1) and verify the pubkey is added to the write cache. let slot = slot + 1; ancestors.insert(slot); accounts_db.store_accounts_unfrozen( @@ -6172,7 +6751,7 @@ fn test_new_zero_lamport_accounts_skipped() { UpdateIndexThreadSelection::Inline, &ancestors, ); - assert!(accounts_db.accounts_index.contains(&pubkey1)); + assert!(accounts_db.accounts_cache.contains_pubkey(&pubkey1)); // 6. Set pubkey3 to zero lamports and flush. Verify pubkey3 is present in the index with // a zero-lamport AccountInfo after flushing. @@ -6285,7 +6864,7 @@ fn test_write_accounts_to_cache_scenarios( ); // Verify results - let loaded = db.load_without_fixed_root(&ancestors, &key); + let loaded = db.do_load_for_tests(&ancestors, &key); match expected_lamports { Some(expected) => { assert!(loaded.is_some(), "Account should be loadable"); @@ -6385,42 +6964,49 @@ fn test_is_ancestor_zero_lamport_index_only() { assert_eq!(db.is_ancestor_zero_lamport(&pubkey, &ancestors), Some(true)); } -/// Verifies that when the cache entry is in a `*BeingFlushed` state, is_ancestor_zero_lamport -/// also consults the index and returns the zero lamport status of whichever version sits on the -/// higher slot, regardless of whether that version is in the cache or the index. -#[test_case(10, 5, true; "newer_cache_wins")] -#[test_case(5, 10, false; "newer_index_wins")] -fn test_is_ancestor_zero_lamport_being_flushed_picks_higher_slot( - cache_slot: Slot, - index_slot: Slot, - expected: bool, -) { +/// Verifies that when a pubkey is in both storage and the write cache, is_ancestor_zero_lamport +/// returns the cached version's zero lamport status. +#[test] +fn test_is_ancestor_zero_lamport_cache_over_storage() { let db = AccountsDb::new_single_for_tests(); let pubkey = Pubkey::new_unique(); + let storage_slot = 5; + let cache_slot = 10; - // Non-zero lamport entry in the index. Set up first: flushing goes through the cache, - // so doing it after the cache setup would conflict with `begin_flush_roots`. - let account = AccountSharedData::new(100, 0, &Pubkey::default()); - db.store_for_tests((index_slot, [(&pubkey, &account)].as_slice())); - db.add_root_and_flush_write_cache(index_slot); + // Non-zero lamport entry in storage at the older slot. + let nonzero_account = AccountSharedData::new(100, 0, &Pubkey::default()); + db.store_for_tests((storage_slot, [(&pubkey, &nonzero_account)].as_slice())); + db.add_root_and_flush_write_cache(storage_slot); assert!(!db.accounts_cache.contains_pubkey(&pubkey)); - // Zero lamport entry in the cache. Note: this entry may be older than the slot that was - // flushed above which is normally not allowed to be added to the cache. However, - // this state is a valid intermediate state when flushing the cache so needs - // test coverage - let account = AccountSharedData::new(0, 0, &Pubkey::default()); - db.accounts_cache.store(cache_slot, &pubkey, account); - db.accounts_cache.add_root(cache_slot); - db.accounts_cache.begin_flush_roots(Some(cache_slot)); + // Zero lamport entry in the cache at the newer slot. + let zero_account = AccountSharedData::new(0, 0, &Pubkey::default()); + db.accounts_cache.store(cache_slot, &pubkey, zero_account); + + let ancestors = Ancestors::from(vec![cache_slot, storage_slot]); + assert_eq!(db.is_ancestor_zero_lamport(&pubkey, &ancestors), Some(true)); +} - let ancestors = Ancestors::from(vec![cache_slot, index_slot]); +/// A cache-only pubkey has no accounts-index entry (cache writes don't upsert the index), +/// yet `do_load` must still return it — `load_latest` finds it in the write cache before +/// the index is consulted. +#[test] +fn test_do_load_returns_cache_value_for_cache_only_pubkey() { + let db = AccountsDb::new_single_for_tests(); + let pubkey = Pubkey::new_unique(); + let slot = 5; + + let account = AccountSharedData::new(100, 0, &Pubkey::default()); + db.accounts_cache.store(slot, &pubkey, account.clone()); + db.accounts_cache.add_root(slot); + assert!(!db.accounts_index.contains(&pubkey)); + + let ancestors = Ancestors::from(vec![slot]); assert_eq!( - db.is_ancestor_zero_lamport(&pubkey, &ancestors), - Some(expected) + db.do_load_for_tests(&ancestors, &pubkey) + .map(|(loaded, loaded_slot)| (loaded.lamports(), loaded_slot)), + Some((account.lamports(), slot)) ); - - db.accounts_cache.end_flush_roots(); } /// loading an account through an older bank must not return @@ -6454,7 +7040,7 @@ fn test_load_does_not_return_data_from_non_ancestor_root() { // Ancestors = {17, 19}: min_slot = 17. Slot 18 is rooted but not an // ancestor, so it must be excluded. Slot 16 <= 17, so it is returned. let ancestors = Ancestors::from(vec![17, 19]); - let (account, slot) = db.load_without_fixed_root(&ancestors, &pubkey).unwrap(); + let (account, slot) = db.do_load_for_tests(&ancestors, &pubkey).unwrap(); assert_eq!( slot, 16, diff --git a/accounts-db/src/accounts_file.rs b/accounts-db/src/accounts_file.rs index 3f72b556813..3e0d5a67c9b 100644 --- a/accounts-db/src/accounts_file.rs +++ b/accounts-db/src/accounts_file.rs @@ -62,10 +62,10 @@ impl AccountsFile { /// Creates a new AccountsFile for the underlying storage at `file_info` /// /// This version of `new()` may only be called when reconstructing storages as part of startup. - /// It trusts the snapshot's value for `current_len`, and relies on later index generation or - /// accounts verification to ensure it is valid. - pub fn new_for_startup(file_info: FileInfo, current_len: usize) -> Result { - let av = AppendVec::new_for_startup(file_info, current_len)?; + /// The storage length is taken to be the full file size; this is trusted and relies on later + /// index generation or accounts verification to ensure it is valid. + pub fn new_for_startup(file_info: FileInfo) -> Result { + let av = AppendVec::new_for_startup(file_info)?; Ok(Self::AppendVec(av)) } @@ -76,6 +76,14 @@ impl AccountsFile { } } + /// Detach the on-disk file from this storage's lifetime; see + /// [`AppendVec::disable_remove_on_drop`]. + pub fn disable_remove_on_drop(&self) { + match self { + Self::AppendVec(av) => av.disable_remove_on_drop(), + } + } + /// Return the total number of bytes of the zero lamport single ref accounts in the storage. /// Those bytes are "dead" and can be shrunk away. pub(crate) fn dead_bytes_due_to_zero_lamport_single_ref(&self, count: usize) -> usize { diff --git a/accounts-db/src/accounts_index.rs b/accounts-db/src/accounts_index.rs index 0375f04d188..6ed8652bc1d 100644 --- a/accounts-db/src/accounts_index.rs +++ b/accounts-db/src/accounts_index.rs @@ -3,7 +3,6 @@ mod accounts_index_storage; mod bucket_map_holder; pub(crate) mod in_mem_accounts_index; mod iter; -mod roots_tracker; mod secondary; mod stats; use { @@ -13,7 +12,6 @@ use { contains::Contains, is_zero_lamport::IsZeroLamport, pubkey_bins::{PubkeyBinCalculator, PubkeyBinCalculatorBuilder}, - rolling_bit_field::RollingBitField, }, account_map_entry::{AccountMapEntry, PreAllocatedAccountMapEntry, SlotListWriteGuard}, accounts_index_storage::AccountsIndexStorage, @@ -25,12 +23,10 @@ use { log::*, rand::{Rng, rng}, rayon::iter::{IntoParallelIterator, ParallelIterator}, - roots_tracker::RootsTracker, secondary::{RwLockSecondaryIndexEntry, SecondaryIndex, SecondaryIndexEntry}, smallvec::SmallVec, solana_account::ReadableAccount, solana_clock::Slot, - solana_measure::measure::Measure, solana_pubkey::Pubkey, stats::Stats, std::{ @@ -39,7 +35,7 @@ use { num::NonZeroUsize, path::PathBuf, sync::{ - Arc, RwLock, + Arc, atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}, }, }, @@ -125,8 +121,6 @@ pub enum ScanFilter { #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// how accounts index 'upsert' should handle reclaims pub enum UpsertReclaim { - /// previous entry for this slot in the index is expected to be cached, so irrelevant to reclaims - PreviousSlotEntryWasCached, /// previous entry for this slot in the index may need to be reclaimed, so return it. /// reclaims is the only output of upsert, requiring a synchronous execution PopulateReclaims, @@ -136,12 +130,7 @@ pub enum UpsertReclaim { // in the 'reclaims' ReclaimOldSlots, } - -pub trait IsCached { - fn is_cached(&self) -> bool; -} - -pub trait IndexValue: 'static + IsCached + IsZeroLamport + DiskIndexValue {} +pub trait IndexValue: 'static + IsZeroLamport + DiskIndexValue {} pub trait DiskIndexValue: 'static + Clone + Debug + PartialEq + Copy + Default + Sync + Send @@ -199,17 +188,6 @@ pub fn default_num_flush_threads() -> NonZeroUsize { NonZeroUsize::new(std::cmp::max(2, num_cpus::get() / 4)).expect("non-zero system threads") } -#[derive(Debug, Default)] -pub struct AccountsIndexRootsStats { - pub roots_len: Option, - pub uncleaned_roots_len: Option, - pub roots_range: Option, - pub rooted_cleaned_count: usize, - pub unrooted_cleaned_count: usize, - pub clean_unref_from_storage_us: u64, - pub clean_dead_slot_us: u64, -} - #[derive(Copy, Clone)] pub enum AccountsIndexScanResult { /// if the entry is not in the in-memory index, do not add it unless the entry becomes dirty @@ -233,16 +211,10 @@ pub struct AccountsIndex + Into> { program_id_index: SecondaryIndex, spl_token_mint_index: SecondaryIndex, spl_token_owner_index: SecondaryIndex, - pub roots_tracker: RwLock, storage: AccountsIndexStorage, pub purge_older_root_entries_one_slot_list: AtomicUsize, - - /// # roots added since last check - pub roots_added: AtomicUsize, - /// # roots removed since last check - pub roots_removed: AtomicUsize, } impl + Into> AccountsIndex { @@ -266,10 +238,7 @@ impl + Into> AccountsIndex { spl_token_owner_index: SecondaryIndex::::new( "spl_token_owner_index_stats", ), - roots_tracker: RwLock::::default(), storage, - roots_added: AtomicUsize::default(), - roots_removed: AtomicUsize::default(), } } @@ -433,16 +402,15 @@ impl + Into> AccountsIndex { } } - pub fn get_rooted_entries( + pub fn get_entries_up_to_inclusive( &self, slot_list: &[SlotListItem], max_inclusive: Option, ) -> SlotList { let max_inclusive = max_inclusive.unwrap_or(Slot::MAX); - let lock = &self.roots_tracker.read().unwrap().alive_roots; slot_list .iter() - .filter(|(slot, _)| *slot <= max_inclusive && lock.contains(slot)) + .filter(|(slot, _)| *slot <= max_inclusive) .cloned() .collect() } @@ -480,13 +448,13 @@ impl + Into> AccountsIndex { ) -> Option { let mut current_max = 0; let mut rv = None; - if let Some(ancestors) = ancestors { - if !ancestors.is_empty() { - for (i, (slot, _t)) in slot_list.iter().rev().enumerate() { - if (rv.is_none() || *slot > current_max) && ancestors.contains_key(slot) { - rv = Some(i); - current_max = *slot; - } + if let Some(ancestors) = ancestors + && !ancestors.is_empty() + { + for (i, (slot, _t)) in slot_list.iter().rev().enumerate() { + if (rv.is_none() || *slot > current_max) && ancestors.contains_key(slot) { + rv = Some(i); + current_max = *slot; } } } @@ -499,23 +467,13 @@ impl + Into> AccountsIndex { } let max_root_inclusive = max_root_inclusive.unwrap_or(Slot::MAX); - let mut tracker = None; - - for (i, (slot, _t)) in slot_list.iter().rev().enumerate() { - if (rv.is_none() || *slot > current_max) && *slot <= max_root_inclusive { - let lock = match tracker { - Some(inner) => inner, - None => self.roots_tracker.read().unwrap(), - }; - if lock.alive_roots.contains(slot) { - rv = Some(i); - current_max = *slot; - } - tracker = Some(lock); - } - } - rv.map(|index| slot_list.len() - 1 - index) + slot_list + .iter() + .enumerate() + .filter(|(_, (slot, _t))| *slot <= max_root_inclusive) + .max_by_key(|(_, (slot, _t))| *slot) + .map(|(index, _)| index) } pub(crate) fn stats(&self) -> &Stats { @@ -666,22 +624,6 @@ impl + Into> AccountsIndex { }); } - // Get the maximum root <= `max_allowed_root` from the given `slot_list` - fn get_newest_root_in_slot_list( - alive_roots: &RollingBitField, - slot_list: &[SlotListItem], - max_allowed_root_inclusive: Option, - ) -> Slot { - slot_list - .iter() - .map(|(slot, _)| slot) - .filter(|slot| max_allowed_root_inclusive.is_none_or(|max_root| **slot <= max_root)) - .filter(|slot| alive_roots.contains(slot)) - .max() - .copied() - .unwrap_or(0) - } - fn update_spl_token_secondary_indexes( &self, token_id: &Pubkey, @@ -691,20 +633,18 @@ impl + Into> AccountsIndex { account_indexes: &AccountSecondaryIndexes, ) { if *account_owner == *token_id { - if account_indexes.contains(&AccountIndex::SplTokenOwner) { - if let Some(owner_key) = G::unpack_account_owner(account_data) { - if account_indexes.include_key(owner_key) { - self.spl_token_owner_index.insert(owner_key, pubkey); - } - } + if account_indexes.contains(&AccountIndex::SplTokenOwner) + && let Some(owner_key) = G::unpack_account_owner(account_data) + && account_indexes.include_key(owner_key) + { + self.spl_token_owner_index.insert(owner_key, pubkey); } - if account_indexes.contains(&AccountIndex::SplTokenMint) { - if let Some(mint_key) = G::unpack_account_mint(account_data) { - if account_indexes.include_key(mint_key) { - self.spl_token_mint_index.insert(mint_key, pubkey); - } - } + if account_indexes.contains(&AccountIndex::SplTokenMint) + && let Some(mint_key) = G::unpack_account_mint(account_data) + && account_indexes.include_key(mint_key) + { + self.spl_token_mint_index.insert(mint_key, pubkey); } } } @@ -797,6 +737,18 @@ impl + Into> AccountsIndex { self.account_maps.len() } + /// Write through to disk the in-mem entries for `pubkeys`. Each entry is only persisted if it + /// is dirty, `slot_list.len() == 1`, and `ref_count == 1`. Persisting an entry clears its + /// dirty flag so it becomes eligible for eviction. No-op when disk index is disabled. + pub fn write_through_pubkeys(&self, pubkeys: Vec) { + if !self.storage.storage.should_write_through() { + return; + } + for pubkey in pubkeys { + self.get_bin(&pubkey).try_write_through(&pubkey); + } + } + /// Same functionally to upsert, but: /// 1. operates on a batch of items in reusable Vec, draining all elements /// 2. holds the write lock for the duration of adding the items @@ -920,33 +872,25 @@ impl + Into> AccountsIndex { .for_each(f); } - /// Updates the given pubkey at the given slot with the new account information. - /// on return, the index's previous account info may be returned in 'reclaims' depending on 'previous_slot_entry_was_cached' + /// Updates the primary index for `pubkey` at `new_slot` with `account_info`. + /// + /// Does NOT update the secondary indexes — callers that need that must update separately. + /// The primary and secondary indexes are not updated atomically, and a brief inconsistency is + /// acceptable: the secondary index is only consulted for `scan`, which is only supported on + /// frozen banks, and is never used as a source of truth for gets/stores. + /// + /// On return, the previous account info may be returned in `reclaims` depending on `reclaim`. pub fn upsert( &self, new_slot: Slot, old_slot: Slot, pubkey: &Pubkey, - account: &impl ReadableAccount, - account_indexes: &AccountSecondaryIndexes, account_info: T, reclaims: &mut ReclaimsSlotList, reclaim: UpsertReclaim, ) { // vast majority of updates are to item already in accounts index, so store as raw to avoid unnecessary allocations let store_raw = true; - - // We don't atomically update both primary index and secondary index together. - // This certainly creates a small time window with inconsistent state across the two indexes. - // However, this is acceptable because: - // - // - A strict consistent view at any given moment of time is not necessary, because the only - // use case for the secondary index is `scan`, and `scans` are only supported/require consistency - // on frozen banks, and this inconsistency is only possible on working banks. - // - // - The secondary index is never consulted as primary source of truth for gets/stores. - // So, what the accounts_index sees alone is sufficient as a source of truth for other non-scan - // account operations. let new_item = PreAllocatedAccountMapEntry::new( new_slot, account_info, @@ -954,9 +898,19 @@ impl + Into> AccountsIndex { store_raw, ); let map = self.get_bin(pubkey); - map.upsert(pubkey, new_item, Some(old_slot), reclaims, reclaim); - self.update_secondary_indexes(pubkey, account, account_indexes); + } + + /// Replaces the slot list entry at `old_slot` with `(new_slot, account_info)` for `pubkey`. + /// + /// Used by the shrink path: the account already exists in the index at `old_slot`, and + /// shrink is rewriting it into a new storage at `new_slot`. The previous entry is discarded + /// (no reclaims are returned — the caller manages the source storage's alive-bytes accounting). + /// + /// Panics if `old_slot` is not present in the slot list. + pub fn replace(&self, new_slot: Slot, old_slot: Slot, pubkey: &Pubkey, account_info: T) { + let map = self.get_bin(pubkey); + map.replace(pubkey, (new_slot, account_info), old_slot); } pub fn ref_count_from_storage(&self, pubkey: &Pubkey) -> RefCount { @@ -998,26 +952,16 @@ impl + Into> AccountsIndex { self.purge_older_root_entries_one_slot_list .fetch_add(1, Ordering::Relaxed); } - let newest_root_in_slot_list; - let max_clean_root_inclusive = { - let roots_tracker = &self.roots_tracker.read().unwrap(); - newest_root_in_slot_list = Self::get_newest_root_in_slot_list( - &roots_tracker.alive_roots, - slot_list, - max_clean_root_inclusive, - ); - max_clean_root_inclusive.unwrap_or_else(|| roots_tracker.alive_roots.max_inclusive()) - }; + // Find the newest slot at or below the clean root, then reclaim every slot older than it. + let newest_slot = slot_list + .iter() + .map(|(slot, _)| *slot) + .filter(|slot| slot <= &max_clean_root_inclusive.unwrap_or(Slot::MAX)) + .max() + .unwrap_or_default(); slot_list.retain_and_count(|(slot, value)| { - let should_purge = Self::can_purge_older_entries( - // Note that we have a root that is inclusive here. - // Calling a function that expects 'exclusive' - // This is expected behavior for this call. - max_clean_root_inclusive, - newest_root_in_slot_list, - *slot, - ) && !value.is_cached(); + let should_purge = *slot < newest_slot; if should_purge { reclaims.push((*slot, *value)); } @@ -1025,8 +969,7 @@ impl + Into> AccountsIndex { }) == 0 } - /// return true if pubkey was removed from the accounts index - /// or does not exist in the accounts index + /// return true if pubkey does not exist in the accounts index. /// This means it should NOT be unref'd later. #[must_use] pub fn clean_rooted_entries( @@ -1035,27 +978,10 @@ impl + Into> AccountsIndex { reclaims: &mut ReclaimsSlotList, max_clean_root_inclusive: Option, ) -> bool { - let mut is_slot_list_empty = false; - let missing_in_accounts_index = self - .slot_list_mut(pubkey, |mut slot_list| { - is_slot_list_empty = self.purge_older_root_entries( - &mut slot_list, - reclaims, - max_clean_root_inclusive, - ); - }) - .is_none(); - - let mut removed = false; - // If the slot list is empty, remove the pubkey from `account_maps`. Make sure to grab the - // lock and double check the slot list is still empty, because another writer could have - // locked and inserted the pubkey in-between when `is_slot_list_empty=true` and the call to - // remove() below. - if is_slot_list_empty { - let w_maps = self.get_bin(pubkey); - removed = w_maps.remove_if_slot_list_empty(*pubkey); - } - removed || missing_in_accounts_index + self.slot_list_mut(pubkey, |mut slot_list| { + self.purge_older_root_entries(&mut slot_list, reclaims, max_clean_root_inclusive); + }) + .is_none() } /// Cleans and unrefs all older rooted entries for each pubkey in the accounts index. @@ -1076,133 +1002,6 @@ impl + Into> AccountsIndex { } reclaims } - - /// When can an entry be purged? - /// - /// If we get a slot update where slot != newest_root_in_slot_list for an account where slot < - /// max_clean_root_exclusive, then we know it's safe to delete because: - /// - /// a) If slot < newest_root_in_slot_list, then we know the update is outdated by a later rooted - /// update, namely the one in newest_root_in_slot_list - /// - /// b) If slot > newest_root_in_slot_list, then because slot < max_clean_root_exclusive and we know there are - /// no roots in the slot list between newest_root_in_slot_list and max_clean_root_exclusive, (otherwise there - /// would be a bigger newest_root_in_slot_list, which is a contradiction), then we know slot must be - /// an unrooted slot less than max_clean_root_exclusive and thus safe to clean as well. - fn can_purge_older_entries( - max_clean_root_exclusive: Slot, - newest_root_in_slot_list: Slot, - slot: Slot, - ) -> bool { - slot < max_clean_root_exclusive && slot != newest_root_in_slot_list - } - - /// Given a list of slots, return a new list of only the slots that are rooted - pub fn get_rooted_from_list<'a>(&self, slots: impl Iterator) -> Vec { - let roots_tracker = self.roots_tracker.read().unwrap(); - slots - .filter_map(|s| { - if roots_tracker.alive_roots.contains(s) { - Some(*s) - } else { - None - } - }) - .collect() - } - - pub fn is_alive_root(&self, slot: Slot) -> bool { - self.roots_tracker - .read() - .unwrap() - .alive_roots - .contains(&slot) - } - - pub fn add_root(&self, slot: Slot) { - self.roots_added.fetch_add(1, Ordering::Relaxed); - let mut w_roots_tracker = self.roots_tracker.write().unwrap(); - // `AccountsDb::flush_accounts_cache()` relies on roots being added in order - assert!( - slot >= w_roots_tracker.alive_roots.max_inclusive(), - "Roots must be added in order: {} < {}", - slot, - w_roots_tracker.alive_roots.max_inclusive() - ); - // 'slot' is a root, so it is both 'root' and 'original' - w_roots_tracker.alive_roots.insert(slot); - } - - pub fn max_root_inclusive(&self) -> Slot { - self.roots_tracker - .read() - .unwrap() - .alive_roots - .max_inclusive() - } - - pub(crate) fn clean_dead_slots<'a>( - &'a self, - dead_slots_iter: impl Iterator, - ) -> AccountsIndexRootsStats { - let mut accounts_index_root_stats = AccountsIndexRootsStats::default(); - let mut measure = Measure::start("clean_dead_slot"); - let mut rooted_cleaned_count = 0; - let mut unrooted_cleaned_count = 0; - dead_slots_iter.for_each(|slot| { - if self.clean_dead_slot(*slot) { - rooted_cleaned_count += 1; - } else { - unrooted_cleaned_count += 1; - } - }); - measure.stop(); - accounts_index_root_stats.clean_dead_slot_us += measure.as_us(); - self.update_roots_stats(&mut accounts_index_root_stats); - accounts_index_root_stats.rooted_cleaned_count += rooted_cleaned_count; - accounts_index_root_stats.unrooted_cleaned_count += unrooted_cleaned_count; - - accounts_index_root_stats - } - - /// Remove the slot when the storage for the slot is freed - /// Accounts no longer reference this slot. - /// return true if slot was a root - pub fn clean_dead_slot(&self, slot: Slot) -> bool { - let mut w_roots_tracker = self.roots_tracker.write().unwrap(); - if !w_roots_tracker.alive_roots.remove(&slot) { - false - } else { - drop(w_roots_tracker); - self.roots_removed.fetch_add(1, Ordering::Relaxed); - true - } - } - - pub(crate) fn update_roots_stats(&self, stats: &mut AccountsIndexRootsStats) { - let roots_tracker = self.roots_tracker.read().unwrap(); - stats.roots_len = Some(roots_tracker.alive_roots.len()); - stats.roots_range = Some(roots_tracker.alive_roots.range_width()); - } - - pub fn all_alive_roots(&self) -> Vec { - let tracker = self.roots_tracker.read().unwrap(); - tracker.alive_roots.get_all() - } - - // These functions/fields are only usable from a dev context (i.e. tests and benches) - #[cfg(feature = "dev-context-only-utils")] - // filter any rooted entries and return them along with a bool that indicates - // if this account has no more entries. Note this does not update the secondary - // indexes! - pub fn purge_roots(&self, pubkey: &Pubkey) -> (SlotList, bool) { - self.slot_list_mut(pubkey, |mut slot_list| { - let reclaims = self.get_rooted_entries(&slot_list, None); - let is_empty = slot_list.retain_and_count(|(slot, _)| !self.is_alive_root(*slot)) == 0; - (reclaims, is_empty) - }) - .unwrap() - } } /// modes the system can be in @@ -1222,6 +1021,14 @@ pub(crate) mod test_utils { super::{AccountIndex, secondary::AccountSecondaryIndexes}, std::collections::HashSet, }; + pub fn program_id_index_enabled() -> AccountSecondaryIndexes { + let mut account_indexes = HashSet::new(); + account_indexes.insert(AccountIndex::ProgramId); + AccountSecondaryIndexes { + indexes: account_indexes, + keys: None, + } + } pub fn spl_token_mint_index_enabled() -> AccountSecondaryIndexes { let mut account_indexes = HashSet::new(); account_indexes.insert(AccountIndex::SplTokenMint); @@ -1292,7 +1099,7 @@ mod tests { let mut num = 0; index.scan_accounts( &ancestors, - index.max_root_inclusive(), + 0, |_pubkey, _index| num += 1, &ScanConfig::default(), ); @@ -1351,40 +1158,26 @@ mod tests { let key = solana_pubkey::new_rand(); let index = AccountsIndex::::default_for_tests(); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(0, 0, &key, true, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert!(gc.is_empty()); let ancestors = Ancestors::default(); - assert!(!index.contains_with(&key, &ancestors)); + assert!(index.contains_with(&key, &ancestors)); let mut num = 0; index.scan_accounts( &ancestors, - index.max_root_inclusive(), + 0, |_pubkey, _index| num += 1, &ScanConfig::default(), ); - assert_eq!(num, 0); + assert_eq!(num, 1); } type AccountInfoTest = f64; impl IndexValue for AccountInfoTest {} impl DiskIndexValue for AccountInfoTest {} - impl IsCached for AccountInfoTest { - fn is_cached(&self) -> bool { - true - } - } impl IsZeroLamport for AccountInfoTest { fn is_zero_lamport(&self) -> bool { @@ -1424,23 +1217,14 @@ mod tests { assert_eq!(result.count, expected_len); index.set_startup(Startup::Normal); - let mut ancestors = Ancestors::default(); - assert!(!index.contains_with(pubkey, &ancestors)); - - let mut num = 0; - index.scan_accounts( - &ancestors, - index.max_root_inclusive(), - |_pubkey, _index| num += 1, - &ScanConfig::default(), - ); - assert_eq!(num, 0); - ancestors.insert(slot); + let ancestors = Ancestors::default(); assert!(index.contains_with(pubkey, &ancestors)); assert_eq!(index.ref_count_from_storage(pubkey), 1); + + let mut num = 0; index.scan_accounts( &ancestors, - index.max_root_inclusive(), + 0, |_pubkey, _index| num += 1, &ScanConfig::default(), ); @@ -1456,23 +1240,14 @@ mod tests { assert_eq!(result.count, expected_len); index.set_startup(Startup::Normal); - let mut ancestors = Ancestors::default(); - assert!(!index.contains_with(pubkey, &ancestors)); - - let mut num = 0; - index.scan_accounts( - &ancestors, - index.max_root_inclusive(), - |_pubkey, _index| num += 1, - &ScanConfig::default(), - ); - assert_eq!(num, 0); - ancestors.insert(slot); + let ancestors = Ancestors::default(); assert!(index.contains_with(pubkey, &ancestors)); assert_eq!(index.ref_count_from_storage(pubkey), 1); + + let mut num = 0; index.scan_accounts( &ancestors, - index.max_root_inclusive(), + 0, |_pubkey, _index| num += 1, &ScanConfig::default(), ); @@ -1520,8 +1295,6 @@ mod tests { slot, slot, &pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), account_info, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT, @@ -1549,8 +1322,6 @@ mod tests { slot, slot, &pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), account_info, &mut gc, UpsertReclaim::IgnoreReclaims, @@ -1591,8 +1362,6 @@ mod tests { slot, slot, pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), true, &mut gc, UpsertReclaim::IgnoreReclaims, @@ -1615,25 +1384,6 @@ mod tests { for store_raw in [false, true] { for to_raw_first in [false, true] { let slot = 0; - // account_info type that IS cached - let account_info = AccountInfoTest::default(); - let index = AccountsIndex::default_for_tests(); - - let new_entry = get_pre_allocated( - slot, - account_info, - &index.storage.storage, - store_raw, - to_raw_first, - ) - .into_account_map_entry(&index.storage.storage); - assert_eq!(new_entry.ref_count(), 0); - assert_eq!(new_entry.slot_list_lock_read_len(), 1); - assert_eq!( - new_entry.slot_list_read_lock().to_vec(), - vec![(slot, account_info)] - ); - // account_info type that is NOT cached let account_info = true; let index = AccountsIndex::default_for_tests(); @@ -1686,17 +1436,9 @@ mod tests { fn test_new_entry_code_paths_helper( account_infos: [T; 2], - is_cached: bool, upsert_method: Option, use_disk: bool, ) { - if is_cached && upsert_method.is_none() { - // This is an illegal combination when we are using queued lazy inserts. - // Cached items don't ever leave the in-mem cache. - // But the queued lazy insert code relies on there being nothing in the in-mem cache. - return; - } - let slot0 = 0; let slot1 = 1; let key = solana_pubkey::new_rand(); @@ -1713,16 +1455,7 @@ mod tests { match upsert_method { Some(upsert_method) => { // insert first entry for pubkey. This will use new_entry_after_update and not call update. - index.upsert( - slot0, - slot0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - account_infos[0], - &mut gc, - upsert_method, - ); + index.upsert(slot0, slot0, &key, account_infos[0], &mut gc, upsert_method); } None => { let mut items = vec![(key, account_infos[0])]; @@ -1739,7 +1472,7 @@ mod tests { index.get_and_then(&key, |entry| { let entry = entry.unwrap(); let slot_list = entry.slot_list_read_lock(); - assert_eq!(entry.ref_count(), RefCount::from(!is_cached)); + assert_eq!(entry.ref_count(), 1); assert_eq!(slot_list.as_ref(), &[(slot0, account_infos[0])]); let new_entry = PreAllocatedAccountMapEntry::new( slot0, @@ -1755,16 +1488,7 @@ mod tests { match upsert_method { Some(upsert_method) => { // insert second entry for pubkey. This will use update and NOT use new_entry_after_update. - index.upsert( - slot1, - slot1, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - account_infos[1], - &mut gc, - upsert_method, - ); + index.upsert(slot1, slot1, &key, account_infos[1], &mut gc, upsert_method); } None => { // this has the effect of aging out everything in the in-mem cache @@ -1783,8 +1507,7 @@ mod tests { } // There should be reclaims if entries are uncached and old slots are being reclaimed - let should_have_reclaims = - upsert_method == Some(UpsertReclaim::ReclaimOldSlots) && !is_cached; + let should_have_reclaims = upsert_method == Some(UpsertReclaim::ReclaimOldSlots); if should_have_reclaims { assert!(!gc.is_empty()); @@ -1804,7 +1527,7 @@ mod tests { assert_eq!(entry.ref_count(), 1); assert_eq!(slot_list.as_ref(), &[(slot1, account_infos[1])],); } else { - assert_eq!(entry.ref_count(), if is_cached { 0 } else { 2 }); + assert_eq!(entry.ref_count(), 2); assert_eq!( slot_list.as_ref(), &[(slot0, account_infos[0]), (slot1, account_infos[1])], @@ -1825,102 +1548,12 @@ mod tests { #[test_matrix( [false, true], - [None, Some(UpsertReclaim::PopulateReclaims), Some(UpsertReclaim::ReclaimOldSlots)], - [true, false] + [None, Some(UpsertReclaim::PopulateReclaims), Some(UpsertReclaim::ReclaimOldSlots)] )] - fn test_new_entry_and_update_code_paths( - use_disk: bool, - upsert_method: Option, - is_cached: bool, - ) { - if is_cached { - // account_info type that IS cached - test_new_entry_code_paths_helper([1.0, 2.0], true, upsert_method, use_disk); - } else { - // account_info type that is NOT cached - test_new_entry_code_paths_helper([1, 2], false, upsert_method, use_disk); - } - } - - #[test] - fn test_insert_with_lock_no_ancestors() { - let key = solana_pubkey::new_rand(); - let index = AccountsIndex::::default_for_tests(); - let slot = 0; - let account_info = true; - - let new_entry = - PreAllocatedAccountMapEntry::new(slot, account_info, &index.storage.storage, false); - assert_eq!(0, account_maps_stats_len(&index)); - - assert_eq!(0, account_maps_stats_len(&index)); - let r_account_maps = index.get_bin(&key); - r_account_maps.upsert( - &key, - new_entry, - None, - &mut ReclaimsSlotList::default(), - UPSERT_RECLAIM_TEST_DEFAULT, - ); - assert_eq!(1, account_maps_stats_len(&index)); - - let mut ancestors = Ancestors::default(); - assert!(!index.contains_with(&key, &ancestors)); - index.get_and_then(&key, |entry| { - let (stored_slot, value) = entry.unwrap().slot_list_read_lock()[0]; - assert_eq!(stored_slot, slot); - assert_eq!(value, account_info); - (false, ()) - }); - - let mut num = 0; - index.scan_accounts( - &ancestors, - index.max_root_inclusive(), - |_pubkey, _index| num += 1, - &ScanConfig::default(), - ); - assert_eq!(num, 0); - ancestors.insert(slot); - assert!(index.contains_with(&key, &ancestors)); - index.scan_accounts( - &ancestors, - index.max_root_inclusive(), - |_pubkey, _index| num += 1, - &ScanConfig::default(), - ); - assert_eq!(num, 1); + fn test_new_entry_and_update_code_paths(use_disk: bool, upsert_method: Option) { + test_new_entry_code_paths_helper([1, 2], upsert_method, use_disk); } - #[test] - fn test_insert_wrong_ancestors() { - let key = solana_pubkey::new_rand(); - let index = AccountsIndex::::default_for_tests(); - let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); - assert!(gc.is_empty()); - - let ancestors = Ancestors::from(vec![1]); - assert!(!index.contains_with(&key, &ancestors)); - - let mut num = 0; - index.scan_accounts( - &ancestors, - index.max_root_inclusive(), - |_pubkey, _index| num += 1, - &ScanConfig::default(), - ); - assert_eq!(num, 0); - } #[test] fn test_insert_ignore_reclaims() { { @@ -1930,13 +1563,10 @@ mod tests { let mut reclaims = ReclaimsSlotList::new(); let slot = 0; let value = 1; - assert!(!value.is_cached()); index.upsert( slot, slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), value, &mut reclaims, UpsertReclaim::PopulateReclaims, @@ -1946,8 +1576,6 @@ mod tests { slot, slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), value, &mut reclaims, UpsertReclaim::PopulateReclaims, @@ -1959,53 +1587,6 @@ mod tests { slot, slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - value, - &mut reclaims, - // since IgnoreReclaims, we should expect reclaims to be empty - UpsertReclaim::IgnoreReclaims, - ); - // reclaims is ignored - assert!(reclaims.is_empty()); - } - { - // cached - let key = solana_pubkey::new_rand(); - let index = AccountsIndex::::default_for_tests(); - let mut reclaims = ReclaimsSlotList::new(); - let slot = 0; - let value = 1.0; - assert!(value.is_cached()); - index.upsert( - slot, - slot, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - value, - &mut reclaims, - UpsertReclaim::PopulateReclaims, - ); - assert!(reclaims.is_empty()); - index.upsert( - slot, - slot, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - value, - &mut reclaims, - UpsertReclaim::PopulateReclaims, - ); - // No reclaims, since the entry replaced was cached - assert!(reclaims.is_empty()); - index.upsert( - slot, - slot, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), value, &mut reclaims, // since IgnoreReclaims, we should expect reclaims to be empty @@ -2021,16 +1602,7 @@ mod tests { let key = solana_pubkey::new_rand(); let index = AccountsIndex::::default_for_tests(); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(0, 0, &key, true, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert!(gc.is_empty()); let ancestors = Ancestors::from(vec![0]); @@ -2045,7 +1617,7 @@ mod tests { let mut found_key = false; index.scan_accounts( &ancestors, - index.max_root_inclusive(), + 0, |pubkey, _index| { if pubkey == &key { found_key = true @@ -2069,8 +1641,6 @@ mod tests { root_slot, root_slot, &new_pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), true, &mut ReclaimsSlotList::new(), UPSERT_RECLAIM_TEST_DEFAULT, @@ -2086,16 +1656,12 @@ mod tests { root_slot, root_slot, &Pubkey::default(), - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), true, &mut ReclaimsSlotList::new(), UPSERT_RECLAIM_TEST_DEFAULT, ); } - index.add_root(root_slot); - (index, pubkeys) } @@ -2105,7 +1671,7 @@ mod tests { let mut scanned_keys = HashSet::new(); index.scan_accounts( &Ancestors::default(), - index.max_root_inclusive(), + 0, |pubkey, _index| { scanned_keys.insert(*pubkey); }, @@ -2123,33 +1689,15 @@ mod tests { run_test_scan_accounts(10_001) } - #[test] - fn test_is_alive_root() { - let index = AccountsIndex::::default_for_tests(); - assert!(!index.is_alive_root(0)); - index.add_root(0); - assert!(index.is_alive_root(0)); - } - #[test] fn test_insert_with_root() { let key = solana_pubkey::new_rand(); let index = AccountsIndex::::default_for_tests(); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(0, 0, &key, true, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert!(gc.is_empty()); - index.add_root(0); - let ancestors = Ancestors::from(vec![index.max_root_inclusive()]); + let ancestors = Ancestors::from(vec![0]); index .get_with_and_then(&key, &ancestors, false, |(slot, account_info)| { assert_eq!(slot, 0); @@ -2158,43 +1706,13 @@ mod tests { .unwrap(); } - #[test] - fn test_clean_first() { - let index = AccountsIndex::::default_for_tests(); - index.add_root(0); - index.add_root(1); - index.clean_dead_slot(0); - assert!(index.is_alive_root(1)); - assert!(!index.is_alive_root(0)); - } - - #[test] - fn test_clean_last() { - //this behavior might be undefined, clean up should only occur on older slots - let index = AccountsIndex::::default_for_tests(); - index.add_root(0); - index.add_root(1); - index.clean_dead_slot(1); - assert!(!index.is_alive_root(1)); - assert!(index.is_alive_root(0)); - } - #[test] fn test_update_last_wins() { let key = solana_pubkey::new_rand(); let index = AccountsIndex::::default_for_tests(); let ancestors = Ancestors::from(vec![0]); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - 1, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(0, 0, &key, 1, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert!(gc.is_empty()); index .get_with_and_then(&key, &ancestors, false, |(slot, account_info)| { @@ -2204,16 +1722,7 @@ mod tests { .unwrap(); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - 0, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(0, 0, &key, 0, &mut gc, UPSERT_RECLAIM_TEST_DEFAULT); assert_eq!(gc, ReclaimsSlotList::from([(0, 1)])); index .get_with_and_then(&key, &ancestors, false, |(slot, account_info)| { @@ -2230,27 +1739,9 @@ mod tests { let index = AccountsIndex::::default_for_tests(); let ancestors = Ancestors::from(vec![0]); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); + index.upsert(0, 0, &key, true, &mut gc, UpsertReclaim::PopulateReclaims); assert!(gc.is_empty()); - index.upsert( - 1, - 1, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - false, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); + index.upsert(1, 1, &key, false, &mut gc, UpsertReclaim::PopulateReclaims); assert!(gc.is_empty()); index .get_with_and_then(&key, &ancestors, false, |(slot, account_info)| { @@ -2272,65 +1763,18 @@ mod tests { let key = solana_pubkey::new_rand(); let index = AccountsIndex::::default_for_tests(); let mut gc = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); + let max_root = 3; + index.upsert(0, 0, &key, true, &mut gc, UpsertReclaim::PopulateReclaims); assert!(gc.is_empty()); - index.upsert( - 1, - 1, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - false, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); - index.upsert( - 2, - 2, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); - index.upsert( - 3, - 3, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); - index.add_root(0); - index.add_root(1); - index.add_root(3); - index.upsert( - 4, - 4, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - true, - &mut gc, - UpsertReclaim::PopulateReclaims, - ); + index.upsert(1, 1, &key, false, &mut gc, UpsertReclaim::PopulateReclaims); + index.upsert(2, 2, &key, true, &mut gc, UpsertReclaim::PopulateReclaims); + index.upsert(3, 3, &key, true, &mut gc, UpsertReclaim::PopulateReclaims); + index.upsert(4, 4, &key, true, &mut gc, UpsertReclaim::PopulateReclaims); // Updating index should not purge older roots, only purges // previous updates within the same slot assert_eq!(gc, ReclaimsSlotList::new()); - let ancestors = Ancestors::from(vec![index.max_root_inclusive()]); + let ancestors = Ancestors::from(vec![max_root]); index .get_with_and_then(&key, &ancestors, false, |(slot, account_info)| { assert_eq!(slot, 3); @@ -2342,7 +1786,7 @@ mod tests { let mut found_key = false; index.scan_accounts( &Ancestors::default(), - index.max_root_inclusive(), + max_root, |pubkey, index| { if pubkey == &key { found_key = true; @@ -2359,32 +1803,10 @@ mod tests { #[test] fn test_upsert_reclaims() { let key = solana_pubkey::new_rand(); - let index = - AccountsIndex::::default_for_tests(); + let index = AccountsIndex::::default_for_tests(); let mut reclaims = ReclaimsSlotList::new(); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - CacheableIndexValueTest(true), - &mut reclaims, - UPSERT_RECLAIM_TEST_DEFAULT, - ); - // No reclaims should be returned on the first item - assert!(reclaims.is_empty()); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - CacheableIndexValueTest(false), - &mut reclaims, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(0, 0, &key, 0, &mut reclaims, UPSERT_RECLAIM_TEST_DEFAULT); // Cached item should not be reclaimed assert!(reclaims.is_empty()); @@ -2394,16 +1816,7 @@ mod tests { }); assert_eq!(slot_list_len, 1); - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - CacheableIndexValueTest(false), - &mut reclaims, - UPSERT_RECLAIM_TEST_DEFAULT, - ); + index.upsert(0, 0, &key, 0, &mut reclaims, UPSERT_RECLAIM_TEST_DEFAULT); // Uncached item should be returned as reclaim assert!(!reclaims.is_empty()); @@ -2415,59 +1828,77 @@ mod tests { assert_eq!(slot_list_len, 1); } - fn account_maps_stats_len(index: &AccountsIndex) -> usize { - index.storage.storage.stats.total_count() - } - #[test] - fn test_purge() { + fn test_replace_same_slot() { + // When new_slot == old_slot, replace acts as an in-place update of the account_info. let key = solana_pubkey::new_rand(); let index = AccountsIndex::::default_for_tests(); let mut gc = ReclaimsSlotList::new(); - assert_eq!(0, account_maps_stats_len(&index)); - index.upsert( - 1, - 1, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - 12, - &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, - ); - assert_eq!(1, account_maps_stats_len(&index)); + let slot = 5; index.upsert( - 1, - 1, + slot, + slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - 10, + 100, &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, + UpsertReclaim::IgnoreReclaims, ); - assert_eq!(1, account_maps_stats_len(&index)); + assert_eq!(index.ref_count_from_storage(&key), 1); + + let account_info = 200; + + index.replace(slot, slot, &key, account_info); - let purges = index.purge_roots(&key); - assert_eq!(purges, (SlotList::new(), false)); - index.add_root(1); + // Slot list now holds the new account_info at the same slot. + let slot_list = index.get_and_then(&key, |entry| { + (false, entry.unwrap().slot_list_read_lock().clone_list()) + }); + assert_eq!(slot_list, SlotList::from([(slot, account_info)])); + // Replace doesn't change refcounts. + assert_eq!(index.ref_count_from_storage(&key), 1); + } - let purges = index.purge_roots(&key); - assert_eq!(purges, (SlotList::from([(1, 10)]), true)); + #[test] + fn test_replace_moves_entry_to_new_slot() { + // Replace finds the entry at old_slot, swaps it out for one at new_slot. + let key = solana_pubkey::new_rand(); + let index = AccountsIndex::::default_for_tests(); + let mut gc = ReclaimsSlotList::new(); - assert_eq!(1, account_maps_stats_len(&index)); + let old_slot = 5; + let new_slot = 10; + let account_info = 200; index.upsert( - 1, - 1, + old_slot, + old_slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - 9, + 100, &mut gc, - UPSERT_RECLAIM_TEST_DEFAULT, + UpsertReclaim::IgnoreReclaims, ); - assert_eq!(1, account_maps_stats_len(&index)); + assert_eq!(index.ref_count_from_storage(&key), 1); + + index.replace(new_slot, old_slot, &key, account_info); + + let slot_list = index.get_and_then(&key, |entry| { + (false, entry.unwrap().slot_list_read_lock().clone_list()) + }); + assert_eq!(slot_list, SlotList::from([(new_slot, account_info)])); + // Moving an entry between slots must not change the ref count. + assert_eq!(index.ref_count_from_storage(&key), 1); + } + + #[test] + #[should_panic(expected = "Expected to find a slot to replace in the slot list")] + fn test_replace_missing_old_slot_panics() { + let key = solana_pubkey::new_rand(); + let index = AccountsIndex::::default_for_tests(); + let mut gc = ReclaimsSlotList::new(); + + index.upsert(5, 5, &key, 100, &mut gc, UpsertReclaim::IgnoreReclaims); + // No entry at slot 99 — replace must panic rather than silently appending. + index.replace(10, 99, &key, 200); } #[test] @@ -2475,18 +1906,14 @@ mod tests { let slot_slice = vec![(0, true), (5, true), (3, true), (7, true)]; let index = AccountsIndex::::default_for_tests(); - // No ancestors, no root, should return None - assert!(index.latest_slot(None, &slot_slice, None).is_none()); + // No ancestors: every slot is a root, so return the newest slot (7) + assert_eq!(index.latest_slot(None, &slot_slice, None).unwrap(), 3); - // Given a root, should return the root - index.add_root(5); - assert_eq!(index.latest_slot(None, &slot_slice, None).unwrap(), 1); - - // Given a max_root == root, should still return the root + // Given a max_root, should return the newest slot <= max_root (5) assert_eq!(index.latest_slot(None, &slot_slice, Some(5)).unwrap(), 1); - // Given a max_root < root, should filter out the root - assert!(index.latest_slot(None, &slot_slice, Some(4)).is_none()); + // Given a max_root between slots, should return the newest slot <= max_root (3) + assert_eq!(index.latest_slot(None, &slot_slice, Some(4)).unwrap(), 2); // Given a max_root, should filter out roots < max_root, but specified // ancestors should not be affected @@ -2552,7 +1979,13 @@ mod tests { *slot, *slot, &account_key, - // Make sure these accounts are added to secondary index + true, + &mut ReclaimsSlotList::new(), + UPSERT_RECLAIM_TEST_DEFAULT, + ); + // Make sure these accounts are added to secondary index + index.update_secondary_indexes( + &account_key, &AccountSharedData::create_from_existing_shared_data( 0, Arc::new(account_data.to_vec()), @@ -2561,9 +1994,6 @@ mod tests { 0, ), secondary_indexes, - true, - &mut ReclaimsSlotList::new(), - UPSERT_RECLAIM_TEST_DEFAULT, ); } @@ -2610,8 +2040,6 @@ mod tests { slot, slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), slot, &mut gc, UpsertReclaim::IgnoreReclaims, @@ -2627,8 +2055,6 @@ mod tests { reclaim_slot + 1, reclaim_slot + 1, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), account_value + 1, &mut gc, UpsertReclaim::IgnoreReclaims, @@ -2643,8 +2069,6 @@ mod tests { reclaim_slot, reclaim_slot, &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), account_value, &mut gc, UpsertReclaim::ReclaimOldSlots, @@ -2676,93 +2100,6 @@ mod tests { .unwrap(); } - #[test] - fn test_reclaim_do_not_reclaim_cached_other_slot() { - agave_logger::setup(); - let key = solana_pubkey::new_rand(); - let index = - AccountsIndex::::default_for_tests(); - let mut gc = ReclaimsSlotList::new(); - - // Insert an uncached account at slot 0 and an cached account at slot 1 - index.upsert( - 0, - 0, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - CacheableIndexValueTest(false), - &mut gc, - UpsertReclaim::IgnoreReclaims, - ); - - index.upsert( - 1, - 1, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - CacheableIndexValueTest(true), - &mut gc, - UpsertReclaim::IgnoreReclaims, - ); - - // Now insert a cached account at slot 2 - index.upsert( - 2, - 2, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - CacheableIndexValueTest(true), - &mut gc, - UpsertReclaim::IgnoreReclaims, - ); - - // Replace the cached account at slot 2 with a uncached account - index.upsert( - 2, - 2, - &key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - CacheableIndexValueTest(false), - &mut gc, - UpsertReclaim::ReclaimOldSlots, - ); - - // Verify that the slot list is length two and consists of the cached account at slot 1 - // and the uncached account at slot 2 - index.get_and_then(&key, |entry| { - let entry = entry.unwrap(); - assert_eq!(entry.slot_list_lock_read_len(), 2); - assert_eq!( - entry.slot_list_read_lock()[0], - PreAllocatedAccountMapEntry::new( - 1, - CacheableIndexValueTest(true), - &index.storage.storage, - false - ) - .into() - ); - assert_eq!( - entry.slot_list_read_lock()[1], - PreAllocatedAccountMapEntry::new( - 2, - CacheableIndexValueTest(false), - &index.storage.storage, - false - ) - .into() - ); - // Verify that the uncached account at slot 0 was reclaimed - assert_eq!(gc.len(), 1); - assert_eq!(gc[0], (0, CacheableIndexValueTest(false))); - (false, ()) - }); - } - #[test] fn test_purge_exact_spl_token_mint_secondary_index() { let (key_start, key_end, secondary_indexes) = create_spl_token_mint_secondary_index_state(); @@ -2792,7 +2129,6 @@ mod tests { #[test] fn test_purge_older_root_entries() { - // No roots, should be no reclaims let index = AccountsIndex::::default_for_tests(); let entry = AccountMapEntry::new( SlotList::from_iter([(1, true), (2, true), (5, true), (9, true)]), @@ -2801,35 +2137,14 @@ mod tests { ); let mut slot_list = entry.slot_list_write_lock(); let mut reclaims = ReclaimsSlotList::new(); - assert!(!index.purge_older_root_entries(&mut slot_list, &mut reclaims, None)); - assert!(reclaims.is_empty()); - assert_eq!( - slot_list.clone_list(), - SlotList::from_iter([(1, true), (2, true), (5, true), (9, true)]) - ); - // Add a later root, earlier slots should be reclaimed - slot_list.assign([(1, true), (2, true), (5, true), (9, true)]); - index.add_root(1); - // Note 2 is not a root - index.add_root(5); - reclaims = ReclaimsSlotList::new(); + // No max clean root: keep the newest slot (9), reclaim everything older. assert!(!index.purge_older_root_entries(&mut slot_list, &mut reclaims, None)); - assert_eq!(reclaims, ReclaimsSlotList::from([(1, true), (2, true)])); assert_eq!( - slot_list.clone_list(), - SlotList::from_iter([(5, true), (9, true)]) - ); - // Add a later root that is not in the list, should not affect the outcome - slot_list.assign([(1 as Slot, true), (2, true), (5, true), (9, true)]); - index.add_root(6); - reclaims = ReclaimsSlotList::new(); - assert!(!index.purge_older_root_entries(&mut slot_list, &mut reclaims, None)); - assert_eq!(reclaims, ReclaimsSlotList::from([(1, true), (2, true)])); - assert_eq!( - slot_list.clone_list(), - SlotList::from_iter([(5, true), (9, true)]) + reclaims, + ReclaimsSlotList::from([(1, true), (2, true), (5, true)]) ); + assert_eq!(slot_list.clone_list(), SlotList::from_iter([(9, true)])); // Pass a max root >= than any root in the slot list, should not affect // outcome @@ -2852,19 +2167,18 @@ mod tests { SlotList::from_iter([(5, true), (9, true)]) ); - // Pass a max root 2. This means the latest root < 2 is 1 because 2 is not a root - // so nothing will be purged + // Max clean root 2: newest slot <= 2 is 2, so only slot 1 is older and reclaimed. slot_list.assign([(1, true), (2, true), (5, true), (9, true)]); reclaims = ReclaimsSlotList::new(); assert!(!index.purge_older_root_entries(&mut slot_list, &mut reclaims, Some(2))); - assert!(reclaims.is_empty()); + assert_eq!(reclaims, ReclaimsSlotList::from([(1, true)])); assert_eq!( slot_list.clone_list(), - SlotList::from_iter([(1, true), (2, true), (5, true), (9, true)]) + SlotList::from_iter([(2, true), (5, true), (9, true)]) ); - // Pass a max root 1. This means the latest root < 3 is 1 because 2 is not a root - // so nothing will be purged + // Max clean root 1: newest slot <= 1 is 1 and nothing is older, so nothing + // is reclaimed. slot_list.assign([(1, true), (2, true), (5, true), (9, true)]); reclaims = ReclaimsSlotList::new(); assert!(!index.purge_older_root_entries(&mut slot_list, &mut reclaims, Some(1))); @@ -2929,6 +2243,12 @@ mod tests { index.upsert( 0, 0, + &account_key, + true, + &mut ReclaimsSlotList::new(), + UPSERT_RECLAIM_TEST_DEFAULT, + ); + index.update_secondary_indexes( &account_key, &AccountSharedData::create_from_existing_shared_data( 0, @@ -2938,9 +2258,6 @@ mod tests { 0, ), &secondary_indexes, - true, - &mut ReclaimsSlotList::new(), - UPSERT_RECLAIM_TEST_DEFAULT, ); assert!(secondary_index.index.is_empty()); assert!(secondary_index.reverse_index.is_empty()); @@ -2949,6 +2266,12 @@ mod tests { index.upsert( 0, 0, + &account_key, + true, + &mut ReclaimsSlotList::new(), + UPSERT_RECLAIM_TEST_DEFAULT, + ); + index.update_secondary_indexes( &account_key, &AccountSharedData::create_from_existing_shared_data( 0, @@ -2958,9 +2281,6 @@ mod tests { 0, ), &secondary_indexes, - true, - &mut ReclaimsSlotList::new(), - UPSERT_RECLAIM_TEST_DEFAULT, ); assert!(secondary_index.index.is_empty()); assert!(secondary_index.reverse_index.is_empty()); @@ -3098,6 +2418,12 @@ mod tests { index.upsert( slot, slot, + &account_key, + true, + &mut ReclaimsSlotList::new(), + UPSERT_RECLAIM_TEST_DEFAULT, + ); + index.update_secondary_indexes( &account_key, &AccountSharedData::create_from_existing_shared_data( 0, @@ -3107,15 +2433,18 @@ mod tests { 0, ), secondary_indexes, - true, - &mut ReclaimsSlotList::new(), - UPSERT_RECLAIM_TEST_DEFAULT, ); // Now write a different mint index for the same account index.upsert( slot, slot, + &account_key, + true, + &mut ReclaimsSlotList::new(), + UPSERT_RECLAIM_TEST_DEFAULT, + ); + index.update_secondary_indexes( &account_key, &AccountSharedData::create_from_existing_shared_data( 0, @@ -3125,9 +2454,6 @@ mod tests { 0, ), secondary_indexes, - true, - &mut ReclaimsSlotList::new(), - UPSERT_RECLAIM_TEST_DEFAULT, ); // Both pubkeys will now be present in the index @@ -3142,6 +2468,12 @@ mod tests { index.upsert( later_slot, later_slot, + &account_key, + true, + &mut ReclaimsSlotList::new(), + UPSERT_RECLAIM_TEST_DEFAULT, + ); + index.update_secondary_indexes( &account_key, &AccountSharedData::create_from_existing_shared_data( 0, @@ -3151,16 +2483,12 @@ mod tests { 0, ), secondary_indexes, - true, - &mut ReclaimsSlotList::new(), - UPSERT_RECLAIM_TEST_DEFAULT, ); assert_eq!(secondary_index.get(&secondary_key1), vec![account_key]); // If we set a root at `later_slot`, and clean, then even though the account with secondary_key1 // was outdated by the update in the later slot, the primary account key is still alive, // so both secondary keys will still be kept alive. - index.add_root(later_slot); index.slot_list_mut(&account_key, |mut slot_list| { index.purge_older_root_entries(&mut slot_list, &mut ReclaimsSlotList::new(), None) }); @@ -3216,16 +2544,7 @@ mod tests { impl IndexValue for u64 {} impl DiskIndexValue for bool {} impl DiskIndexValue for u64 {} - impl IsCached for bool { - fn is_cached(&self) -> bool { - false - } - } - impl IsCached for u64 { - fn is_cached(&self) -> bool { - false - } - } + impl IsZeroLamport for bool { fn is_zero_lamport(&self) -> bool { false @@ -3238,117 +2557,15 @@ mod tests { } } - /// Type that supports caching for tests. Used to test upsert behaviour - /// when the slot list has mixed cached and uncached items. - #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] - struct CacheableIndexValueTest(bool); - impl IndexValue for CacheableIndexValueTest {} - impl DiskIndexValue for CacheableIndexValueTest {} - impl IsCached for CacheableIndexValueTest { - fn is_cached(&self) -> bool { - // Return self value as whether the item is cached or not - self.0 - } - } - impl IsZeroLamport for CacheableIndexValueTest { - fn is_zero_lamport(&self) -> bool { - false - } - } - - #[test] - fn test_get_newest_root_in_slot_list() { - let index = AccountsIndex::::default_for_tests(); - let return_0 = 0; - let slot1 = 1; - let slot2 = 2; - let slot99 = 99; - - // no roots, so always 0 - { - let roots_tracker = &index.roots_tracker.read().unwrap(); - let slot_list = Vec::<(Slot, bool)>::default(); - assert_eq!( - return_0, - AccountsIndex::::get_newest_root_in_slot_list( - &roots_tracker.alive_roots, - &slot_list, - Some(slot1), - ) - ); - assert_eq!( - return_0, - AccountsIndex::::get_newest_root_in_slot_list( - &roots_tracker.alive_roots, - &slot_list, - Some(slot2), - ) - ); - assert_eq!( - return_0, - AccountsIndex::::get_newest_root_in_slot_list( - &roots_tracker.alive_roots, - &slot_list, - Some(slot99), - ) - ); - } - - index.add_root(slot2); - - { - let roots_tracker = &index.roots_tracker.read().unwrap(); - let slot_list = vec![(slot2, true)]; - assert_eq!( - slot2, - AccountsIndex::::get_newest_root_in_slot_list( - &roots_tracker.alive_roots, - &slot_list, - Some(slot2), - ) - ); - // no newest root - assert_eq!( - return_0, - AccountsIndex::::get_newest_root_in_slot_list( - &roots_tracker.alive_roots, - &slot_list, - Some(slot1), - ) - ); - assert_eq!( - slot2, - AccountsIndex::::get_newest_root_in_slot_list( - &roots_tracker.alive_roots, - &slot_list, - Some(slot99), - ) - ); - } - } - impl AccountsIndex { fn upsert_simple_test(&self, key: &Pubkey, slot: Slot, value: T) { let mut gc = ReclaimsSlotList::new(); // It is invalid to reclaim older slots if the slot being upserted // is unrooted - let reclaim_method = if self.is_alive_root(slot) { - UPSERT_RECLAIM_TEST_DEFAULT - } else { - UpsertReclaim::IgnoreReclaims - }; + let reclaim_method = UpsertReclaim::IgnoreReclaims; - self.upsert( - slot, - slot, - key, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), - value, - &mut gc, - reclaim_method, - ); + self.upsert(slot, slot, key, value, &mut gc, reclaim_method); assert!(gc.is_empty()); } } @@ -3404,6 +2621,7 @@ mod tests { let key_unknown = solana_pubkey::new_rand(); let index = AccountsIndex::::default_for_tests(); let slot1 = 1; + let slot2 = 2; let mut gc = ReclaimsSlotList::new(); // return true if we don't know anything about 'key_unknown' @@ -3412,23 +2630,6 @@ mod tests { index.upsert_simple_test(&key, slot1, value); - let slot2 = 2; - // none for max root because we don't want to delete the entry yet - assert!(!index.clean_rooted_entries(&key, &mut gc, None)); - // this is because of inclusive vs exclusive in the call to can_purge_older_entries - assert!(!index.clean_rooted_entries(&key, &mut gc, Some(slot1))); - // this will delete the entry because it is <= max_root_inclusive and NOT a root - // note this has to be slot2 because of inclusive vs exclusive in the call to can_purge_older_entries - { - let mut gc = ReclaimsSlotList::new(); - assert!(index.clean_rooted_entries(&key, &mut gc, Some(slot2))); - assert_eq!(gc, ReclaimsSlotList::from([(slot1, value)])); - } - - // re-add it - index.upsert_simple_test(&key, slot1, value); - - index.add_root(slot1); assert!(!index.clean_rooted_entries(&key, &mut gc, Some(slot2))); index.upsert_simple_test(&key, slot2, value); @@ -3439,7 +2640,7 @@ mod tests { assert_eq!(&[(slot1, value), (slot2, value)], slot_list.as_ref()); (false, ()) }); - assert!(!index.clean_rooted_entries(&key, &mut gc, Some(slot2))); + assert!(!index.clean_rooted_entries(&key, &mut gc, Some(slot1))); assert_eq!( 2, index.get_and_then(&key, |entry| ( @@ -3447,51 +2648,10 @@ mod tests { entry.unwrap().slot_list_lock_read_len() )) ); - assert!(gc.is_empty()); - { - { - let roots_tracker = &index.roots_tracker.read().unwrap(); - let slot_list = SlotList::from([(slot2, value)]); - assert_eq!( - 0, - AccountsIndex::::get_newest_root_in_slot_list( - &roots_tracker.alive_roots, - &slot_list, - None, - ) - ); - } - index.add_root(slot2); - { - let roots_tracker = &index.roots_tracker.read().unwrap(); - let slot_list = SlotList::from([(slot2, value)]); - assert_eq!( - slot2, - AccountsIndex::::get_newest_root_in_slot_list( - &roots_tracker.alive_roots, - &slot_list, - None, - ) - ); - assert_eq!( - 0, - AccountsIndex::::get_newest_root_in_slot_list( - &roots_tracker.alive_roots, - &slot_list, - Some(0), - ) - ); - } - } assert!(gc.is_empty()); assert!(!index.clean_rooted_entries(&key, &mut gc, Some(slot2))); assert_eq!(gc, ReclaimsSlotList::from([(slot1, value)])); - gc.clear(); - index.clean_dead_slot(slot2); - let slot3 = 3; - assert!(index.clean_rooted_entries(&key, &mut gc, Some(slot3))); - assert_eq!(gc, ReclaimsSlotList::from([(slot2, value)])); } #[test] diff --git a/accounts-db/src/accounts_index/account_map_entry.rs b/accounts-db/src/accounts_index/account_map_entry.rs index b15773ab96b..b7b2275d204 100644 --- a/accounts-db/src/accounts_index/account_map_entry.rs +++ b/accounts-db/src/accounts_index/account_map_entry.rs @@ -296,9 +296,8 @@ impl PreAllocatedAccountMapEntry { account_info: T, storage: &BucketMapHolder, ) -> Box> { - let is_cached = account_info.is_cached(); - let ref_count = RefCount::from(!is_cached); - let meta = AccountMapEntryMeta::new_dirty(storage, is_cached); + let ref_count = 1; + let meta = AccountMapEntryMeta::new_dirty(storage, false); Box::new(AccountMapEntry::new( SlotList::from([(slot, account_info)]), ref_count, diff --git a/accounts-db/src/accounts_index/bucket_map_holder.rs b/accounts-db/src/accounts_index/bucket_map_holder.rs index b47932e1f82..6b253c7d991 100644 --- a/accounts-db/src/accounts_index/bucket_map_holder.rs +++ b/accounts-db/src/accounts_index/bucket_map_holder.rs @@ -93,6 +93,10 @@ pub struct BucketMapHolder + Into> /// Precomputed thresholds per bin for flushing and eviction /// None for Minimal/InMemOnly, Some(threshold_entries_per_bin) for Threshold pub(super) threshold_entries_per_bin: Option, + + /// If true, flush dirty entries to disk once `slot_list.len() == 1` and + /// `ref_count == 1`, making it evictable + should_write_through: bool, } impl + Into> Debug for BucketMapHolder { @@ -108,6 +112,12 @@ impl + Into> BucketMapHolder self.disk.is_some() } + /// If true, flush dirty entries to disk once `slot_list.len() == 1` and + /// `ref_count == 1`, making it evictable + pub(crate) fn should_write_through(&self) -> bool { + self.should_write_through + } + /// Returns true when a bin's entry count is high enough that eviction should begin. /// The threshold is the configured `high_water_mark`. pub fn should_evict_based_on_count(&self, count: usize) -> bool { @@ -315,6 +325,13 @@ impl + Into> BucketMapHolder } }; + // Write through is currently only used with a memory threshold, to ensure + // the memory threshold is not exceeded + let should_write_through = match config.index_limit { + IndexLimit::InMemOnly | IndexLimit::Minimal => false, + IndexLimit::Threshold(_) => true, + }; + // The age interval, `age_ms`, varies depending on `config.index_limit` let age_ms = match config.index_limit { IndexLimit::Minimal => { @@ -359,6 +376,7 @@ impl + Into> BucketMapHolder _phantom: PhantomData, startup_stats: Arc::default(), threshold_entries_per_bin, + should_write_through, } } diff --git a/accounts-db/src/accounts_index/in_mem_accounts_index.rs b/accounts-db/src/accounts_index/in_mem_accounts_index.rs index 38ed952a23f..4fd31173609 100644 --- a/accounts-db/src/accounts_index/in_mem_accounts_index.rs +++ b/accounts-db/src/accounts_index/in_mem_accounts_index.rs @@ -60,7 +60,8 @@ pub struct InMemAccountsIndex + Into< /// stats related to starting up pub(crate) startup_stats: Arc, - /// Whether to write through to disk on upsert (true when threshold-based bin management is active) + /// If true, flush dirty entries to disk once `slot_list.len() == 1` and + /// `ref_count == 1`, making it evictable should_write_through: bool, } @@ -147,8 +148,7 @@ impl + Into> InMemAccountsIndex + Into> InMemAccountsIndex( &self, @@ -481,6 +481,27 @@ impl + Into> InMemAccountsIndex Some(*info), + _ => None, + } + }) + }); + if let Some((slot, info)) = to_write { + self.write_through(pubkey, slot, info); + } + } + /// Clean the slot list by removing all slot_list items older than the max_slot. /// Decrease the reference count of the entry by the number of removed accounts. /// Note: This must only be called on startup, and reclaims must be reclaimed. @@ -500,7 +521,6 @@ impl + Into> InMemAccountsIndex + Into> InMemAccountsIndex, new_value: SlotListItem) { - let mut slot_list = current.slot_list_write_lock(); - let (slot, new_entry) = new_value; - // Find and replace existing entry at this slot, or append if not found - if let Some(existing_entry) = slot_list.iter_mut().find(|(s, _)| *s == slot) { - existing_entry.1 = new_entry; - } else { - slot_list.push((slot, new_entry)); - } - current.mark_dirty(); - } - pub fn upsert( &self, pubkey: &Pubkey, @@ -547,27 +553,51 @@ impl + Into> InMemAccountsIndex 1); + }); + } + + /// Replaces the slot list entry at `old_slot` with `new_item`. + /// + /// Panics if `old_slot` is not present in the slot list, or if more than one entry at + /// `old_slot` is found (which would indicate prior corruption). + pub fn replace(&self, pubkey: &Pubkey, new_item: SlotListItem, old_slot: Slot) { let mut should_write_through = false; self.get_or_create_index_entry_for_pubkey(pubkey, |entry| { - if is_cached { - Self::cache_entry_at_slot(entry, (slot, account_info)); - self.set_age_to_future(entry, true); - } else { - let slot_list_length = Self::lock_and_update_slot_list( - entry, - (slot, account_info), - other_slot, - reclaims, - reclaim, - ); - should_write_through = - self.should_write_through && slot_list_length == 1 && entry.ref_count() == 1; - self.set_age_to_future(entry, slot_list_length > 1); - } + let mut slot_list = entry.slot_list_write_lock(); + let mut found_slot = false; + let slot_list_length = slot_list.retain_and_count(|cur_item| { + if cur_item.0 == old_slot { + assert!( + !found_slot, + "duplicate entry at slot {old_slot} in slot_list" + ); + found_slot = true; + *cur_item = new_item; + } + true + }); + assert!( + found_slot, + "Expected to find a slot to replace in the slot list" + ); + entry.mark_dirty(); + + should_write_through = + self.should_write_through && slot_list_length == 1 && entry.ref_count() == 1; }); if should_write_through { + let (slot, account_info) = new_item; self.write_through(pubkey, slot, account_info); } } @@ -735,33 +765,21 @@ impl + Into> InMemAccountsIndex (i32, usize) { let mut ref_count_change = 1; - // Cached accounts are not expected by this function, use cache_entry_at_slot instead - assert!(!account_info.is_cached()); - let old_slot = other_slot.unwrap_or(slot); // If we find an existing account at old_slot, replace it rather than adding a new entry to the list let mut found_slot = false; let mut final_len = slot_list.retain_and_count(|cur_item| { - let (cur_slot, cur_account_info) = cur_item; + let (cur_slot, _) = cur_item; if *cur_slot == old_slot { // Ensure we only find one! assert!(!found_slot); - let is_cur_account_cached = cur_account_info.is_cached(); // Replace the item let reclaim_item = mem::replace(cur_item, (slot, account_info)); match reclaim { UpsertReclaim::ReclaimOldSlots | UpsertReclaim::PopulateReclaims => { - // Reclaims are used to reclaim other versions of accounts when they are - // rewritten elsewhere. Cached accounts are not in storage, so there is - // no reason to store the reclaim. - if !is_cur_account_cached { - reclaims.push(reclaim_item); - } - } - UpsertReclaim::PreviousSlotEntryWasCached => { - assert!(is_cur_account_cached); + reclaims.push(reclaim_item); } UpsertReclaim::IgnoreReclaims => { // do nothing. nothing to assert. nothing to return in reclaims @@ -770,13 +788,9 @@ impl + Into> InMemAccountsIndex + Into> InMemAccountsIndex + Into> InMemAccountsIndex + Into> InMemAccountsIndex { flush_stats.num_not_flushed_slot_list_len += 1 } - ReasonToNotFlush::SlotListCached => { - flush_stats.num_not_flushed_slot_list_cached += 1 - } } continue; } @@ -1611,8 +1616,6 @@ struct DiskFlushStats { num_not_flushed_ref_count: u64, /// Number of entries not flushed because slot list len != 1 num_not_flushed_slot_list_len: u64, - /// Number of entries not flushed because slot list contained a cached entry - num_not_flushed_slot_list_cached: u64, } impl DiskFlushStats { @@ -1636,10 +1639,6 @@ impl DiskFlushStats { &stats.held_in_mem.slot_list_len, self.num_not_flushed_slot_list_len, ); - Self::update_stat( - &stats.held_in_mem.slot_list_cached, - self.num_not_flushed_slot_list_cached, - ); } fn update_stat(stat: &AtomicU64, value: u64) { @@ -1713,10 +1712,6 @@ enum ReasonToNotFlush { /// slot list len was != 1 /// This account has versions in multiple locations, and will be cleaned/shrunk soon. SlotListLen, - /// slot list contained an item pointing to a cached account - /// An account in the write cache will be flushed soon, so do not flush this index entry yet, - /// as it will be modified soon. - SlotListCached, } #[cfg(test)] @@ -1796,7 +1791,13 @@ mod tests { assert_eq!(entry.slot_list_lock_read_len(), 0); assert_eq!(entry.ref_count(), 0); assert!(entry.dirty()); - InMemAccountsIndex::::cache_entry_at_slot(entry, (slot, 0)); + InMemAccountsIndex::::lock_and_update_slot_list( + entry, + (slot, 0), + None, + &mut ReclaimsSlotList::new(), + UpsertReclaim::IgnoreReclaims, + ); callback_called = true; }); @@ -1865,7 +1866,13 @@ mod tests { assert_eq!(entry.slot_list_lock_read_len(), 1); assert_eq!(entry.ref_count(), 1); assert!(!entry.dirty()); // Entry loaded from disk should not be dirty - InMemAccountsIndex::::cache_entry_at_slot(entry, (slot, 0)); + InMemAccountsIndex::::lock_and_update_slot_list( + entry, + (slot, 0), + None, + &mut ReclaimsSlotList::new(), + UpsertReclaim::IgnoreReclaims, + ); callback_called = true; }); @@ -2505,23 +2512,6 @@ mod tests { ShouldFlush::No(ReasonToNotFlush::SlotListLen), ); } - - // test: do not flush due to slot list cached - { - let bucket = new_for_test::(); - let entry = AccountMapEntry::new( - SlotList::from_iter([(0, 0f64)]), // <-- f64 acts as cached - /*ref count*/ 1, - AccountMapEntryMeta::default(), - ); - entry.mark_dirty(); - - let entry_for_flush = bucket.try_make_entry_for_flush(&entry, 0, 0); - assert_eq!( - entry_for_flush, - ShouldFlush::No(ReasonToNotFlush::SlotListCached), - ); - } } #[test] @@ -2970,7 +2960,7 @@ mod tests { ); } - /// `slot_list_mut` write-through fires only for single-slot, ref_count=1 entries. + /// `slot_list_mut` write-through fires only for `slot_list.len() == 1`, `ref_count == 1` entries. #[test_case(SlotList::from([(1, 0)]), 1, true ; "writes_through")] #[test_case(SlotList::from(vec![(1, 10), (2, 20)]), 1, false ; "multi_slot")] #[test_case(SlotList::from([(1, 0)]), 2, false ; "multi_ref")] @@ -3003,9 +2993,9 @@ mod tests { ); } - /// `upsert` with `should_write_through=true` writes through to disk and clears the dirty flag. + /// `upsert` then `try_write_through` clears the dirty flag. #[test] - fn test_upsert_write_through_clears_dirty() { + fn test_try_write_through_clears_dirty() { let index = new_should_write_through_for_test(None); let pubkey = solana_pubkey::new_rand(); let slot = 1; @@ -3021,6 +3011,7 @@ mod tests { &mut ReclaimsSlotList::new(), UpsertReclaim::IgnoreReclaims, ); + index.try_write_through(&pubkey); index.get_only_in_mem(&pubkey, false, |entry| { let entry = entry.expect("entry should be in memory"); @@ -3034,6 +3025,66 @@ mod tests { assert_eq!(ref_count, 1); } + /// `try_write_through` must leave a multi-ref entry alone: if the pubkey still has more + /// than one slot-list entry (ref_count > 1), persisting just one of them to disk would let a + /// later eviction drop the fresher in-mem entry in favor of an incomplete disk entry. The + /// entry must stay dirty and nothing must be written to disk. + #[test] + fn test_try_write_through_skips_multi_ref_entry() { + let index = new_should_write_through_for_test(None); + let pubkey = solana_pubkey::new_rand(); + let info = 10; + + assert!(index.load_from_disk(&pubkey).is_none(), "not on disk yet"); + + // Upsert the same pubkey at two different (uncached) slots so its slot list has two + // entries and ref_count == 2 + for slot in [1, 2] { + let new_value = PreAllocatedAccountMapEntry::new(slot, info, &index.storage, true); + index.upsert( + &pubkey, + new_value, + None, + &mut ReclaimsSlotList::new(), + UpsertReclaim::IgnoreReclaims, + ); + } + index.get_only_in_mem(&pubkey, false, |entry| { + let entry = entry.expect("entry should be in memory"); + assert_eq!( + entry.slot_list_read_lock().len(), + 2, + "two-version slot list" + ); + assert_eq!(entry.ref_count(), 2); + assert!(entry.dirty(), "dirty before write-through attempt"); + }); + + let immediate_writes_before = index + .stats() + .flush_entries_updated_on_disk_immediate + .load(Ordering::Relaxed); + + index.try_write_through(&pubkey); + + index.get_only_in_mem(&pubkey, false, |entry| { + let entry = entry.expect("entry should still be in memory"); + assert!(entry.dirty(), "multi-version entry must stay dirty"); + }); + assert!( + index.load_from_disk(&pubkey).is_none(), + "multi-version entry must not be written through to disk" + ); + assert_eq!( + index + .stats() + .flush_entries_updated_on_disk_immediate + .load(Ordering::Relaxed), + immediate_writes_before, + "no immediate disk write should have fired" + ); + } + /// When the bin exceeds the threshold and a new pubkey is inserted in `should_write_through` /// mode, one clean entry should be evicted inline to make room. #[test] @@ -3054,6 +3105,7 @@ mod tests { &mut ReclaimsSlotList::new(), UpsertReclaim::IgnoreReclaims, ); + index.try_write_through(pubkey); } assert_eq!(index.map_internal.read().unwrap().len(), 3); diff --git a/accounts-db/src/accounts_index/iter.rs b/accounts-db/src/accounts_index/iter.rs index 95135db58fa..200cedf26af 100644 --- a/accounts-db/src/accounts_index/iter.rs +++ b/accounts-db/src/accounts_index/iter.rs @@ -43,12 +43,8 @@ impl + Into> Iterator #[cfg(test)] mod tests { use { - super::{ - super::{UpsertReclaim, secondary::AccountSecondaryIndexes}, - *, - }, + super::{super::UpsertReclaim, *}, crate::accounts_index::ReclaimsSlotList, - solana_account::AccountSharedData, std::iter, }; @@ -77,8 +73,6 @@ mod tests { 0, 0, &solana_pubkey::new_rand(), - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), true, &mut gc, UpsertReclaim::PopulateReclaims, @@ -111,8 +105,6 @@ mod tests { slot, slot, pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), value, &mut gc, UpsertReclaim::PopulateReclaims, @@ -154,8 +146,6 @@ mod tests { slot, slot, pubkey, - &AccountSharedData::default(), - &AccountSecondaryIndexes::default(), value, &mut gc, UpsertReclaim::PopulateReclaims, diff --git a/accounts-db/src/accounts_index/roots_tracker.rs b/accounts-db/src/accounts_index/roots_tracker.rs deleted file mode 100644 index 6ca3ed9141d..00000000000 --- a/accounts-db/src/accounts_index/roots_tracker.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::rolling_bit_field::RollingBitField; - -#[derive(Debug)] -pub struct RootsTracker { - /// Current roots where appendvecs or write cache has account data. - /// Constructed during load from snapshots. - /// Updated every time we add a new root or clean/shrink an append vec into irrelevancy. - /// Range is approximately the last N slots where N is # slots per epoch. - pub alive_roots: RollingBitField, -} - -impl Default for RootsTracker { - fn default() -> Self { - // we expect to keep a rolling set of 400k slots around at a time - // 4M gives us plenty of extra(?!) room to handle a width 10x what we should need. - // cost is 4M bits of memory, which is .5MB - Self::new(4194304) - } -} - -impl RootsTracker { - pub fn new(max_width: u64) -> Self { - Self { - alive_roots: RollingBitField::new(max_width), - } - } -} diff --git a/accounts-db/src/accounts_index/secondary.rs b/accounts-db/src/accounts_index/secondary.rs index 26137158c45..8abecc12f0f 100644 --- a/accounts-db/src/accounts_index/secondary.rs +++ b/accounts-db/src/accounts_index/secondary.rs @@ -132,7 +132,11 @@ impl /// Inserts `inner_key` into `key`'s map. pub fn insert(&self, key: &Pubkey, inner_key: &Pubkey) { // Note: Always lock the reverse index first, so we synchronize with remove(). - let reverse_index_entry = self.reverse_index.entry(*inner_key).or_default(); + // Pre-size to 1 to avoid push() over-allocating an empty Vec to capacity 4. + let reverse_index_entry = self + .reverse_index + .entry(*inner_key) + .or_insert_with(|| RwLock::new(Vec::with_capacity(1))); let mut outer_keys = reverse_index_entry.write().unwrap(); // Now insert into the index. @@ -369,4 +373,47 @@ mod tests { 0, ); } + + /// Regression guard for reverse-index entry capacity. Each entry must be + /// created with capacity 1. An empty Vec plus push() would over-allocate to + /// capacity 4 (Rust RawVec rule), wasting 96 bytes per entry. + #[test] + fn test_reverse_index_entry_not_over_allocated() { + let secondary_index = + SecondaryIndex::::new("test_secondary_index"); + let outer_key = Pubkey::new_unique(); + let inner_key = Pubkey::new_unique(); + + secondary_index.insert(&outer_key, &inner_key); + + let reverse_entry = secondary_index + .reverse_index + .get(&inner_key) + .expect("reverse index entry should exist after insert"); + let outer_keys = reverse_entry.read().unwrap(); + assert_eq!(outer_keys.len(), 1); + assert_eq!(outer_keys[0], outer_key); + // Regression guard: `or_default()` + push() would make this 4. + assert_eq!(outer_keys.capacity(), 1); + } + + /// A reverse entry with multiple outer keys still grows correctly when + /// started at capacity 1, exercising the push() realloc path. + #[test] + fn test_reverse_index_multiple_outer_keys() { + let secondary_index = + SecondaryIndex::::new("test_secondary_index"); + let inner_key = Pubkey::new_unique(); + let outer_key_1 = Pubkey::new_unique(); + let outer_key_2 = Pubkey::new_unique(); + + secondary_index.insert(&outer_key_1, &inner_key); + secondary_index.insert(&outer_key_2, &inner_key); + + let reverse_entry = secondary_index.reverse_index.get(&inner_key).unwrap(); + let outer_keys = reverse_entry.read().unwrap(); + assert_eq!(outer_keys.len(), 2); + assert!(outer_keys.contains(&outer_key_1)); + assert!(outer_keys.contains(&outer_key_2)); + } } diff --git a/accounts-db/src/accounts_index/stats.rs b/accounts-db/src/accounts_index/stats.rs index 80299d4a90c..645f13913f5 100644 --- a/accounts-db/src/accounts_index/stats.rs +++ b/accounts-db/src/accounts_index/stats.rs @@ -23,7 +23,6 @@ pub struct HeldInMemStats { pub age: AtomicU64, pub ref_count: AtomicU64, pub slot_list_len: AtomicU64, - pub slot_list_cached: AtomicU64, } #[derive(Debug, Default)] @@ -266,8 +265,6 @@ impl Stats { let held_in_mem_ref_count = self.held_in_mem.ref_count.swap(0, Ordering::Relaxed); let held_in_mem_slot_list_len = self.held_in_mem.slot_list_len.swap(0, Ordering::Relaxed); - let held_in_mem_slot_list_cached = - self.held_in_mem.slot_list_cached.swap(0, Ordering::Relaxed); // If an entry is held in-mem due to ref count or slot list length, // then assume it has two slot list entries. // Since `approx_size_of_one_entry()` assumes 'regular' entries @@ -320,11 +317,6 @@ impl Stats { held_in_mem_slot_list_len, i64 ), - ( - "num_not_flushed_slot_list_cached", - held_in_mem_slot_list_cached, - i64 - ), ("min_in_bin_disk", disk_stats.0, i64), ("max_in_bin_disk", disk_stats.1, i64), ("count_from_bins_disk", disk_stats.2, i64), diff --git a/accounts-db/src/accounts_scan.rs b/accounts-db/src/accounts_scan.rs index 3011888c850..fe85d5d8707 100644 --- a/accounts-db/src/accounts_scan.rs +++ b/accounts-db/src/accounts_scan.rs @@ -132,13 +132,12 @@ impl<'a> ScanGuard<'a> { let max_root_inclusive = max_root_inclusive_fn(); if let Some(min_ongoing_scan_root) = ScanTracker::min_ongoing_scan_root_from_btree(&w_ongoing_scan_roots) + && min_ongoing_scan_root < max_root_inclusive { - if min_ongoing_scan_root < max_root_inclusive { - let current = max_root_inclusive - min_ongoing_scan_root; - scan_tracker - .max_distance_to_min_scan_slot - .fetch_max(current, Ordering::Relaxed); - } + let current = max_root_inclusive - min_ongoing_scan_root; + scan_tracker + .max_distance_to_min_scan_slot + .fetch_max(current, Ordering::Relaxed); } *w_ongoing_scan_roots.entry(max_root_inclusive).or_default() += 1; max_root_inclusive diff --git a/accounts-db/src/ancestors.rs b/accounts-db/src/ancestors.rs index a054cdbb828..951a9417cc3 100644 --- a/accounts-db/src/ancestors.rs +++ b/accounts-db/src/ancestors.rs @@ -46,6 +46,10 @@ impl Ancestors { self.ancestors.get_all() } + pub fn iter(&self) -> impl Iterator + '_ { + self.ancestors.iter_ones() + } + pub fn remove(&mut self, slot: &Slot) { self.ancestors.remove(slot); } @@ -201,4 +205,16 @@ mod tests { assert_eq!(count, count2); } } + + #[test] + fn test_ancestors_iter_matches_keys() { + let ancestors = Ancestors::from(vec![3, 42, 128_007, 128_017, 128_107]); + + let mut keys = ancestors.keys(); + let mut iter_keys = ancestors.iter().collect::>(); + keys.sort_unstable(); + iter_keys.sort_unstable(); + + assert_eq!(iter_keys, keys); + } } diff --git a/accounts-db/src/ancient_append_vecs.rs b/accounts-db/src/ancient_append_vecs.rs index 20614beec82..00672e83940 100644 --- a/accounts-db/src/ancient_append_vecs.rs +++ b/accounts-db/src/ancient_append_vecs.rs @@ -10,8 +10,8 @@ use { account_storage_entry::AccountStorageEntry, accounts_db::{ AccountFromStorage, AccountsDb, AliveAccounts, GetUniqueAccountsResult, ShrinkCollect, - ShrinkCollectAliveSeparatedByRefs, UpdateIndexThreadSelection, - stats::{ShrinkAncientStats, ShrinkStatsSub}, + ShrinkCollectAliveSeparatedByRefs, + stats::{ShrinkAncientStats, SquashStatsSub}, }, active_stats::ActiveStatItem, storable_accounts::{StorableAccounts, StorableAccountsBySlot}, @@ -61,7 +61,7 @@ struct SlotInfo { slot: Slot, /// total capacity of storage capacity: u64, - /// # alive bytes in storage + /// # alive bytes in storage *after* shrinking alive_bytes: u64, /// true if this should be shrunk due to ratio should_shrink: bool, @@ -95,14 +95,14 @@ impl AncientSlotInfos { &mut self, slot: Slot, storage: Arc, + alive_bytes_after_shrink: u64, can_randomly_shrink: bool, ideal_size: NonZeroU64, is_high_slot: bool, is_candidate_for_shrink: bool, ) -> bool { let mut was_randomly_shrunk = false; - let alive_bytes = storage.alive_bytes() as u64; - if alive_bytes > 0 { + if alive_bytes_after_shrink > 0 { let capacity = storage.accounts.capacity(); let should_shrink = if capacity > 0 { if is_candidate_for_shrink { @@ -123,11 +123,11 @@ impl AncientSlotInfos { if should_shrink { // alive ratio is too low, so prioritize combining this slot with others // to reduce disk space used - self.total_alive_bytes_shrink += alive_bytes; + self.total_alive_bytes_shrink += alive_bytes_after_shrink; self.shrink_indexes.push(self.all_infos.len()); } else { let already_ideal_size = u64::from(ideal_size) * 80 / 100; - if alive_bytes > already_ideal_size { + if alive_bytes_after_shrink > already_ideal_size { // do not include this append vec at all. It is already ideal size and not a candidate for shrink. return was_randomly_shrunk; } @@ -136,11 +136,11 @@ impl AncientSlotInfos { slot, capacity, storage, - alive_bytes, + alive_bytes: alive_bytes_after_shrink, should_shrink, is_high_slot, }); - self.total_alive_bytes += alive_bytes; + self.total_alive_bytes += alive_bytes_after_shrink; } was_randomly_shrunk } @@ -331,7 +331,7 @@ struct WriteAncientAccounts<'a> { /// 'ShrinkInProgress' instances created by starting a shrink operation shrinks_in_progress: HashMap>, - metrics: ShrinkStatsSub, + metrics: SquashStatsSub, } #[derive(Debug, PartialEq, Clone, Copy)] @@ -367,7 +367,7 @@ impl AccountsDb { let _guard = self.active_stats.activate(ActiveStatItem::SquashAncient); - let mut stats_sub = ShrinkStatsSub::default(); + let mut stats_sub = SquashStatsSub::default(); let (_, total_us) = measure_us!(self.combine_ancient_slots_packed_internal( sorted_slots, @@ -375,7 +375,7 @@ impl AccountsDb { &mut stats_sub )); - Self::update_shrink_stats(&self.shrink_ancient_stats.shrink_stats, stats_sub, false); + self.shrink_ancient_stats.accumulate_sub_stats(stats_sub); self.shrink_ancient_stats .total_us .fetch_add(total_us, Ordering::Relaxed); @@ -418,7 +418,7 @@ impl AccountsDb { &self, sorted_slots: Vec, mut tuning: PackedAncientStorageTuning, - metrics: &mut ShrinkStatsSub, + metrics: &mut SquashStatsSub, ) { self.shrink_ancient_stats .slot @@ -563,17 +563,15 @@ impl AccountsDb { .expect("ancient shrink target slot must already have a storage"); let (shrink_in_progress, create_and_insert_store_elapsed_us) = measure_us!(self.get_store_for_shrink(target_slot, old_store, bytes)); - let (store_accounts_timing, rewrite_elapsed_us) = measure_us!(self.store_accounts_frozen( - accounts_to_write, - shrink_in_progress.new_storage(), - UpdateIndexThreadSelection::PoolWithThreshold - )); + let (store_accounts_stats, rewrite_elapsed_us) = measure_us!( + self.store_accounts_for_squash(accounts_to_write, shrink_in_progress.new_storage()) + ); - write_ancient_accounts.metrics.accumulate(&ShrinkStatsSub { - store_accounts_timing, + write_ancient_accounts.metrics.accumulate(&SquashStatsSub { + store_accounts_stats, rewrite_elapsed_us: Saturating(rewrite_elapsed_us), create_and_insert_store_elapsed_us: Saturating(create_and_insert_store_elapsed_us), - ..ShrinkStatsSub::default() + ..SquashStatsSub::default() }); write_ancient_accounts @@ -601,9 +599,11 @@ impl AccountsDb { for slot in &slots { if let Some(storage) = self.storage.get_slot_storage_entry(*slot) { let is_candidate_for_shrink = self.is_candidate_for_shrink(&storage); + let alive_bytes_after_shrink = self.alive_bytes_after_shrink(&storage) as u64; if infos.add( *slot, storage, + alive_bytes_after_shrink, tuning.can_randomly_shrink, tuning.ideal_storage_size, is_high_slot(*slot), @@ -726,7 +726,7 @@ impl AccountsDb { &self, accounts_to_combine: AccountsToCombine<'_>, mut write_ancient_accounts: WriteAncientAccounts, - metrics: &mut ShrinkStatsSub, + metrics: &mut SquashStatsSub, ) { let mut dropped_roots = Vec::with_capacity(accounts_to_combine.accounts_to_combine.len()); for shrink_collect in accounts_to_combine.accounts_to_combine { @@ -762,7 +762,6 @@ impl AccountsDb { self.reopen_storage_as_readonly_shrinking_in_progress_ok(slot); } } - self.handle_dropped_roots_for_ancient(dropped_roots.into_iter()); metrics.accumulate(&write_ancient_accounts.metrics); } @@ -1557,7 +1556,7 @@ mod tests { &default_tuning(), IncludeManyRefSlots::Include, ); - let mut stats = ShrinkStatsSub::default(); + let mut stats = SquashStatsSub::default(); let mut write_ancient_accounts = WriteAncientAccounts::default(); slots.clone().for_each(|slot| { @@ -1568,15 +1567,6 @@ mod tests { } }); - let roots = db - .accounts_index - .roots_tracker - .read() - .unwrap() - .alive_roots - .get_all(); - assert_eq!(roots, slots.clone().collect::>()); - if all_slots_shrunk { // make it look like each of the slots was shrunk slots.clone().for_each(|slot| { @@ -1599,24 +1589,6 @@ mod tests { slots.clone().for_each(|slot| { assert!(!db.shrink_candidate_slots.lock().unwrap().contains(&slot)); }); - - let roots_after = db - .accounts_index - .roots_tracker - .read() - .unwrap() - .alive_roots - .get_all(); - - assert_eq!( - roots_after, - if all_slots_shrunk { - slots.clone().collect::>() - } else { - vec![] - }, - "all_slots_shrunk: {all_slots_shrunk}" - ); slots.for_each(|slot| { let storage = db.storage.get_slot_storage_entry(slot); if all_slots_shrunk { @@ -2431,6 +2403,7 @@ mod tests { infos.add( slot1, Arc::clone(&storage), + db.alive_bytes_after_shrink(&storage) as u64, can_randomly_shrink, NonZeroU64::new(get_ancient_append_vec_capacity()).unwrap(), high_slot, @@ -2486,6 +2459,7 @@ mod tests { infos.add( slot1, Arc::clone(&storage), + db.alive_bytes_after_shrink(&storage) as u64, can_randomly_shrink, NonZeroU64::new(get_ancient_append_vec_capacity()).unwrap(), high_slot, @@ -3420,7 +3394,7 @@ mod tests { db.combine_ancient_slots_packed_internal( (0..num_slots).map(|slot| (slot as Slot) + slot1).collect(), tuning, - &mut ShrinkStatsSub::default(), + &mut SquashStatsSub::default(), ); let storage = db.storage.get_slot_storage_entry(slot1); if num_slots == 0 { @@ -3498,7 +3472,7 @@ mod tests { ..default_tuning() }; - let mut stats_sub = ShrinkStatsSub::default(); + let mut stats_sub = SquashStatsSub::default(); db.combine_ancient_slots_packed_internal(sorted_slots, tuning, &mut stats_sub); } @@ -3631,7 +3605,7 @@ mod tests { // non-empty slot list (but ignored) because slot_list = 1 let slot_list = vec![( slot, - AccountInfo::new(StorageLocation::Cached, lamports == 0), + AccountInfo::new(StorageLocation::AppendVec(0, 0), lamports == 0), )]; alive_accounts.add(2, &account, &slot_list); assert!(alive_accounts.one_ref.accounts.is_empty()); @@ -3648,11 +3622,17 @@ mod tests { let slot_list = vec![ ( slot, - AccountInfo::new(StorageLocation::Cached, lamports == 0), + AccountInfo::new( + StorageLocation::AppendVec(0, 0), + lamports == 0, + ), ), ( slot + 1, - AccountInfo::new(StorageLocation::Cached, lamports == 0), + AccountInfo::new( + StorageLocation::AppendVec(0, 0), + lamports == 0, + ), ), ]; alive_accounts.add(2, &account, &slot_list); @@ -3670,11 +3650,17 @@ mod tests { let slot_list = vec![ ( slot, - AccountInfo::new(StorageLocation::Cached, lamports == 0), + AccountInfo::new( + StorageLocation::AppendVec(0, 0), + lamports == 0, + ), ), ( slot - 1, - AccountInfo::new(StorageLocation::Cached, lamports == 0), + AccountInfo::new( + StorageLocation::AppendVec(0, 0), + lamports == 0, + ), ), ]; alive_accounts.add(2, &account, &slot_list); @@ -3767,7 +3753,6 @@ mod tests { #[test] fn test_shrink_ancient_expected_unref() { let db = AccountsDb::new_single_for_tests(); - let empty_account = AccountSharedData::default(); for count in 0..3 { let pubkeys_to_unref = (0..count) .map(|_| solana_pubkey::new_rand()) @@ -3783,8 +3768,6 @@ mod tests { slot, slot, k, - &empty_account, - &crate::accounts_index::AccountSecondaryIndexes::default(), AccountInfo::default(), &mut ReclaimsSlotList::new(), UpsertReclaim::IgnoreReclaims, diff --git a/accounts-db/src/append_vec.rs b/accounts-db/src/append_vec.rs index 7f7521252e6..de1edf5209a 100644 --- a/accounts-db/src/append_vec.rs +++ b/accounts-db/src/append_vec.rs @@ -198,7 +198,7 @@ impl Drop for AppendVec { // promote this to panic soon. // disabled due to many false positive warnings while running tests. // blocked by rpc's upgrade to jsonrpc v17 - warn!("AppendVec failed to remove {}: {err}", &self.path.display()); + warn!("AppendVec failed to remove {}: {err}", self.path.display()); } } } @@ -283,6 +283,13 @@ impl AppendVec { Ok(()) } + /// Detach the on-disk file from this AppendVec's lifetime so dropping the AppendVec no + /// longer removes the file. Used when ownership of the file is being handed off (e.g. to a + /// bank snapshot's storages list that needs the file to outlive validator exit). + pub fn disable_remove_on_drop(&self) { + self.remove_file_on_drop.store(false, Ordering::Release); + } + /// Return AppendVec opened in read-only file-io mode or `None` if it already is such pub(crate) fn reopen_as_readonly_file_io(&self) -> Option { if matches!(self.read_write_state, ReadWriteState::ReadOnly) { @@ -335,36 +342,11 @@ impl AppendVec { /// Creates a new AppendVec for the underlying storage at `file_info` /// /// This version of `new()` may only be called when reconstructing storages as part of startup. - /// It trusts the snapshot's value for `current_len`, and relies on later index generation or - /// accounts verification to ensure it is valid. - pub fn new_for_startup(file_info: FileInfo, current_len: usize) -> Result { - let new = Self::new_from_file_info_unchecked(file_info, current_len)?; - - // The current_len is allowed to be either exactly the same as file_size, or - // u64-aligned-equivalent to file_size. This is because `flush` and `shink` compute the - // required file size with *aligned* stored size per account, including the very last - // account. So the file size may have padding to the next u64-alignment. - // For our usage, this is still safe as these padding bytes could never be used for an - // account. This renders the `get_` and `scan_` functions safe. - if (current_len as u64 == new.file_size) - || (u64_align!(current_len) as u64 == new.file_size) - { - Ok(new) - } else { - // However, if opening a minimized snapshot, the file sizes can be - // larger than current length [^1]. So when the `if` condition fails, - // fallback to the old/slow impl that does the full sanitization. - // [^1]: https://github.com/anza-xyz/agave/issues/6797 - info!( - "Could not optimistically create new AppendVec, falling back to pessimistic impl: \ - file size ({}) and current length ({}) do not match for '{}'", - new.file_size, - current_len, - new.path.display(), - ); - let _num_accounts = new.sanitize_layout_and_length()?; - Ok(new) - } + /// The storage length is taken to be the full file size; this is trusted and relies on later + /// index generation or accounts verification to ensure it is valid. + pub fn new_for_startup(file_info: FileInfo) -> Result { + let current_len = file_info.size as usize; + Self::new_from_file_info_unchecked(file_info, current_len) } /// Creates an appendvec in read-only mode from existing `FileInfo` and without full data checks @@ -395,6 +377,7 @@ impl AppendVec { } /// Checks that all accounts layout is correct and returns the number of accounts. + #[cfg(feature = "dev-context-only-utils")] fn sanitize_layout_and_length(&self) -> Result { // This discards allocated accounts immediately after check at each loop iteration. // @@ -629,13 +612,13 @@ impl AppendVec { ValidSlice(unsafe { slice::from_raw_parts(buf.as_ptr() as *const u8, bytes_read) }); let (meta, next) = Self::get_type::(valid_bytes, 0)?; let (account_meta, _) = Self::get_type::(valid_bytes, next)?; - let stored_size = Self::calculate_stored_size_checked(meta.data_len as usize)?; + // Guard against a corrupt `data_len` whose stored size would overflow. + Self::calculate_stored_size_checked(meta.data_len as usize)?; Some(callback(StoredAccountNoData { meta, account_meta, offset, - stored_size, })) } @@ -985,7 +968,6 @@ impl AppendVec { meta: stored_meta, account_meta, offset, - stored_size, }); reader.consume_or_skip(stored_size); } @@ -2002,7 +1984,7 @@ mod tests { let offset = account_offsets.get(i).unwrap(); assert_eq!( - stored_account.stored_size, + stored_account.stored_size(), AppendVec::calculate_stored_size(account.data().len()), ); assert_eq!(stored_account.offset(), *offset); diff --git a/accounts-db/src/append_vec/meta.rs b/accounts-db/src/append_vec/meta.rs index dc901183e27..f36275e3fcf 100644 --- a/accounts-db/src/append_vec/meta.rs +++ b/accounts-db/src/append_vec/meta.rs @@ -1,6 +1,6 @@ use { crate::is_zero_lamport::IsZeroLamport, solana_account::ReadableAccount, solana_clock::Epoch, - solana_pubkey::Pubkey, std::ptr, + solana_pubkey::Pubkey, }; /// Meta contains enough context to recover the index from storage itself @@ -96,7 +96,6 @@ pub struct StoredAccountNoData<'append_vec> { pub meta: &'append_vec StoredMeta, pub account_meta: &'append_vec AccountMeta, pub offset: usize, - pub stored_size: usize, } impl<'append_vec> StoredAccountNoData<'append_vec> { @@ -115,6 +114,7 @@ impl<'append_vec> StoredAccountNoData<'append_vec> { &self.meta.pubkey } + #[cfg(feature = "dev-context-only-utils")] pub fn sanitize(&self) -> bool { self.sanitize_executable() && self.sanitize_lamports() } @@ -124,9 +124,12 @@ impl<'append_vec> StoredAccountNoData<'append_vec> { self.offset } + /// The on-disk size of this account, derived from its data length. The view is transient and + /// not persisted, so the size is computed on demand rather than stored. + #[cfg(feature = "dev-context-only-utils")] #[inline(always)] pub fn stored_size(&self) -> usize { - self.stored_size + super::AppendVec::calculate_stored_size(self.meta.data_len as usize) } #[inline(always)] @@ -144,6 +147,7 @@ impl<'append_vec> StoredAccountNoData<'append_vec> { self.account_meta.rent_epoch } + #[cfg(feature = "dev-context-only-utils")] pub fn sanitize_executable(&self) -> bool { // Sanitize executable to ensure higher 7-bits are cleared correctly. self.ref_executable_byte() & !1 == 0 @@ -153,6 +157,7 @@ impl<'append_vec> StoredAccountNoData<'append_vec> { /// /// Note that we are not comparing against AccountSharedData::default() because we do not have access to the account data, /// so we compare data _length_ in lieu of actual data. This check otherwise identical to AccountSharedData::default(). + #[cfg(feature = "dev-context-only-utils")] pub fn is_default_account(&self) -> bool { self.account_meta.lamports == 0 && self.meta.data_len == 0 @@ -161,12 +166,15 @@ impl<'append_vec> StoredAccountNoData<'append_vec> { && self.account_meta.owner == Pubkey::default() } + #[cfg(feature = "dev-context-only-utils")] pub fn sanitize_lamports(&self) -> bool { // Check if the account data matches that of a default account if it has 0 lamports. self.account_meta.lamports != 0 || self.is_default_account() } + #[cfg(feature = "dev-context-only-utils")] pub fn ref_executable_byte(&self) -> &u8 { + use std::ptr; // Use extra references to avoid value silently clamped to 1 (=true) and 0 (=false) // Yes, this really happens; see test_new_from_file_crafted_executable let executable_bool: &bool = &self.account_meta.executable; diff --git a/accounts-db/src/blockhash_queue.rs b/accounts-db/src/blockhash_queue.rs index 5ce2043122a..2320a5ce8e8 100644 --- a/accounts-db/src/blockhash_queue.rs +++ b/accounts-db/src/blockhash_queue.rs @@ -9,7 +9,7 @@ use { std::collections::HashMap, }; -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct HashInfo { fee_calculator: FeeCalculator, @@ -26,10 +26,11 @@ impl HashInfo { /// Low memory overhead, so can be cloned for every checkpoint #[cfg_attr( feature = "frozen-abi", - derive(AbiExample, StableAbi), + derive(AbiExample, StableAbi, StableAbiSample), frozen_abi( api_digest = "DZVVXt4saSgH1CWGrzBcX2sq5yswCuRqGx1Y1ZehtWT6", - abi_digest = "CGD97vsYSQpPbYkzYnHmrwRZc4BbHqTEvP5vz4jg8jzU" + abi_digest = "5ojmBDhhu9AjKUc1LSHhZfXF6KeicvZpKP6XdLNaFAdy", + test_roundtrip = "eq_and_wire" ) )] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -158,37 +159,6 @@ impl BlockhashQueue { } } -#[cfg(feature = "frozen-abi")] -impl solana_frozen_abi::rand::prelude::Distribution - for solana_frozen_abi::rand::distr::StandardUniform -{ - fn sample(&self, rng: &mut R) -> BlockhashQueue { - let seed1: u64 = rng.random(); - let seed2: u64 = rng.random(); - let seed3: u64 = rng.random(); - let seed4: u64 = rng.random(); - - let mut hashes = - HashMap::with_hasher(ahash::RandomState::with_seeds(seed1, seed2, seed3, seed4)); - hashes.insert( - Hash::new_from_array(rng.random()), - HashInfo { - fee_calculator: FeeCalculator { - lamports_per_signature: rng.random(), - }, - hash_index: rng.random(), - timestamp: rng.random(), - }, - ); - - BlockhashQueue { - last_hash_index: rng.random(), - last_hash: Some(Hash::new_from_array(rng.random())), - hashes, - max_age: rng.random_range(0..MAX_RECENT_BLOCKHASHES), - } - } -} #[cfg(test)] mod tests { #[allow(deprecated)] diff --git a/accounts-db/src/partitioned_rewards.rs b/accounts-db/src/partitioned_rewards.rs index 85758a4df84..e6565c2c2c9 100644 --- a/accounts-db/src/partitioned_rewards.rs +++ b/accounts-db/src/partitioned_rewards.rs @@ -1,16 +1,22 @@ //! Code related to partitioned rewards distribution -/// # stake accounts to store in one block during partitioned reward interval -/// Target to store 64 rewards per entry/tick in a block. A block has a minimum of 64 -/// entries/tick. This gives 4096 total rewards to store in one block. -/// This constant affects consensus. -const MAX_PARTITIONED_REWARDS_PER_BLOCK: u64 = 4096; +/// Baseline number of stake accounts to store in one 400ms block during the +/// partitioned reward interval. +/// +/// The target is 64 rewards per entry/tick. A block has a minimum of 64 +/// entries/ticks, giving 4096 total rewards to store in one 400ms block. This +/// constant affects consensus; shorter slot-time targets scale this value down +/// in `Bank` state. +pub const MAX_PARTITIONED_REWARDS_PER_BLOCK: u64 = 4096; -#[derive(Debug, Clone, Copy)] /// Configuration options for partitioned epoch rewards. +#[derive(Debug, Clone, Copy)] pub struct PartitionedEpochRewardsConfig { - /// number of stake accounts to store in one block during partitioned reward interval - /// normally, this is a number tuned for reasonable performance, such as 4096 accounts/block + /// Baseline number of stake accounts to store in one block during the + /// partitioned reward interval. + /// + /// This value is stored as the 400ms-slot baseline. Runtime `Bank` state + /// derives the effective per-bank value from the active slot-time target. pub stake_account_stores_per_block: u64, } @@ -30,7 +36,8 @@ impl Default for PartitionedEpochRewardsConfig { } impl PartitionedEpochRewardsConfig { - /// Only for tests and benchmarks + /// Constructs a config with an explicit 400ms-slot baseline for tests and + /// benchmarks. pub fn new_for_test(stake_account_stores_per_block: u64) -> Self { Self { stake_account_stores_per_block, diff --git a/accounts-db/src/read_only_accounts_cache.rs b/accounts-db/src/read_only_accounts_cache.rs index 36fab4f62af..eb1b376e94a 100644 --- a/accounts-db/src/read_only_accounts_cache.rs +++ b/accounts-db/src/read_only_accounts_cache.rs @@ -57,8 +57,7 @@ pub struct ReadOnlyCacheStats { pub load_us: u64, pub store_us: u64, pub evict_us: u64, - pub evictor_wakeup_count_all: u64, - pub evictor_wakeup_count_productive: u64, + pub evict_run_count: u64, } #[derive(Default, Debug)] @@ -69,8 +68,7 @@ struct AtomicReadOnlyCacheStats { load_us: AtomicU64, store_us: AtomicU64, evict_us: AtomicU64, - evictor_wakeup_count_all: AtomicU64, - evictor_wakeup_count_productive: AtomicU64, + evict_run_count: AtomicU64, } /// Shared state between the cache and its evictor thread, used to signal @@ -162,16 +160,16 @@ impl ReadOnlyAccountsCache { pub(crate) fn load(&self, pubkey: Pubkey, slot: Slot) -> Option { let (account, load_us) = measure_us!({ let mut found = None; - if let Some(entry) = self.cache.get(&pubkey) { - if entry.slot == slot { - entry - .last_update_time - .store(self.timestamp(), Ordering::Relaxed); - let account = entry.account.clone(); - drop(entry); - self.stats.hits.fetch_add(1, Ordering::Relaxed); - found = Some(account); - } + if let Some(entry) = self.cache.get(&pubkey) + && entry.slot == slot + { + entry + .last_update_time + .store(self.timestamp(), Ordering::Relaxed); + let account = entry.account.clone(); + drop(entry); + self.stats.hits.fetch_add(1, Ordering::Relaxed); + found = Some(account); } if found.is_none() { @@ -274,14 +272,7 @@ impl ReadOnlyAccountsCache { let load_us = self.stats.load_us.swap(0, Ordering::Relaxed); let store_us = self.stats.store_us.swap(0, Ordering::Relaxed); let evict_us = self.stats.evict_us.swap(0, Ordering::Relaxed); - let evictor_wakeup_count_all = self - .stats - .evictor_wakeup_count_all - .swap(0, Ordering::Relaxed); - let evictor_wakeup_count_productive = self - .stats - .evictor_wakeup_count_productive - .swap(0, Ordering::Relaxed); + let evict_run_count = self.stats.evict_run_count.swap(0, Ordering::Relaxed); ReadOnlyCacheStats { hits, @@ -290,8 +281,7 @@ impl ReadOnlyAccountsCache { load_us, store_us, evict_us, - evictor_wakeup_count_all, - evictor_wakeup_count_productive, + evict_run_count, } } @@ -312,28 +302,22 @@ impl ReadOnlyAccountsCache { info!("AccountsReadCacheEvictor has started"); let mut rng = SmallRng::from_os_rng(); loop { - // Wait up to 100 ms, or until the exit flag is set. - // 100 ms is already four times per slot, which should be plenty. + // Wait up to 10 ms, or until the exit flag is set. + // Ensure this timeout stays many times smaller than the slot time. let exit_flag = control.exit.lock().unwrap(); let (exit_flag, _) = control .wake - .wait_timeout_while(exit_flag, Duration::from_millis(100), |exit| !*exit) + .wait_timeout_while(exit_flag, Duration::from_millis(10), |exit| !*exit) .unwrap(); if *exit_flag { break; } drop(exit_flag); - stats - .evictor_wakeup_count_all - .fetch_add(1, Ordering::Relaxed); - if data_size.load(Ordering::Relaxed) <= max_data_size_hi { continue; } - stats - .evictor_wakeup_count_productive - .fetch_add(1, Ordering::Relaxed); + stats.evict_run_count.fetch_add(1, Ordering::Relaxed); #[cfg(not(feature = "dev-context-only-utils"))] let (num_evicts, evict_us) = measure_us!(Self::evict( diff --git a/accounts-db/src/rolling_bit_field.rs b/accounts-db/src/rolling_bit_field.rs index 9d299e12325..46869603d26 100644 --- a/accounts-db/src/rolling_bit_field.rs +++ b/accounts-db/src/rolling_bit_field.rs @@ -3,10 +3,7 @@ //! Old key values are removed from the lesser values and do not accumulate. mod iterators; -use { - bv::BitVec, iterators::RollingBitFieldOnesIter, solana_clock::Slot, - solana_nohash_hasher::IntSet, -}; +use {bv::BitVec, iterators::RollingBitFieldOnesIter, solana_nohash_hasher::IntSet}; #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] #[derive(Clone)] @@ -115,11 +112,6 @@ impl RollingBitField { key % self.max_width } - pub fn range_width(&self) -> u64 { - // note that max isn't updated on remove, so it can be above the current max - self.max_exclusive - self.min - } - pub fn min(&self) -> Option { if self.is_empty() { None @@ -291,47 +283,6 @@ impl RollingBitField { self.max_exclusive } - pub fn max_inclusive(&self) -> u64 { - self.max_exclusive.saturating_sub(1) - } - - /// return all items < 'max_slot_exclusive' - pub fn get_all_less_than(&self, max_slot_exclusive: Slot) -> Vec { - let mut all = Vec::with_capacity(self.count); - self.excess.iter().for_each(|slot| { - if slot < &max_slot_exclusive { - all.push(*slot) - } - }); - for key in self.min..self.max_exclusive { - if key >= max_slot_exclusive { - break; - } - - if self.contains_assume_in_range(&key) { - all.push(key); - } - } - all - } - - /// return highest item < 'max_slot_exclusive' - pub fn get_prior(&self, max_slot_exclusive: Slot) -> Option { - let mut slot = max_slot_exclusive.saturating_sub(1); - self.min().and_then(|min| { - loop { - if self.contains(&slot) { - return Some(slot); - } - slot = slot.saturating_sub(1); - if slot == 0 || slot < min { - break; - } - } - None - }) - } - pub fn get_all(&self) -> Vec { let mut all = Vec::with_capacity(self.count); self.excess.iter().for_each(|slot| all.push(*slot)); @@ -354,7 +305,10 @@ impl RollingBitField { #[cfg(test)] mod tests { - use {super::*, log::*, solana_measure::measure::Measure, std::collections::HashSet}; + use { + super::*, log::*, solana_clock::Slot, solana_measure::measure::Measure, + std::collections::HashSet, + }; impl RollingBitField { pub fn clear(&mut self) { @@ -362,59 +316,6 @@ mod tests { } } - #[test] - fn test_get_all_less_than() { - agave_logger::setup(); - let len = 16; - let mut bitfield = RollingBitField::new(len); - assert!(bitfield.get_all_less_than(0).is_empty()); - bitfield.insert(0); - assert!(bitfield.get_all_less_than(0).is_empty()); - assert_eq!(bitfield.get_all_less_than(1), vec![0]); - bitfield.insert(1); - assert_eq!(bitfield.get_all_less_than(1), vec![0]); - let last_item_not_in_excess = len - 1; - bitfield.insert(last_item_not_in_excess); - assert!(bitfield.excess.is_empty()); - assert_eq!( - bitfield.get_all_less_than(last_item_not_in_excess), - vec![0, 1] - ); - assert_eq!( - bitfield.get_all_less_than(last_item_not_in_excess + 1), - vec![0, 1, last_item_not_in_excess] - ); - let first_item_in_excess = last_item_not_in_excess + 1; - bitfield.insert(first_item_in_excess); - assert!(bitfield.excess.contains(&0)); - assert_eq!( - bitfield.get_all_less_than(last_item_not_in_excess), - vec![0, 1] - ); - assert_eq!( - bitfield.get_all_less_than(last_item_not_in_excess + 1), - vec![0, 1, last_item_not_in_excess] - ); - assert_eq!( - bitfield.get_all_less_than(first_item_in_excess + 1), - vec![0, 1, last_item_not_in_excess, first_item_in_excess] - ); - - bitfield.insert(len * 2); - let mut less = bitfield.get_all_less_than(len * 2); - less.sort_unstable(); - assert_eq!( - vec![0, 1, last_item_not_in_excess, first_item_in_excess], - less - ); - let mut less = bitfield.get_all_less_than(len * 2 + 1); - less.sort_unstable(); - assert_eq!( - vec![0, 1, last_item_not_in_excess, first_item_in_excess, len * 2], - less - ); - } - #[test] fn test_bitfield_delete_non_excess() { agave_logger::setup(); @@ -811,22 +712,6 @@ mod tests { } assert_eq!(bitfield.min(), Some(overall_min)); assert_eq!(bitfield.get_all().len(), hashset.len()); - // range isn't tracked for excess items - if bitfield.excess.len() != bitfield.len() { - let width = if bitfield.is_empty() { - 0 - } else { - max + 1 - min - }; - assert!( - bitfield.range_width() >= width, - "hashset: {:?}, bitfield: {:?}, bitfield.range_width: {}, width: {}", - hashset, - bitfield.get_all(), - bitfield.range_width(), - width, - ); - } } else { assert_eq!(bitfield.min(), None); } diff --git a/accounts-db/src/stake_rewards.rs b/accounts-db/src/stake_rewards.rs index 60839d8fdc2..296a7876b6f 100644 --- a/accounts-db/src/stake_rewards.rs +++ b/accounts-db/src/stake_rewards.rs @@ -226,15 +226,21 @@ fn create_stake_account( ..Meta::default() }; - let mut stake = Stake { - delegation: Delegation::new(voter_pubkey, stake_amount, Epoch::MAX), + let stake = Stake { + delegation: Delegation { + voter_pubkey: *voter_pubkey, + stake: stake_amount, + activation_epoch: Epoch::MAX, + deactivation_epoch: if stake_amount == 0 { + last_epoch + } else { + Epoch::MAX + }, + ..Default::default() + }, credits_observed, }; - if stake_amount == 0 { - stake.delegation.deactivation_epoch = last_epoch; - } - stake_account .set_state(&StakeStateV2::Stake(meta, stake, StakeFlags::empty())) .expect("set_state"); diff --git a/accounts-db/src/storable_accounts.rs b/accounts-db/src/storable_accounts.rs index 9cbf1cbc509..f1575deb7fd 100644 --- a/accounts-db/src/storable_accounts.rs +++ b/accounts-db/src/storable_accounts.rs @@ -327,10 +327,10 @@ impl<'a> StorableAccounts<'a> for StorableAccountsBySlot<'a> { }; { let reader = self.cached_storage.read().unwrap(); - if reader.slot == slot { - if let Some(storage) = reader.storage.as_ref() { - return call_callback(storage); - } + if reader.slot == slot + && let Some(storage) = reader.storage.as_ref() + { + return call_callback(storage); } } // cache doesn't contain a storage for this slot, so lookup storage in db. diff --git a/accounts-db/store-histogram/Cargo.toml b/accounts-db/store-histogram/Cargo.toml index 52b25994f46..2613d40cd95 100644 --- a/accounts-db/store-histogram/Cargo.toml +++ b/accounts-db/store-histogram/Cargo.toml @@ -16,3 +16,6 @@ dev-context-only-utils = [] [dependencies] clap = { workspace = true } solana-version = { workspace = true } + +[lints] +workspace = true diff --git a/accounts-db/store-tool/Cargo.toml b/accounts-db/store-tool/Cargo.toml index 930e20214a3..1b6ab18304f 100644 --- a/accounts-db/store-tool/Cargo.toml +++ b/accounts-db/store-tool/Cargo.toml @@ -25,3 +25,6 @@ solana-accounts-db = { workspace = true, features = ["dev-context-only-utils"] } solana-pubkey = { workspace = true } solana-system-interface = { workspace = true } solana-version = { workspace = true } + +[lints] +workspace = true diff --git a/banking-stage-ingress-types/Cargo.toml b/banking-stage-ingress-types/Cargo.toml index 599876d201d..f532ab95456 100644 --- a/banking-stage-ingress-types/Cargo.toml +++ b/banking-stage-ingress-types/Cargo.toml @@ -9,9 +9,17 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] [dependencies] crossbeam-channel = { workspace = true } solana-perf = { workspace = true } + +[lints] +workspace = true diff --git a/banking-stage-ingress-types/src/lib.rs b/banking-stage-ingress-types/src/lib.rs index 6432b509aa6..eb8c982670d 100644 --- a/banking-stage-ingress-types/src/lib.rs +++ b/banking-stage-ingress-types/src/lib.rs @@ -1,5 +1,44 @@ #![cfg(feature = "agave-unstable-api")] -use {crossbeam_channel::Receiver, solana_perf::packet::PacketBatch, std::sync::Arc}; +use { + crossbeam_channel::Receiver, + solana_perf::packet::PacketBatch, + std::sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, +}; pub type BankingPacketBatch = Arc>; pub type BankingPacketReceiver = Receiver; + +/// Priority floor shared from the banking-stage scheduler to sigverify. +/// +/// When saturated, the scheduler publishes the queue-min transaction's +/// priority. Sigverify drops at-or-below-floor arrivals. +/// In practice, transactions always have non-zero priorities. +#[derive(Debug)] +pub struct SchedulerPriorityFloor(AtomicU64); + +impl SchedulerPriorityFloor { + pub fn new() -> Self { + Self(AtomicU64::new(0)) + } + + pub fn set(&self, floor: u64) { + self.0.store(floor, Ordering::Relaxed); + } + + pub fn clear(&self) { + self.set(0); + } + + pub fn get(&self) -> u64 { + self.0.load(Ordering::Relaxed) + } +} + +impl Default for SchedulerPriorityFloor { + fn default() -> Self { + Self::new() + } +} diff --git a/banks-client/Cargo.toml b/banks-client/Cargo.toml index cdcf920a9b5..4d0c03555cd 100644 --- a/banks-client/Cargo.toml +++ b/banks-client/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -47,3 +49,6 @@ solana-pubkey = { workspace = true } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-signer = { workspace = true } solana-system-interface = { workspace = true } + +[lints] +workspace = true diff --git a/banks-interface/Cargo.toml b/banks-interface/Cargo.toml index 3054a768e5c..534bfae0d20 100644 --- a/banks-interface/Cargo.toml +++ b/banks-interface/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -32,3 +34,6 @@ solana-transaction = { workspace = true, features = ["serde"] } solana-transaction-context = { workspace = true, features = ["serde"] } solana-transaction-error = { workspace = true, features = ["serde"] } tarpc = { workspace = true, features = ["full"] } + +[lints] +workspace = true diff --git a/banks-server/Cargo.toml b/banks-server/Cargo.toml index 4918440c22e..272d02893a9 100644 --- a/banks-server/Cargo.toml +++ b/banks-server/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -42,3 +44,6 @@ tarpc = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] } tokio-serde = { workspace = true, features = ["bincode"] } tokio-util = { workspace = true } + +[lints] +workspace = true diff --git a/banks-server/src/banks_server.rs b/banks-server/src/banks_server.rs index a918e59fb6c..4335c21a0f9 100644 --- a/banks-server/src/banks_server.rs +++ b/banks-server/src/banks_server.rs @@ -506,7 +506,7 @@ pub async fn start_tcp_server( let client = create_client(None, tpu_addr, exit.clone()); SendTransactionService::new( - &bank_forks, + bank_forks.clone(), receiver, client, Config { diff --git a/bench-tps/.gitignore b/bench-tps/.gitignore deleted file mode 100644 index 252411f34dc..00000000000 --- a/bench-tps/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/target/ -/config/ -/config-local/ -/farf/ diff --git a/bench-tps/Cargo.toml b/bench-tps/Cargo.toml deleted file mode 100644 index 7b0bf98d4bc..00000000000 --- a/bench-tps/Cargo.toml +++ /dev/null @@ -1,86 +0,0 @@ -[package] -name = "solana-bench-tps" -publish = false -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = { workspace = true } -workspace = "../dev-bins" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[features] -default = ["agave-unstable-api"] -agave-unstable-api = [] -dev-context-only-utils = [] -dummy-for-ci-check = [] -frozen-abi = [] - -[dependencies] -agave-logger = { workspace = true } -chrono = { workspace = true } -clap = { workspace = true } -crossbeam-channel = { workspace = true } -csv = { workspace = true } -log = { workspace = true } -rand = { workspace = true } -rayon = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -serde_yaml = { workspace = true } -solana-account = { workspace = true } -solana-clap-utils = { workspace = true } -solana-cli-config = { workspace = true } -solana-client = { workspace = true } -solana-clock = { workspace = true } -solana-commitment-config = { workspace = true } -solana-compute-budget-interface = { workspace = true } -solana-connection-cache = { workspace = true } -solana-core = { workspace = true, features = ["dev-context-only-utils"] } -solana-fee-calculator = { workspace = true } -solana-genesis = { workspace = true } -solana-genesis-config = { workspace = true } -solana-hash = { workspace = true } -solana-instruction = { workspace = true } -solana-keypair = { workspace = true } -solana-measure = { workspace = true } -solana-message = { workspace = true } -solana-metrics = { workspace = true } -solana-native-token = { workspace = true } -solana-net-utils = { workspace = true } -solana-nonce = { workspace = true } -solana-pubkey = { workspace = true } -solana-quic-client = { workspace = true } -solana-rpc = { workspace = true } -solana-rpc-client = { workspace = true } -solana-rpc-client-api = { workspace = true } -solana-runtime = { workspace = true, features = ["dev-context-only-utils"] } -solana-signature = { workspace = true } -solana-signer = { workspace = true } -solana-streamer = { workspace = true } -solana-system-interface = { workspace = true } -solana-time-utils = { workspace = true } -solana-tps-client = { workspace = true } -solana-tpu-client = { workspace = true } -solana-transaction = { workspace = true } -solana-transaction-status = { workspace = true } -solana-version = { workspace = true } -spl-instruction-padding-interface = { version = "=1.0.0" } - -[target.'cfg(not(any(target_env = "msvc", target_os = "freebsd")))'.dependencies] -jemallocator = { workspace = true } - -[dev-dependencies] -agave-feature-set = { workspace = true } -serial_test = { workspace = true } -solana-faucet = { workspace = true, features = ["dev-context-only-utils"] } -solana-local-cluster = { workspace = true } -solana-rent = { workspace = true } -solana-runtime = { workspace = true, features = ["dev-context-only-utils"] } -solana-sdk-ids = { workspace = true } -solana-test-validator = { workspace = true } -solana-tps-client = { workspace = true, features = ["bank-client"] } -tempfile = { workspace = true } diff --git a/bench-tps/src/bench.rs b/bench-tps/src/bench.rs deleted file mode 100644 index cc9e0b1b621..00000000000 --- a/bench-tps/src/bench.rs +++ /dev/null @@ -1,1511 +0,0 @@ -use { - crate::{ - cli::{ComputeUnitPrice, Config, InstructionPaddingConfig}, - log_transaction_service::{ - SignatureBatchSender, TransactionInfoBatch, create_log_transactions_service_and_sender, - }, - perf_utils::{SampleStats, sample_txs}, - send_batch::*, - }, - chrono::Utc, - log::*, - rand::distr::{Distribution, Uniform}, - rayon::prelude::*, - solana_account::Account, - solana_client::nonce_utils, - solana_clock::{DEFAULT_MS_PER_SLOT, DEFAULT_S_PER_SLOT, MAX_PROCESSING_AGE}, - solana_compute_budget_interface::ComputeBudgetInstruction, - solana_hash::Hash, - solana_instruction::{AccountMeta, Instruction}, - solana_keypair::Keypair, - solana_message::{ - Message, VersionedMessage, - v1::{Message as V1Message, TransactionConfig}, - }, - solana_metrics::{self, datapoint_info}, - solana_native_token::Sol, - solana_pubkey::Pubkey, - solana_rpc_client_api::request::MAX_MULTIPLE_ACCOUNTS, - solana_signer::Signer, - solana_system_interface::instruction as system_instruction, - solana_time_utils::timestamp, - solana_tps_client::*, - solana_transaction::{Transaction, versioned::VersionedTransaction}, - spl_instruction_padding_interface::instruction::wrap_instruction, - std::{ - collections::{HashSet, VecDeque}, - process::exit, - sync::{ - Arc, RwLock, - atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering}, - }, - thread::{Builder, JoinHandle, sleep}, - time::{Duration, Instant}, - }, -}; - -// The point at which transactions become "too old", in seconds. -const MAX_TX_QUEUE_AGE: u64 = (MAX_PROCESSING_AGE as f64 * DEFAULT_S_PER_SLOT) as u64; - -// Add prioritization fee to transfer transactions, if `compute_unit_price` is set. -// If `Random` the compute-unit-price is determined by generating a random number in the range -// 0..MAX_RANDOM_COMPUTE_UNIT_PRICE then multiplying by COMPUTE_UNIT_PRICE_MULTIPLIER. -// If `Fixed` the compute-unit-price is the value of the `compute-unit-price` parameter. -// It also sets transaction's compute-unit to TRANSFER_TRANSACTION_COMPUTE_UNIT. Therefore the -// max additional cost is: -// `TRANSFER_TRANSACTION_COMPUTE_UNIT * MAX_COMPUTE_UNIT_PRICE * COMPUTE_UNIT_PRICE_MULTIPLIER / 1_000_000` -const MAX_RANDOM_COMPUTE_UNIT_PRICE: u64 = 50; -const COMPUTE_UNIT_PRICE_MULTIPLIER: u64 = 1_000; -const TRANSFER_TRANSACTION_COMPUTE_UNIT: u32 = 600; // 1 transfer is plus 3 compute_budget ixs -const PADDED_TRANSFER_COMPUTE_UNIT: u32 = 3_000; // padding program execution requires consumes this amount - -/// calculate maximum possible prioritization fee, if `use-randomized-compute-unit-price` is -/// enabled, round to nearest lamports. -pub fn max_lamports_for_prioritization(compute_unit_price: &Option) -> u64 { - let Some(compute_unit_price) = compute_unit_price else { - return 0; - }; - - let compute_unit_price = match compute_unit_price { - ComputeUnitPrice::Random => (MAX_RANDOM_COMPUTE_UNIT_PRICE as u128) - .saturating_mul(COMPUTE_UNIT_PRICE_MULTIPLIER as u128), - ComputeUnitPrice::Fixed(compute_unit_price) => *compute_unit_price as u128, - }; - - const MICRO_LAMPORTS_PER_LAMPORT: u64 = 1_000_000; - let micro_lamport_fee: u128 = - compute_unit_price.saturating_mul(TRANSFER_TRANSACTION_COMPUTE_UNIT as u128); - let fee = micro_lamport_fee - .saturating_add(MICRO_LAMPORTS_PER_LAMPORT.saturating_sub(1) as u128) - .saturating_div(MICRO_LAMPORTS_PER_LAMPORT as u128); - u64::try_from(fee).unwrap_or(u64::MAX) -} - -// In case of plain transfer transaction, set loaded account data size to 30K. -// It is large enough yet smaller than 32K page size, so it'd cost 0 extra CU. -const TRANSFER_TRANSACTION_LOADED_ACCOUNTS_DATA_SIZE: u32 = 30 * 1024; -// In case of padding program usage, we need to take into account program size -const PADDING_PROGRAM_ACCOUNT_DATA_SIZE: u32 = 28 * 1024; -fn get_transaction_loaded_accounts_data_size(enable_padding: bool) -> u32 { - if enable_padding { - TRANSFER_TRANSACTION_LOADED_ACCOUNTS_DATA_SIZE + PADDING_PROGRAM_ACCOUNT_DATA_SIZE - } else { - TRANSFER_TRANSACTION_LOADED_ACCOUNTS_DATA_SIZE - } -} - -#[derive(Debug, PartialEq, Default, Eq, Clone)] -pub(crate) struct TimestampedTransaction { - transaction: VersionedTransaction, - timestamp: Option, - compute_unit_price: Option, -} - -pub(crate) type SharedTransactions = Arc>>>; - -/// Keypairs split into source and destination -/// used for transfer transactions -struct KeypairChunks<'a> { - source: Vec>, - dest: Vec>, -} - -impl<'a> KeypairChunks<'a> { - /// Split input slice of keypairs into two sets of chunks of given size - fn new(keypairs: &'a [Keypair], chunk_size: usize) -> Self { - // Use `chunk_size` as the number of conflict groups per chunk so that each destination key is unique - Self::new_with_conflict_groups(keypairs, chunk_size, chunk_size) - } - - /// Split input slice of keypairs into two sets of chunks of given size. Each chunk - /// has a set of source keys and a set of destination keys. There will be - /// `num_conflict_groups_per_chunk` unique destination keys per chunk, so that the - /// destination keys may conflict with each other. - fn new_with_conflict_groups( - keypairs: &'a [Keypair], - chunk_size: usize, - num_conflict_groups_per_chunk: usize, - ) -> Self { - let mut source_keypair_chunks: Vec> = Vec::new(); - let mut dest_keypair_chunks: Vec> = Vec::new(); - for chunk in keypairs.chunks_exact(2 * chunk_size) { - source_keypair_chunks.push(chunk[..chunk_size].iter().collect()); - dest_keypair_chunks.push( - std::iter::repeat(&chunk[chunk_size..chunk_size + num_conflict_groups_per_chunk]) - .flatten() - .take(chunk_size) - .collect(), - ); - } - KeypairChunks { - source: source_keypair_chunks, - dest: dest_keypair_chunks, - } - } -} - -struct TransactionChunkGenerator<'a, 'b, T: ?Sized> { - client: Arc, - account_chunks: KeypairChunks<'a>, - nonce_chunks: Option>, - chunk_index: usize, - reclaim_lamports_back_to_source_account: bool, - compute_unit_price: Option, - instruction_padding_config: Option, - skip_tx_account_data_size: bool, - use_txv1: bool, -} - -impl<'a, 'b, T> TransactionChunkGenerator<'a, 'b, T> -where - T: 'static + TpsClient + Send + Sync + ?Sized, -{ - fn new( - client: Arc, - gen_keypairs: &'a [Keypair], - nonce_keypairs: Option<&'b Vec>, - chunk_size: usize, - compute_unit_price: Option, - instruction_padding_config: Option, - num_conflict_groups: Option, - skip_tx_account_data_size: bool, - use_txv1: bool, - ) -> Self { - let account_chunks = if let Some(num_conflict_groups) = num_conflict_groups { - KeypairChunks::new_with_conflict_groups(gen_keypairs, chunk_size, num_conflict_groups) - } else { - KeypairChunks::new(gen_keypairs, chunk_size) - }; - let nonce_chunks = - nonce_keypairs.map(|nonce_keypairs| KeypairChunks::new(nonce_keypairs, chunk_size)); - - TransactionChunkGenerator { - client, - account_chunks, - nonce_chunks, - chunk_index: 0, - reclaim_lamports_back_to_source_account: false, - compute_unit_price, - instruction_padding_config, - skip_tx_account_data_size, - use_txv1, - } - } - - /// generate transactions to transfer lamports from source to destination accounts - /// if durable nonce is used, blockhash is None - fn generate(&mut self, blockhash: Option<&Hash>) -> Vec { - let tx_count = self.account_chunks.source.len(); - info!( - "Signing transactions... {} (reclaim={}, blockhash={:?})", - tx_count, self.reclaim_lamports_back_to_source_account, blockhash - ); - let signing_start = Instant::now(); - - let source_chunk = &self.account_chunks.source[self.chunk_index]; - let dest_chunk = &self.account_chunks.dest[self.chunk_index]; - let transactions = if let Some(nonce_chunks) = &self.nonce_chunks { - let source_nonce_chunk = &nonce_chunks.source[self.chunk_index]; - let dest_nonce_chunk: &VecDeque<&Keypair> = &nonce_chunks.dest[self.chunk_index]; - generate_nonced_system_txs( - self.client.clone(), - source_chunk, - dest_chunk, - source_nonce_chunk, - dest_nonce_chunk, - self.reclaim_lamports_back_to_source_account, - self.skip_tx_account_data_size, - &self.instruction_padding_config, - ) - } else { - assert!(blockhash.is_some()); - generate_system_txs( - source_chunk, - dest_chunk, - self.reclaim_lamports_back_to_source_account, - blockhash.unwrap(), - &self.instruction_padding_config, - &self.compute_unit_price, - self.skip_tx_account_data_size, - self.use_txv1, - ) - }; - - let duration = signing_start.elapsed(); - let ns = duration.as_secs() * 1_000_000_000 + u64::from(duration.subsec_nanos()); - let bsps = (tx_count) as f64 / ns as f64; - let nsps = ns as f64 / (tx_count) as f64; - info!( - "Done. {:.2} thousand signatures per second, {:.2} us per signature, {} ms total \ - time, {:?}", - bsps * 1_000_000_f64, - nsps / 1_000_f64, - duration.as_millis(), - blockhash, - ); - datapoint_info!( - "bench-tps-generate_txs", - ("duration", duration.as_micros() as i64, i64) - ); - - transactions - } - - fn advance(&mut self) { - // Rotate destination keypairs so that the next round of transactions will have different - // transaction signatures even when blockhash is reused. - self.account_chunks.dest[self.chunk_index].rotate_left(1); - if let Some(nonce_chunks) = &mut self.nonce_chunks { - nonce_chunks.dest[self.chunk_index].rotate_left(1); - } - // Move on to next chunk - self.chunk_index = (self.chunk_index + 1) % self.account_chunks.source.len(); - - // Switch directions after transferring for each "chunk" - if self.chunk_index == 0 { - self.reclaim_lamports_back_to_source_account = - !self.reclaim_lamports_back_to_source_account; - } - } -} - -fn wait_for_target_slots_per_epoch(target_slots_per_epoch: u64, client: &Arc) -where - T: 'static + TpsClient + Send + Sync + ?Sized, -{ - if target_slots_per_epoch != 0 { - info!("Waiting until epochs are {target_slots_per_epoch} slots long..."); - loop { - if let Ok(epoch_info) = client.get_epoch_info() { - if epoch_info.slots_in_epoch >= target_slots_per_epoch { - info!("Done epoch_info: {epoch_info:?}"); - break; - } - info!( - "Waiting for epoch: {} now: {}", - target_slots_per_epoch, epoch_info.slots_in_epoch - ); - } - sleep(Duration::from_secs(3)); - } - } -} - -fn create_sampler_thread( - client: &Arc, - exit_signal: Arc, - sample_period: u64, - maxes: &Arc>>, -) -> JoinHandle<()> -where - T: 'static + TpsClient + Send + Sync + ?Sized, -{ - info!("Sampling TPS every {sample_period} second..."); - let maxes = maxes.clone(); - let client = client.clone(); - Builder::new() - .name("solana-client-sample".to_string()) - .spawn(move || { - sample_txs(exit_signal, &maxes, sample_period, &client); - }) - .unwrap() -} - -fn generate_chunked_transfers( - recent_blockhash: Arc>, - shared_txs: &SharedTransactions, - shared_tx_active_thread_count: Arc, - mut chunk_generator: TransactionChunkGenerator<'_, '_, T>, - threads: usize, - duration: Duration, - sustained: bool, - use_durable_nonce: bool, -) { - // generate and send transactions for the specified duration - let start = Instant::now(); - let mut last_generate_txs_time = Instant::now(); - - while start.elapsed() < duration { - generate_txs( - shared_txs, - &recent_blockhash, - &mut chunk_generator, - threads, - use_durable_nonce, - ); - - datapoint_info!( - "blockhash_stats", - ( - "time_elapsed_since_last_generate_txs", - last_generate_txs_time.elapsed().as_millis(), - i64 - ) - ); - - last_generate_txs_time = Instant::now(); - - // In sustained mode, overlap the transfers with generation. This has higher average - // performance but lower peak performance in tested environments. - if sustained { - // Ensure that we don't generate more transactions than we can handle. - while shared_txs.read().unwrap().len() > 2 * threads { - sleep(Duration::from_millis(1)); - } - } else { - while !shared_txs.read().unwrap().is_empty() - || shared_tx_active_thread_count.load(Ordering::Relaxed) > 0 - { - sleep(Duration::from_millis(1)); - } - } - chunk_generator.advance(); - } -} - -fn create_sender_threads( - client: &Arc, - shared_txs: &SharedTransactions, - thread_batch_sleep_ms: usize, - total_tx_sent_count: &Arc, - threads: usize, - exit_signal: Arc, - shared_tx_active_thread_count: &Arc, - signatures_sender: Option, -) -> Vec> -where - T: 'static + TpsClient + Send + Sync + ?Sized, -{ - (0..threads) - .map(|_| { - let exit_signal = exit_signal.clone(); - let shared_txs = shared_txs.clone(); - let shared_tx_active_thread_count = shared_tx_active_thread_count.clone(); - let total_tx_sent_count = total_tx_sent_count.clone(); - let client = client.clone(); - let signatures_sender = signatures_sender.clone(); - Builder::new() - .name("solana-client-sender".to_string()) - .spawn(move || { - do_tx_transfers( - &exit_signal, - &shared_txs, - &shared_tx_active_thread_count, - &total_tx_sent_count, - thread_batch_sleep_ms, - &client, - signatures_sender, - ); - }) - .unwrap() - }) - .collect() -} - -pub fn do_bench_tps( - client: Arc, - config: Config, - gen_keypairs: Vec, - nonce_keypairs: Option>, -) -> u64 -where - T: 'static + TpsClient + Send + Sync + ?Sized, -{ - let Config { - id, - threads, - thread_batch_sleep_ms, - duration, - tx_count, - sustained, - target_slots_per_epoch, - compute_unit_price, - skip_tx_account_data_size, - use_durable_nonce, - instruction_padding_config, - num_conflict_groups, - block_data_file, - transaction_data_file, - use_txv1, - .. - } = config; - - assert!(gen_keypairs.len() >= 2 * tx_count); - let chunk_generator = TransactionChunkGenerator::new( - client.clone(), - &gen_keypairs, - nonce_keypairs.as_ref(), - tx_count, - compute_unit_price, - instruction_padding_config, - num_conflict_groups, - skip_tx_account_data_size, - use_txv1, - ); - - let first_tx_count = loop { - match client.get_transaction_count() { - Ok(count) => break count, - Err(err) => { - info!("Couldn't get transaction count: {err:?}"); - sleep(Duration::from_secs(1)); - } - } - }; - info!("Initial transaction count {first_tx_count}"); - - let exit_signal = Arc::new(AtomicBool::new(false)); - - // Setup a thread per validator to sample every period - // collect the max transaction rate and total tx count seen - let maxes = Arc::new(RwLock::new(Vec::new())); - let sample_period = 1; // in seconds - let sample_thread = create_sampler_thread(&client, exit_signal.clone(), sample_period, &maxes); - - let shared_txs: SharedTransactions = Arc::new(RwLock::new(VecDeque::new())); - - let blockhash = Arc::new(RwLock::new(get_latest_blockhash(client.as_ref()))); - let shared_tx_active_thread_count = Arc::new(AtomicIsize::new(0)); - let total_tx_sent_count = Arc::new(AtomicUsize::new(0)); - - // if we use durable nonce, we don't need blockhash thread - let blockhash_thread = if !use_durable_nonce { - let exit_signal = exit_signal.clone(); - let blockhash = blockhash.clone(); - let client = client.clone(); - let id = id.pubkey(); - Some( - Builder::new() - .name("solana-blockhash-poller".to_string()) - .spawn(move || { - poll_blockhash(&exit_signal, &blockhash, &client, &id); - }) - .unwrap(), - ) - } else { - None - }; - - let (log_transaction_service, signatures_sender) = create_log_transactions_service_and_sender( - &client, - block_data_file.as_deref(), - transaction_data_file.as_deref(), - ); - - let sender_threads = create_sender_threads( - &client, - &shared_txs, - thread_batch_sleep_ms, - &total_tx_sent_count, - threads, - exit_signal.clone(), - &shared_tx_active_thread_count, - signatures_sender, - ); - - wait_for_target_slots_per_epoch(target_slots_per_epoch, &client); - - let start = Instant::now(); - - generate_chunked_transfers( - blockhash, - &shared_txs, - shared_tx_active_thread_count, - chunk_generator, - threads, - duration, - sustained, - use_durable_nonce, - ); - - // Stop the sampling threads so it will collect the stats - exit_signal.store(true, Ordering::Relaxed); - - info!("Waiting for sampler threads..."); - if let Err(err) = sample_thread.join() { - info!(" join() failed with: {err:?}"); - } - - // join the tx send threads - info!("Waiting for transmit threads..."); - for t in sender_threads { - if let Err(err) = t.join() { - info!(" join() failed with: {err:?}"); - } - } - - if let Some(blockhash_thread) = blockhash_thread { - info!("Waiting for blockhash thread..."); - if let Err(err) = blockhash_thread.join() { - info!(" join() failed with: {err:?}"); - } - } - - if let Some(log_transaction_service) = log_transaction_service { - info!("Waiting for log_transaction_service thread..."); - if let Err(err) = log_transaction_service.join() { - info!(" join() failed with: {err:?}"); - } - } - - if let Some(nonce_keypairs) = nonce_keypairs { - withdraw_durable_nonce_accounts(client.clone(), &gen_keypairs, &nonce_keypairs); - } - - let balance = client.get_balance(&id.pubkey()).unwrap_or(0); - metrics_submit_lamport_balance(balance); - - compute_and_report_stats( - &maxes, - sample_period, - &start.elapsed(), - total_tx_sent_count.load(Ordering::Relaxed), - ); - - let r_maxes = maxes.read().unwrap(); - r_maxes.first().unwrap().1.txs -} - -fn metrics_submit_lamport_balance(lamport_balance: u64) { - info!("Token balance: {lamport_balance}"); - datapoint_info!( - "bench-tps-lamport_balance", - ("balance", lamport_balance, i64) - ); -} - -fn generate_system_txs( - source: &[&Keypair], - dest: &VecDeque<&Keypair>, - reclaim: bool, - blockhash: &Hash, - instruction_padding_config: &Option, - compute_unit_price: &Option, - skip_tx_account_data_size: bool, - use_txv1: bool, -) -> Vec { - let pairs: Vec<_> = if !reclaim { - source.iter().zip(dest.iter()).collect() - } else { - dest.iter().zip(source.iter()).collect() - }; - - if let Some(compute_unit_price) = compute_unit_price { - let compute_unit_prices = match compute_unit_price { - ComputeUnitPrice::Random => { - let mut rng = rand::rng(); - let range = Uniform::try_from(0..MAX_RANDOM_COMPUTE_UNIT_PRICE) - .expect("ok for non-empty range"); - (0..pairs.len()) - .map(|_| { - range - .sample(&mut rng) - .saturating_mul(COMPUTE_UNIT_PRICE_MULTIPLIER) - }) - .collect() - } - ComputeUnitPrice::Fixed(compute_unit_price) => vec![*compute_unit_price; pairs.len()], - }; - - let pairs_with_compute_unit_prices: Vec<_> = - pairs.iter().zip(compute_unit_prices.iter()).collect(); - - pairs_with_compute_unit_prices - .par_iter() - .map(|((from, to), compute_unit_price)| { - let compute_unit_price = Some(**compute_unit_price); - TimestampedTransaction { - transaction: transfer_with_compute_unit_price_and_padding( - from, - &to.pubkey(), - 1, - *blockhash, - instruction_padding_config, - compute_unit_price, - skip_tx_account_data_size, - use_txv1, - ), - timestamp: Some(timestamp()), - compute_unit_price, - } - }) - .collect() - } else { - pairs - .par_iter() - .map(|(from, to)| TimestampedTransaction { - transaction: transfer_with_compute_unit_price_and_padding( - from, - &to.pubkey(), - 1, - *blockhash, - instruction_padding_config, - None, - skip_tx_account_data_size, - use_txv1, - ), - timestamp: Some(timestamp()), - compute_unit_price: None, - }) - .collect() - } -} - -fn transfer_with_compute_unit_price_and_padding( - from_keypair: &Keypair, - to: &Pubkey, - lamports: u64, - recent_blockhash: Hash, - instruction_padding_config: &Option, - compute_unit_price: Option, - skip_tx_account_data_size: bool, - use_txv1: bool, -) -> VersionedTransaction { - if use_txv1 { - transfer_with_compute_unit_price_and_padding_v1( - from_keypair, - to, - lamports, - recent_blockhash, - instruction_padding_config, - compute_unit_price, - skip_tx_account_data_size, - ) - } else { - transfer_with_compute_unit_price_and_padding_legacy( - from_keypair, - to, - lamports, - recent_blockhash, - instruction_padding_config, - compute_unit_price, - skip_tx_account_data_size, - ) - } -} - -fn transfer_with_compute_unit_price_and_padding_legacy( - from_keypair: &Keypair, - to: &Pubkey, - lamports: u64, - recent_blockhash: Hash, - instruction_padding_config: &Option, - compute_unit_price: Option, - skip_tx_account_data_size: bool, -) -> VersionedTransaction { - let from_pubkey = from_keypair.pubkey(); - let transfer_instruction = system_instruction::transfer(&from_pubkey, to, lamports); - let instruction = if let Some(instruction_padding_config) = instruction_padding_config { - wrap_instruction( - instruction_padding_config.program_id, - transfer_instruction, - vec![], - instruction_padding_config.data_size, - ) - .expect("Could not create padded instruction") - } else { - transfer_instruction - }; - let mut instructions = vec![]; - if !skip_tx_account_data_size { - instructions.push( - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( - get_transaction_loaded_accounts_data_size(instruction_padding_config.is_some()), - ), - ) - } - instructions.push(instruction); - if instruction_padding_config.is_some() { - // By default, CU budget is DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT which is much larger than needed - instructions.push(ComputeBudgetInstruction::set_compute_unit_limit( - PADDED_TRANSFER_COMPUTE_UNIT, - )); - } - - if let Some(compute_unit_price) = compute_unit_price { - instructions.extend_from_slice(&[ - ComputeBudgetInstruction::set_compute_unit_limit(TRANSFER_TRANSACTION_COMPUTE_UNIT), - ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price), - ]) - } - let message = Message::new(&instructions, Some(&from_pubkey)); - Transaction::new(&[from_keypair], message, recent_blockhash).into() -} - -fn transfer_with_compute_unit_price_and_padding_v1( - from_keypair: &Keypair, - to: &Pubkey, - lamports: u64, - recent_blockhash: Hash, - instruction_padding_config: &Option, - compute_unit_price: Option, - skip_tx_account_data_size: bool, -) -> VersionedTransaction { - let from_pubkey = from_keypair.pubkey(); - let transfer_instruction = system_instruction::transfer(&from_pubkey, to, lamports); - let instruction = if let Some(instruction_padding_config) = instruction_padding_config { - wrap_instruction( - instruction_padding_config.program_id, - transfer_instruction, - vec![], - instruction_padding_config.data_size, - ) - .expect("Could not create padded instruction") - } else { - transfer_instruction - }; - let instructions = vec![instruction]; - let mut config = TransactionConfig::empty(); - if !skip_tx_account_data_size { - config.loaded_accounts_data_size_limit = Some(get_transaction_loaded_accounts_data_size( - instruction_padding_config.is_some(), - )); - } - if instruction_padding_config.is_some() { - config.compute_unit_limit = Some(PADDED_TRANSFER_COMPUTE_UNIT); - } - - if let Some(compute_unit_price) = compute_unit_price { - config.compute_unit_limit = Some(TRANSFER_TRANSACTION_COMPUTE_UNIT); - config.priority_fee = Some(compute_unit_price); - } - let message = - V1Message::try_compile_with_config(&from_pubkey, &instructions, recent_blockhash, config) - .unwrap(); - let versioned_message = VersionedMessage::V1(message); - VersionedTransaction::try_new(versioned_message, &[from_keypair]).unwrap() -} - -fn get_nonce_accounts( - client: &Arc, - nonce_pubkeys: &[Pubkey], -) -> Vec> { - // get_multiple_accounts supports maximum MAX_MULTIPLE_ACCOUNTS pubkeys in request - assert!(nonce_pubkeys.len() <= MAX_MULTIPLE_ACCOUNTS); - loop { - match client.get_multiple_accounts(nonce_pubkeys) { - Ok(nonce_accounts) => { - return nonce_accounts; - } - Err(err) => { - info!("Couldn't get durable nonce account: {err:?}"); - sleep(Duration::from_secs(1)); - } - } - } -} - -fn get_nonce_blockhashes( - client: &Arc, - nonce_pubkeys: &[Pubkey], -) -> Vec { - let num_accounts = nonce_pubkeys.len(); - let mut blockhashes = vec![Hash::default(); num_accounts]; - let mut unprocessed = (0..num_accounts).collect::>(); - - let mut request_pubkeys = Vec::::with_capacity(num_accounts); - let mut request_indexes = Vec::::with_capacity(num_accounts); - - while !unprocessed.is_empty() { - for i in &unprocessed { - request_pubkeys.push(nonce_pubkeys[*i]); - request_indexes.push(*i); - } - - let num_unprocessed_before = unprocessed.len(); - let accounts: Vec> = nonce_pubkeys - .chunks(MAX_MULTIPLE_ACCOUNTS) - .flat_map(|pubkeys| get_nonce_accounts(client, pubkeys)) - .collect(); - - for (account, index) in accounts.iter().zip(request_indexes.iter()) { - if let Some(nonce_account) = account { - let nonce_data = nonce_utils::data_from_account(nonce_account).unwrap(); - blockhashes[*index] = nonce_data.blockhash(); - unprocessed.remove(index); - } - } - let num_unprocessed_after = unprocessed.len(); - debug!( - "Received {} durable nonce accounts", - num_unprocessed_before - num_unprocessed_after - ); - request_pubkeys.clear(); - request_indexes.clear(); - } - blockhashes -} - -fn nonced_transfer_with_padding( - from_keypair: &Keypair, - to: &Pubkey, - lamports: u64, - nonce_account: &Pubkey, - nonce_authority: &Keypair, - nonce_hash: Hash, - skip_tx_account_data_size: bool, - instruction_padding_config: &Option, -) -> VersionedTransaction { - let from_pubkey = from_keypair.pubkey(); - let transfer_instruction = system_instruction::transfer(&from_pubkey, to, lamports); - let instruction = if let Some(instruction_padding_config) = instruction_padding_config { - wrap_instruction( - instruction_padding_config.program_id, - transfer_instruction, - vec![], - instruction_padding_config.data_size, - ) - .expect("Could not create padded instruction") - } else { - transfer_instruction - }; - let mut instructions = vec![]; - if !skip_tx_account_data_size { - instructions.push( - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit( - get_transaction_loaded_accounts_data_size(instruction_padding_config.is_some()), - ), - ) - } - instructions.push(instruction); - let message = Message::new_with_nonce( - instructions, - Some(&from_pubkey), - nonce_account, - &nonce_authority.pubkey(), - ); - Transaction::new(&[from_keypair, nonce_authority], message, nonce_hash).into() -} - -fn generate_nonced_system_txs( - client: Arc, - source: &[&Keypair], - dest: &VecDeque<&Keypair>, - source_nonce: &[&Keypair], - dest_nonce: &VecDeque<&Keypair>, - reclaim: bool, - skip_tx_account_data_size: bool, - instruction_padding_config: &Option, -) -> Vec { - let length = source.len(); - let mut transactions: Vec = Vec::with_capacity(length); - if !reclaim { - let pubkeys: Vec = source_nonce - .iter() - .map(|keypair| keypair.pubkey()) - .collect(); - - let blockhashes: Vec = get_nonce_blockhashes(&client, &pubkeys); - for i in 0..length { - transactions.push(TimestampedTransaction { - transaction: nonced_transfer_with_padding( - source[i], - &dest[i].pubkey(), - 1, - &source_nonce[i].pubkey(), - source[i], - blockhashes[i], - skip_tx_account_data_size, - instruction_padding_config, - ), - timestamp: None, - compute_unit_price: None, - }); - } - } else { - let pubkeys: Vec = dest_nonce.iter().map(|keypair| keypair.pubkey()).collect(); - let blockhashes: Vec = get_nonce_blockhashes(&client, &pubkeys); - - for i in 0..length { - transactions.push(TimestampedTransaction { - transaction: nonced_transfer_with_padding( - dest[i], - &source[i].pubkey(), - 1, - &dest_nonce[i].pubkey(), - dest[i], - blockhashes[i], - skip_tx_account_data_size, - instruction_padding_config, - ), - timestamp: None, - compute_unit_price: None, - }); - } - } - transactions -} - -fn generate_txs( - shared_txs: &SharedTransactions, - blockhash: &Arc>, - chunk_generator: &mut TransactionChunkGenerator<'_, '_, T>, - threads: usize, - use_durable_nonce: bool, -) { - let transactions = if use_durable_nonce { - chunk_generator.generate(None) - } else { - let blockhash = blockhash.read().map(|x| *x).ok(); - chunk_generator.generate(blockhash.as_ref()) - }; - - let sz = transactions.len() / threads; - let chunks: Vec<_> = transactions.chunks(sz).collect(); - { - let mut shared_txs_wl = shared_txs.write().unwrap(); - for chunk in chunks { - shared_txs_wl.push_back(chunk.to_vec()); - } - } -} - -fn get_new_latest_blockhash( - client: &Arc, - blockhash: &Hash, -) -> Option { - let start = Instant::now(); - while start.elapsed().as_secs() < 5 { - if let Ok(new_blockhash) = client.get_latest_blockhash() { - if new_blockhash != *blockhash { - return Some(new_blockhash); - } - } - debug!("Got same blockhash ({blockhash:?}), will retry..."); - - // Retry ~twice during a slot - sleep(Duration::from_millis(DEFAULT_MS_PER_SLOT / 2)); - } - None -} - -fn poll_blockhash( - exit_signal: &AtomicBool, - blockhash: &Arc>, - client: &Arc, - id: &Pubkey, -) { - let mut blockhash_last_updated = Instant::now(); - let mut last_error_log = Instant::now(); - loop { - let blockhash_updated = { - let old_blockhash = *blockhash.read().unwrap(); - if let Some(new_blockhash) = get_new_latest_blockhash(client, &old_blockhash) { - *blockhash.write().unwrap() = new_blockhash; - blockhash_last_updated = Instant::now(); - true - } else { - if blockhash_last_updated.elapsed().as_secs() > 120 { - eprintln!("Blockhash is stuck"); - exit(1) - } else if blockhash_last_updated.elapsed().as_secs() > 30 - && last_error_log.elapsed().as_secs() >= 1 - { - last_error_log = Instant::now(); - error!("Blockhash is not updating"); - } - false - } - }; - - if blockhash_updated { - let balance = client.get_balance(id).unwrap_or(0); - metrics_submit_lamport_balance(balance); - datapoint_info!( - "blockhash_stats", - ( - "time_elapsed_since_last_blockhash_update", - blockhash_last_updated.elapsed().as_millis(), - i64 - ) - ) - } - - if exit_signal.load(Ordering::Relaxed) { - break; - } - - sleep(Duration::from_millis(50)); - } -} - -fn do_tx_transfers( - exit_signal: &AtomicBool, - shared_txs: &SharedTransactions, - shared_tx_thread_count: &Arc, - total_tx_sent_count: &Arc, - thread_batch_sleep_ms: usize, - client: &Arc, - signatures_sender: Option, -) { - let mut last_sent_time = timestamp(); - 'thread_loop: loop { - if thread_batch_sleep_ms > 0 { - sleep(Duration::from_millis(thread_batch_sleep_ms as u64)); - } - let txs = { - let mut shared_txs_wl = shared_txs.write().expect("write lock in do_tx_transfers"); - shared_txs_wl.pop_front() - }; - if let Some(txs) = txs { - shared_tx_thread_count.fetch_add(1, Ordering::Relaxed); - let num_txs = txs.len(); - info!("Transferring 1 unit {num_txs} times..."); - let transfer_start = Instant::now(); - let mut old_transactions = false; - let mut min_timestamp = u64::MAX; - let mut transactions = Vec::<_>::with_capacity(num_txs); - let mut signatures = Vec::<_>::with_capacity(num_txs); - let mut compute_unit_prices = Vec::<_>::with_capacity(num_txs); - for tx in txs { - let now = timestamp(); - // Transactions without durable nonce that are too old will be rejected by the cluster Don't bother - // sending them. - if let Some(tx_timestamp) = tx.timestamp { - if tx_timestamp < min_timestamp { - min_timestamp = tx_timestamp; - } - if now > tx_timestamp && now - tx_timestamp > 1000 * MAX_TX_QUEUE_AGE { - old_transactions = true; - continue; - } - } - signatures.push(tx.transaction.signatures[0]); - transactions.push(tx.transaction); - compute_unit_prices.push(tx.compute_unit_price); - } - - if min_timestamp != u64::MAX { - datapoint_info!( - "bench-tps-do_tx_transfers", - ("oldest-blockhash-age", timestamp() - min_timestamp, i64), - ); - } - - if let Some(signatures_sender) = &signatures_sender { - if let Err(error) = signatures_sender.send(TransactionInfoBatch { - signatures, - sent_at: Utc::now(), - compute_unit_prices, - }) { - error!( - "Receiver has been dropped with error `{error}`, stop sending \ - transactions." - ); - break 'thread_loop; - } - } - - if let Err(error) = client.send_batch(transactions) { - warn!("send_batch_sync in do_tx_transfers failed: {error}"); - } - - datapoint_info!( - "bench-tps-do_tx_transfers", - ( - "time-elapsed-since-last-send", - timestamp() - last_sent_time, - i64 - ), - ); - - last_sent_time = timestamp(); - - if old_transactions { - let mut shared_txs_wl = shared_txs.write().expect("write lock in do_tx_transfers"); - shared_txs_wl.clear(); - } - shared_tx_thread_count.fetch_add(-1, Ordering::Relaxed); - total_tx_sent_count.fetch_add(num_txs, Ordering::Relaxed); - info!( - "Tx send done. {} ms {} tps", - transfer_start.elapsed().as_millis(), - num_txs as f32 / transfer_start.elapsed().as_secs_f32(), - ); - datapoint_info!( - "bench-tps-do_tx_transfers", - ("duration", transfer_start.elapsed().as_micros() as i64, i64), - ("count", num_txs, i64) - ); - } - if exit_signal.load(Ordering::Relaxed) { - break; - } - } -} - -fn compute_and_report_stats( - maxes: &Arc>>, - sample_period: u64, - tx_send_elapsed: &Duration, - total_tx_send_count: usize, -) { - // Compute/report stats - let mut max_of_maxes = 0.0; - let mut max_tx_count = 0; - let mut nodes_with_zero_tps = 0; - let mut total_maxes = 0.0; - info!(" Node address | Max TPS | Total Transactions"); - info!("---------------------+---------------+--------------------"); - - for (sock, stats) in maxes.read().unwrap().iter() { - let maybe_flag = match stats.txs { - 0 => "!!!!!", - _ => "", - }; - - info!( - "{:20} | {:13.2} | {} {}", - sock, stats.tps, stats.txs, maybe_flag - ); - - if stats.tps == 0.0 { - nodes_with_zero_tps += 1; - } - total_maxes += stats.tps; - - if stats.tps > max_of_maxes { - max_of_maxes = stats.tps; - } - if stats.txs > max_tx_count { - max_tx_count = stats.txs; - } - } - - if total_maxes > 0.0 { - let num_nodes_with_tps = maxes.read().unwrap().len() - nodes_with_zero_tps; - let average_max = total_maxes / num_nodes_with_tps as f32; - info!("\nAverage max TPS: {average_max:.2}, {nodes_with_zero_tps} nodes had 0 TPS"); - } - - let total_tx_send_count = total_tx_send_count as u64; - let drop_rate = if total_tx_send_count > max_tx_count { - (total_tx_send_count - max_tx_count) as f64 / total_tx_send_count as f64 - } else { - 0.0 - }; - info!( - "\nHighest TPS: {:.2} sampling period {}s max transactions: {} clients: {} drop rate: \ - {:.2}", - max_of_maxes, - sample_period, - max_tx_count, - maxes.read().unwrap().len(), - drop_rate, - ); - info!( - "\tAverage TPS: {}", - max_tx_count as f32 / tx_send_elapsed.as_secs_f32() - ); -} - -pub fn generate_and_fund_keypairs( - client: Arc, - funding_key: &Keypair, - keypair_count: usize, - lamports_per_account: u64, - skip_tx_account_data_size: bool, - enable_padding: bool, -) -> TpsClientResult> { - let rent = client.get_minimum_balance_for_rent_exemption(0)?; - let lamports_per_account = lamports_per_account + rent; - - info!("Creating {keypair_count} keypairs..."); - let (mut keypairs, extra) = generate_keypairs(funding_key, keypair_count as u64); - fund_keypairs( - client, - funding_key, - &keypairs, - extra, - lamports_per_account, - skip_tx_account_data_size, - enable_padding, - )?; - - // 'generate_keypairs' generates extra keys to be able to have size-aligned funding batches for fund_keys. - keypairs.truncate(keypair_count); - - Ok(keypairs) -} - -pub fn fund_keypairs( - client: Arc, - funding_key: &Keypair, - keypairs: &[Keypair], - extra: u64, - lamports_per_account: u64, - skip_tx_account_data_size: bool, - enable_padding: bool, -) -> TpsClientResult<()> { - let rent = client.get_minimum_balance_for_rent_exemption(0)?; - info!("Get lamports..."); - - // Sample the first keypair, to prevent lamport loss on repeated solana-bench-tps executions - let first_key = keypairs[0].pubkey(); - let first_keypair_balance = client.get_balance(&first_key).unwrap_or(0); - - // Sample the last keypair, to check if funding was already completed - let last_key = keypairs[keypairs.len() - 1].pubkey(); - let last_keypair_balance = client.get_balance(&last_key).unwrap_or(0); - - // Repeated runs will eat up keypair balances from transaction fees. In order to quickly - // start another bench-tps run without re-funding all of the keypairs, check if the - // keypairs still have at least 80% of the expected funds. That should be enough to - // pay for the transaction fees in a new run. - let enough_lamports = 8 * lamports_per_account / 10; - if first_keypair_balance < enough_lamports || last_keypair_balance < enough_lamports { - let single_sig_message = Message::new_with_blockhash( - &[Instruction::new_with_bytes( - Pubkey::new_unique(), - &[], - vec![AccountMeta::new(Pubkey::new_unique(), true)], - )], - None, - &client.get_latest_blockhash().unwrap(), - ); - let max_fee = client.get_fee_for_message(&single_sig_message).unwrap(); - let extra_fees = extra * max_fee; - let total_keypairs = keypairs.len() as u64 + 1; // Add one for funding keypair - let total = lamports_per_account * total_keypairs + extra_fees; - - let funding_key_balance = client.get_balance(&funding_key.pubkey()).unwrap_or(0); - info!( - "Funding keypair balance: {funding_key_balance} max_fee: {max_fee} \ - lamports_per_account: {lamports_per_account} extra: {extra} total: {total}" - ); - - if funding_key_balance < total + rent { - error!( - "funder has {}, needed {}", - Sol(funding_key_balance), - Sol(total) - ); - let latest_blockhash = get_latest_blockhash(client.as_ref()); - if client - .request_airdrop_with_blockhash( - &funding_key.pubkey(), - total + rent - funding_key_balance, - &latest_blockhash, - ) - .is_err() - { - return Err(TpsClientError::AirdropFailure); - } - } - let data_size_limit = (!skip_tx_account_data_size) - .then(|| get_transaction_loaded_accounts_data_size(enable_padding)); - - fund_keys( - client, - funding_key, - keypairs, - total, - max_fee, - lamports_per_account, - data_size_limit, - ); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use { - super::*, - agave_feature_set::FeatureSet, - solana_commitment_config::CommitmentConfig, - solana_fee_calculator::FeeRateGovernor, - solana_genesis_config::{GenesisConfig, create_genesis_config}, - solana_native_token::LAMPORTS_PER_SOL, - solana_nonce::state::State, - solana_runtime::{bank::Bank, bank_client::BankClient, bank_forks::BankForks}, - }; - - fn bank_with_all_features( - genesis_config: &GenesisConfig, - ) -> (Arc, Arc>) { - let mut bank = Bank::new_for_tests(genesis_config); - bank.feature_set = Arc::new(FeatureSet::all_enabled()); - bank.wrap_with_bank_forks_for_tests() - } - - fn run_bench_tps_bank_client(mut config: Config) { - let (genesis_config, id) = create_genesis_config(10_000 * LAMPORTS_PER_SOL); - config.id = id; - config.tx_count = 10; - config.duration = Duration::from_secs(5); - let (bank, _bank_forks) = bank_with_all_features(&genesis_config); - let client = Arc::new(BankClient::new_shared(bank)); - - let keypair_count = config.tx_count * config.keypair_multiplier; - let keypairs = generate_and_fund_keypairs( - client.clone(), - &config.id, - keypair_count, - 1_000_000_000, - false, - false, - ) - .unwrap(); - let nonce_keypairs = if config.use_durable_nonce { - Some(generate_durable_nonce_accounts(client.clone(), &keypairs)) - } else { - None - }; - - do_bench_tps(client, config, keypairs, nonce_keypairs); - } - - #[test] - fn test_bench_tps_bank_client() { - run_bench_tps_bank_client(Config::default()); - } - - #[test] - fn test_bench_tps_bank_client_nonce() { - let config = Config { - use_durable_nonce: true, - ..Config::default() - }; - run_bench_tps_bank_client(config); - } - - #[test] - fn test_bench_tps_bank_client_with_padding() { - let config = Config { - instruction_padding_config: Some(InstructionPaddingConfig { - program_id: spl_instruction_padding_interface::ID, - data_size: 0, - }), - ..Config::default() - }; - run_bench_tps_bank_client(config); - } - - #[test] - fn test_bench_tps_fund_keys() { - let (genesis_config, id) = create_genesis_config(10_000 * LAMPORTS_PER_SOL); - let (bank, _bank_forks) = bank_with_all_features(&genesis_config); - let client = Arc::new(BankClient::new_shared(bank)); - let keypair_count = 20; - let lamports = 20; - let rent = client.get_minimum_balance_for_rent_exemption(0).unwrap(); - - let keypairs = - generate_and_fund_keypairs(client.clone(), &id, keypair_count, lamports, false, false) - .unwrap(); - - for kp in &keypairs { - assert_eq!( - client - .get_balance_with_commitment(&kp.pubkey(), CommitmentConfig::processed()) - .unwrap(), - lamports + rent - ); - } - } - - #[test] - fn test_bench_tps_fund_keys_with_fees() { - let (mut genesis_config, id) = create_genesis_config(10_000 * LAMPORTS_PER_SOL); - let fee_rate_governor = FeeRateGovernor::new(11, 0); - genesis_config.fee_rate_governor = fee_rate_governor; - let (bank, _bank_forks) = bank_with_all_features(&genesis_config); - let client = Arc::new(BankClient::new_shared(bank)); - let keypair_count = 20; - let lamports = 20; - let rent = client.get_minimum_balance_for_rent_exemption(0).unwrap(); - - let keypairs = - generate_and_fund_keypairs(client.clone(), &id, keypair_count, lamports, false, false) - .unwrap(); - - for kp in &keypairs { - assert_eq!(client.get_balance(&kp.pubkey()).unwrap(), lamports + rent); - } - } - - #[test] - fn test_bench_tps_create_durable_nonce() { - let (genesis_config, id) = create_genesis_config(10_000 * LAMPORTS_PER_SOL); - let (bank, _bank_forks) = bank_with_all_features(&genesis_config); - let client = Arc::new(BankClient::new_shared(bank)); - let keypair_count = 10; - let lamports = 10_000_000; - - let authority_keypairs = - generate_and_fund_keypairs(client.clone(), &id, keypair_count, lamports, false, false) - .unwrap(); - - let nonce_keypairs = generate_durable_nonce_accounts(client.clone(), &authority_keypairs); - - let rent = client - .get_minimum_balance_for_rent_exemption(State::size()) - .unwrap(); - for kp in &nonce_keypairs { - assert_eq!( - client - .get_balance_with_commitment(&kp.pubkey(), CommitmentConfig::processed()) - .unwrap(), - rent - ); - } - withdraw_durable_nonce_accounts(client, &authority_keypairs, &nonce_keypairs) - } - - #[test] - fn test_bench_tps_key_chunks_new() { - let num_keypairs = 16; - let chunk_size = 4; - let keypairs = std::iter::repeat_with(Keypair::new) - .take(num_keypairs) - .collect::>(); - - let chunks = KeypairChunks::new(&keypairs, chunk_size); - assert_eq!( - chunks.source[0], - &[&keypairs[0], &keypairs[1], &keypairs[2], &keypairs[3]] - ); - assert_eq!( - chunks.dest[0], - &[&keypairs[4], &keypairs[5], &keypairs[6], &keypairs[7]] - ); - assert_eq!( - chunks.source[1], - &[&keypairs[8], &keypairs[9], &keypairs[10], &keypairs[11]] - ); - assert_eq!( - chunks.dest[1], - &[&keypairs[12], &keypairs[13], &keypairs[14], &keypairs[15]] - ); - } - - #[test] - fn test_bench_tps_key_chunks_new_with_conflict_groups() { - let num_keypairs = 16; - let chunk_size = 4; - let num_conflict_groups = 2; - let keypairs = std::iter::repeat_with(Keypair::new) - .take(num_keypairs) - .collect::>(); - - let chunks = - KeypairChunks::new_with_conflict_groups(&keypairs, chunk_size, num_conflict_groups); - assert_eq!( - chunks.source[0], - &[&keypairs[0], &keypairs[1], &keypairs[2], &keypairs[3]] - ); - assert_eq!( - chunks.dest[0], - &[&keypairs[4], &keypairs[5], &keypairs[4], &keypairs[5]] - ); - assert_eq!( - chunks.source[1], - &[&keypairs[8], &keypairs[9], &keypairs[10], &keypairs[11]] - ); - assert_eq!( - chunks.dest[1], - &[&keypairs[12], &keypairs[13], &keypairs[12], &keypairs[13]] - ); - } -} diff --git a/bench-tps/src/cli.rs b/bench-tps/src/cli.rs deleted file mode 100644 index 0211fbc982c..00000000000 --- a/bench-tps/src/cli.rs +++ /dev/null @@ -1,734 +0,0 @@ -use { - clap::{App, Arg, ArgMatches, crate_description, crate_name, value_t_or_exit}, - solana_clap_utils::{ - hidden_unless_forced, - input_validators::{is_keypair, is_url, is_url_or_moniker, is_within_range}, - }, - solana_cli_config::{CONFIG_FILE, ConfigInput}, - solana_commitment_config::CommitmentConfig, - solana_fee_calculator::FeeRateGovernor, - solana_keypair::{Keypair, read_keypair_file}, - solana_pubkey::Pubkey, - solana_streamer::quic::DEFAULT_MAX_CONNECTIONS_PER_IPADDR_PER_MINUTE, - solana_tpu_client::tpu_client::DEFAULT_TPU_CONNECTION_POOL_SIZE, - std::{ - net::{IpAddr, Ipv4Addr}, - time::Duration, - }, -}; - -const NUM_LAMPORTS_PER_ACCOUNT_DEFAULT: u64 = solana_native_token::LAMPORTS_PER_SOL; - -#[derive(Eq, PartialEq, Debug, Default)] -pub enum ExternalClientType { - // Submits transactions to an Rpc node using an RpcClient - RpcClient, - // Submits transactions directly to leaders using a TpuClient, broadcasting to upcoming leaders - // via TpuClient default configuration - #[default] - TpuClient, -} - -#[derive(Eq, PartialEq, Debug)] -pub struct InstructionPaddingConfig { - pub program_id: Pubkey, - pub data_size: u32, -} - -#[derive(Debug, PartialEq)] -pub enum ComputeUnitPrice { - Fixed(u64), - Random, -} - -/// Holds the configuration for a single run of the benchmark -#[derive(PartialEq, Debug)] -pub struct Config { - pub json_rpc_url: String, - pub websocket_url: String, - pub id: Keypair, - pub threads: usize, - pub duration: Duration, - pub tx_count: usize, - pub keypair_multiplier: usize, - pub thread_batch_sleep_ms: usize, - pub sustained: bool, - pub client_ids_and_stake_file: String, - pub write_to_client_file: bool, - pub read_from_client_file: bool, - pub target_lamports_per_signature: u64, - pub num_lamports_per_account: u64, - pub target_slots_per_epoch: u64, - pub external_client_type: ExternalClientType, - pub tpu_connection_pool_size: usize, - pub tpu_max_connections_per_ipaddr_per_minute: u64, - pub compute_unit_price: Option, - pub skip_tx_account_data_size: bool, - pub use_durable_nonce: bool, - pub instruction_padding_config: Option, - pub num_conflict_groups: Option, - pub bind_address: IpAddr, - pub client_node_id: Option, - pub commitment_config: CommitmentConfig, - pub block_data_file: Option, - pub transaction_data_file: Option, - pub use_txv1: bool, -} - -impl Eq for Config {} - -impl Default for Config { - fn default() -> Config { - Config { - json_rpc_url: ConfigInput::default().json_rpc_url, - websocket_url: ConfigInput::default().websocket_url, - id: Keypair::new(), - threads: 4, - duration: Duration::new(u64::MAX, 0), - tx_count: 50_000, - keypair_multiplier: 8, - thread_batch_sleep_ms: 1000, - sustained: false, - client_ids_and_stake_file: String::new(), - write_to_client_file: false, - read_from_client_file: false, - target_lamports_per_signature: FeeRateGovernor::default().target_lamports_per_signature, - num_lamports_per_account: NUM_LAMPORTS_PER_ACCOUNT_DEFAULT, - target_slots_per_epoch: 0, - external_client_type: ExternalClientType::default(), - tpu_connection_pool_size: DEFAULT_TPU_CONNECTION_POOL_SIZE, - tpu_max_connections_per_ipaddr_per_minute: - DEFAULT_MAX_CONNECTIONS_PER_IPADDR_PER_MINUTE, - compute_unit_price: None, - skip_tx_account_data_size: false, - use_durable_nonce: false, - instruction_padding_config: None, - num_conflict_groups: None, - bind_address: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), - client_node_id: None, - commitment_config: CommitmentConfig::confirmed(), - block_data_file: None, - transaction_data_file: None, - use_txv1: false, - } - } -} - -/// Defines and builds the CLI args for a run of the benchmark -pub fn build_args<'a>(version: &'_ str) -> App<'a, '_> { - App::new(crate_name!()) - .about(crate_description!()) - .version(version) - .arg({ - let arg = Arg::with_name("config_file") - .short("C") - .long("config") - .value_name("FILEPATH") - .takes_value(true) - .global(true) - .help("Configuration file to use"); - if let Some(ref config_file) = *CONFIG_FILE { - arg.default_value(config_file) - } else { - arg - } - }) - .arg( - Arg::with_name("json_rpc_url") - .short("u") - .long("url") - .value_name("URL_OR_MONIKER") - .takes_value(true) - .global(true) - .validator(is_url_or_moniker) - .help( - "URL for Solana's JSON RPC or moniker (or their first letter): [mainnet-beta, \ - testnet, devnet, localhost]", - ), - ) - .arg( - Arg::with_name("websocket_url") - .long("ws") - .value_name("URL") - .takes_value(true) - .global(true) - .validator(is_url) - .help("WebSocket URL for the solana cluster"), - ) - .arg( - Arg::with_name("faucet") - .short("d") - .long("faucet") - .value_name("HOST:PORT") - .takes_value(true) - .hidden(hidden_unless_forced()) - .help("Deprecated. BenchTps no longer queries the faucet directly"), - ) - .arg( - Arg::with_name("identity") - .short("i") - .long("identity") - .value_name("PATH") - .takes_value(true) - .hidden(hidden_unless_forced()) - .help("Deprecated. Use --authority instead"), - ) - .arg( - Arg::with_name("authority") - .short("a") - .long("authority") - .value_name("PATH") - .takes_value(true) - .help( - "File containing a client authority (keypair) to fund participating accounts", - ), - ) - .arg( - Arg::with_name("threads") - .short("t") - .long("threads") - .value_name("NUM") - .takes_value(true) - .help("Number of threads"), - ) - .arg( - Arg::with_name("duration") - .long("duration") - .value_name("SECS") - .takes_value(true) - .help("Seconds to run benchmark, then exit; default is forever"), - ) - .arg(Arg::with_name("sustained").long("sustained").help( - "Use sustained performance mode vs. peak mode. This overlaps the tx generation with \ - transfers.", - )) - .arg( - Arg::with_name("tx_count") - .long("tx-count") - .alias("tx_count") - .value_name("NUM") - .takes_value(true) - .help("Number of transactions to send per batch"), - ) - .arg( - Arg::with_name("keypair_multiplier") - .long("keypair-multiplier") - .value_name("NUM") - .takes_value(true) - .help("Multiply by transaction count to determine number of keypairs to create"), - ) - .arg( - Arg::with_name("thread-batch-sleep-ms") - .short("z") - .long("thread-batch-sleep-ms") - .value_name("NUM") - .takes_value(true) - .help("Per-thread-per-iteration sleep in ms"), - ) - .arg( - Arg::with_name("write-client-keys") - .long("write-client-keys") - .value_name("FILENAME") - .takes_value(true) - .help("Generate client keys and stakes and write the list to YAML file"), - ) - .arg( - Arg::with_name("read-client-keys") - .long("read-client-keys") - .value_name("FILENAME") - .takes_value(true) - .help("Read client keys and stakes from the YAML file"), - ) - .arg( - Arg::with_name("target_lamports_per_signature") - .long("target-lamports-per-signature") - .value_name("LAMPORTS") - .takes_value(true) - .help( - "The cost in lamports that the cluster will charge for signature verification \ - when the cluster is operating at target-signatures-per-slot", - ), - ) - .arg( - Arg::with_name("num_lamports_per_account") - .long("num-lamports-per-account") - .value_name("LAMPORTS") - .takes_value(true) - .help("Number of lamports per account."), - ) - .arg( - Arg::with_name("target_slots_per_epoch") - .long("target-slots-per-epoch") - .value_name("SLOTS") - .takes_value(true) - .help("Wait until epochs are this many slots long."), - ) - .arg( - Arg::with_name("rpc_client") - .long("use-rpc-client") - .conflicts_with("tpu_client") - .takes_value(false) - .help("Submit transactions with a RpcClient"), - ) - .arg( - Arg::with_name("tpu_client") - .long("use-tpu-client") - .conflicts_with("rpc_client") - .takes_value(false) - .help("Submit transactions with a TpuClient"), - ) - .arg( - Arg::with_name("tpu_connection_pool_size") - .long("tpu-connection-pool-size") - .takes_value(true) - .help( - "Controls the connection pool size per remote address; only affects TpuClient \ - (default) sends", - ), - ) - .arg( - Arg::with_name("compute_unit_price") - .long("compute-unit-price") - .takes_value(true) - .validator(|s| is_within_range(s, 0..)) - .help("Sets constant compute-unit-price to transfer transactions"), - ) - .arg( - Arg::with_name("use_randomized_compute_unit_price") - .long("use-randomized-compute-unit-price") - .takes_value(false) - .conflicts_with("compute_unit_price") - .help("Sets random compute-unit-price in range [0..100] to transfer transactions"), - ) - .arg( - Arg::with_name("skip_tx_account_data_size") - .long("skip-tx-account-data-size") - .takes_value(false) - .conflicts_with("instruction_padding_data_size") - .help("Skips setting the account data size for each transaction"), - ) - .arg( - Arg::with_name("use_durable_nonce") - .long("use-durable-nonce") - .help("Use durable transaction nonce instead of recent blockhash"), - ) - .arg( - Arg::with_name("instruction_padding_program_id") - .long("instruction-padding-program-id") - .requires("instruction_padding_data_size") - .takes_value(true) - .value_name("PUBKEY") - .help( - "If instruction data is padded, optionally specify the padding program id to \ - target", - ), - ) - .arg( - Arg::with_name("instruction_padding_data_size") - .long("instruction-padding-data-size") - .takes_value(true) - .help( - "If set, wraps all instructions in the instruction padding program, with the \ - given amount of padding bytes in instruction data.", - ), - ) - .arg( - Arg::with_name("num_conflict_groups") - .long("num-conflict-groups") - .takes_value(true) - .validator(|arg| is_within_range(arg, 1..)) - .help( - "The number of unique destination accounts per transactions 'chunk'. Lower \ - values will result in more transaction conflicts.", - ), - ) - .arg( - Arg::with_name("bind_address") - .long("bind-address") - .value_name("HOST") - .takes_value(true) - .validator(solana_net_utils::is_host) - .requires("client_node_id") - .help("IP address to use with connection cache"), - ) - .arg( - Arg::with_name("client_node_id") - .long("client-node-id") - .value_name("PATH") - .takes_value(true) - .requires("json_rpc_url") - .validator(is_keypair) - .help( - "File containing the node identity (keypair) of a validator with active \ - stake. This allows communicating with network using staked connection", - ), - ) - .arg( - Arg::with_name("commitment_config") - .long("commitment-config") - .takes_value(true) - .possible_values(&["processed", "confirmed", "finalized"]) - .default_value("confirmed") - .help("Block commitment config for getting latest blockhash"), - ) - .arg( - Arg::with_name("block_data_file") - .long("block-data-file") - .value_name("FILENAME") - .takes_value(true) - .help("File to save block statistics relevant to the submitted transactions."), - ) - .arg( - Arg::with_name("transaction_data_file") - .long("transaction-data-file") - .value_name("FILENAME") - .takes_value(true) - .help( - "File to save details about all the submitted transactions.This option is \ - useful for debug purposes.", - ), - ) - .arg( - Arg::with_name("use_txv1") - .long("use-txv1") - .takes_value(false) - .help("Generate and send Transfer transaction in V1 format"), - ) -} - -/// Parses a clap `ArgMatches` structure into a `Config` -pub fn parse_args(matches: &ArgMatches) -> Result { - let mut args = Config::default(); - - let config = if let Some(config_file) = matches.value_of("config_file") { - solana_cli_config::Config::load(config_file).unwrap_or_default() - } else { - solana_cli_config::Config::default() - }; - let (_, json_rpc_url) = ConfigInput::compute_json_rpc_url_setting( - matches.value_of("json_rpc_url").unwrap_or(""), - &config.json_rpc_url, - ); - args.json_rpc_url = json_rpc_url; - - let (_, websocket_url) = ConfigInput::compute_websocket_url_setting( - matches.value_of("websocket_url").unwrap_or(""), - &config.websocket_url, - matches.value_of("json_rpc_url").unwrap_or(""), - &config.json_rpc_url, - ); - args.websocket_url = websocket_url; - - if matches.is_present("identity") { - eprintln!("Warning: --identity is deprecated. Please use --authority"); - } - - let (_, id_path) = ConfigInput::compute_keypair_path_setting( - matches - .value_of("authority") - .or(matches.value_of("identity")) - .unwrap_or(""), - &config.keypair_path, - ); - if let Ok(id) = read_keypair_file(id_path) { - args.id = id; - } else if matches.is_present("identity") || matches.is_present("authority") { - return Err("could not parse authority path"); - } - - if matches.is_present("rpc_client") { - args.external_client_type = ExternalClientType::RpcClient; - } - - if let Some(v) = matches.value_of("tpu_connection_pool_size") { - args.tpu_connection_pool_size = v - .to_string() - .parse::() - .map_err(|_| "can't parse tpu-connection-pool-size")?; - } - - if let Some(t) = matches.value_of("threads") { - args.threads = t.to_string().parse().map_err(|_| "can't parse threads")?; - } - - if let Some(duration) = matches.value_of("duration") { - let seconds = duration - .to_string() - .parse() - .map_err(|_| "can't parse duration")?; - args.duration = Duration::new(seconds, 0); - } - - if let Some(s) = matches.value_of("tx_count") { - args.tx_count = s.to_string().parse().map_err(|_| "can't parse tx_count")?; - } - - if let Some(s) = matches.value_of("keypair_multiplier") { - args.keypair_multiplier = s - .to_string() - .parse() - .map_err(|_| "can't parse keypair-multiplier")?; - if args.keypair_multiplier < 2 { - return Err("args.keypair_multiplier must be greater than or equal to 2"); - } - } - - if let Some(t) = matches.value_of("thread-batch-sleep-ms") { - args.thread_batch_sleep_ms = t - .to_string() - .parse() - .map_err(|_| "can't parse thread-batch-sleep-ms")?; - } - - args.sustained = matches.is_present("sustained"); - - if let Some(s) = matches.value_of("write-client-keys") { - args.write_to_client_file = true; - args.client_ids_and_stake_file = s.to_string(); - } - - if let Some(s) = matches.value_of("read-client-keys") { - assert!(!args.write_to_client_file); - args.read_from_client_file = true; - args.client_ids_and_stake_file = s.to_string(); - } - - if let Some(v) = matches.value_of("target_lamports_per_signature") { - args.target_lamports_per_signature = v - .to_string() - .parse() - .map_err(|_| "can't parse target-lamports-per-signature")?; - } - - if let Some(v) = matches.value_of("num_lamports_per_account") { - args.num_lamports_per_account = v - .to_string() - .parse() - .map_err(|_| "can't parse num-lamports-per-account")?; - } - - if let Some(t) = matches.value_of("target_slots_per_epoch") { - args.target_slots_per_epoch = t - .to_string() - .parse() - .map_err(|_| "can't parse target-slots-per-epoch")?; - } - - if let Some(str) = matches.value_of("compute_unit_price") { - args.compute_unit_price = Some(ComputeUnitPrice::Fixed( - str.parse().map_err(|_| "can't parse compute-unit-price")?, - )); - } - - if matches.is_present("use_randomized_compute_unit_price") { - args.compute_unit_price = Some(ComputeUnitPrice::Random); - } - - if matches.is_present("skip_tx_account_data_size") { - args.skip_tx_account_data_size = true; - } - - if matches.is_present("use_durable_nonce") { - args.use_durable_nonce = true; - } - - if let Some(data_size) = matches.value_of("instruction_padding_data_size") { - let program_id = matches - .value_of("instruction_padding_program_id") - .map(|target_str| target_str.parse().unwrap()) - .unwrap_or_else(|| spl_instruction_padding_interface::ID); - let data_size = data_size - .parse() - .map_err(|_| "Can't parse padded instruction data size")?; - args.instruction_padding_config = Some(InstructionPaddingConfig { - program_id, - data_size, - }); - } - - if let Some(num_conflict_groups) = matches.value_of("num_conflict_groups") { - let parsed_num_conflict_groups = num_conflict_groups - .parse() - .map_err(|_| "Can't parse num-conflict-groups")?; - args.num_conflict_groups = Some(parsed_num_conflict_groups); - } - - if let Some(addr) = matches.value_of("bind_address") { - args.bind_address = - solana_net_utils::parse_host(addr).map_err(|_| "Failed to parse bind-address")?; - } - - if let Some(client_node_id_filename) = matches.value_of("client_node_id") { - // error is checked by arg validator - let client_node_id = read_keypair_file(client_node_id_filename).map_err(|_| "")?; - args.client_node_id = Some(client_node_id); - } - - args.commitment_config = value_t_or_exit!(matches, "commitment_config", CommitmentConfig); - args.block_data_file = matches.value_of("block_data_file").map(|s| s.to_string()); - args.transaction_data_file = matches - .value_of("transaction_data_file") - .map(|s| s.to_string()); - - args.use_txv1 = matches.is_present("use_txv1"); - - Ok(args) -} - -#[cfg(test)] -mod tests { - use { - super::*, - solana_keypair::{Keypair, read_keypair_file, write_keypair_file}, - solana_signer::Signer, - std::{ - net::{IpAddr, Ipv4Addr}, - time::Duration, - }, - tempfile::{TempDir, tempdir}, - }; - - /// create a keypair and write it to json file in temporary directory - /// return both generated keypair and full file name - fn write_tmp_keypair(out_dir: &TempDir) -> (Keypair, String) { - let keypair = Keypair::new(); - let file_path = out_dir - .path() - .join(format!("keypair_file-{}", keypair.pubkey())); - let keypair_file_name = file_path.into_os_string().into_string().unwrap(); - write_keypair_file(&keypair, &keypair_file_name).unwrap(); - (keypair, keypair_file_name) - } - - #[test] - #[allow(clippy::cognitive_complexity)] - fn test_cli_parse() { - // create a directory inside of std::env::temp_dir(), removed when out_dir goes out of scope - let out_dir = tempdir().unwrap(); - let (keypair, keypair_file_name) = write_tmp_keypair(&out_dir); - - // parse provided rpc address, check that default ws address is correct - // always specify authority in these tests because otherwise a random one will be used - let matches = build_args("1.0.0").get_matches_from(vec![ - "solana-bench-tps", - "--authority", - &keypair_file_name, - "-u", - "http://123.4.5.6:8899", - ]); - let actual = parse_args(&matches).unwrap(); - assert_eq!( - actual, - Config { - json_rpc_url: "http://123.4.5.6:8899".to_string(), - websocket_url: "ws://123.4.5.6:8900/".to_string(), - id: keypair, - ..Config::default() - } - ); - - // check if --identity is working - let keypair = read_keypair_file(&keypair_file_name).unwrap(); - let matches = build_args("1.0.0").get_matches_from(vec![ - "solana-bench-tps", - "--identity", - &keypair_file_name, - "-u", - "http://123.4.5.6:8899", - ]); - let actual = parse_args(&matches).unwrap(); - assert_eq!( - actual, - Config { - json_rpc_url: "http://123.4.5.6:8899".to_string(), - websocket_url: "ws://123.4.5.6:8900/".to_string(), - id: keypair, - ..Config::default() - } - ); - - // parse cli args typical for private cluster tests - let keypair = read_keypair_file(&keypair_file_name).unwrap(); - let matches = build_args("1.0.0").get_matches_from(vec![ - "solana-bench-tps", - "--authority", - &keypair_file_name, - "-u", - "http://123.4.5.6:8899", - "--duration", - "1000", - "--sustained", - "--threads", - "4", - "--read-client-keys", - "./client-accounts.yml", - ]); - let actual = parse_args(&matches).unwrap(); - assert_eq!( - actual, - Config { - json_rpc_url: "http://123.4.5.6:8899".to_string(), - websocket_url: "ws://123.4.5.6:8900/".to_string(), - id: keypair, - duration: Duration::new(1000, 0), - sustained: true, - threads: 4, - read_from_client_file: true, - client_ids_and_stake_file: "./client-accounts.yml".to_string(), - ..Config::default() - } - ); - - // select different client type and CommitmentConfig - let keypair = read_keypair_file(&keypair_file_name).unwrap(); - let matches = build_args("1.0.0").get_matches_from(vec![ - "solana-bench-tps", - "--authority", - &keypair_file_name, - "-u", - "http://123.4.5.6:8899", - "--use-rpc-client", - "--commitment-config", - "finalized", - ]); - let actual = parse_args(&matches).unwrap(); - assert_eq!( - actual, - Config { - json_rpc_url: "http://123.4.5.6:8899".to_string(), - websocket_url: "ws://123.4.5.6:8900/".to_string(), - id: keypair, - external_client_type: ExternalClientType::RpcClient, - commitment_config: CommitmentConfig::finalized(), - ..Config::default() - } - ); - - // with client node id - let keypair = read_keypair_file(&keypair_file_name).unwrap(); - let (client_id, client_id_file_name) = write_tmp_keypair(&out_dir); - let matches = build_args("1.0.0").get_matches_from(vec![ - "solana-bench-tps", - "--authority", - &keypair_file_name, - "-u", - "http://192.0.0.1:8899", - "--bind-address", - "192.9.8.7", - "--client-node-id", - &client_id_file_name, - ]); - let actual = parse_args(&matches).unwrap(); - assert_eq!( - actual, - Config { - json_rpc_url: "http://192.0.0.1:8899".to_string(), - websocket_url: "ws://192.0.0.1:8900/".to_string(), - id: keypair, - bind_address: IpAddr::V4(Ipv4Addr::new(192, 9, 8, 7)), - client_node_id: Some(client_id), - ..Config::default() - } - ); - } -} diff --git a/bench-tps/src/keypairs.rs b/bench-tps/src/keypairs.rs deleted file mode 100644 index b278bc0e619..00000000000 --- a/bench-tps/src/keypairs.rs +++ /dev/null @@ -1,82 +0,0 @@ -use { - crate::bench::{fund_keypairs, generate_and_fund_keypairs}, - log::*, - solana_genesis::Base64Account, - solana_keypair::Keypair, - solana_signer::Signer, - solana_tps_client::TpsClient, - std::{collections::HashMap, fs::File, path::Path, process::exit, sync::Arc}, -}; - -pub fn get_keypairs( - client: Arc, - id: &Keypair, - keypair_count: usize, - num_lamports_per_account: u64, - client_ids_and_stake_file: &str, - read_from_client_file: bool, - skip_tx_account_data_size: bool, - enable_padding: bool, -) -> Vec -where - T: 'static + TpsClient + Send + Sync + ?Sized, -{ - if read_from_client_file { - let path = Path::new(client_ids_and_stake_file); - let file = File::open(path).unwrap(); - - info!("Reading {client_ids_and_stake_file}"); - let accounts: HashMap = serde_yaml::from_reader(file).unwrap(); - let mut keypairs = vec![]; - let mut last_balance = 0; - - accounts - .into_iter() - .for_each(|(keypair, primordial_account)| { - let bytes: Vec = serde_json::from_str(keypair.as_str()).unwrap(); - keypairs.push(Keypair::try_from(bytes.as_ref()).unwrap()); - last_balance = primordial_account.balance; - }); - - if keypairs.len() < keypair_count { - eprintln!( - "Expected {} accounts in {}, only received {} (--tx_count mismatch?)", - keypair_count, - client_ids_and_stake_file, - keypairs.len(), - ); - exit(1); - } - // Sort keypairs so that do_bench_tps() uses the same subset of accounts for each run. - // This prevents the amount of storage needed for bench-tps accounts from creeping up - // across multiple runs. - keypairs.sort_by_key(|x| x.pubkey().to_string()); - fund_keypairs( - client, - id, - &keypairs, - keypairs.len().saturating_sub(keypair_count) as u64, - last_balance, - skip_tx_account_data_size, - enable_padding, - ) - .unwrap_or_else(|e| { - eprintln!("Error could not fund keys: {e:?}"); - exit(1); - }); - keypairs - } else { - generate_and_fund_keypairs( - client, - id, - keypair_count, - num_lamports_per_account, - skip_tx_account_data_size, - enable_padding, - ) - .unwrap_or_else(|e| { - eprintln!("Error could not fund keys: {e:?}"); - exit(1); - }) - } -} diff --git a/bench-tps/src/lib.rs b/bench-tps/src/lib.rs deleted file mode 100644 index 395f684a6a4..00000000000 --- a/bench-tps/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -#![cfg(feature = "agave-unstable-api")] -#![allow(clippy::arithmetic_side_effects)] -pub mod bench; -pub mod cli; -pub mod keypairs; -mod log_transaction_service; -mod perf_utils; -mod rpc_with_retry_utils; -pub mod send_batch; diff --git a/bench-tps/src/log_transaction_service.rs b/bench-tps/src/log_transaction_service.rs deleted file mode 100644 index 3b8a1f45b07..00000000000 --- a/bench-tps/src/log_transaction_service.rs +++ /dev/null @@ -1,496 +0,0 @@ -//! `LogTransactionService` requests confirmed blocks, analyses transactions submitted by bench-tps, -//! and saves log files in csv format. - -use { - crate::rpc_with_retry_utils::{get_blocks_with_retry, get_slot_with_retry}, - chrono::{DateTime, TimeZone, Utc}, - crossbeam_channel::{Receiver, Sender, select, tick, unbounded}, - log::*, - serde::Serialize, - solana_clock::{DEFAULT_MS_PER_SLOT, MAX_PROCESSING_AGE, Slot}, - solana_commitment_config::{CommitmentConfig, CommitmentLevel}, - solana_measure::measure::Measure, - solana_rpc_client_api::config::RpcBlockConfig, - solana_signature::Signature, - solana_tps_client::TpsClient, - solana_transaction_status::{ - EncodedTransactionWithStatusMeta, RewardType, TransactionDetails, UiConfirmedBlock, - UiTransactionEncoding, UiTransactionStatusMeta, option_serializer::OptionSerializer, - }, - std::{ - collections::HashMap, - fs::File, - sync::Arc, - thread::{self, Builder, JoinHandle}, - time::Duration, - }, -}; - -// Data to establish communication between sender thread and -// LogTransactionService. -#[derive(Clone)] -pub(crate) struct TransactionInfoBatch { - pub signatures: Vec, - pub sent_at: DateTime, - pub compute_unit_prices: Vec>, -} - -pub(crate) type SignatureBatchSender = Sender; - -pub(crate) struct LogTransactionService { - thread_handler: JoinHandle<()>, -} - -pub(crate) fn create_log_transactions_service_and_sender( - client: &Arc, - block_data_file: Option<&str>, - transaction_data_file: Option<&str>, -) -> (Option, Option) -where - Client: 'static + TpsClient + Send + Sync + ?Sized, -{ - if data_file_provided(block_data_file, transaction_data_file) { - let (sender, receiver) = unbounded(); - let log_tx_service = - LogTransactionService::new(client, receiver, block_data_file, transaction_data_file); - (Some(log_tx_service), Some(sender)) - } else { - (None, None) - } -} - -// How many blocks to process during one iteration. -// The time to process blocks is dominated by get_block calls. -// Each call takes slightly less time than slot. -const NUM_SLOTS_PER_ITERATION: u64 = 16; -// How often process blocks. -const PROCESS_BLOCKS_EVERY_MS: u64 = NUM_SLOTS_PER_ITERATION * DEFAULT_MS_PER_SLOT; -// Max age for transaction in the transaction map, older transactions are cleaned up and marked as timeout. -const REMOVE_TIMEOUT_TX_EVERY_MS: i64 = MAX_PROCESSING_AGE as i64 * DEFAULT_MS_PER_SLOT as i64; - -// Map used to filter submitted transactions. -#[derive(Clone)] -struct TransactionSendInfo { - pub sent_at: DateTime, - pub compute_unit_price: Option, -} -type MapSignatureToTxInfo = HashMap; - -type SignatureBatchReceiver = Receiver; - -impl LogTransactionService { - fn new( - client: &Arc, - signature_receiver: SignatureBatchReceiver, - block_data_file: Option<&str>, - transaction_data_file: Option<&str>, - ) -> Self - where - Client: 'static + TpsClient + Send + Sync + ?Sized, - { - if !data_file_provided(block_data_file, transaction_data_file) { - panic!( - "Expect block-data-file or transaction-data-file is specified, must have been \ - verified by callee." - ); - } - - let client = client.clone(); - let tx_log_writer = TransactionLogWriter::new(transaction_data_file); - let block_log_writer = BlockLogWriter::new(block_data_file); - - let thread_handler = Builder::new() - .name("LogTransactionService".to_string()) - .spawn(move || { - Self::run(client, signature_receiver, tx_log_writer, block_log_writer); - }) - .expect("LogTransactionService should have started successfully."); - Self { thread_handler } - } - - pub fn join(self) -> thread::Result<()> { - self.thread_handler.join() - } - - fn run( - client: Arc, - signature_receiver: SignatureBatchReceiver, - mut tx_log_writer: TransactionLogWriter, - mut block_log_writer: BlockLogWriter, - ) where - Client: 'static + TpsClient + Send + Sync + ?Sized, - { - // used to request blocks data and only confirmed makes sense in this context. - let commitment: CommitmentConfig = CommitmentConfig { - commitment: CommitmentLevel::Confirmed, - }; - let block_processing_timer_receiver = tick(Duration::from_millis(PROCESS_BLOCKS_EVERY_MS)); - - let mut start_slot = get_slot_with_retry(&client, commitment).expect( - "get_slot_with_retry should have succeed, cannot proceed without having slot. Must be \ - a problem with RPC.", - ); - - let mut sender_stopped = false; - let mut signature_to_tx_info = MapSignatureToTxInfo::new(); - loop { - select! { - recv(signature_receiver) -> msg => { - match msg { - Ok(TransactionInfoBatch { - signatures, - sent_at, - compute_unit_prices - }) => { - signatures.iter().zip(compute_unit_prices).for_each( |(sign, compute_unit_price)| {signature_to_tx_info.insert(*sign, TransactionSendInfo { - sent_at, - compute_unit_price - });}); - } - Err(_) => { - sender_stopped = true; - } - } - }, - recv(block_processing_timer_receiver) -> _ => { - info!("sign_receiver queue len: {}", signature_receiver.len()); - if !signature_receiver.is_empty() { - continue; - } - let mut measure_get_blocks = Measure::start("measure_get_blocks"); - let block_slots = get_blocks_with_retry(&client, start_slot, Some(start_slot + NUM_SLOTS_PER_ITERATION - 1), commitment); - measure_get_blocks.stop(); - let time_get_blocks_us = measure_get_blocks.as_us(); - info!("Time to get_blocks : {time_get_blocks_us}us."); - let Ok(block_slots) = block_slots else { - error!("Failed to get blocks, stop LogWriterService."); - break; - }; - if block_slots.is_empty() { - continue; - } - let last_block_time = Self::process_blocks( - &client, - block_slots, - &mut signature_to_tx_info, - &mut tx_log_writer, - &mut block_log_writer, - commitment, - ); - Self::clean_transaction_map(&mut tx_log_writer, &mut signature_to_tx_info, last_block_time); - - start_slot = start_slot.saturating_add(NUM_SLOTS_PER_ITERATION); - tx_log_writer.flush(); - block_log_writer.flush(); - if sender_stopped && signature_to_tx_info.is_empty() { - info!("Stop LogTransactionService"); - break; - } - }, - } - } - } - - /// Download and process the blocks. - /// Returns the time when the last processed block has been confirmed or now(). - fn process_blocks( - client: &Arc, - block_slots: Vec, - signature_to_tx_info: &mut MapSignatureToTxInfo, - tx_log_writer: &mut TransactionLogWriter, - block_log_writer: &mut BlockLogWriter, - commitment: CommitmentConfig, - ) -> DateTime - where - Client: 'static + TpsClient + Send + Sync + ?Sized, - { - let rpc_block_config = RpcBlockConfig { - encoding: Some(UiTransactionEncoding::Base64), - transaction_details: Some(TransactionDetails::Full), - rewards: Some(true), - commitment: Some(commitment), - max_supported_transaction_version: Some(0), - }; - let mut measure_process_blocks = Measure::start("measure_process_blocks"); - let blocks = block_slots - .iter() - .map(|slot| client.get_block_with_config(*slot, rpc_block_config)); - let num_blocks = blocks.len(); - let mut last_block_time = None; - for (block, slot) in blocks.zip(&block_slots) { - let Ok(block) = block else { - continue; - }; - let block_time = Self::process_block( - block, - signature_to_tx_info, - *slot, - tx_log_writer, - block_log_writer, - ); - // if last_time is some, it means that there is at least one valid block - if block_time.is_some() { - last_block_time = block_time; - } - } - measure_process_blocks.stop(); - let time_process_blocks_us = measure_process_blocks.as_us(); - info!("Time to process {num_blocks} blocks: {time_process_blocks_us}us."); - last_block_time.unwrap_or_else(Utc::now) - } - - fn process_block( - block: UiConfirmedBlock, - signature_to_tx_info: &mut MapSignatureToTxInfo, - slot: u64, - tx_log_writer: &mut TransactionLogWriter, - block_log_writer: &mut BlockLogWriter, - ) -> Option> { - let rewards = block - .rewards - .as_ref() - .expect("Rewards should be part of the block information."); - let slot_leader = rewards - .iter() - .find(|r| r.reward_type == Some(RewardType::Fee)) - .map_or("".to_string(), |x| x.pubkey.clone()); - - let Some(transactions) = &block.transactions else { - warn!("Empty block: {slot}"); - return None; - }; - - let mut num_bench_tps_transactions: usize = 0; - let mut total_cu_consumed: u64 = 0; - let mut bench_tps_cu_consumed: u64 = 0; - for EncodedTransactionWithStatusMeta { - transaction, meta, .. - } in transactions - { - let Some(transaction) = transaction.decode() else { - continue; - }; - let cu_consumed = meta - .as_ref() - .map_or(0, |meta| match meta.compute_units_consumed { - OptionSerializer::Some(cu_consumed) => cu_consumed, - _ => 0, - }); - let signature = &transaction.signatures[0]; - - total_cu_consumed = total_cu_consumed.saturating_add(cu_consumed); - if let Some(TransactionSendInfo { - sent_at, - compute_unit_price, - }) = signature_to_tx_info.remove(signature) - { - num_bench_tps_transactions = num_bench_tps_transactions.saturating_add(1); - bench_tps_cu_consumed = bench_tps_cu_consumed.saturating_add(cu_consumed); - - tx_log_writer.write( - Some(block.blockhash.clone()), - Some(slot_leader.clone()), - signature, - sent_at, - Some(slot), - block.block_time, - meta.as_ref(), - false, - compute_unit_price, - ); - } - } - block_log_writer.write( - block.blockhash.clone(), - slot_leader, - slot, - block.block_time, - num_bench_tps_transactions, - transactions.len(), - bench_tps_cu_consumed, - total_cu_consumed, - ); - - block.block_time.map(|time| { - Utc.timestamp_opt(time, 0) - .latest() - .expect("valid timestamp") - }) - } - - /// Remove from map all the signatures which we haven't processed before and they are - /// older than the timestamp of the last processed block plus max blockhash age. - fn clean_transaction_map( - tx_log_writer: &mut TransactionLogWriter, - signature_to_tx_info: &mut MapSignatureToTxInfo, - last_block_time: DateTime, - ) { - signature_to_tx_info.retain(|signature, tx_info| { - let duration_since_sent = last_block_time.signed_duration_since(tx_info.sent_at); - let is_timeout_tx = duration_since_sent.num_milliseconds() > REMOVE_TIMEOUT_TX_EVERY_MS; - if is_timeout_tx { - tx_log_writer.write( - None, - None, - signature, - tx_info.sent_at, - None, - None, - None, - true, - tx_info.compute_unit_price, - ); - } - !is_timeout_tx - }); - } -} - -fn data_file_provided(block_data_file: Option<&str>, transaction_data_file: Option<&str>) -> bool { - block_data_file.is_some() || transaction_data_file.is_some() -} - -type CsvFileWriter = csv::Writer; - -#[derive(Clone, Serialize)] -struct BlockData { - pub blockhash: String, - pub block_slot: Slot, - pub slot_leader: String, - pub block_time: Option>, - pub total_num_transactions: usize, - pub num_bench_tps_transactions: usize, - pub total_cu_consumed: u64, - pub bench_tps_cu_consumed: u64, -} - -struct BlockLogWriter { - log_writer: Option, -} - -impl BlockLogWriter { - fn new(block_data_file: Option<&str>) -> Self { - let block_log_writer = block_data_file.map(|block_data_file| { - CsvFileWriter::from_writer( - File::create(block_data_file) - .expect("Application should be able to create a file."), - ) - }); - Self { - log_writer: block_log_writer, - } - } - - #[allow(clippy::too_many_arguments)] - fn write( - &mut self, - blockhash: String, - slot_leader: String, - slot: Slot, - block_time: Option, - num_bench_tps_transactions: usize, - total_num_transactions: usize, - bench_tps_cu_consumed: u64, - total_cu_consumed: u64, - ) { - let Some(block_log_writer) = &mut self.log_writer else { - return; - }; - let block_data = BlockData { - blockhash, - slot_leader, - block_slot: slot, - block_time: block_time.map(|time| { - Utc.timestamp_opt(time, 0) - .latest() - .expect("timestamp should be valid") - }), - num_bench_tps_transactions, - total_num_transactions, - bench_tps_cu_consumed, - total_cu_consumed, - }; - let _ = block_log_writer.serialize(block_data); - } - - fn flush(&mut self) { - if let Some(block_log_writer) = &mut self.log_writer { - let _ = block_log_writer.flush(); - } - } -} - -#[derive(Clone, Serialize)] -struct TransactionData { - pub blockhash: Option, - pub slot_leader: Option, - pub signature: String, - pub sent_at: Option>, - pub confirmed_slot: Option, - pub block_time: Option>, - pub successful: bool, - pub error: Option, - pub timed_out: bool, - pub compute_unit_price: u64, -} - -struct TransactionLogWriter { - log_writer: Option, -} - -impl TransactionLogWriter { - fn new(transaction_data_file: Option<&str>) -> Self { - let transaction_log_writer = transaction_data_file.map(|transaction_data_file| { - CsvFileWriter::from_writer( - File::create(transaction_data_file) - .expect("Application should be able to create a file."), - ) - }); - Self { - log_writer: transaction_log_writer, - } - } - - #[allow(clippy::too_many_arguments)] - fn write( - &mut self, - blockhash: Option, - slot_leader: Option, - signature: &Signature, - sent_at: DateTime, - confirmed_slot: Option, - block_time: Option, - meta: Option<&UiTransactionStatusMeta>, - timed_out: bool, - compute_unit_price: Option, - ) { - let Some(transaction_log_writer) = &mut self.log_writer else { - return; - }; - let tx_data = TransactionData { - blockhash, - slot_leader, - signature: signature.to_string(), - sent_at: Some(sent_at), - confirmed_slot, - block_time: block_time.map(|time| { - Utc.timestamp_opt(time, 0) - .latest() - .expect("valid timestamp") - }), - successful: meta.as_ref().is_some_and(|m| m.status.is_ok()), - error: meta - .as_ref() - .and_then(|m| m.err.as_ref().map(|x| x.to_string())), - timed_out, - compute_unit_price: compute_unit_price.unwrap_or(0), - }; - let _ = transaction_log_writer.serialize(tx_data); - } - - fn flush(&mut self) { - if let Some(transaction_log_writer) = &mut self.log_writer { - let _ = transaction_log_writer.flush(); - } - } -} diff --git a/bench-tps/src/main.rs b/bench-tps/src/main.rs deleted file mode 100644 index 76ad2367335..00000000000 --- a/bench-tps/src/main.rs +++ /dev/null @@ -1,277 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -use { - log::*, - solana_bench_tps::{ - bench::{do_bench_tps, max_lamports_for_prioritization}, - cli::{self, ExternalClientType}, - keypairs::get_keypairs, - send_batch::{generate_durable_nonce_accounts, generate_keypairs}, - }, - solana_client::connection_cache::ConnectionCache, - solana_commitment_config::CommitmentConfig, - solana_fee_calculator::FeeRateGovernor, - solana_genesis::Base64Account, - solana_keypair::Keypair, - solana_pubkey::Pubkey, - solana_rpc_client::rpc_client::RpcClient, - solana_signer::Signer, - solana_streamer::streamer::StakedNodes, - solana_system_interface::program as system_program, - solana_tps_client::TpsClient, - solana_tpu_client::tpu_client::{TpuClient, TpuClientConfig}, - std::{ - collections::HashMap, - fs::File, - io::prelude::*, - net::IpAddr, - path::Path, - process::exit, - sync::{Arc, RwLock}, - }, -}; - -#[cfg(not(any(target_env = "msvc", target_os = "freebsd")))] -#[global_allocator] -static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; - -/// Number of signatures for all transactions in ~1 week at ~100K TPS -pub const NUM_SIGNATURES_FOR_TXS: u64 = 100_000 * 60 * 60 * 24 * 7; - -/// Request information about node's stake -/// If fail to get requested information, return error -/// Otherwise return stake of the node -/// along with total activated stake of the network -fn find_node_activated_stake( - rpc_client: Arc, - node_id: Pubkey, -) -> Result<(u64, u64), ()> { - let vote_accounts = rpc_client.get_vote_accounts(); - if let Err(error) = vote_accounts { - error!("Failed to get vote accounts, error: {error}"); - return Err(()); - } - - let vote_accounts = vote_accounts.unwrap(); - - let total_active_stake: u64 = vote_accounts - .current - .iter() - .map(|vote_account| vote_account.activated_stake) - .sum(); - - let node_id_as_str = node_id.to_string(); - let find_result = vote_accounts - .current - .iter() - .find(|&vote_account| vote_account.node_pubkey == node_id_as_str); - match find_result { - Some(value) => Ok((value.activated_stake, total_active_stake)), - None => { - error!("Failed to find stake for requested node"); - Err(()) - } - } -} - -fn create_connection_cache( - json_rpc_url: &str, - tpu_connection_pool_size: usize, - bind_address: IpAddr, - client_node_id: Option<&Keypair>, - commitment_config: CommitmentConfig, -) -> ConnectionCache { - if client_node_id.is_none() { - return ConnectionCache::new_quic( - "bench-tps-connection_cache_quic", - tpu_connection_pool_size, - ); - } - - let rpc_client = Arc::new(RpcClient::new_with_commitment( - json_rpc_url.to_string(), - commitment_config, - )); - - let client_node_id = client_node_id.unwrap(); - let (stake, total_stake) = - find_node_activated_stake(rpc_client, client_node_id.pubkey()).unwrap_or_default(); - info!("Stake for specified client_node_id: {stake}, total stake: {total_stake}"); - let stakes = HashMap::from([ - (client_node_id.pubkey(), stake), - (Pubkey::new_unique(), total_stake - stake), - ]); - let staked_nodes = Arc::new(RwLock::new(StakedNodes::new( - Arc::new(stakes), - HashMap::::default(), // overrides - ))); - ConnectionCache::new_with_client_options( - "bench-tps-connection_cache_quic", - tpu_connection_pool_size, - None, - Some((client_node_id, bind_address)), - Some((&staked_nodes, &client_node_id.pubkey())), - ) -} - -#[allow(clippy::too_many_arguments)] -fn create_client( - external_client_type: &ExternalClientType, - json_rpc_url: &str, - websocket_url: &str, - connection_cache: ConnectionCache, - commitment_config: CommitmentConfig, -) -> Arc { - match external_client_type { - ExternalClientType::RpcClient => Arc::new(RpcClient::new_with_commitment( - json_rpc_url.to_string(), - commitment_config, - )), - ExternalClientType::TpuClient => { - let rpc_client = Arc::new(RpcClient::new_with_commitment( - json_rpc_url.to_string(), - commitment_config, - )); - match connection_cache { - ConnectionCache::Udp(cache) => Arc::new( - TpuClient::new_with_connection_cache( - rpc_client, - websocket_url, - TpuClientConfig::default(), - cache, - ) - .unwrap_or_else(|err| { - eprintln!("Could not create TpuClient {err:?}"); - exit(1); - }), - ), - ConnectionCache::Quic(cache) => Arc::new( - TpuClient::new_with_connection_cache( - rpc_client, - websocket_url, - TpuClientConfig::default(), - cache, - ) - .unwrap_or_else(|err| { - eprintln!("Could not create TpuClient {err:?}"); - exit(1); - }), - ), - } - } - } -} - -fn main() { - agave_logger::setup_with_default_filter(); - solana_metrics::set_panic_hook("bench-tps", /*version:*/ None); - - let matches = cli::build_args(solana_version::version!()).get_matches(); - let cli_config = match cli::parse_args(&matches) { - Ok(config) => config, - Err(error) => { - eprintln!("{error}"); - exit(1); - } - }; - - let cli::Config { - json_rpc_url, - websocket_url, - id, - tx_count, - keypair_multiplier, - client_ids_and_stake_file, - write_to_client_file, - read_from_client_file, - target_lamports_per_signature, - num_lamports_per_account, - external_client_type, - tpu_connection_pool_size, - skip_tx_account_data_size, - compute_unit_price, - use_durable_nonce, - instruction_padding_config, - bind_address, - client_node_id, - commitment_config, - .. - } = &cli_config; - - let keypair_count = *tx_count * keypair_multiplier; - if *write_to_client_file { - info!("Generating {keypair_count} keypairs"); - let (keypairs, _) = generate_keypairs(id, keypair_count as u64); - let num_accounts = keypairs.len() as u64; - let max_fee = FeeRateGovernor::new(*target_lamports_per_signature, 0) - .max_lamports_per_signature - .saturating_add(max_lamports_for_prioritization(compute_unit_price)); - let num_lamports_per_account = - (NUM_SIGNATURES_FOR_TXS * max_fee).div_ceil(num_accounts) + num_lamports_per_account; - let mut accounts = HashMap::new(); - keypairs.iter().for_each(|keypair| { - accounts.insert( - serde_json::to_string(&keypair.to_bytes().to_vec()).unwrap(), - Base64Account { - balance: num_lamports_per_account, - executable: false, - owner: system_program::id().to_string(), - data: String::new(), - }, - ); - }); - - info!("Writing {client_ids_and_stake_file}"); - let serialized = serde_yaml::to_string(&accounts).unwrap(); - let path = Path::new(&client_ids_and_stake_file); - let mut file = File::create(path).unwrap(); - file.write_all(b"---\n").unwrap(); - file.write_all(&serialized.into_bytes()).unwrap(); - return; - } - - let connection_cache = create_connection_cache( - json_rpc_url, - *tpu_connection_pool_size, - *bind_address, - client_node_id.as_ref(), - *commitment_config, - ); - let client = create_client( - external_client_type, - json_rpc_url, - websocket_url, - connection_cache, - *commitment_config, - ); - if let Some(instruction_padding_config) = instruction_padding_config { - info!( - "Checking for existence of instruction padding program: {}", - instruction_padding_config.program_id - ); - client - .get_account(&instruction_padding_config.program_id) - .expect( - "Instruction padding program must be deployed to this cluster. Deploy the program \ - using `solana program deploy \ - ./bench-tps/tests/fixtures/spl_instruction_padding.so` and pass the resulting \ - program id with `--instruction-padding-program-id`", - ); - } - let keypairs = get_keypairs( - client.clone(), - id, - keypair_count, - *num_lamports_per_account, - client_ids_and_stake_file, - *read_from_client_file, - *skip_tx_account_data_size, - instruction_padding_config.is_some(), - ); - - let nonce_keypairs = if *use_durable_nonce { - Some(generate_durable_nonce_accounts(client.clone(), &keypairs)) - } else { - None - }; - do_bench_tps(client, cli_config, keypairs, nonce_keypairs); -} diff --git a/bench-tps/src/perf_utils.rs b/bench-tps/src/perf_utils.rs deleted file mode 100644 index 0f33ff84963..00000000000 --- a/bench-tps/src/perf_utils.rs +++ /dev/null @@ -1,86 +0,0 @@ -use { - log::*, - solana_commitment_config::CommitmentConfig, - solana_tps_client::TpsClient, - std::{ - sync::{ - Arc, RwLock, - atomic::{AtomicBool, Ordering}, - }, - thread::sleep, - time::{Duration, Instant}, - }, -}; - -#[derive(Default)] -pub struct SampleStats { - /// Maximum TPS reported by this node - pub tps: f32, - /// Total transactions reported by this node - pub txs: u64, -} - -pub fn sample_txs( - exit_signal: Arc, - sample_stats: &Arc>>, - sample_period: u64, - client: &Arc, -) where - T: TpsClient + ?Sized, -{ - let mut max_tps = 0.0; - let mut total_elapsed; - let mut total_txs; - let mut now = Instant::now(); - let start_time = now; - let initial_txs = client - .get_transaction_count_with_commitment(CommitmentConfig::processed()) - .expect("transaction count"); - let mut last_txs = initial_txs; - - loop { - total_elapsed = start_time.elapsed(); - let elapsed = now.elapsed(); - now = Instant::now(); - let mut txs = - match client.get_transaction_count_with_commitment(CommitmentConfig::processed()) { - Err(e) => { - info!("Couldn't get transaction count {e:?}"); - sleep(Duration::from_secs(sample_period)); - continue; - } - Ok(tx_count) => tx_count, - }; - - if txs < last_txs { - info!("Expected txs({txs}) >= last_txs({last_txs})"); - txs = last_txs; - } - total_txs = txs - initial_txs; - let sample_txs = txs - last_txs; - last_txs = txs; - - let tps = sample_txs as f32 / elapsed.as_secs_f32(); - if tps > max_tps { - max_tps = tps; - } - - info!( - "Sampler {:9.2} TPS, Transactions: {:6}, Total transactions: {} over {} s", - tps, - sample_txs, - total_txs, - total_elapsed.as_secs(), - ); - - if exit_signal.load(Ordering::Relaxed) { - let stats = SampleStats { - tps: max_tps, - txs: total_txs, - }; - sample_stats.write().unwrap().push((client.addr(), stats)); - return; - } - sleep(Duration::from_secs(sample_period)); - } -} diff --git a/bench-tps/src/rpc_with_retry_utils.rs b/bench-tps/src/rpc_with_retry_utils.rs deleted file mode 100644 index 89f2aaa35be..00000000000 --- a/bench-tps/src/rpc_with_retry_utils.rs +++ /dev/null @@ -1,60 +0,0 @@ -use { - log::*, - solana_clock::{DEFAULT_MS_PER_SLOT, Slot}, - solana_commitment_config::CommitmentConfig, - solana_tps_client::{TpsClient, TpsClientResult}, - std::{sync::Arc, thread::sleep, time::Duration}, -}; - -const NUM_RETRY: u64 = 5; -const RETRY_EVERY_MS: u64 = 4 * DEFAULT_MS_PER_SLOT; - -fn call_rpc_with_retry(f: Func, retry_warning: &str) -> TpsClientResult -where - Func: Fn() -> TpsClientResult, -{ - let mut iretry = 0; - loop { - match f() { - Ok(slot) => { - return Ok(slot); - } - Err(error) => { - if iretry == NUM_RETRY { - return Err(error); - } - warn!("{retry_warning}: {error}, retry."); - sleep(Duration::from_millis(RETRY_EVERY_MS)); - } - } - iretry += 1; - } -} - -pub(crate) fn get_slot_with_retry( - client: &Arc, - commitment: CommitmentConfig, -) -> TpsClientResult -where - Client: 'static + TpsClient + Send + Sync + ?Sized, -{ - call_rpc_with_retry( - || client.get_slot_with_commitment(commitment), - "Failed to get slot", - ) -} - -pub(crate) fn get_blocks_with_retry( - client: &Arc, - start_slot: Slot, - end_slot: Option, - commitment: CommitmentConfig, -) -> TpsClientResult> -where - Client: 'static + TpsClient + Send + Sync + ?Sized, -{ - call_rpc_with_retry( - || client.get_blocks_with_commitment(start_slot, end_slot, commitment), - "Failed to download blocks", - ) -} diff --git a/bench-tps/src/send_batch.rs b/bench-tps/src/send_batch.rs deleted file mode 100644 index 6ecef8dccc6..00000000000 --- a/bench-tps/src/send_batch.rs +++ /dev/null @@ -1,519 +0,0 @@ -use { - log::*, - rayon::prelude::*, - solana_commitment_config::CommitmentConfig, - solana_compute_budget_interface::ComputeBudgetInstruction, - solana_core::gen_keys::GenKeys, - solana_hash::Hash, - solana_keypair::Keypair, - solana_measure::measure::Measure, - solana_message::Message, - solana_nonce::state::State, - solana_pubkey::Pubkey, - solana_signer::{Signer, signers::Signers}, - solana_system_interface::instruction as system_instruction, - solana_tps_client::*, - solana_transaction::Transaction, - std::{ - collections::HashSet, - marker::Send, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, AtomicUsize, Ordering}, - }, - thread::sleep, - time::{Duration, Instant}, - }, -}; - -pub fn get_latest_blockhash(client: &T) -> Hash { - loop { - match client.get_latest_blockhash() { - Ok(blockhash) => return blockhash, - Err(err) => { - info!("Couldn't get last blockhash: {err:?}"); - sleep(Duration::from_secs(1)); - } - }; - } -} - -pub fn generate_keypairs(seed_keypair: &Keypair, count: u64) -> (Vec, u64) { - let mut seed = [0u8; 32]; - seed.copy_from_slice(&seed_keypair.to_bytes()[..32]); - let mut rnd = GenKeys::new(seed); - - let mut total_keys = 0; - let mut extra = 0; // This variable tracks the number of keypairs needing extra transaction fees funded - let mut delta = 1; - while total_keys < count { - extra += delta; - delta *= MAX_SPENDS_PER_TX; - total_keys += delta; - } - (rnd.gen_n_keypairs(total_keys), extra) -} - -/// fund the dests keys by spending all of the source keys into MAX_SPENDS_PER_TX -/// on every iteration. This allows us to replay the transfers because the source is either empty, -/// or full -pub fn fund_keys( - client: Arc, - source: &Keypair, - dests: &[Keypair], - total: u64, - max_fee: u64, - lamports_per_account: u64, - data_size_limit: Option, -) { - let mut funded: Vec<&Keypair> = vec![source]; - let mut funded_funds = total; - let mut not_funded: Vec<&Keypair> = dests.iter().collect(); - while !not_funded.is_empty() { - // Build to fund list and prepare funding sources for next iteration - let mut new_funded: Vec<&Keypair> = vec![]; - let mut to_fund: Vec<(&Keypair, Vec<(Pubkey, u64)>)> = vec![]; - let to_lamports = (funded_funds - lamports_per_account - max_fee) / MAX_SPENDS_PER_TX; - for f in funded { - let start = not_funded.len() - MAX_SPENDS_PER_TX as usize; - let dests: Vec<_> = not_funded.drain(start..).collect(); - let spends: Vec<_> = dests.iter().map(|k| (k.pubkey(), to_lamports)).collect(); - to_fund.push((f, spends)); - new_funded.extend(dests); - } - - to_fund.chunks(FUND_CHUNK_LEN).for_each(|chunk| { - Vec::<(&Keypair, Transaction)>::with_capacity(chunk.len()).fund( - &client, - chunk, - to_lamports, - data_size_limit, - ); - }); - - info!("funded: {} left: {}", new_funded.len(), not_funded.len()); - funded = new_funded; - funded_funds = to_lamports; - } -} - -pub fn generate_durable_nonce_accounts( - client: Arc, - authority_keypairs: &[Keypair], -) -> Vec { - let nonce_rent = client - .get_minimum_balance_for_rent_exemption(State::size()) - .unwrap(); - - let seed_keypair = &authority_keypairs[0]; - let count = authority_keypairs.len(); - let (mut nonce_keypairs, _extra) = generate_keypairs(seed_keypair, count as u64); - nonce_keypairs.truncate(count); - - info!("Creating {count} nonce accounts..."); - let to_fund: Vec = authority_keypairs - .iter() - .zip(nonce_keypairs.iter()) - .map(|x| NonceCreateSigners(x.0, x.1)) - .collect(); - - to_fund.chunks(FUND_CHUNK_LEN).for_each(|chunk| { - NonceCreateContainer::with_capacity(chunk.len()) - .create_accounts(&client, chunk, nonce_rent); - }); - nonce_keypairs -} - -pub fn withdraw_durable_nonce_accounts( - client: Arc, - authority_keypairs: &[Keypair], - nonce_keypairs: &[Keypair], -) { - let to_withdraw: Vec = authority_keypairs - .iter() - .zip(nonce_keypairs.iter()) - .map(|x| NonceWithdrawSigners(x.0, x.1.pubkey())) - .collect(); - - to_withdraw.chunks(FUND_CHUNK_LEN).for_each(|chunk| { - NonceWithdrawContainer::with_capacity(chunk.len()).withdraw_accounts(&client, chunk); - }); -} - -const MAX_SPENDS_PER_TX: u64 = 4; - -// Size of the chunk of transactions -// try to transfer a "few" at a time with recent blockhash -// assume 4MB network buffers, and 512 byte packets -const FUND_CHUNK_LEN: usize = 4 * 1024 * 1024 / 512; - -fn verify_funding_transfer( - client: &Arc, - tx: &Transaction, - amount: u64, -) -> bool { - for a in &tx.message().account_keys[1..] { - match client.get_balance_with_commitment(a, CommitmentConfig::processed()) { - Ok(balance) => return balance >= amount, - Err(err) => error!("failed to get balance {err:?}"), - } - } - false -} - -/// Helper trait to encapsulate common logic for sending transactions batch -/// -trait SendBatchTransactions<'a, T: Sliceable + Send + Sync> { - fn make (T, Transaction) + Send + Sync>( - &mut self, - chunk: &[V], - create_transaction: F, - ); - fn send_transactions(&mut self, client: &Arc, to_lamports: u64, log_progress: F) - where - C: 'static + TpsClient + Send + Sync + ?Sized, - F: Fn(usize, usize); - fn sign(&mut self, blockhash: Hash); - fn send(&self, client: &Arc); - fn verify( - &mut self, - client: &Arc, - to_lamports: u64, - ); -} - -/// This trait allows reuse SendBatchTransactions to send -/// transactions which require more than one signature -trait Sliceable { - type Slice; - fn as_slice(&self) -> Self::Slice; - // Pubkey used as unique id to identify verified transactions - fn get_pubkey(&self) -> Pubkey; -} - -impl SendBatchTransactions<'_, T> for Vec<(T, Transaction)> -where - ::Slice: Signers, -{ - fn make (T, Transaction) + Send + Sync>( - &mut self, - chunk: &[V], - create_transaction: F, - ) { - let mut make_txs = Measure::start("make_txs"); - let txs: Vec<(T, Transaction)> = chunk.par_iter().map(create_transaction).collect(); - make_txs.stop(); - debug!("make {} unsigned txs: {}us", txs.len(), make_txs.as_us()); - self.extend(txs); - } - - fn send_transactions(&mut self, client: &Arc, to_lamports: u64, log_progress: F) - where - C: 'static + TpsClient + Send + Sync + ?Sized, - F: Fn(usize, usize), - { - let mut tries: usize = 0; - while !self.is_empty() { - log_progress(tries, self.len()); - let blockhash = get_latest_blockhash(client.as_ref()); - - // re-sign retained to_fund_txes with updated blockhash - self.sign(blockhash); - self.send(client); - - // Sleep a few slots to allow transactions to process - sleep(Duration::from_secs(1)); - - self.verify(client, to_lamports); - - // retry anything that seems to have dropped through cracks - // again since these txs are all or nothing, they're fine to - // retry - tries += 1; - } - info!("transactions sent in {tries} tries"); - } - - fn sign(&mut self, blockhash: Hash) { - let mut sign_txs = Measure::start("sign_txs"); - self.par_iter_mut().for_each(|(k, tx)| { - tx.sign(&k.as_slice(), blockhash); - }); - sign_txs.stop(); - debug!("sign {} txs: {}us", self.len(), sign_txs.as_us()); - } - - fn send(&self, client: &Arc) { - let mut send_txs = Measure::start("send_and_clone_txs"); - let batch: Vec<_> = self - .iter() - .map(|(_keypair, tx)| tx.clone().into()) - .collect(); - let result = client.send_batch(batch); - send_txs.stop(); - if result.is_err() { - debug!("Failed to send batch {result:?}"); - } else { - debug!("send {} {}", self.len(), send_txs); - } - } - - fn verify( - &mut self, - client: &Arc, - to_lamports: u64, - ) { - let starting_txs = self.len(); - let verified_txs = Arc::new(AtomicUsize::new(0)); - let too_many_failures = Arc::new(AtomicBool::new(false)); - let loops = if starting_txs < 1000 { 3 } else { 1 }; - // Only loop multiple times for small (quick) transaction batches - let time = Arc::new(Mutex::new(Instant::now())); - for _ in 0..loops { - let time = time.clone(); - let failed_verify = Arc::new(AtomicUsize::new(0)); - let client = client.clone(); - let verified_txs = &verified_txs; - let failed_verify = &failed_verify; - let too_many_failures = &too_many_failures; - let verified_set: HashSet = self - .par_iter() - .filter_map(move |(k, tx)| { - let pubkey = k.get_pubkey(); - if too_many_failures.load(Ordering::Relaxed) { - return None; - } - - let verified = if verify_funding_transfer(&client, tx, to_lamports) { - verified_txs.fetch_add(1, Ordering::Relaxed); - Some(pubkey) - } else { - failed_verify.fetch_add(1, Ordering::Relaxed); - None - }; - - let verified_txs = verified_txs.load(Ordering::Relaxed); - let failed_verify = failed_verify.load(Ordering::Relaxed); - let remaining_count = starting_txs.saturating_sub(verified_txs + failed_verify); - if failed_verify > 100 && failed_verify > verified_txs { - too_many_failures.store(true, Ordering::Relaxed); - warn!( - "Too many failed transfers... {remaining_count} remaining, \ - {verified_txs} verified, {failed_verify} failures" - ); - } - if remaining_count > 0 { - let mut time_l = time.lock().unwrap(); - if time_l.elapsed().as_secs() > 2 { - info!( - "Verifying transfers... {remaining_count} remaining, \ - {verified_txs} verified, {failed_verify} failures" - ); - *time_l = Instant::now(); - } - } - - verified - }) - .collect(); - - self.retain(|(k, _)| !verified_set.contains(&k.get_pubkey())); - if self.is_empty() { - break; - } - info!("Looping verification"); - - let verified_txs = verified_txs.load(Ordering::Relaxed); - let failed_verify = failed_verify.load(Ordering::Relaxed); - let remaining_count = starting_txs.saturating_sub(verified_txs + failed_verify); - info!( - "Verifying transfers... {remaining_count} remaining, {verified_txs} verified, \ - {failed_verify} failures" - ); - sleep(Duration::from_millis(100)); - } - } -} - -type FundingSigners<'a> = &'a Keypair; -type FundingChunk<'a> = [(FundingSigners<'a>, Vec<(Pubkey, u64)>)]; -type FundingContainer<'a> = Vec<(FundingSigners<'a>, Transaction)>; - -impl<'a> Sliceable for FundingSigners<'a> { - type Slice = [FundingSigners<'a>; 1]; - fn as_slice(&self) -> Self::Slice { - [self] - } - fn get_pubkey(&self) -> Pubkey { - self.pubkey() - } -} - -trait FundingTransactions<'a>: SendBatchTransactions<'a, FundingSigners<'a>> { - fn fund( - &mut self, - client: &Arc, - to_fund: &FundingChunk<'a>, - to_lamports: u64, - data_size_limit: Option, - ); -} - -impl<'a> FundingTransactions<'a> for FundingContainer<'a> { - fn fund( - &mut self, - client: &Arc, - to_fund: &FundingChunk<'a>, - to_lamports: u64, - data_size_limit: Option, - ) { - self.make(to_fund, |(k, t)| -> (FundingSigners<'a>, Transaction) { - let mut instructions = system_instruction::transfer_many(&k.pubkey(), t); - if let Some(data_size_limit) = data_size_limit { - instructions.push( - ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(data_size_limit), - ); - } - let message = Message::new(&instructions, Some(&k.pubkey())); - (*k, Transaction::new_unsigned(message)) - }); - - let log_progress = |tries: usize, batch_len: usize| { - info!( - "{} {} each to {} accounts in {} txs", - if tries == 0 { - "transferring" - } else { - " retrying" - }, - to_lamports, - batch_len * MAX_SPENDS_PER_TX as usize, - batch_len, - ); - }; - self.send_transactions(client, to_lamports, log_progress); - } -} - -// Introduce a new structure to specify Sliceable implementations -// which uses both Keypairs to sign the transaction -struct NonceCreateSigners<'a>(&'a Keypair, &'a Keypair); -type NonceCreateChunk<'a> = [NonceCreateSigners<'a>]; -type NonceCreateContainer<'a> = Vec<(NonceCreateSigners<'a>, Transaction)>; - -impl<'a> Sliceable for NonceCreateSigners<'a> { - type Slice = [&'a Keypair; 2]; - fn as_slice(&self) -> Self::Slice { - [self.0, self.1] - } - fn get_pubkey(&self) -> Pubkey { - self.0.pubkey() - } -} - -trait NonceTransactions<'a>: SendBatchTransactions<'a, NonceCreateSigners<'a>> { - fn create_accounts( - &mut self, - client: &Arc, - to_fund: &'a NonceCreateChunk<'a>, - nonce_rent: u64, - ); -} - -impl<'a> NonceTransactions<'a> for NonceCreateContainer<'a> { - fn create_accounts( - &mut self, - client: &Arc, - to_fund: &'a NonceCreateChunk<'a>, - nonce_rent: u64, - ) { - self.make(to_fund, |kp| -> (NonceCreateSigners<'a>, Transaction) { - let authority = kp.0; - let nonce: &Keypair = kp.1; - let instructions = system_instruction::create_nonce_account( - &authority.pubkey(), - &nonce.pubkey(), - &authority.pubkey(), - nonce_rent, - ); - ( - NonceCreateSigners(authority, nonce), - Transaction::new_with_payer(&instructions, Some(&authority.pubkey())), - ) - }); - - let log_progress = |tries: usize, batch_len: usize| { - info!( - "@ {} {} accounts", - if tries == 0 { "creating" } else { " retrying" }, - batch_len, - ); - }; - self.send_transactions(client, nonce_rent, log_progress); - } -} - -// Only Pubkey is required for nonce because it doesn't sign withdraw account transaction -struct NonceWithdrawSigners<'a>(&'a Keypair, Pubkey); -type NonceWithdrawChunk<'a> = [NonceWithdrawSigners<'a>]; -type NonceWithdrawContainer<'a> = Vec<(NonceWithdrawSigners<'a>, Transaction)>; - -impl<'a> Sliceable for NonceWithdrawSigners<'a> { - type Slice = [&'a Keypair; 1]; - fn as_slice(&self) -> Self::Slice { - [self.0] - } - fn get_pubkey(&self) -> Pubkey { - self.0.pubkey() - } -} - -trait NonceWithdrawTransactions<'a>: SendBatchTransactions<'a, NonceWithdrawSigners<'a>> { - fn withdraw_accounts( - &mut self, - client: &Arc, - to_withdraw: &'a NonceWithdrawChunk<'a>, - ); -} -impl<'a> NonceWithdrawTransactions<'a> for NonceWithdrawContainer<'a> { - fn withdraw_accounts( - &mut self, - client: &Arc, - to_withdraw: &'a NonceWithdrawChunk<'a>, - ) { - self.make( - to_withdraw, - |kp| -> (NonceWithdrawSigners<'a>, Transaction) { - let authority = kp.0; - let nonce_pubkey: Pubkey = kp.1; - let nonce_balance = client.get_balance(&nonce_pubkey).unwrap(); - let instructions = vec![ - system_instruction::withdraw_nonce_account( - &nonce_pubkey, - &authority.pubkey(), - &authority.pubkey(), - nonce_balance, - ); - 1 - ]; - ( - NonceWithdrawSigners(authority, nonce_pubkey), - Transaction::new_with_payer(&instructions, Some(&authority.pubkey())), - ) - }, - ); - - let log_progress = |tries: usize, batch_len: usize| { - info!( - "@ {} {} accounts", - if tries == 0 { - "withdrawing" - } else { - " retrying" - }, - batch_len, - ); - }; - self.send_transactions(client, 0, log_progress); - } -} diff --git a/bench-tps/tests/bench_tps.rs b/bench-tps/tests/bench_tps.rs deleted file mode 100644 index 96303cc22b0..00000000000 --- a/bench-tps/tests/bench_tps.rs +++ /dev/null @@ -1,169 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] - -use { - serial_test::serial, - solana_bench_tps::{ - bench::{do_bench_tps, generate_and_fund_keypairs}, - cli::Config, - send_batch::generate_durable_nonce_accounts, - }, - solana_commitment_config::CommitmentConfig, - solana_connection_cache::connection_cache::NewConnectionConfig, - solana_core::validator::ValidatorConfig, - solana_faucet::faucet::run_local_faucet_for_tests, - solana_fee_calculator::FeeRateGovernor, - solana_keypair::Keypair, - solana_local_cluster::{ - cluster::Cluster, - local_cluster::{ClusterConfig, LocalCluster}, - validator_configs::make_identical_validator_configs, - }, - solana_net_utils::SocketAddrSpace, - solana_quic_client::{QuicConfig, QuicConnectionManager, QuicPool}, - solana_rent::Rent, - solana_rpc::rpc::JsonRpcConfig, - solana_rpc_client::rpc_client::RpcClient, - solana_signer::Signer, - solana_test_validator::{TestValidator, TestValidatorGenesis}, - solana_tpu_client::tpu_client::{TpuClient, TpuClientConfig}, - std::{sync::Arc, time::Duration}, -}; - -fn create_local_cluster_and_client() -> ( - LocalCluster, - Arc>, -) { - let faucet_keypair = Keypair::new(); - let faucet_pubkey = faucet_keypair.pubkey(); - let faucet_addr = run_local_faucet_for_tests( - faucet_keypair, - None, /* per_time_cap */ - 0, /* port */ - ); - - const NUM_NODES: usize = 1; - let cluster = LocalCluster::new( - &mut ClusterConfig { - node_stakes: vec![999_990; NUM_NODES], - mint_lamports: 400_000_000_000, - validator_configs: make_identical_validator_configs( - &ValidatorConfig { - rpc_config: JsonRpcConfig { - faucet_addr: Some(faucet_addr), - ..JsonRpcConfig::default_for_test() - }, - ..ValidatorConfig::default_for_test() - }, - NUM_NODES, - ), - ..ClusterConfig::default() - }, - SocketAddrSpace::Unspecified, - ); - - cluster.transfer(&cluster.funding_keypair, &faucet_pubkey, 100_000_000_000); - - let client = Arc::new( - cluster - .build_validator_tpu_quic_client(cluster.entry_point_info.pubkey()) - .unwrap_or_else(|err| { - panic!("Could not create TpuClient with Quic Cache {err:?}"); - }), - ); - - (cluster, client) -} - -fn create_test_validator_and_client() -> ( - TestValidator, - Arc>, -) { - let mint_keypair = Keypair::new(); - let mint_pubkey = mint_keypair.pubkey(); - let faucet_addr = run_local_faucet_for_tests( - mint_keypair, - None, /* per_time_cap */ - 0, /* port */ - ); - - let test_validator = TestValidatorGenesis::default() - .fee_rate_governor(FeeRateGovernor::new(0, 0)) - .rent(Rent { - lamports_per_byte: 1, - ..Rent::default() - }) - .faucet_addr(Some(faucet_addr)) - .add_program( - "spl_instruction_padding", - spl_instruction_padding_interface::ID, - ) - .start_with_mint_address(mint_pubkey, SocketAddrSpace::Unspecified) - .expect("validator start failed"); - - let rpc_client = Arc::new(RpcClient::new_with_commitment( - test_validator.rpc_url(), - CommitmentConfig::processed(), - )); - let websocket_url = test_validator.rpc_pubsub_url(); - - ( - test_validator, - Arc::new( - TpuClient::new( - "tpu_client_quic_bench_tps", - rpc_client, - &websocket_url, - TpuClientConfig::default(), - QuicConnectionManager::new_with_connection_config(QuicConfig::new().unwrap()), - ) - .expect("Should build Quic Tpu Client."), - ), - ) -} - -fn run_bench_tps(client: Arc>) { - let config = Config { - tx_count: 100, - duration: Duration::from_secs(5), - ..Config::default() - }; - let keypair_count = config.tx_count * config.keypair_multiplier; - let lamports_per_account = 100_000; - let keypairs = generate_and_fund_keypairs( - client.clone(), - &config.id, - keypair_count, - lamports_per_account, - false, - false, - ) - .unwrap(); - let nonce_keypairs = if config.use_durable_nonce { - Some(generate_durable_nonce_accounts(client.clone(), &keypairs)) - } else { - None - }; - - let tps = do_bench_tps(client, config, keypairs, nonce_keypairs); - assert!(tps > 100, "TPS less than expected {tps}"); -} - -/// Verifies the bench-tps QUIC client, faucet plumbing, and funding flow work against a -/// real `LocalCluster`, and does not cover the lighter `TestValidator` path. -#[test] -#[serial] -fn test_bench_tps_local_cluster() { - agave_logger::setup(); - let (local_cluster, client) = create_local_cluster_and_client(); - run_bench_tps(client); - drop(local_cluster); -} - -#[test] -#[serial] -fn test_bench_tps_test_validator() { - agave_logger::setup(); - let (test_validator, client) = create_test_validator_and_client(); - run_bench_tps(client); - drop(test_validator) -} diff --git a/bench-tps/tests/fixtures/spl_instruction_padding.so b/bench-tps/tests/fixtures/spl_instruction_padding.so deleted file mode 100755 index a00aabeb93b..00000000000 Binary files a/bench-tps/tests/fixtures/spl_instruction_padding.so and /dev/null differ diff --git a/bloom/Cargo.toml b/bloom/Cargo.toml index 49131932441..cc0bc523cfd 100644 --- a/bloom/Cargo.toml +++ b/bloom/Cargo.toml @@ -7,10 +7,12 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -38,12 +40,14 @@ solana-frozen-abi-macro = { workspace = true, optional = true, features = [ ] } solana-sanitize = { workspace = true } solana-time-utils = { workspace = true } +wincode = { workspace = true, features = ["bv"] } [dev-dependencies] bencher = { workspace = true } +bincode = { workspace = true } rayon = { workspace = true } solana-bloom = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } -solana-hash = { workspace = true, features = ["atomic"] } +solana-hash = { workspace = true, features = ["atomic", "wincode"] } solana-sha256-hasher = { workspace = true, features = ["sha2"] } solana-signature = { workspace = true, features = ["std"] } diff --git a/bloom/src/bloom.rs b/bloom/src/bloom.rs index c54cdd12ee7..3b93fe486ed 100644 --- a/bloom/src/bloom.rs +++ b/bloom/src/bloom.rs @@ -5,7 +5,6 @@ use { fnv::FnvHasher, rand::{self, Rng}, serde::{Deserialize, Serialize}, - solana_sanitize::{Sanitize, SanitizeError}, solana_time_utils::AtomicInterval, std::{ cmp, fmt, @@ -14,6 +13,7 @@ use { ops::Deref, sync::atomic::{AtomicU64, Ordering}, }, + wincode::{SchemaRead, SchemaWrite}, }; /// Generate a stable hash of `self` for each `hash_index` @@ -22,10 +22,35 @@ pub trait BloomHashIndex { fn hash_at_index(&self, hash_index: u64) -> u64; } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq)] +/// Samples a random, non-empty [`BitVec`] for `StableAbi` abi-digest tests. +/// +/// `BitVec` implements neither `StableAbi` nor `FromIterator`, so the derive can't +/// sample it. We build one from a non-empty random block buffer: an empty +/// `BitVec::from_bits(&[])` has an inner `Some([])` that serde encodes as `[1, 0]`, +/// whereas wincode normalizes empty to `[0]` (`None`) — an empty sample would make +/// the bincode and wincode abi digests diverge. Shared via +/// `#[stable_abi_sample(with = "...")]` by `BitVec` fields here and in gossip. +#[cfg(feature = "frozen-abi")] +pub fn sample_bit_vec( + rng: &mut (impl solana_frozen_abi::rand::RngCore + ?Sized), +) -> BitVec +where + Block: bv::BlockType + solana_frozen_abi::stable_abi::StableAbi, +{ + use solana_frozen_abi::stable_abi::{context::SequenceLenRange, sample_collection_sized}; + BitVec::from_bits( + sample_collection_sized::, Block>(rng, SequenceLenRange::new(1..=5)).as_slice(), + ) +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Eq, SchemaWrite, SchemaRead)] pub struct Bloom { pub keys: Vec, + #[cfg_attr( + feature = "frozen-abi", + stable_abi_sample(with = "sample_bit_vec(rng)") + )] pub bits: BitVec, num_bits_set: u64, _phantom: PhantomData, @@ -55,17 +80,6 @@ impl fmt::Debug for Bloom { } } -impl Sanitize for Bloom { - fn sanitize(&self) -> Result<(), SanitizeError> { - // Avoid division by zero in self.pos(...). - if self.bits.is_empty() { - Err(SanitizeError::InvalidValue) - } else { - Ok(()) - } - } -} - impl Bloom { #[cfg(feature = "dev-context-only-utils")] pub fn num_bits_set(&self) -> u64 { @@ -119,6 +133,9 @@ impl Bloom { self.num_bits_set = 0; } pub fn add(&mut self, key: &T) { + if self.bits.is_empty() { + return; + } for k in &self.keys { let pos = self.pos(key, *k); if !self.bits.get(pos) { @@ -128,6 +145,9 @@ impl Bloom { } } pub fn contains(&self, key: &T) -> bool { + if self.keys.is_empty() || self.bits.is_empty() { + return false; + } for k in &self.keys { let pos = self.pos(key, *k); if !self.bits.get(pos) { @@ -194,6 +214,9 @@ impl ConcurrentBloom { /// Adds an item to the bloom filter and returns true if the item /// was not in the filter before. pub fn add(&self, key: &T) -> bool { + if self.bits.is_empty() { + return false; + } let mut added = false; for k in &self.keys { let (index, mask) = self.pos(key, *k); @@ -204,6 +227,9 @@ impl ConcurrentBloom { } pub fn contains(&self, key: &T) -> bool { + if self.keys.is_empty() || self.bits.is_empty() { + return false; + } self.keys.iter().all(|k| { let (index, mask) = self.pos(key, *k); let bit = self.bits[index].load(Ordering::Relaxed) & mask; @@ -308,6 +334,7 @@ mod test { bloom.add(&key); assert!(bloom.contains(&key)); } + #[test] fn test_random() { let mut b1: Bloom = Bloom::random(10, 0.1, 100); @@ -479,4 +506,27 @@ mod test { } assert_eq!(bits, bloom.bits); } + + #[test] + fn test_wincode_compatibility_bloom() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let num_keys = rng.random_range(1..8usize); + let keys: Vec = (0..num_keys).map(|_| rng.random()).collect(); + let num_bits = rng.random_range(1..1024usize); + let mut bloom = Bloom::::new(num_bits, keys); + let num_inserts = rng.random_range(0..32usize); + for _ in 0..num_inserts { + bloom.add(&generate_random_hash()); + } + + let bincode_bytes = bincode::serialize(&bloom).unwrap(); + let wincode_bytes = wincode::serialize(&bloom).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + let wincode_decoded: Bloom = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(bloom, wincode_decoded); + let bincode_decoded: Bloom = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(bloom, bincode_decoded); + } + } } diff --git a/bls-cert-verify/Cargo.toml b/bls-cert-verify/Cargo.toml index c0f2b57c386..633572e189f 100644 --- a/bls-cert-verify/Cargo.toml +++ b/bls-cert-verify/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] dev-context-only-utils = [] @@ -29,6 +34,7 @@ agave-bls-cert-verify = { path = ".", features = ["agave-unstable-api", "dev-con # See order-crates-for-publishing.py for using this unusual `path` agave-votor = { path = "../votor", features = ["agave-unstable-api"] } criterion = { workspace = true } +rand = { workspace = true } solana-hash = { workspace = true } [[bench]] diff --git a/bls-cert-verify/benches/cert_verify.rs b/bls-cert-verify/benches/cert_verify.rs index 761dc124a64..d6de27f0579 100644 --- a/bls-cert-verify/benches/cert_verify.rs +++ b/bls-cert-verify/benches/cert_verify.rs @@ -2,17 +2,22 @@ use { agave_bls_cert_verify::cert_verify::{aggregate_pubkeys, collect_pubkeys, verify_certificate}, agave_votor::consensus_pool::certificate_builder::CertificateBuilder, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType, VoteMessage}, + certificate::CertificateType, + consensus_message::{Block, VoteMessage}, + unverified_vote_message::UnverifiedCertificate, vote::Vote, + wire::get_vote_payload_to_sign, }, bitvec::vec::BitVec, - criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}, + criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}, + rand::Rng, solana_bls_signatures::{ keypair::Keypair as BlsKeypair, pubkey::{PopVerified, PubkeyAffine as BlsPubkeyAffine}, signature::Signature as BlsSignature, }, solana_hash::Hash, + std::num::NonZero, }; // Creates random BLS keypairs for bench tests @@ -21,8 +26,13 @@ fn create_bls_keypairs(num_signers: usize) -> Vec { } // Creates vote messages for bench tests -fn create_signed_vote_message(bls_keypair: &BlsKeypair, vote: Vote, rank: usize) -> VoteMessage { - let payload = wincode::serialize(&vote).expect("Failed to serialize vote"); +fn create_signed_vote_message( + bls_keypair: &BlsKeypair, + shred_version: u16, + vote: Vote, + rank: usize, +) -> VoteMessage { + let payload = get_vote_payload_to_sign(vote, shred_version); let signature: BlsSignature = bls_keypair.sign(&payload).into(); VoteMessage { vote, @@ -32,41 +42,69 @@ fn create_signed_vote_message(bls_keypair: &BlsKeypair, vote: Vote, rank: usize) } // Creates a standard Base2 Certificate (All validators sign the same vote) -fn create_base2_cert(keypairs: &[BlsKeypair], num_signers: usize) -> Certificate { +fn create_base2_cert( + keypairs: &[BlsKeypair], + shred_version: u16, + num_signers: usize, +) -> UnverifiedCertificate { let slot = 100; let hash = Hash::new_unique(); - let cert_type = CertificateType::Notarize(slot, hash); + let cert_type = CertificateType::Notarize(Block { + slot, + block_id: hash, + }); let vote = cert_type.to_source_vote(); let vote_messages: Vec = (0..num_signers) - .map(|rank| create_signed_vote_message(&keypairs[rank], vote, rank)) + .map(|rank| create_signed_vote_message(&keypairs[rank], shred_version, vote, rank)) .collect(); let mut builder = CertificateBuilder::new(cert_type); builder.aggregate(&vote_messages).unwrap(); - builder.build().unwrap() + let cert = builder.build().unwrap(); + UnverifiedCertificate { + cert_type: cert.cert_type, + signature: cert.signature, + bitmap: cert.bitmap, + shred_version, + } } // Creates a Split Vote Base3 Certificate (Validators split between Notarize and Fallback) #[allow(clippy::arithmetic_side_effects)] fn create_base3_cert( keypairs: &[BlsKeypair], + shred_version: u16, num_notarize: usize, num_fallback: usize, -) -> Certificate { +) -> UnverifiedCertificate { let slot = 100; let hash = Hash::new_unique(); - let cert_type = CertificateType::NotarizeFallback(slot, hash); + let cert_type = CertificateType::NotarizeFallback(Block { + slot, + block_id: hash, + }); - let vote_notarize = Vote::new_notarization_vote(slot, hash); - let vote_fallback = Vote::new_notarization_fallback_vote(slot, hash); + let vote_notarize = Vote::new_notarization_vote(Block { + slot, + block_id: hash, + }); + let vote_fallback = Vote::new_notarization_fallback_vote(Block { + slot, + block_id: hash, + }); let mut vote_messages = Vec::new(); // Group 1: Signs Notarize for (i, keypair) in keypairs.iter().take(num_notarize).enumerate() { let rank = i; - vote_messages.push(create_signed_vote_message(keypair, vote_notarize, rank)); + vote_messages.push(create_signed_vote_message( + keypair, + shred_version, + vote_notarize, + rank, + )); } // Group 2: Signs Fallback @@ -77,12 +115,23 @@ fn create_base3_cert( .enumerate() { let rank = num_notarize + i; - vote_messages.push(create_signed_vote_message(keypair, vote_fallback, rank)); + vote_messages.push(create_signed_vote_message( + keypair, + shred_version, + vote_fallback, + rank, + )); } let mut builder = CertificateBuilder::new(cert_type); builder.aggregate(&vote_messages).unwrap(); - builder.build().unwrap() + let cert = builder.build().unwrap(); + UnverifiedCertificate { + cert_type: cert.cert_type, + signature: cert.signature, + bitmap: cert.bitmap, + shred_version, + } } #[allow(clippy::arithmetic_side_effects)] @@ -106,6 +155,7 @@ fn bench_verify_cert(c: &mut Criterion) { for &size in &validator_sizes { let keypairs = create_bls_keypairs(size); + let shred_version = rand::rng().random(); // Pre-calculate public keys to simulate efficient Bank lookup let pubkeys: Vec> = @@ -115,7 +165,7 @@ fn bench_verify_cert(c: &mut Criterion) { // Base2 Setup // Assume 2/3rds of validators sign let num_signers_base2 = (size * 2) / 3; - let cert_base2 = create_base2_cert(&keypairs, num_signers_base2); + let cert_base2 = create_base2_cert(&keypairs, shred_version, num_signers_base2); // Collect pubkeys let mut ranks_bitvec = BitVec::::with_capacity(size); @@ -146,16 +196,21 @@ fn bench_verify_cert(c: &mut Criterion) { BenchmarkId::new("Base2_Notarize", size), &size, |b, &total_validators| { - b.iter(|| { - // The rank_map closure simulates the Bank lookup. - // It adds stake (we use 1000 per validator) and returns the pubkey. - let _stake = verify_certificate(&cert_base2, total_validators, |rank| { - pubkeys_ref - .get(rank) - .map(|bls_pubkey| (TEST_STAKE, *bls_pubkey)) - }) - .unwrap(); - }) + let total_stake = NonZero::new(TEST_STAKE * total_validators as u64).unwrap(); + b.iter_batched( + || cert_base2.clone(), + |cert_base2| { + // The rank_map closure simulates the Bank lookup. + // It adds stake (we use 1000 per validator) and returns the pubkey. + verify_certificate(cert_base2, total_validators, total_stake, |rank| { + pubkeys_ref + .get(rank) + .map(|bls_pubkey| (NonZero::new(TEST_STAKE).unwrap(), *bls_pubkey)) + }) + .unwrap(); + }, + BatchSize::SmallInput, + ) }, ); @@ -163,20 +218,25 @@ fn bench_verify_cert(c: &mut Criterion) { // 40% sign Notarize, 30% sign Fallback (Total 70%) let num_notarize = (size * 40) / 100; let num_fallback = (size * 30) / 100; - let cert_base3 = create_base3_cert(&keypairs, num_notarize, num_fallback); + let cert_base3 = create_base3_cert(&keypairs, shred_version, num_notarize, num_fallback); group.bench_with_input( BenchmarkId::new("Base3_NotarizeFallback", size), &size, |b, &total_validators| { - b.iter(|| { - let _stake = verify_certificate(&cert_base3, total_validators, |rank| { - pubkeys_ref - .get(rank) - .map(|bls_pubkey| (TEST_STAKE, *bls_pubkey)) - }) - .unwrap(); - }) + let total_stake = NonZero::new(TEST_STAKE * total_validators as u64).unwrap(); + b.iter_batched( + || cert_base3.clone(), + |cert_base3| { + verify_certificate(cert_base3, total_validators, total_stake, |rank| { + pubkeys_ref + .get(rank) + .map(|bls_pubkey| (NonZero::new(TEST_STAKE).unwrap(), *bls_pubkey)) + }) + .unwrap(); + }, + BatchSize::SmallInput, + ) }, ); } diff --git a/bls-cert-verify/src/cert_verify.rs b/bls-cert-verify/src/cert_verify.rs index e01cf4b987c..66e065fda66 100644 --- a/bls-cert-verify/src/cert_verify.rs +++ b/bls-cert-verify/src/cert_verify.rs @@ -2,8 +2,8 @@ use qualifier_attr::qualifiers; use { agave_votor_messages::{ - consensus_message::{Certificate, CertificateType}, - vote::Vote, + certificate::Certificate, fraction::Fraction, + unverified_vote_message::UnverifiedCertificate, }, bitvec::vec::BitVec, rayon::iter::IntoParallelRefIterator, @@ -14,6 +14,7 @@ use { signature::AsSignatureAffine, }, solana_signer_store::{DecodeError, Decoded, decode}, + std::num::NonZero, thiserror::Error, }; @@ -35,20 +36,24 @@ pub enum Error { WrongEncoding, #[error("overlapping primary and fallback bitmaps")] BitmapOverlap, + #[error("Not enough stake {aggregate_stake}: {cert_fraction} < {required_fraction}")] + NotEnoughStake { + aggregate_stake: u64, + cert_fraction: Fraction, + required_fraction: Fraction, + }, } -/// Verifies an Alpenglow `Certificate` and calculates the total signing stake. +/// Verifies an Alpenglow `UnverifiedCertificate`. /// /// This function verifies that the aggregate signature matches the vote payload(s) -/// derived from the certificate type. It concurrently accumulates the stake of -/// the signers using the provided `rank_map`. +/// derived from the certificate type. /// /// # Core Functionality /// - **Signature Verification**: Validates aggregate BLS signatures for `votor-messages`. /// - **Bitmap Processing**: Supports verification using bitmaps that conform to the /// `solana-signer-store` format. -/// - **Stake Accumulation**: Calculates the total signing stake during the verification -/// process. +/// - **Stake verification**: Verifies that sufficient stake signed the certificate. /// /// # Ledger State and Rank Map /// @@ -60,31 +65,27 @@ pub enum Error { /// /// * `cert` - The certificate to verify. /// * `max_validators` - The maximum number of validators (used for decoding the bitmap). +/// * `total_stake` - The total stake for the network for the epoch in which this certificate was produced. /// * `rank_map` - A closure that maps a validator's rank (index) to their stake and public key. -/// -/// # Returns -/// -/// On success, returns the total stake of validators present in the certificate. pub fn verify_certificate( - cert: &Certificate, + cert: UnverifiedCertificate, max_validators: usize, - mut rank_map: impl FnMut(usize) -> Option<(u64, PopVerified)>, -) -> Result { - let mut total_stake = 0u64; + total_stake: NonZero, + mut rank_map: impl FnMut(usize) -> Option<(NonZero, PopVerified)>, +) -> Result { + let mut aggregate_stake = 0u64; // Wrap the `rank_map` to accumulate stake as a side-effect let accumulating_rank_map = |ind: usize| { rank_map(ind).map(|(stake, pubkey)| { - total_stake = total_stake.saturating_add(stake); + aggregate_stake = aggregate_stake.saturating_add(stake.get()); pubkey }) }; - let (primary_vote, fallback_vote) = get_vote_payloads(cert.cert_type); - let primary_payload = serialize_vote(&primary_vote); + let (primary_payload, fallback_payload) = cert.cert_type.get_vote_payload(cert.shred_version); - if let Some(fallback_vote) = fallback_vote { - let fallback_payload = serialize_vote(&fallback_vote); + if let Some(fallback_payload) = fallback_payload { verify_base3( &primary_payload, &fallback_payload, @@ -102,35 +103,32 @@ pub fn verify_certificate( accumulating_rank_map, ) }?; - - Ok(total_stake) + verify_stake(&cert, aggregate_stake, total_stake)?; + Ok(Certificate { + cert_type: cert.cert_type, + signature: cert.signature, + bitmap: cert.bitmap, + }) } -fn get_vote_payloads(cert_type: CertificateType) -> (Vote, Option) { - match cert_type { - CertificateType::Notarize(slot, hash) | CertificateType::FinalizeFast(slot, hash) => { - (Vote::new_notarization_vote(slot, hash), None) - } - CertificateType::Finalize(slot) => (Vote::new_finalization_vote(slot), None), - CertificateType::Genesis(slot, hash) => (Vote::new_genesis_vote(slot, hash), None), - CertificateType::NotarizeFallback(slot, hash) => ( - Vote::new_notarization_vote(slot, hash), - Some(Vote::new_notarization_fallback_vote(slot, hash)), - ), - CertificateType::Skip(slot) => ( - Vote::new_skip_vote(slot), - Some(Vote::new_skip_fallback_vote(slot)), - ), +fn verify_stake( + cert: &UnverifiedCertificate, + aggregate_stake: u64, + total_stake: NonZero, +) -> Result<(), Error> { + let (required_fraction, _) = cert.cert_type.limits_and_vote_types(); + let cert_fraction = Fraction::new(aggregate_stake, total_stake); + if cert_fraction >= required_fraction { + Ok(()) + } else { + Err(Error::NotEnoughStake { + aggregate_stake, + cert_fraction, + required_fraction, + }) } } -fn serialize_vote(vote: &Vote) -> Vec { - // `expect` is safe because the `Vote` struct is composed entirely of primitive - // types (u64, Hash, enums) that are inherently serializable and it is constructed - // locally within this module. - wincode::serialize(vote).expect("Vote serialization should never fail for valid Vote structs") -} - /// Verifies a signature for a single payload signed by a set of validators. /// /// This function handles the "Base2" case where all participating validators have signed @@ -272,12 +270,18 @@ mod test { use { super::*, agave_votor::consensus_pool::certificate_builder::CertificateBuilder, - agave_votor_messages::{consensus_message::VoteMessage, vote::Vote}, + agave_votor_messages::{ + certificate::CertificateType, + consensus_message::{Block, VoteMessage}, + vote::Vote, + wire::get_vote_payload_to_sign, + }, + rand::Rng, solana_bls_signatures::{ keypair::Keypair as BLSKeypair, signature::Signature as BLSSignature, }, solana_hash::Hash, - solana_signer_store::encode_base2, + solana_signer_store::{encode_base2, encode_base3}, }; fn create_bls_keypairs(num_signers: usize) -> Vec { @@ -288,11 +292,12 @@ mod test { fn create_signed_vote_message( bls_keypairs: &[BLSKeypair], + shred_version: u16, vote: Vote, rank: usize, ) -> VoteMessage { let bls_keypair = &bls_keypairs[rank]; - let payload = wincode::serialize(&vote).expect("Failed to serialize vote"); + let payload = get_vote_payload_to_sign(vote, shred_version); let signature: BLSSignature = bls_keypair.sign(&payload).into(); VoteMessage { vote, @@ -303,82 +308,164 @@ mod test { fn create_signed_certificate_message( bls_keypairs: &[BLSKeypair], + shred_version: u16, cert_type: CertificateType, ranks: &[usize], - ) -> Certificate { + ) -> UnverifiedCertificate { let mut builder = CertificateBuilder::new(cert_type); // Assumes Base2 encoding (single vote type) for simplicity in this helper. let vote = cert_type.to_source_vote(); let vote_messages: Vec = ranks .iter() - .map(|&rank| create_signed_vote_message(bls_keypairs, vote, rank)) + .map(|&rank| create_signed_vote_message(bls_keypairs, shred_version, vote, rank)) .collect(); builder .aggregate(&vote_messages) .expect("Failed to aggregate votes"); - builder.build().expect("Failed to build certificate") + let cert = builder.build().expect("Failed to build certificate"); + UnverifiedCertificate { + cert_type: cert.cert_type, + signature: cert.signature, + bitmap: cert.bitmap, + shred_version, + } } #[test] fn test_verify_certificate_base2_valid() { let bls_keypairs = create_bls_keypairs(10); - let cert_type = CertificateType::Notarize(10, Hash::new_unique()); + let shred_version = rand::rng().random(); + let cert_type = CertificateType::Notarize(Block { + slot: 10, + block_id: Hash::new_unique(), + }); let cert = create_signed_certificate_message( &bls_keypairs, + shred_version, cert_type, &(0..6).collect::>(), ); - assert_eq!( - verify_certificate(&cert, 10, |rank| { - bls_keypairs.get(rank).map(|kp| (100, kp.public)) - }) - .unwrap(), - 600 - ); + verify_certificate(cert, 10, NonZero::new(600).unwrap(), |rank| { + bls_keypairs + .get(rank) + .map(|kp| (NonZero::new(100).unwrap(), kp.public)) + }) + .unwrap(); } + #[test] + fn test_stake_verification() { + let bls_keypairs = create_bls_keypairs(10); + let shred_version = rand::rng().random(); + let cert_type = CertificateType::Notarize(Block { + slot: 10, + block_id: Hash::new_unique(), + }); + let per_validator_stake = 100; + let num_validators = 10; + let total_stake = NonZero::new(per_validator_stake * num_validators as u64).unwrap(); + + // with enough stake should succeed. + let cert = create_signed_certificate_message( + &bls_keypairs, + shred_version, + cert_type, + &(0..6).collect::>(), + ); + verify_certificate(cert, 10, total_stake, |rank| { + bls_keypairs + .get(rank) + .map(|kp| (NonZero::new(100).unwrap(), kp.public)) + }) + .unwrap(); + + // not enough stake, should fail. + let cert = create_signed_certificate_message( + &bls_keypairs, + shred_version, + cert_type, + &(0..5).collect::>(), + ); + let Err(err) = verify_certificate(cert, 10, total_stake, |rank| { + bls_keypairs + .get(rank) + .map(|kp| (NonZero::new(100).unwrap(), kp.public)) + }) else { + panic!("should fail"); + }; + match err { + Error::NotEnoughStake { + aggregate_stake, + cert_fraction, + required_fraction, + } => { + assert_eq!(aggregate_stake, 500); + assert_eq!(required_fraction, cert_type.limits_and_vote_types().0); + assert_eq!(cert_fraction, Fraction::new(500, total_stake)); + } + rest => panic!("wrong variant {rest:?}"), + } + } #[test] fn test_verify_certificate_base3_valid() { let bls_keypairs = create_bls_keypairs(10); - let slot = 20; - let block_hash = Hash::new_unique(); - let notarize_vote = Vote::new_notarization_vote(slot, block_hash); - let notarize_fallback_vote = Vote::new_notarization_fallback_vote(slot, block_hash); + let shred_version = rand::rng().random(); + let block = Block { + slot: 20, + block_id: Hash::new_unique(), + }; + let notarize_vote = Vote::new_notarization_vote(block); + let notarize_fallback_vote = Vote::new_notarization_fallback_vote(block); let mut all_vote_messages = Vec::new(); (0..4).for_each(|i| { - all_vote_messages.push(create_signed_vote_message(&bls_keypairs, notarize_vote, i)) + all_vote_messages.push(create_signed_vote_message( + &bls_keypairs, + shred_version, + notarize_vote, + i, + )) }); (4..7).for_each(|i| { all_vote_messages.push(create_signed_vote_message( &bls_keypairs, + shred_version, notarize_fallback_vote, i, )) }); - let cert_type = CertificateType::NotarizeFallback(slot, block_hash); + let cert_type = CertificateType::NotarizeFallback(block); let mut builder = CertificateBuilder::new(cert_type); builder .aggregate(&all_vote_messages) .expect("Failed to aggregate votes"); let cert = builder.build().expect("Failed to build certificate"); - assert_eq!( - verify_certificate(&cert, 10, |rank| { - bls_keypairs.get(rank).map(|kp| (100, kp.public)) - }) - .unwrap(), - 700 - ); + let cert = UnverifiedCertificate { + cert_type: cert.cert_type, + signature: cert.signature, + bitmap: cert.bitmap, + shred_version, + }; + + verify_certificate(cert, 10, NonZero::new(700).unwrap(), |rank| { + bls_keypairs + .get(rank) + .map(|kp| (NonZero::new(100).unwrap(), kp.public)) + }) + .unwrap(); } #[test] fn test_verify_certificate_invalid_signature() { let bls_keypairs = create_bls_keypairs(10); + let shred_version = rand::rng().random(); let num_signers = 7; - let slot = 10; - let block_hash = Hash::new_unique(); - let cert_type = CertificateType::Notarize(slot, block_hash); + let block = Block { + slot: 10, + block_id: Hash::new_unique(), + }; + let cert_type = CertificateType::Notarize(block); let mut bitmap = BitVec::new(); bitmap.resize(num_signers, false); for i in 0..num_signers { @@ -386,14 +473,17 @@ mod test { } let encoded_bitmap = encode_base2(&bitmap).unwrap(); - let cert = Certificate { + let cert = UnverifiedCertificate { cert_type, signature: BLSSignature([0; 192]), // Use a default/wrong signature bitmap: encoded_bitmap, + shred_version, }; assert_eq!( - verify_certificate(&cert, 10, |rank| { - bls_keypairs.get(rank).map(|kp| (100, kp.public)) + verify_certificate(cert, 10, NonZero::new(1000).unwrap(), |rank| { + bls_keypairs + .get(rank) + .map(|kp| (NonZero::new(100).unwrap(), kp.public)) }) .unwrap_err(), Error::VerifySig(BlsError::PointConversion) @@ -404,26 +494,37 @@ mod test { fn base3_cert_with_no_primary_verifies() { let max_validators = 10; let bls_keypairs = create_bls_keypairs(max_validators); - let slot = 20; - let block_hash = Hash::new_unique(); - let cert_type = CertificateType::NotarizeFallback(slot, block_hash); + let shred_version = rand::rng().random(); + let block = Block { + slot: 20, + block_id: Hash::new_unique(), + }; + let cert_type = CertificateType::NotarizeFallback(block); let mut builder = CertificateBuilder::new(cert_type); - let vote = Vote::new_notarization_fallback_vote(slot, block_hash); + let vote = Vote::new_notarization_fallback_vote(block); let vote_msgs = (0..max_validators) - .map(|i| create_signed_vote_message(&bls_keypairs, vote, i)) + .map(|i| create_signed_vote_message(&bls_keypairs, shred_version, vote, i)) .collect::>(); builder.aggregate(&vote_msgs).unwrap(); let cert = builder.build().unwrap(); + let cert = UnverifiedCertificate { + cert_type: cert.cert_type, + signature: cert.signature, + bitmap: cert.bitmap, + shred_version, + }; let per_validator_stake = 100; - assert_eq!( - verify_certificate(&cert, 10, |rank| { + verify_certificate( + cert, + 10, + NonZero::new(per_validator_stake * max_validators as u64).unwrap(), + |rank| { bls_keypairs .get(rank) - .map(|kp| (per_validator_stake, kp.public)) - }) - .unwrap(), - per_validator_stake * max_validators as u64 - ); + .map(|kp| (NonZero::new(per_validator_stake).unwrap(), kp.public)) + }, + ) + .unwrap(); } #[test] @@ -453,4 +554,352 @@ mod test { Err(Error::WrongEncoding) ); } + + // ---------------------------------------------------------------------------- + // Adversarial / corruption robustness. + // + // These tests cover the unit-level core of "a single bad validator corrupting a + // BLS certificate" (anza-xyz/alpenglow#474): a tampered certificate must be + // rejected and must never report stake it cannot prove a signature for. The + // network-level blacklisting test described in that issue builds on top of this + // verification contract. + // ---------------------------------------------------------------------------- + + const STAKE_PER_VALIDATOR: NonZero = NonZero::new(100).unwrap(); + + /// A `Block` for `slot` with a fresh, unique block id. + fn fresh_block(slot: u64) -> Block { + Block { + slot, + block_id: Hash::new_unique(), + } + } + + /// Encode a Base2 rank bitmap with the given `ranks` set out of `num_bits`. + fn encoded_base2_bitmap(ranks: &[usize], num_bits: usize) -> Vec { + let mut bitmap = BitVec::new(); + bitmap.resize(num_bits, false); + for &rank in ranks { + bitmap.set(rank, true); + } + encode_base2(&bitmap).unwrap() + } + + /// Encode a Base3 rank bitmap where `primary` ranks signed the primary vote and + /// `fallback` ranks signed the fallback vote. The two sets must be disjoint. + fn encoded_base3_bitmap(primary: &[usize], fallback: &[usize], num_bits: usize) -> Vec { + let mut base = BitVec::new(); + base.resize(num_bits, false); + for &rank in primary { + base.set(rank, true); + } + let mut fallback_bits = BitVec::new(); + fallback_bits.resize(num_bits, false); + for &rank in fallback { + fallback_bits.set(rank, true); + } + encode_base3(&base, &fallback_bits).unwrap() + } + + /// Uniform `rank_map` over `keypairs`, each with `STAKE_PER_VALIDATOR` stake. + fn rank_map( + keypairs: &[BLSKeypair], + ) -> impl FnMut(usize) -> Option<(NonZero, PopVerified)> + '_ { + move |rank| { + keypairs + .get(rank) + .map(|kp| (STAKE_PER_VALIDATOR, kp.public)) + } + } + + /// A certificate with the given bitmap and a placeholder (all-zero) signature, + /// for tests whose rejection is independent of the signature contents. + fn unsigned_cert( + cert_type: CertificateType, + bitmap: Vec, + shred_version: u16, + ) -> UnverifiedCertificate { + UnverifiedCertificate { + cert_type, + signature: BLSSignature([0; 192]), + bitmap, + shred_version, + } + } + + /// Build a valid Base3 `NotarizeFallback` certificate where `primary_ranks` signed + /// the notarization vote and `fallback_ranks` signed the notarization fallback vote. + fn signed_notarize_fallback_cert( + keypairs: &[BLSKeypair], + shred_version: u16, + block: Block, + primary_ranks: &[usize], + fallback_ranks: &[usize], + ) -> Certificate { + let notarize = Vote::new_notarization_vote(block); + let fallback = Vote::new_notarization_fallback_vote(block); + let mut messages = Vec::new(); + for &rank in primary_ranks { + messages.push(create_signed_vote_message( + keypairs, + shred_version, + notarize, + rank, + )); + } + for &rank in fallback_ranks { + messages.push(create_signed_vote_message( + keypairs, + shred_version, + fallback, + rank, + )); + } + let mut builder = CertificateBuilder::new(CertificateType::NotarizeFallback(block)); + builder.aggregate(&messages).unwrap(); + builder.build().unwrap() + } + + /// Adding a validator to the bitmap who did not actually sign must fail + /// verification (the aggregate no longer matches the signature) - a bad actor + /// cannot inflate the signing stake of a certificate. + #[test] + fn tampered_bitmap_adding_unsigned_validator_fails() { + let keypairs = create_bls_keypairs(10); + let shred_version = rand::rng().random(); + let cert_type = CertificateType::Notarize(fresh_block(10)); + let mut cert = create_signed_certificate_message( + &keypairs, + shred_version, + cert_type, + &[0, 1, 2, 3, 4, 5], + ); + + // Claim rank 6 also signed, even though it never did. + cert.bitmap = encoded_base2_bitmap(&[0, 1, 2, 3, 4, 5, 6], 7); + + // Must fail at the signature stage (not via a decode / missing-rank path), + // proving the extra validator cannot be smuggled in. + assert_eq!( + verify_certificate(cert, 10, NonZero::new(10).unwrap(), rank_map(&keypairs)) + .unwrap_err(), + Error::VerifySig(BlsError::VerificationFailed) + ); + } + + /// Removing a genuine signer from the bitmap must also fail: the aggregate + /// signature still carries that signer's contribution, so the reduced key set + /// will not verify. + #[test] + fn tampered_bitmap_removing_signer_fails() { + let keypairs = create_bls_keypairs(10); + let shred_version = rand::rng().random(); + let cert_type = CertificateType::Notarize(fresh_block(10)); + let mut cert = create_signed_certificate_message( + &keypairs, + shred_version, + cert_type, + &[0, 1, 2, 3, 4, 5], + ); + + // Drop rank 5 from the bitmap while keeping its signature in the aggregate. + cert.bitmap = encoded_base2_bitmap(&[0, 1, 2, 3, 4], 6); + + assert_eq!( + verify_certificate(cert, 10, NonZero::new(10).unwrap(), rank_map(&keypairs)) + .unwrap_err(), + Error::VerifySig(BlsError::VerificationFailed) + ); + } + + /// A certificate whose `cert_type` (here the block id) is altered after the votes + /// were signed must fail: the reconstructed payload no longer matches what the + /// validators signed. + #[test] + fn tampered_cert_type_payload_fails() { + let keypairs = create_bls_keypairs(10); + let shred_version = rand::rng().random(); + let cert = create_signed_certificate_message( + &keypairs, + shred_version, + CertificateType::Notarize(fresh_block(10)), + &[0, 1, 2, 3, 4, 5], + ); + + let forged = UnverifiedCertificate { + cert_type: CertificateType::Notarize(fresh_block(10)), + signature: cert.signature, + bitmap: cert.bitmap.clone(), + shred_version, + }; + + assert_eq!( + verify_certificate(forged, 10, NonZero::new(10).unwrap(), rank_map(&keypairs)) + .unwrap_err(), + Error::VerifySig(BlsError::VerificationFailed) + ); + } + + /// A certificate with no signers (empty bitmap) must NOT verify. Accepting it + /// would let an attacker present a "certificate" backed by zero stake. + #[test] + fn empty_bitmap_certificate_does_not_verify() { + let keypairs = create_bls_keypairs(10); + let shred_version = rand::rng().random(); + let cert = unsigned_cert( + CertificateType::Notarize(fresh_block(10)), + encoded_base2_bitmap(&[], 0), + shred_version, + ); + + // An empty signer set cannot produce a valid aggregate signature. + assert_eq!( + verify_certificate(cert, 10, NonZero::new(10).unwrap(), rank_map(&keypairs)) + .unwrap_err(), + Error::VerifySig(BlsError::EmptyAggregation) + ); + } + + /// A Base3-encoded bitmap supplied for a certificate type that is verified as + /// Base2 (e.g. `Notarize`) must be rejected with `WrongEncoding` rather than + /// silently mis-decoded. + #[test] + fn base3_bitmap_rejected_for_base2_certificate() { + let keypairs = create_bls_keypairs(10); + let shred_version = rand::rng().random(); + let bitmap = encoded_base3_bitmap(&[0, 1], &[2], 6); + let cert = unsigned_cert( + CertificateType::Notarize(fresh_block(10)), + bitmap, + shred_version, + ); + + assert_eq!( + verify_certificate(cert, 10, NonZero::new(10).unwrap(), rank_map(&keypairs)) + .unwrap_err(), + Error::WrongEncoding + ); + } + + /// If a rank present in the bitmap is unknown to the `rank_map` (e.g. not in the + /// epoch's stake table), verification fails with `MissingRank` rather than + /// silently skipping that validator. + #[test] + fn unknown_rank_in_bitmap_fails() { + let keypairs = create_bls_keypairs(10); + let shred_version = rand::rng().random(); + let cert_type = CertificateType::Notarize(fresh_block(10)); + let cert = create_signed_certificate_message( + &keypairs, + shred_version, + cert_type, + &[0, 1, 2, 3, 4, 5], + ); + + // rank_map returns None for rank 3, simulating an unknown/unstaked validator. + let result = verify_certificate(cert, 10, NonZero::new(10).unwrap(), |rank| { + if rank == 3 { + None + } else { + keypairs + .get(rank) + .map(|kp| (STAKE_PER_VALIDATOR, kp.public)) + } + }); + + assert_eq!(result.unwrap_err(), Error::MissingRank); + } + + /// A bitmap declaring more bits than `max_validators` must be rejected by the + /// decoder (no panic, no out-of-bounds rank lookups). + #[test] + fn oversized_bitmap_is_rejected() { + let keypairs = create_bls_keypairs(10); + let shred_version = rand::rng().random(); + // 20 bits declared, but only 10 validators allowed. + let cert = unsigned_cert( + CertificateType::Notarize(fresh_block(10)), + encoded_base2_bitmap(&[0, 1], 20), + shred_version, + ); + + assert_eq!( + verify_certificate(cert, 10, NonZero::new(10).unwrap(), rank_map(&keypairs)) + .unwrap_err(), + Error::Decode(DecodeError::CorruptDataPayload) + ); + } + + /// Arbitrary attacker-controlled bitmap bytes must never panic the verifier and + /// must never verify (these certificates carry no valid aggregate signature). + #[test] + fn garbage_bitmap_never_panics_and_never_verifies() { + let keypairs = create_bls_keypairs(10); + let shred_version = rand::rng().random(); + for seed in 0u64..512 { + let len = (seed % 40) as usize; + let bitmap: Vec = (0..len) + .map(|i| seed.wrapping_mul(2_654_435_761).wrapping_add(i as u64 * 7) as u8) + .collect(); + let cert = unsigned_cert( + CertificateType::Notarize(fresh_block(10)), + bitmap, + shred_version, + ); + // Must return (Ok/Err) without panicking; a zero signature can never + // produce a valid certificate, so the result must be an error. + verify_certificate(cert, 10, NonZero::new(10).unwrap(), rank_map(&keypairs)) + .unwrap_err(); + } + } + + /// In a Base3 (split-vote) certificate, swapping which payload each validator is + /// claimed to have signed must fail: every validator's signature is bound to a + /// specific vote payload, so misattributing it breaks the aggregate check. + #[test] + fn base3_tampered_swapping_vote_types_fails() { + let keypairs = create_bls_keypairs(10); + let shred_version = rand::rng().random(); + let block = fresh_block(20); + // 0,1 signed the primary (notarize) vote; 2,3 signed the fallback vote. + let cert = signed_notarize_fallback_cert(&keypairs, shred_version, block, &[0, 1], &[2, 3]); + + // Forge a bitmap claiming the opposite: 2,3 as primary and 0,1 as fallback. + let forged = UnverifiedCertificate { + cert_type: cert.cert_type, + signature: cert.signature, + bitmap: encoded_base3_bitmap(&[2, 3], &[0, 1], 4), + shred_version, + }; + + assert_eq!( + verify_certificate(forged, 10, NonZero::new(10).unwrap(), rank_map(&keypairs)) + .unwrap_err(), + Error::VerifySig(BlsError::VerificationFailed) + ); + } + + /// Adding an unsigned validator to the fallback set of a Base3 certificate must + /// fail verification - the split-vote path is no easier to inflate than Base2. + #[test] + fn base3_tampered_adding_unsigned_validator_fails() { + let keypairs = create_bls_keypairs(10); + let shred_version = rand::rng().random(); + let block = fresh_block(20); + let cert = signed_notarize_fallback_cert(&keypairs, shred_version, block, &[0, 1], &[2, 3]); + + // Claim rank 4 also cast a fallback vote, though it never signed. + let forged = UnverifiedCertificate { + cert_type: cert.cert_type, + signature: cert.signature, + bitmap: encoded_base3_bitmap(&[0, 1], &[2, 3, 4], 5), + shred_version, + }; + + assert_eq!( + verify_certificate(forged, 10, NonZero::new(10).unwrap(), rank_map(&keypairs)) + .unwrap_err(), + Error::VerifySig(BlsError::VerificationFailed) + ); + } } diff --git a/bls-sigverify/Cargo.toml b/bls-sigverify/Cargo.toml new file mode 100644 index 00000000000..1dd539f91c6 --- /dev/null +++ b/bls-sigverify/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "agave-bls-sigverify" +description = "Logic to process incoming alpenglow consensus messages" +readme = "../README.md" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + +[features] +agave-unstable-api = [] +dev-context-only-utils = [ + "agave-votor-messages/dev-context-only-utils", + "solana-perf/dev-context-only-utils", + "solana-runtime/dev-context-only-utils", +] + +[dependencies] +agave-bls-cert-verify = { workspace = true } +agave-math-utils = { workspace = true } +agave-votor-messages = { workspace = true } +bitvec = { workspace = true } +crossbeam-channel = { workspace = true } +log = { workspace = true } +parking_lot = { workspace = true } +qualifier_attr = { workspace = true } +rayon = { workspace = true } +solana-bls-signatures = { workspace = true, features = ["parallel"] } +solana-clock = { workspace = true } +solana-gossip = { workspace = true } +solana-ledger = { workspace = true } +solana-measure = { workspace = true } +solana-metrics = { workspace = true } +solana-packet = { workspace = true } +solana-perf = { workspace = true } +solana-pubkey = { workspace = true } +solana-runtime = { workspace = true } +solana-signer-store = { workspace = true } +solana-streamer = { workspace = true } +thiserror = { workspace = true } +wincode = { workspace = true } + +[dev-dependencies] +# See order-crates-for-publishing.py for using this unusual `path = "."` +agave-bls-sigverify = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } +agave-logger = { workspace = true } +# See order-crates-for-publishing.py for using this unusual `path` +agave-votor = { path = "../votor", features = ["agave-unstable-api"] } +bincode = { workspace = true } +criterion = { workspace = true } +solana-epoch-schedule = { workspace = true } +solana-hash = { workspace = true } +solana-keypair = { workspace = true } +solana-net-utils = { workspace = true } +solana-perf = { workspace = true } +solana-signer = { workspace = true } + +[[bench]] +name = "bls_vote_sigverify" +harness = false + +[lints] +workspace = true diff --git a/bls-sigverify/benches/bls_vote_sigverify.rs b/bls-sigverify/benches/bls_vote_sigverify.rs new file mode 100644 index 00000000000..987a6ee1e2f --- /dev/null +++ b/bls-sigverify/benches/bls_vote_sigverify.rs @@ -0,0 +1,138 @@ +/* + To run this benchmark: + `cargo bench --bench bls_vote_sigverify` +*/ + +use { + agave_bls_sigverify::bls_vote_sigverify::{UnverifiedVotePayload, verify_individual_votes}, + agave_votor_messages::{ + consensus_message::Block, + unverified_vote_message::UnverifiedVoteMessage, + vote::Vote, + wire::{VotePayloadToSign, get_vote_payload_to_sign}, + }, + criterion::{BatchSize, Criterion, criterion_group, criterion_main}, + rayon::{ThreadPool, ThreadPoolBuilder}, + solana_bls_signatures::{Keypair as BLSKeypair, PreparedHashedMessage, VerifySignature}, + solana_hash::Hash, + solana_keypair::Keypair, + solana_signer::Signer, + std::hint::black_box, +}; + +static BATCH_SIZES: &[usize] = &[8, 16, 32, 64, 128]; + +fn get_thread_pool() -> ThreadPool { + let num_threads = 4; + ThreadPoolBuilder::new() + .num_threads(num_threads) + .build() + .unwrap() +} + +fn generate_test_data( + shred_version: u16, + batch_size: usize, +) -> (VotePayloadToSign, Vec) { + // Pre-calculate the payloads to ensure exact distinctness + let slot = 100; + let vote = Vote::new_notarization_vote(Block { + slot, + block_id: Hash::new_unique(), + }); + let payload = get_vote_payload_to_sign(vote, shred_version); + ( + VotePayloadToSign::new_from_vote(vote, shred_version), + (0..batch_size) + .map(|_| { + let bls_keypair = BLSKeypair::new(); + let signature = bls_keypair.sign(&payload); + let vote_message = UnverifiedVoteMessage { + vote, + signature: signature.into(), + rank: 0, + shred_version, + }; + UnverifiedVotePayload { + vote_message, + sender_bls_pubkey: bls_keypair.public, + sender_vote_account_pubkey: Keypair::new().pubkey(), + sender_identity_pubkey: Keypair::new().pubkey(), + } + }) + .collect(), + ) +} + +// Single Signature Verification +// This is just for reference +fn bench_verify_single_signature(c: &mut Criterion) { + let mut group = c.benchmark_group("verify_single_signature"); + + let keypair = BLSKeypair::new(); + let msg = b"benchmark_message_payload"; + let sig = keypair.sign(msg); + let pubkey = keypair.public; + + group.bench_function("1_item", |b| { + b.iter(|| { + // We use the raw verify method from the underlying library + // to establish the cryptographic floor. + let res = pubkey.verify_signature(black_box(&sig), black_box(msg)); + black_box(res).unwrap(); + }) + }); + group.finish(); +} + +fn bench_verify_single_signature_with_prepared_message(c: &mut Criterion) { + let mut group = c.benchmark_group("verify_single_signature_with_prepared_message"); + + let keypair = BLSKeypair::new(); + let msg = b"benchmark_message_payload"; + let sig = keypair.sign(msg); + let pubkey = keypair.public; + let prepared_msg = PreparedHashedMessage::new(msg); + + group.bench_function("1_item", |b| { + b.iter(|| { + let res = pubkey.verify_signature_prepared(black_box(&sig), black_box(&prepared_msg)); + black_box(res).unwrap(); + }) + }); + group.finish(); +} + +// Individual Verification - verifies each signatures in parallel threads +// Message distinctness is irrelevant. +fn bench_verify_individual_votes(c: &mut Criterion) { + let shred_version = 134; + let mut group = c.benchmark_group("verify_votes_fallback"); + let thread_pool = get_thread_pool(); + + for &batch_size in BATCH_SIZES { + // Distinctness doesn't affect the cost of N individual verifications. + let (_vote, unverified_votes) = generate_test_data(shred_version, batch_size); + let label = format!("batch_{batch_size}"); + + group.bench_function(&label, |b| { + b.iter_batched( + || unverified_votes.clone(), + |votes| { + let res = verify_individual_votes(black_box(votes), &thread_pool); + black_box(res); + }, + BatchSize::SmallInput, + ) + }); + } + group.finish(); +} + +criterion_group!( + benches, + bench_verify_single_signature, + bench_verify_single_signature_with_prepared_message, + bench_verify_individual_votes +); +criterion_main!(benches); diff --git a/core/src/bls_sigverify/bls_cert_sigverify.rs b/bls-sigverify/src/bls_cert_sigverify.rs similarity index 61% rename from core/src/bls_sigverify/bls_cert_sigverify.rs rename to bls-sigverify/src/bls_cert_sigverify.rs index ba1fa66dbcc..fd0a0707713 100644 --- a/core/src/bls_sigverify/bls_cert_sigverify.rs +++ b/bls-sigverify/src/bls_cert_sigverify.rs @@ -1,12 +1,18 @@ use { - super::{bls_sigverifier::BAN_TIMEOUT, errors::SigVerifyCertError, stats::SigVerifyCertStats}, - crate::bls_sigverify::{bls_sigverifier::NUM_SLOTS_FOR_VERIFY, utils::send_certs_to_pool}, + crate::{ + bls_sigverifier::{BAN_TIMEOUT, NUM_SLOTS_FOR_VERIFY}, + errors::SigVerifyCertError, + sig_verified_messages::SigVerifiedBatch, + stats::SigVerifyCertStats, + utils::send_certs_to_pool, + }, agave_bls_cert_verify::cert_verify::Error as BlsCertVerifyError, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType, ConsensusMessage}, - fraction::Fraction, + certificate::{Certificate, CertificateType}, + unverified_vote_message::UnverifiedCertificate, }, crossbeam_channel::Sender, + log::info, rayon::{ ThreadPool, iter::{IntoParallelIterator, ParallelIterator}, @@ -16,26 +22,19 @@ use { solana_pubkey::Pubkey, solana_runtime::bank::Bank, solana_streamer::nonblocking::simple_qos::SimpleQosBanlist, - std::{collections::HashSet, num::NonZeroU64}, + std::collections::HashSet, thiserror::Error, }; -#[derive(Clone, Debug)] pub(super) struct CertPayload { - pub(super) cert: Certificate, - pub(super) remote_pubkey: Pubkey, + pub(super) cert: UnverifiedCertificate, + pub(super) sender_identity_pubkey: Pubkey, } #[derive(Debug, Error)] enum CertVerifyError { #[error("Cert Verification Error {0}")] CertVerifyFailed(#[from] BlsCertVerifyError), - #[error("Not enough stake {aggregate_stake}: {cert_fraction} < {required_fraction}")] - NotEnoughStake { - aggregate_stake: u64, - cert_fraction: Fraction, - required_fraction: Fraction, - }, #[error("discarding cert with slot {cert_slot} too far in future from root slot {root_slot}")] TooFarInFuture { cert_slot: Slot, root_slot: Slot }, } @@ -51,7 +50,7 @@ pub(super) fn verify_and_send_certificates( verified_certs_set: &mut HashSet, certs: Vec, root_bank: &Bank, - channel_to_pool: &Sender>, + channel_to_pool: &Sender, banlist: &SimpleQosBanlist, thread_pool: &ThreadPool, ) -> Result { @@ -88,7 +87,7 @@ pub(super) fn verify_and_send_certificates( /// /// The valid certs are inserted into the [`verified_certs_set`]. /// Invalid cert senders are banlisted. -/// Returns a Vec of [`ConsensusMessage`] constructed from the valid certs. +/// Returns a [`SigVerifiedBatch`] constructed from the valid certs. fn verify_certs( certs: Vec, root_bank: &Bank, @@ -96,37 +95,35 @@ fn verify_certs( stats: &mut SigVerifyCertStats, banlist: &SimpleQosBanlist, thread_pool: &ThreadPool, -) -> Vec { +) -> SigVerifiedBatch { let verified = thread_pool.install(|| { certs .into_par_iter() .map(|cert_payload| { - let res = verify_cert(&cert_payload.cert, root_bank); - (cert_payload, res) + let res = verify_cert(cert_payload.cert, root_bank); + (res, cert_payload.sender_identity_pubkey) }) .collect::>() }); - verified + let certs = verified .into_iter() - .filter_map(|(cert_payload, res)| match res { - Ok(()) => { - let cert = cert_payload.cert; + .filter_map(|(res, sender_identity_pubkey)| match res { + Ok(cert) => { if !verified_certs_set.insert(cert.cert_type) { stats.unnecessary_certs_verified += 1; } - Some(ConsensusMessage::Certificate(cert)) + Some(cert) } Err(e) => { match &e { - CertVerifyError::NotEnoughStake { .. } - | CertVerifyError::CertVerifyFailed(_) => { - if banlist.ban(cert_payload.remote_pubkey, BAN_TIMEOUT) { + CertVerifyError::CertVerifyFailed(_) => { + if banlist.ban(sender_identity_pubkey, BAN_TIMEOUT) { stats.already_banned += 1; } else { info!( - "bls_cert_sigverify: banned sender={} due to error {e}", - cert_payload.remote_pubkey + "bls_cert_sigverify: banned sender={sender_identity_pubkey} due \ + to error {e}" ); } } @@ -134,11 +131,8 @@ fn verify_certs( } match e { - CertVerifyError::NotEnoughStake { .. } => { - stats.stake_verification_failed += 1; - } CertVerifyError::CertVerifyFailed(_) => { - stats.signature_verification_failed += 1; + stats.certificate_verification_failed += 1; } CertVerifyError::TooFarInFuture { .. } => { stats.too_far_in_future += 1; @@ -147,10 +141,14 @@ fn verify_certs( None } }) - .collect() + .collect(); + SigVerifiedBatch::Certificates(certs) } -fn verify_cert(cert: &Certificate, root_bank: &Bank) -> Result<(), CertVerifyError> { +fn verify_cert( + cert: UnverifiedCertificate, + root_bank: &Bank, +) -> Result { let cert_slot = cert.cert_type.slot(); let root_slot = root_bank.slot(); if cert_slot > root_slot.saturating_add(NUM_SLOTS_FOR_VERIFY) { @@ -159,26 +157,6 @@ fn verify_cert(cert: &Certificate, root_bank: &Bank) -> Result<(), CertVerifyErr root_slot, }); } - let (aggregate_stake, total_stake) = root_bank.verify_certificate(cert)?; - debug_assert!(aggregate_stake <= total_stake); - verify_stake(cert, aggregate_stake, total_stake) -} - -fn verify_stake( - cert: &Certificate, - aggregate_stake: u64, - total_stake: u64, -) -> Result<(), CertVerifyError> { - let (required_fraction, _) = cert.cert_type.limits_and_vote_types(); - let total_stake = NonZeroU64::new(total_stake).expect("Total stake cannot be zero"); - let cert_fraction = Fraction::new(aggregate_stake, total_stake); - if cert_fraction >= required_fraction { - Ok(()) - } else { - Err(CertVerifyError::NotEnoughStake { - aggregate_stake, - cert_fraction, - required_fraction, - }) - } + let cert = root_bank.verify_certificate(cert)?; + Ok(cert) } diff --git a/core/src/bls_sigverify/bls_sigverifier.rs b/bls-sigverify/src/bls_sigverifier.rs similarity index 64% rename from core/src/bls_sigverify/bls_sigverifier.rs rename to bls-sigverify/src/bls_sigverifier.rs index fbdd629401f..52d1c74125d 100644 --- a/core/src/bls_sigverify/bls_sigverifier.rs +++ b/bls-sigverify/src/bls_sigverifier.rs @@ -1,35 +1,39 @@ //! The BLS signature verifier. use { - super::{ + crate::{ bls_cert_sigverify::{CertPayload, verify_and_send_certificates}, - bls_vote_sigverify::{VotePayload, verify_and_send_votes}, + bls_vote_sigverify::{UnverifiedVotePayload, verify_and_send_votes}, errors::SigVerifyError, - stats::SigVerifierStats, - }, - crate::cluster_info_vote_listener::VerifiedVoterSlotsSender, - agave_votor::{ - consensus_metrics::ConsensusMetricsEventSender, - consensus_rewards::{self}, generated_cert_types::GeneratedCertTypes, + rewards::rewards_wants_vote, + sig_verified_messages::SigVerifiedBatch, + stats::SigVerifierStats, }, agave_votor_messages::{ - consensus_message::{CertificateType, ConsensusMessage, VoteMessage}, + VerifiedVoterSlotsSender, + certificate::CertificateType, + metric_types::ConsensusMetricsEventSender, migration::MigrationStatus, reward_certificate::AddVoteMessage, + unverified_vote_message::{DecodedWireConsensusMessage, UnverifiedVoteMessage}, + vote::Vote, + wire::{VersionedWireConsensusMessage, VotePayloadToSign}, }, crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError}, + log::error, rayon::{ThreadPool, ThreadPoolBuilder}, solana_bls_signatures::pubkey::{PopVerified, PubkeyAffine as BlsPubkeyAffine}, solana_clock::Slot, solana_gossip::cluster_info::ClusterInfo, solana_ledger::leader_schedule_cache::LeaderScheduleCache, solana_measure::measure_us, + solana_perf::packet::packet_config, solana_pubkey::Pubkey, solana_runtime::{bank::Bank, bank_forks::SharableBanks}, solana_streamer::{nonblocking::simple_qos::SimpleQosBanlist, packet::PacketBatch}, std::{ - collections::HashSet, + collections::{HashMap, HashSet}, sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -49,26 +53,26 @@ pub(super) const NUM_SLOTS_FOR_VERIFY: Slot = 90_000; /// We ban the sender for 2 days which roughly corresponds to an epoch pub(super) const BAN_TIMEOUT: Duration = Duration::from_hours(48); -pub(crate) struct SigVerifierContext { - pub(crate) migration_status: Arc, - pub(crate) banlist: Arc, - pub(crate) sharable_banks: SharableBanks, - pub(crate) cluster_info: Arc, - pub(crate) leader_schedule: Arc, - pub(crate) num_threads: usize, - pub(crate) generated_cert_types: Arc, +pub struct SigVerifierContext { + pub migration_status: Arc, + pub banlist: Arc, + pub sharable_banks: SharableBanks, + pub cluster_info: Arc, + pub leader_schedule: Arc, + pub num_threads: usize, + pub generated_cert_types: Arc, } -pub(crate) struct SigVerifierChannels { - pub(crate) packet_receiver: Receiver, - pub(crate) channel_to_repair: VerifiedVoterSlotsSender, - pub(crate) channel_to_reward: Sender, - pub(crate) channel_to_pool: Sender>, - pub(crate) channel_to_metrics: ConsensusMetricsEventSender, +pub struct SigVerifierChannels { + pub packet_receiver: Receiver, + pub channel_to_repair: VerifiedVoterSlotsSender, + pub channel_to_reward: Sender, + pub channel_to_pool: Sender, + pub channel_to_metrics: ConsensusMetricsEventSender, } /// Starts the BLS sigverifier service in its own dedicated thread. -pub(crate) fn spawn_service( +pub fn spawn_service( exit: Arc, context: SigVerifierContext, channels: SigVerifierChannels, @@ -200,7 +204,7 @@ impl SigVerifier { fn maybe_prune_caches(&mut self, root_slot: Slot) { if self.last_checked_root_slot < root_slot { self.last_checked_root_slot = root_slot; - self.verified_certs.retain(|cert| cert.slot() > root_slot); + self.verified_certs.retain(|cert| cert.slot() >= root_slot); } } @@ -208,40 +212,61 @@ impl SigVerifier { &mut self, batches: Vec, root_bank: &Bank, - ) -> (Vec, Vec) { + ) -> ( + Vec, + HashMap>, + ) { let root_slot = root_bank.slot(); let mut certs = Vec::new(); - let mut votes = Vec::new(); + let mut votes: HashMap> = HashMap::new(); let mut num_pkts = 0u64; + let my_shred_version = self.cluster_info.my_shred_version(); for packet in batches.iter().flatten() { num_pkts = num_pkts.saturating_add(1); if packet.meta().discard() { self.stats.num_discarded_pkts += 1; continue; } - let Ok(msg) = packet.deserialize_slice::(..) else { + // TODO(#13227): Enforce shred_version matching during deserialization + let Ok(msg) = wincode::config::deserialize_exact::( + packet.data(..).unwrap_or_default(), + packet_config(), + ) else { self.stats.num_malformed_pkts += 1; continue; }; - let Some(remote_pubkey) = packet.meta().remote_pubkey() else { + let Some(sender_identity_pubkey) = packet.meta().remote_pubkey() else { debug_assert!(false, "BLS packet missing remote pubkey"); self.stats.num_malformed_pkts += 1; continue; }; - match msg { - ConsensusMessage::Vote(vote) => { - if let Some((pubkey, bls_pubkey)) = self.keep_vote(&vote, root_bank) { - votes.push(VotePayload { - vote_message: vote, - bls_pubkey, - pubkey, - remote_pubkey, - prepared_payload: None, - }); + let Some(decoded_msg) = DecodedWireConsensusMessage::try_new(msg, my_shred_version) + else { + self.stats.num_malformed_pkts += 1; + continue; + }; + + match decoded_msg { + DecodedWireConsensusMessage::Vote(unverified_vote) => { + if let Some((sender_vote_account_pubkey, sender_bls_pubkey)) = + self.keep_vote(&unverified_vote.vote, &unverified_vote, root_bank) + { + let vote_payload_to_sign = VotePayloadToSign::new_from_vote( + unverified_vote.vote, + unverified_vote.shred_version, + ); + votes.entry(vote_payload_to_sign).or_default().push( + UnverifiedVotePayload { + vote_message: unverified_vote, + sender_bls_pubkey, + sender_vote_account_pubkey, + sender_identity_pubkey, + }, + ); } } - ConsensusMessage::Certificate(cert) => { - if cert.cert_type.slot() <= root_slot { + DecodedWireConsensusMessage::Certificate(cert) => { + if cert.cert_type.slot() < root_slot { self.stats.num_old_certs_received += 1; continue; } @@ -255,7 +280,7 @@ impl SigVerifier { } certs.push(CertPayload { cert, - remote_pubkey, + sender_identity_pubkey, }); } } @@ -267,26 +292,29 @@ impl SigVerifier { /// If this vote should be verified, then returns the sender's Pubkey and BlsPubkey. fn keep_vote( &mut self, - vote: &VoteMessage, + vote: &Vote, + msg: &UnverifiedVoteMessage, root_bank: &Bank, ) -> Option<(Pubkey, PopVerified)> { let root_slot = root_bank.slot(); - let Some(rank_map) = root_bank.get_rank_map(vote.vote.slot()) else { + let Some(rank_map) = root_bank.get_rank_map(vote.slot()) else { self.stats.discard_vote_no_epoch_stakes += 1; return None; }; let entry = rank_map - .get_pubkey_stake_entry(vote.rank.into()) + .get_pubkey_stake_entry(msg.rank.into()) .or_else(|| { self.stats.discard_vote_invalid_rank += 1; None })?; let ret = Some((entry.vote_account_pubkey, entry.bls_pubkey)); - if vote.vote.slot() > root_slot { + if vote.slot() > root_slot + // Genesis votes should be allowed on the TowerBFT root + || (vote.is_genesis_vote() && vote.slot() >= root_slot) + { return ret; } - if consensus_rewards::wants_vote(&self.cluster_info, &self.leader_schedule, root_slot, vote) - { + if rewards_wants_vote(&self.cluster_info, &self.leader_schedule, root_slot, vote) { return ret; } self.stats.num_old_votes_received += 1; @@ -332,24 +360,24 @@ fn recv_batches( mod tests { use { super::*, - crate::cluster_info_vote_listener::VerifiedVoterSlotsReceiver, - agave_votor::{ - consensus_metrics::ConsensusMetricsEventReceiver, - consensus_pool::certificate_builder::CertificateBuilder, - }, + agave_votor::consensus_pool::certificate_builder::CertificateBuilder, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType, ConsensusMessage, VoteMessage}, + VerifiedVoterSlotsReceiver, + certificate::{Certificate, CertificateType}, + consensus_message::{Block, ConsensusMessage, VoteMessage}, + metric_types::ConsensusMetricsEventReceiver, vote::Vote, + wire::{VersionedWireConsensusMessage, get_vote_payload_to_sign}, }, bitvec::prelude::{BitVec, Lsb0}, - crossbeam_channel::{Receiver, TryRecvError}, + crossbeam_channel::{Receiver, TryRecvError, bounded}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature}, solana_epoch_schedule::EpochSchedule, solana_gossip::contact_info::ContactInfo, solana_hash::Hash, solana_keypair::Keypair, solana_net_utils::SocketAddrSpace, - solana_perf::packet::{Packet, RecycledPacketBatch}, + solana_perf::packet::{BytesPacket, BytesPacketBatch, Packet, RecycledPacketBatch}, solana_pubkey::Pubkey, solana_runtime::{ bank::{Bank, SlotLeader}, @@ -375,27 +403,27 @@ mod tests { _packet_sender: Sender, repair_receiver: VerifiedVoterSlotsReceiver, _reward_receiver: Receiver, - pool_receiver: Receiver>, + pool_receiver: Receiver, _metrics_receiver: ConsensusMetricsEventReceiver, generated_cert_types: Arc, } impl TestContext { fn new() -> Self { - let (channel_to_pool, pool_receiver) = crossbeam_channel::unbounded(); + let (channel_to_pool, pool_receiver) = bounded(1024); Self::new_with_pool_channel(channel_to_pool, pool_receiver) } fn new_with_pool_channel( - channel_to_pool: Sender>, - pool_receiver: Receiver>, + channel_to_pool: Sender, + pool_receiver: Receiver, ) -> Self { let num_validators = 10; let validator_keypairs = (0..num_validators) .map(|_| ValidatorVoteKeypairs::new_rand()) .collect::>(); let stakes_vec = (0..validator_keypairs.len()) - .map(|i| 1_000 - i as u64) + .map(|i| 1_000u64.saturating_sub(i as u64)) .collect::>(); let mut genesis = create_genesis_config_with_alpenglow_vote_accounts( 1_000_000_000, @@ -416,10 +444,10 @@ mod tests { let leader_schedule = Arc::new(LeaderScheduleCache::new_from_bank(&sharable_banks.root())); - let (channel_to_repair, repair_receiver) = crossbeam_channel::unbounded(); - let (channel_to_reward, reward_receiver) = crossbeam_channel::unbounded(); - let (packet_sender, packet_receiver) = crossbeam_channel::unbounded(); - let (channel_to_metrics, metrics_receiver) = crossbeam_channel::unbounded(); + let (channel_to_repair, repair_receiver) = bounded(1024); + let (channel_to_reward, reward_receiver) = bounded(1024); + let (packet_sender, packet_receiver) = bounded(1024); + let (channel_to_metrics, metrics_receiver) = bounded(1024); let generated_cert_types = Arc::new(GeneratedCertTypes::default()); let banlist = new_test_banlist(); @@ -457,11 +485,12 @@ mod tests { fn create_signed_vote_message( validator_keypairs: &[ValidatorVoteKeypairs], + shred_version: u16, vote: Vote, rank: usize, ) -> VoteMessage { let bls_keypair = &validator_keypairs[rank].bls_keypair; - let payload = wincode::serialize(&vote).expect("Failed to serialize vote"); + let payload = get_vote_payload_to_sign(vote, shred_version); let signature: Signature = bls_keypair.sign(&payload).into(); VoteMessage { vote, @@ -471,6 +500,7 @@ mod tests { } fn create_signed_certificate_message( + shred_version: u16, validator_keypairs: &[ValidatorVoteKeypairs], cert_type: CertificateType, ranks: &[usize], @@ -480,7 +510,7 @@ mod tests { let vote = cert_type.to_source_vote(); let vote_messages: Vec = ranks .iter() - .map(|&rank| create_signed_vote_message(validator_keypairs, vote, rank)) + .map(|&rank| create_signed_vote_message(validator_keypairs, shred_version, vote, rank)) .collect(); builder @@ -498,9 +528,13 @@ mod tests { } } - fn message_to_packet(message: &ConsensusMessage, remote_pubkey: Pubkey) -> Packet { - let mut packet = Packet::default(); - packet.populate_packet(None, message).unwrap(); + fn message_to_packet( + message: &ConsensusMessage, + shred_version: u16, + remote_pubkey: Pubkey, + ) -> BytesPacket { + let msg = VersionedWireConsensusMessage::new(message.clone(), shred_version); + let mut packet = BytesPacket::from_data(&msg).unwrap(); packet.meta_mut().set_remote_pubkey(remote_pubkey); packet } @@ -514,22 +548,30 @@ mod tests { let cert_type = CertificateType::Finalize(4); let vote_message1 = create_signed_vote_message( &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version(), Vote::new_finalization_vote(5), vote_rank1, ); - let cert = - create_signed_certificate_message(&ctx.validator_keypairs, cert_type, &cert_ranks); + let cert = create_signed_certificate_message( + ctx.verifier.cluster_info.my_shred_version(), + &ctx.validator_keypairs, + cert_type, + &cert_ranks, + ); let messages1 = vec![ ConsensusMessage::Vote(vote_message1), ConsensusMessage::Certificate(cert), ]; ctx.verifier - .verify_and_send_batches(messages_to_batches(&messages1)) + .verify_and_send_batches(messages_to_batches( + &messages1, + ctx.verifier.cluster_info.my_shred_version(), + )) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 2); - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 1); - assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 1); + assert_eq!(ctx.pool_receiver.try_iter().count(), 2); + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, 1); + assert_eq!(ctx.verifier.stats.cert_stats.pool_sent.0, 1); let received_verified_votes1 = ctx.repair_receiver.try_recv().unwrap(); assert_eq!( received_verified_votes1, @@ -542,18 +584,25 @@ mod tests { let vote_rank2 = 3; let vote_message2 = create_signed_vote_message( &ctx.validator_keypairs, - Vote::new_notarization_vote(6, Hash::new_unique()), + ctx.verifier.cluster_info.my_shred_version(), + Vote::new_notarization_vote(Block { + slot: 6, + block_id: Hash::new_unique(), + }), vote_rank2, ); let messages2 = vec![ConsensusMessage::Vote(vote_message2)]; ctx.verifier.stats = SigVerifierStats::new(ctx.verifier.sharable_banks.root().slot()); ctx.verifier - .verify_and_send_batches(messages_to_batches(&messages2)) + .verify_and_send_batches(messages_to_batches( + &messages2, + ctx.verifier.cluster_info.my_shred_version(), + )) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 1); - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 1); - assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 0); + assert_eq!(ctx.pool_receiver.try_iter().count(), 1); + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, 1); + assert_eq!(ctx.verifier.stats.cert_stats.pool_sent.0, 0); let received_verified_votes2 = ctx.repair_receiver.try_recv().unwrap(); assert_eq!( received_verified_votes2, @@ -566,17 +615,24 @@ mod tests { let vote_rank3 = 9; let vote_message3 = create_signed_vote_message( &ctx.validator_keypairs, - Vote::new_notarization_fallback_vote(7, Hash::new_unique()), + ctx.verifier.cluster_info.my_shred_version(), + Vote::new_notarization_fallback_vote(Block { + slot: 7, + block_id: Hash::new_unique(), + }), vote_rank3, ); let messages3 = vec![ConsensusMessage::Vote(vote_message3)]; ctx.verifier.stats = SigVerifierStats::new(ctx.verifier.sharable_banks.root().slot()); ctx.verifier - .verify_and_send_batches(messages_to_batches(&messages3)) + .verify_and_send_batches(messages_to_batches( + &messages3, + ctx.verifier.cluster_info.my_shred_version(), + )) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 1); - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 1); - assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 0); + assert_eq!(ctx.pool_receiver.try_iter().count(), 1); + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, 1); + assert_eq!(ctx.verifier.stats.cert_stats.pool_sent.0, 0); let received_verified_votes3 = ctx.repair_receiver.try_recv().unwrap(); assert_eq!( received_verified_votes3, @@ -596,9 +652,9 @@ mod tests { ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 0); - assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 0); - assert_eq!(ctx.verifier.stats.num_malformed_pkts, 1); + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, 0); + assert_eq!(ctx.verifier.stats.cert_stats.pool_sent.0, 0); + assert_eq!(ctx.verifier.stats.num_malformed_pkts.0, 1); // Expect no messages since the packet was malformed expect_no_receive(&ctx.pool_receiver); @@ -606,16 +662,20 @@ mod tests { // Send a packet with no epoch stakes let vote_message_no_stakes = create_signed_vote_message( &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version(), Vote::new_finalization_vote(5_000_000_000), // very high slot 0, ); let messages_no_stakes = vec![ConsensusMessage::Vote(vote_message_no_stakes)]; ctx.verifier - .verify_and_send_batches(messages_to_batches(&messages_no_stakes)) + .verify_and_send_batches(messages_to_batches( + &messages_no_stakes, + ctx.verifier.cluster_info.my_shred_version(), + )) .unwrap(); - assert_eq!(ctx.verifier.stats.discard_vote_no_epoch_stakes, 1); + assert_eq!(ctx.verifier.stats.discard_vote_no_epoch_stakes.0, 1); // Expect no messages since the packet was malformed expect_no_receive(&ctx.pool_receiver); @@ -627,43 +687,91 @@ mod tests { rank: 1000, // Invalid rank })]; ctx.verifier - .verify_and_send_batches(messages_to_batches(&messages_invalid_rank)) + .verify_and_send_batches(messages_to_batches( + &messages_invalid_rank, + ctx.verifier.cluster_info.my_shred_version(), + )) .unwrap(); - assert_eq!(ctx.verifier.stats.discard_vote_invalid_rank, 1); + assert_eq!(ctx.verifier.stats.discard_vote_invalid_rank.0, 1); // Expect no messages since the packet was malformed expect_no_receive(&ctx.pool_receiver); } + #[test] + fn test_shred_version_mismatch() { + let mut ctx = TestContext::new(); + let msgs = [ConsensusMessage::Vote(create_signed_vote_message( + &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version() + 1, + Vote::new_finalization_vote(5), + 0, + ))]; + // creating a packet with the wrong shred version + let packets = messages_to_batches(&msgs, ctx.verifier.cluster_info.my_shred_version() + 1); + ctx.verifier.verify_and_send_batches(packets).unwrap(); + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, 0); + assert_eq!(ctx.verifier.stats.cert_stats.pool_sent.0, 0); + assert_eq!(ctx.verifier.stats.num_malformed_pkts.0, 1); + } + #[test] fn test_blssigverifier_send_packets_channel_full() { agave_logger::setup(); let (channel_to_pool, pool_receiver) = crossbeam_channel::bounded(1); let mut ctx = TestContext::new_with_pool_channel(channel_to_pool, pool_receiver); - let msg1 = ConsensusMessage::Vote(create_signed_vote_message( + let msg1 = create_signed_vote_message( &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version(), Vote::new_finalization_vote(5), 0, - )); - let msg2 = ConsensusMessage::Vote(create_signed_vote_message( + ); + let msg2 = create_signed_vote_message( &ctx.validator_keypairs, - Vote::new_notarization_fallback_vote(6, Hash::new_unique()), + ctx.verifier.cluster_info.my_shred_version(), + Vote::new_notarization_fallback_vote(Block { + slot: 6, + block_id: Hash::new_unique(), + }), 2, - )); + ); ctx.verifier - .verify_and_send_batches(messages_to_batches(std::slice::from_ref(&msg1))) + .verify_and_send_batches(messages_to_batches( + std::slice::from_ref(&ConsensusMessage::Vote(msg1.clone())), + ctx.verifier.cluster_info.my_shred_version(), + )) .unwrap(); + + // The cap-1 channel is now full. The second send hits Full and falls + // back to a blocking send (see `send_votes_to_pool`); drain in a + // background thread so the blocking send can complete. + let pool_receiver = ctx.pool_receiver.clone(); + let drain = std::thread::spawn(move || { + let m1 = pool_receiver.recv().expect("recv msg1"); + let m2 = pool_receiver.recv().expect("recv msg2"); + // No leftover messages on the channel after both deliveries. + assert!(matches!( + pool_receiver.try_recv(), + Err(crossbeam_channel::TryRecvError::Empty) + )); + (m1, m2) + }); + ctx.verifier - .verify_and_send_batches(messages_to_batches(&[msg2])) + .verify_and_send_batches(messages_to_batches( + std::slice::from_ref(&ConsensusMessage::Vote(msg2.clone())), + ctx.verifier.cluster_info.my_shred_version(), + )) .unwrap(); - // We failed to send the second message because the channel is full. - let msgs = ctx.pool_receiver.try_iter().flatten().collect::>(); - assert_eq!(msgs.len(), 1); - assert_eq!(msgs, vec![msg1]); - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 1); - assert_eq!(ctx.verifier.stats.vote_stats.pool_channel_full, 1); + let (m1_recv, m2_recv) = drain.join().expect("drain joined"); + // Both messages were eventually delivered (no silent drop). + assert_eq!(m1_recv, SigVerifiedBatch::Votes(vec![msg1])); + assert_eq!(m2_recv, SigVerifiedBatch::Votes(vec![msg2])); + // pool_sent counts every message that made it onto the channel, + // whether via try_send or the blocking fallback. + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, 2); } #[test] @@ -675,13 +783,15 @@ mod tests { let msg = ConsensusMessage::Vote(create_signed_vote_message( &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version(), Vote::new_finalization_vote(5), 0, )); let messages = vec![msg]; - let result = ctx - .verifier - .verify_and_send_batches(messages_to_batches(&messages)); + let result = ctx.verifier.verify_and_send_batches(messages_to_batches( + &messages, + ctx.verifier.cluster_info.my_shred_version(), + )); assert!(result.is_err()); } @@ -691,21 +801,25 @@ mod tests { let message = ConsensusMessage::Vote(create_signed_vote_message( &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version(), Vote::new_finalization_vote(5), 0, )); - let mut packet = message_to_packet(&message, Pubkey::new_unique()); + let mut packet = message_to_packet( + &message, + ctx.verifier.cluster_info.my_shred_version(), + Pubkey::new_unique(), + ); packet.meta_mut().set_discard(true); // Manually discard - let packets = vec![packet]; - let packet_batches = vec![RecycledPacketBatch::new(packets).into()]; + let packet_batches = vec![BytesPacketBatch::from(vec![packet]).into()]; ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); expect_no_receive(&ctx.pool_receiver); - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, 0); - assert_eq!(ctx.verifier.stats.num_discarded_pkts, 1); + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, 0); + assert_eq!(ctx.verifier.stats.num_discarded_pkts.0, 1); } #[test] @@ -715,7 +829,8 @@ mod tests { let num_votes = 5; let mut packets = Vec::with_capacity(num_votes); let vote = Vote::new_skip_vote(42); - let vote_payload = wincode::serialize(&vote).expect("Failed to serialize vote"); + let vote_payload = + get_vote_payload_to_sign(vote, ctx.verifier.cluster_info.my_shred_version()); for (i, validator_keypair) in ctx.validator_keypairs.iter().enumerate().take(num_votes) { let rank = i as u16; @@ -726,18 +841,25 @@ mod tests { signature, rank, }); - packets.push(message_to_packet(&consensus_message, Pubkey::new_unique())); + packets.push(message_to_packet( + &consensus_message, + ctx.verifier.cluster_info.my_shred_version(), + Pubkey::new_unique(), + )); } - let packet_batches = vec![RecycledPacketBatch::new(packets).into()]; + let packet_batches = vec![BytesPacketBatch::from(packets).into()]; ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), - num_votes, - "Did not send all valid packets" - ); + let batches = ctx.pool_receiver.try_iter().collect::>(); + assert_eq!(batches.len(), 1); + match &batches[0] { + SigVerifiedBatch::Votes(votes) => { + assert_eq!(votes.len(), num_votes); + } + rest => panic!("unexpected type: {rest:?}"), + } } #[test] @@ -750,9 +872,10 @@ mod tests { let mut packets = Vec::with_capacity(num_votes); let vote1 = Vote::new_skip_vote(42); - let _vote1_payload = wincode::serialize(&vote1).expect("Failed to serialize vote"); - let vote2 = Vote::new_notarization_vote(43, Hash::new_unique()); - let _vote2_payload = wincode::serialize(&vote2).expect("Failed to serialize vote"); + let vote2 = Vote::new_notarization_vote(Block { + slot: 43, + block_id: Hash::new_unique(), + }); // Group 1 votes for (i, _) in ctx @@ -763,10 +886,15 @@ mod tests { { let msg = ConsensusMessage::Vote(create_signed_vote_message( &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version(), vote1, i, )); - packets.push(message_to_packet(&msg, Pubkey::new_unique())); + packets.push(message_to_packet( + &msg, + ctx.verifier.cluster_info.my_shred_version(), + Pubkey::new_unique(), + )); } // Group 2 votes @@ -779,21 +907,31 @@ mod tests { { let msg = ConsensusMessage::Vote(create_signed_vote_message( &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version(), vote2, i, )); - packets.push(message_to_packet(&msg, Pubkey::new_unique())); + packets.push(message_to_packet( + &msg, + ctx.verifier.cluster_info.my_shred_version(), + Pubkey::new_unique(), + )); } - let packet_batches = vec![RecycledPacketBatch::new(packets).into()]; + let packet_batches = vec![BytesPacketBatch::from(packets).into()]; ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), - num_votes, - "Did not send all valid packets" - ); + let batches = ctx.pool_receiver.try_iter().collect::>(); + assert_eq!(batches.len(), 2); + let total_votes_verified = batches + .into_iter() + .map(|batch| match batch { + SigVerifiedBatch::Votes(votes) => votes.len(), + rest => panic!("unexpected type: {rest:?}"), + }) + .sum::(); + assert_eq!(total_votes_verified, num_votes); assert_eq!( ctx.verifier.stats.vote_stats.distinct_votes_stats.count(), 1 @@ -818,11 +956,15 @@ mod tests { let mut packets = Vec::with_capacity(num_votes); let vote1 = Vote::new_skip_vote(42); - let vote1_payload = wincode::serialize(&vote1).expect("Failed to serialize vote"); + let vote1_payload = + get_vote_payload_to_sign(vote1, ctx.verifier.cluster_info.my_shred_version()); let vote2 = Vote::new_skip_vote(43); - let vote2_payload = wincode::serialize(&vote2).expect("Failed to serialize vote"); - let invalid_payload = - wincode::serialize(&Vote::new_skip_vote(99)).expect("Failed to serialize vote"); + let vote2_payload = + get_vote_payload_to_sign(vote2, ctx.verifier.cluster_info.my_shred_version()); + let invalid_payload = get_vote_payload_to_sign( + Vote::new_skip_vote(99), + ctx.verifier.cluster_info.my_shred_version(), + ); for (i, validator_keypair) in ctx.validator_keypairs.iter().enumerate().take(num_votes) { let rank = i as u16; @@ -846,26 +988,34 @@ mod tests { signature, rank, }); - packets.push(message_to_packet(&consensus_message, Pubkey::new_unique())); + packets.push(message_to_packet( + &consensus_message, + ctx.verifier.cluster_info.my_shred_version(), + Pubkey::new_unique(), + )); } - let packet_batches = vec![RecycledPacketBatch::new(packets).into()]; + let packet_batches = vec![BytesPacketBatch::from(packets).into()]; ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - let sent_messages: Vec<_> = ctx.pool_receiver.try_iter().flatten().collect(); - assert_eq!( - sent_messages.len(), - num_votes - 1, - "Only valid votes should be sent" - ); - assert!(!sent_messages.iter().any(|msg| { - if let ConsensusMessage::Vote(vm) = msg { - vm.vote == vote2 && vm.rank == invalid_rank - } else { - false - } - })); + let batches = ctx.pool_receiver.try_iter().collect::>(); + assert_eq!(batches.len(), 2); + let total_votes_verified = batches + .into_iter() + .map(|batch| match batch { + SigVerifiedBatch::Votes(votes) => { + for vote in &votes { + if vote.vote == vote2 && vote.rank == invalid_rank { + panic!("invalid vote verified"); + } + } + votes.len() + } + rest => panic!("unexpected type: {rest:?}"), + }) + .sum::(); + assert_eq!(total_votes_verified, num_votes - 1); } #[test] @@ -878,9 +1028,12 @@ mod tests { let mut consensus_messages = Vec::with_capacity(num_votes); // ADDED: To hold messages for later comparison. let vote = Vote::new_skip_vote(42); - let valid_vote_payload = wincode::serialize(&vote).expect("Failed to serialize vote"); - let invalid_vote_payload = - wincode::serialize(&Vote::new_skip_vote(99)).expect("Failed to serialize vote"); + let valid_vote_payload = + get_vote_payload_to_sign(vote, ctx.verifier.cluster_info.my_shred_version()); + let invalid_vote_payload = get_vote_payload_to_sign( + Vote::new_skip_vote(99), + ctx.verifier.cluster_info.my_shred_version(), + ); for (i, validator_keypair) in ctx.validator_keypairs.iter().enumerate().take(num_votes) { let rank = i as u16; @@ -900,28 +1053,40 @@ mod tests { consensus_messages.push(consensus_message.clone()); - packets.push(message_to_packet(&consensus_message, Pubkey::new_unique())); + packets.push(message_to_packet( + &consensus_message, + ctx.verifier.cluster_info.my_shred_version(), + Pubkey::new_unique(), + )); } - let packet_batches = vec![RecycledPacketBatch::new(packets).into()]; + let packet_batches = vec![BytesPacketBatch::from(packets).into()]; ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - let sent_messages: Vec<_> = ctx.pool_receiver.try_iter().flatten().collect(); - assert_eq!( - sent_messages.len(), - num_votes - 1, - "Only valid votes should be sent" - ); + let batches: Vec<_> = ctx.pool_receiver.try_iter().collect(); + assert_eq!(batches.len(), 1); + match &batches[0] { + SigVerifiedBatch::Votes(votes) => { + assert_eq!(votes.len(), num_votes - 1); + } + rest => panic!("unexpected type: {rest:?}"), + } // Ensure the message with the invalid rank is not in the sent messages. - assert!(!sent_messages.iter().any(|msg| { - if let ConsensusMessage::Vote(vm) = msg { - vm.rank == invalid_rank - } else { - false + let mut found_msg = false; + match &batches[0] { + SigVerifiedBatch::Votes(votes) => { + for vote in votes { + if vote.rank == invalid_rank { + found_msg = true; + break; + } + } } - })); + rest => panic!("unexpected type: {rest:?}"), + } + assert!(!found_msg); } #[test] @@ -930,20 +1095,27 @@ mod tests { // 2/3 of validators sign the cert. let num_signers = (ctx.validator_keypairs.len() * 2).div_ceil(3); - let cert_type = CertificateType::Notarize(10, Hash::new_unique()); + let cert_type = CertificateType::Notarize(Block { + slot: 10, + block_id: Hash::new_unique(), + }); let cert = create_signed_certificate_message( + ctx.verifier.cluster_info.my_shred_version(), &ctx.validator_keypairs, cert_type, &(0..num_signers).collect::>(), ); let consensus_message = ConsensusMessage::Certificate(cert); - let packet_batches = messages_to_batches(&[consensus_message]); + let packet_batches = messages_to_batches( + &[consensus_message], + ctx.verifier.cluster_info.my_shred_version(), + ); ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), + ctx.pool_receiver.try_iter().count(), 1, "Valid Base2 certificate should be sent" ); @@ -955,65 +1127,49 @@ mod tests { // 60% of validators sign the cert. let num_signers = (ctx.validator_keypairs.len() * 6).div_ceil(10); - let cert_type = CertificateType::Notarize(10, Hash::new_unique()); + let cert_type = CertificateType::Notarize(Block { + slot: 10, + block_id: Hash::new_unique(), + }); let cert = create_signed_certificate_message( + ctx.verifier.cluster_info.my_shred_version(), &ctx.validator_keypairs, cert_type, &(0..num_signers).collect::>(), ); let consensus_message = ConsensusMessage::Certificate(cert); - let packet_batches = messages_to_batches(&[consensus_message]); + let packet_batches = messages_to_batches( + &[consensus_message], + ctx.verifier.cluster_info.my_shred_version(), + ); ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), + ctx.pool_receiver.try_iter().count(), 1, "Valid Base2 certificate should be sent" ); } - #[test] - fn test_verify_certificate_base2_not_enough_stake() { - let mut ctx = TestContext::new(); - - // < 60% of validators sign the cert - assert!(ctx.validator_keypairs.len() >= 2); - let num_signers = (ctx.validator_keypairs.len() * 6) / 10 - 1; - let cert_type = CertificateType::Notarize(10, Hash::new_unique()); - let cert = create_signed_certificate_message( - &ctx.validator_keypairs, - cert_type, - &(0..num_signers).collect::>(), - ); - let consensus_message = ConsensusMessage::Certificate(cert); - let packet_batches = messages_to_batches(&[consensus_message]); - - // The call still succeeds, but the packet is marked for discard. - ctx.verifier - .verify_and_send_batches(packet_batches) - .unwrap(); - assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), - 0, - "This certificate should be invalid" - ); - assert_eq!(ctx.verifier.stats.cert_stats.stake_verification_failed, 1); - } - #[test] fn test_verify_certificate_base3_valid() { let mut ctx = TestContext::new(); let slot = 20; let block_hash = Hash::new_unique(); - let notarize_vote = Vote::new_notarization_vote(slot, block_hash); - let notarize_fallback_vote = Vote::new_notarization_fallback_vote(slot, block_hash); + let block = Block { + slot, + block_id: block_hash, + }; + let notarize_vote = Vote::new_notarization_vote(block); + let notarize_fallback_vote = Vote::new_notarization_fallback_vote(block); let mut all_vote_messages = Vec::new(); (0..4).for_each(|i| { all_vote_messages.push(create_signed_vote_message( &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version(), notarize_vote, i, )) @@ -1021,24 +1177,28 @@ mod tests { (4..7).for_each(|i| { all_vote_messages.push(create_signed_vote_message( &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version(), notarize_fallback_vote, i, )) }); - let cert_type = CertificateType::NotarizeFallback(slot, block_hash); + let cert_type = CertificateType::NotarizeFallback(block); let mut builder = CertificateBuilder::new(cert_type); builder .aggregate(&all_vote_messages) .expect("Failed to aggregate votes"); let cert = builder.build().expect("Failed to build certificate"); let consensus_message = ConsensusMessage::Certificate(cert); - let packet_batches = messages_to_batches(&[consensus_message]); + let packet_batches = messages_to_batches( + &[consensus_message], + ctx.verifier.cluster_info.my_shred_version(), + ); ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), + ctx.pool_receiver.try_iter().count(), 1, "Valid Base3 certificate should be sent" ); @@ -1050,12 +1210,17 @@ mod tests { let slot = 20; let block_hash = Hash::new_unique(); - let notarize_vote = Vote::new_notarization_vote(slot, block_hash); - let notarize_fallback_vote = Vote::new_notarization_fallback_vote(slot, block_hash); + let block = Block { + slot, + block_id: block_hash, + }; + let notarize_vote = Vote::new_notarization_vote(block); + let notarize_fallback_vote = Vote::new_notarization_fallback_vote(block); let mut all_vote_messages = Vec::new(); (0..4).for_each(|i| { all_vote_messages.push(create_signed_vote_message( &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version(), notarize_vote, i, )) @@ -1063,72 +1228,33 @@ mod tests { (4..6).for_each(|i| { all_vote_messages.push(create_signed_vote_message( &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version(), notarize_fallback_vote, i, )) }); - let cert_type = CertificateType::NotarizeFallback(slot, block_hash); + let cert_type = CertificateType::NotarizeFallback(block); let mut builder = CertificateBuilder::new(cert_type); builder .aggregate(&all_vote_messages) .expect("Failed to aggregate votes"); let cert = builder.build().expect("Failed to build certificate"); let consensus_message = ConsensusMessage::Certificate(cert); - let packet_batches = messages_to_batches(&[consensus_message]); + let packet_batches = messages_to_batches( + &[consensus_message], + ctx.verifier.cluster_info.my_shred_version(), + ); ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), + ctx.pool_receiver.try_iter().count(), 1, "Valid Base3 certificate should be sent" ); } - #[test] - fn test_verify_certificate_base3_not_enough_stake() { - let mut ctx = TestContext::new(); - - let slot = 20; - let block_hash = Hash::new_unique(); - let notarize_vote = Vote::new_notarization_vote(slot, block_hash); - let notarize_fallback_vote = Vote::new_notarization_fallback_vote(slot, block_hash); - let mut all_vote_messages = Vec::new(); - (0..4).for_each(|i| { - all_vote_messages.push(create_signed_vote_message( - &ctx.validator_keypairs, - notarize_vote, - i, - )) - }); - (4..5).for_each(|i| { - all_vote_messages.push(create_signed_vote_message( - &ctx.validator_keypairs, - notarize_fallback_vote, - i, - )) - }); - let cert_type = CertificateType::NotarizeFallback(slot, block_hash); - let mut builder = CertificateBuilder::new(cert_type); - builder - .aggregate(&all_vote_messages) - .expect("Failed to aggregate votes"); - let cert = builder.build().expect("Failed to build certificate"); - let consensus_message = ConsensusMessage::Certificate(cert); - let packet_batches = messages_to_batches(&[consensus_message]); - - ctx.verifier - .verify_and_send_batches(packet_batches) - .unwrap(); - assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), - 0, - "This certificate should be invalid" - ); - assert_eq!(ctx.verifier.stats.cert_stats.stake_verification_failed, 1); - } - #[test] fn test_verify_certificate_invalid_signature() { let mut ctx = TestContext::new(); @@ -1137,7 +1263,10 @@ mod tests { let num_signers = (ctx.validator_keypairs.len() * 7).div_ceil(10); let slot = 10; let block_hash = Hash::new_unique(); - let cert_type = CertificateType::Notarize(slot, block_hash); + let cert_type = CertificateType::Notarize(Block { + slot, + block_id: block_hash, + }); let mut bitmap = BitVec::::new(); bitmap.resize(num_signers, false); for i in 0..num_signers { @@ -1151,14 +1280,21 @@ mod tests { bitmap: encoded_bitmap, }; let consensus_message = ConsensusMessage::Certificate(cert); - let packet_batches = messages_to_batches(&[consensus_message]); + let packet_batches = messages_to_batches( + &[consensus_message], + ctx.verifier.cluster_info.my_shred_version(), + ); ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); expect_no_receive(&ctx.pool_receiver); assert_eq!( - ctx.verifier.stats.cert_stats.signature_verification_failed, + ctx.verifier + .stats + .cert_stats + .certificate_verification_failed + .0, 1 ); } @@ -1171,7 +1307,8 @@ mod tests { let num_votes = 2; let vote = Vote::new_skip_vote(42); - let vote_payload = wincode::serialize(&vote).unwrap(); + let vote_payload = + get_vote_payload_to_sign(vote, ctx.verifier.cluster_info.my_shred_version()); for (i, validator_keypair) in ctx.validator_keypairs.iter().enumerate().take(num_votes) { let rank = i as u16; let bls_keypair = &validator_keypair.bls_keypair; @@ -1181,14 +1318,24 @@ mod tests { signature, rank, }); - packets.push(message_to_packet(&consensus_message, Pubkey::new_unique())); + packets.push(message_to_packet( + &consensus_message, + ctx.verifier.cluster_info.my_shred_version(), + Pubkey::new_unique(), + )); } // 70% of validators sign. let num_signers = (ctx.validator_keypairs.len() * 7).div_ceil(10); - let cert_type = CertificateType::Notarize(10, Hash::new_unique()); - let cert_original_vote = Vote::new_notarization_vote(10, cert_type.to_block().unwrap().1); - let cert_payload = wincode::serialize(&cert_original_vote).unwrap(); + let cert_type = CertificateType::Notarize(Block { + slot: 10, + block_id: Hash::new_unique(), + }); + let cert_original_vote = Vote::new_notarization_vote(cert_type.to_block().unwrap()); + let cert_payload = get_vote_payload_to_sign( + cert_original_vote, + ctx.verifier.cluster_info.my_shred_version(), + ); let cert_vote_messages: Vec = (0..num_signers) .map(|i| { @@ -1208,20 +1355,40 @@ mod tests { let consensus_message_cert = ConsensusMessage::Certificate(cert); packets.push(message_to_packet( &consensus_message_cert, + ctx.verifier.cluster_info.my_shred_version(), Pubkey::new_unique(), )); - let packet_batches = vec![RecycledPacketBatch::new(packets).into()]; + let packet_batches = vec![BytesPacketBatch::from(packets).into()]; ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!( - ctx.pool_receiver.try_iter().flatten().count(), - num_votes + 1, - "All valid messages in a mixed batch should be sent" - ); - assert_eq!(ctx.verifier.stats.vote_stats.pool_sent, num_votes as u64); - assert_eq!(ctx.verifier.stats.cert_stats.pool_sent, 1); + let batches = ctx.pool_receiver.try_iter().collect::>(); + assert_eq!(batches.len(), 2); + + let batch_0_was_votes = match &batches[0] { + SigVerifiedBatch::Votes(votes) => { + assert_eq!(votes.len(), num_votes); + true + } + SigVerifiedBatch::Certificates(certs) => { + assert_eq!(certs.len(), 1); + false + } + }; + + match &batches[1] { + SigVerifiedBatch::Votes(votes) => { + assert!(!batch_0_was_votes); + assert_eq!(votes.len(), num_votes); + } + SigVerifiedBatch::Certificates(certs) => { + assert!(batch_0_was_votes); + assert_eq!(certs.len(), 1); + } + } + assert_eq!(ctx.verifier.stats.vote_stats.pool_sent.0, num_votes as u64); + assert_eq!(ctx.verifier.stats.cert_stats.pool_sent.0, 1); } #[test] @@ -1230,7 +1397,8 @@ mod tests { let invalid_rank = 999; let vote = Vote::new_skip_vote(42); - let vote_payload = wincode::serialize(&vote).unwrap(); + let vote_payload = + get_vote_payload_to_sign(vote, ctx.verifier.cluster_info.my_shred_version()); let bls_keypair = &ctx.validator_keypairs[0].bls_keypair; let signature: Signature = bls_keypair.sign(&vote_payload).into(); @@ -1240,20 +1408,23 @@ mod tests { rank: invalid_rank, }); - let packet_batches = messages_to_batches(&[consensus_message]); + let packet_batches = messages_to_batches( + &[consensus_message], + ctx.verifier.cluster_info.my_shred_version(), + ); ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); expect_no_receive(&ctx.pool_receiver); - assert_eq!(ctx.verifier.stats.discard_vote_invalid_rank, 1); + assert_eq!(ctx.verifier.stats.discard_vote_invalid_rank.0, 1); } #[test] fn test_verify_old_vote_and_cert() { - let (message_sender, message_receiver) = crossbeam_channel::unbounded(); - let (votes_for_repair_sender, _) = crossbeam_channel::unbounded(); - let (consensus_metrics_sender, _) = crossbeam_channel::unbounded(); - let (reward_votes_sender, _reward_votes_receiver) = crossbeam_channel::unbounded(); + let (message_sender, message_receiver) = bounded(1024); + let (votes_for_repair_sender, _) = bounded(1024); + let (consensus_metrics_sender, _) = bounded(1024); + let (reward_votes_sender, _reward_votes_receiver) = bounded(1024); let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new_rand()) .collect::>(); @@ -1281,7 +1452,7 @@ mod tests { SocketAddrSpace::Unspecified, )); let leader_schedule = Arc::new(LeaderScheduleCache::new_from_bank(&sharable_banks.root())); - let (_packet_sender, packet_receiver) = crossbeam_channel::unbounded(); + let (_packet_sender, packet_receiver) = bounded(1024); let mut sig_verifier = SigVerifier::new( SigVerifierContext { migration_status: Arc::new(MigrationStatus::default()), @@ -1302,7 +1473,8 @@ mod tests { ); let vote = Vote::new_skip_vote(2); - let vote_payload = wincode::serialize(&vote).unwrap(); + let vote_payload = + get_vote_payload_to_sign(vote, sig_verifier.cluster_info.my_shred_version()); let bls_keypair = &validator_keypairs[0].bls_keypair; let signature: Signature = bls_keypair.sign(&vote_payload).into(); let consensus_message_vote = ConsensusMessage::Vote(VoteMessage { @@ -1310,28 +1482,35 @@ mod tests { signature, rank: 0, }); - let packet_batches_vote = messages_to_batches(&[consensus_message_vote]); + let packet_batches_vote = messages_to_batches( + &[consensus_message_vote], + sig_verifier.cluster_info.my_shred_version(), + ); sig_verifier .verify_and_send_batches(packet_batches_vote) .unwrap(); expect_no_receive(&message_receiver); - assert_eq!(sig_verifier.stats.num_old_votes_received, 1); + assert_eq!(sig_verifier.stats.num_old_votes_received.0, 1); let cert = create_signed_certificate_message( + sig_verifier.cluster_info.my_shred_version(), &validator_keypairs, CertificateType::Finalize(3), &[0], // Signer rank 0 ); let consensus_message_cert = ConsensusMessage::Certificate(cert); - let packet_batches_cert = messages_to_batches(&[consensus_message_cert]); + let packet_batches_cert = messages_to_batches( + &[consensus_message_cert], + sig_verifier.cluster_info.my_shred_version(), + ); sig_verifier .verify_and_send_batches(packet_batches_cert) .unwrap(); expect_no_receive(&message_receiver); - assert_eq!(sig_verifier.stats.num_old_certs_received, 1); - assert_eq!(sig_verifier.stats.num_old_votes_received, 1); + assert_eq!(sig_verifier.stats.num_old_certs_received.0, 1); + assert_eq!(sig_verifier.stats.num_old_votes_received.0, 1); } #[test] @@ -1342,9 +1521,14 @@ mod tests { let num_signers = (ctx.validator_keypairs.len() * 8).div_ceil(10); let slot = 10; let block_hash = Hash::new_unique(); - let cert_type = CertificateType::Notarize(slot, block_hash); - let original_vote = Vote::new_notarization_vote(slot, block_hash); - let signed_payload = wincode::serialize(&original_vote).unwrap(); + let block = Block { + slot, + block_id: block_hash, + }; + let cert_type = CertificateType::Notarize(block); + let original_vote = Vote::new_notarization_vote(block); + let signed_payload = + get_vote_payload_to_sign(original_vote, ctx.verifier.cluster_info.my_shred_version()); let mut vote_messages: Vec = (0..num_signers) .map(|i| { let signature = ctx.validator_keypairs[i].bls_keypair.sign(&signed_payload); @@ -1362,15 +1546,18 @@ mod tests { .expect("Failed to aggregate votes"); let cert1 = builder1.build().expect("Failed to build certificate"); let consensus_message1 = ConsensusMessage::Certificate(cert1); - let packet_batches1 = messages_to_batches(&[consensus_message1]); + let packet_batches1 = messages_to_batches( + &[consensus_message1], + ctx.verifier.cluster_info.my_shred_version(), + ); ctx.verifier .verify_and_send_batches(packet_batches1) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 1); - assert_eq!(ctx.verifier.stats.num_verified_certs_received, 0); - assert_eq!(ctx.verifier.stats.cert_stats.certs_to_sig_verify, 1); + assert_eq!(ctx.pool_receiver.try_iter().count(), 1); + assert_eq!(ctx.verifier.stats.num_verified_certs_received.0, 0); + assert_eq!(ctx.verifier.stats.cert_stats.certs_to_sig_verify.0, 1); vote_messages.pop(); // Remove one signature let mut builder2 = CertificateBuilder::new(cert_type); @@ -1379,15 +1566,18 @@ mod tests { .expect("Failed to aggregate votes"); let cert2 = builder2.build().expect("Failed to build certificate"); let consensus_message2 = ConsensusMessage::Certificate(cert2); - let packet_batches2 = messages_to_batches(&[consensus_message2]); + let packet_batches2 = messages_to_batches( + &[consensus_message2], + ctx.verifier.cluster_info.my_shred_version(), + ); ctx.verifier.stats = SigVerifierStats::new(ctx.verifier.sharable_banks.root().slot()); ctx.verifier .verify_and_send_batches(packet_batches2) .unwrap(); expect_no_receive(&ctx.pool_receiver); - assert_eq!(ctx.verifier.stats.num_verified_certs_received, 1); - assert_eq!(ctx.verifier.stats.cert_stats.certs_to_sig_verify, 0); + assert_eq!(ctx.verifier.stats.num_verified_certs_received.0, 1); + assert_eq!(ctx.verifier.stats.cert_stats.certs_to_sig_verify.0, 0); } #[test] @@ -1396,25 +1586,30 @@ mod tests { let vote_message = ConsensusMessage::Vote(create_signed_vote_message( &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version(), Vote::new_skip_vote(42), 0, )); let cert_message = ConsensusMessage::Certificate(create_signed_certificate_message( + ctx.verifier.cluster_info.my_shred_version(), &ctx.validator_keypairs, - CertificateType::Notarize(43, Hash::new_unique()), + CertificateType::Notarize(Block { + slot: 43, + block_id: Hash::new_unique(), + }), &(0..7).collect::>(), )); let vote_sender = Pubkey::new_unique(); let cert_sender = Pubkey::new_unique(); - let packet_batches = messages_to_batches_with_remote_pubkeys(&[ - (vote_message, vote_sender), - (cert_message, cert_sender), - ]); + let packet_batches = messages_to_batches_with_remote_pubkeys( + &[(vote_message, vote_sender), (cert_message, cert_sender)], + ctx.verifier.cluster_info.my_shred_version(), + ); ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 2); + assert_eq!(ctx.pool_receiver.try_iter().count(), 2); assert!(!ctx.banlist.is_banned(&vote_sender)); assert!(!ctx.banlist.is_banned(&cert_sender)); } @@ -1424,8 +1619,12 @@ mod tests { let mut ctx = TestContext::new(); let vote = Vote::new_skip_vote(42); - let valid_payload = wincode::serialize(&vote).unwrap(); - let invalid_payload = wincode::serialize(&Vote::new_skip_vote(999)).unwrap(); + let valid_payload = + get_vote_payload_to_sign(vote, ctx.verifier.cluster_info.my_shred_version()); + let invalid_payload = get_vote_payload_to_sign( + Vote::new_skip_vote(999), + ctx.verifier.cluster_info.my_shred_version(), + ); let invalid_indexes = [1usize, 3usize]; let messages: Vec<_> = ctx .validator_keypairs @@ -1448,9 +1647,19 @@ mod tests { .collect(); ctx.verifier - .verify_and_send_batches(messages_to_batches_with_remote_pubkeys(&messages)) + .verify_and_send_batches(messages_to_batches_with_remote_pubkeys( + &messages, + ctx.verifier.cluster_info.my_shred_version(), + )) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 3); + let batches = ctx.pool_receiver.try_iter().collect::>(); + assert_eq!(batches.len(), 1); + match &batches[0] { + SigVerifiedBatch::Votes(votes) => { + assert_eq!(votes.len(), 3); + } + rest => panic!("unexpected type: {rest:?}"), + } for (i, (_, sender)) in messages.iter().enumerate() { if invalid_indexes.contains(&i) { @@ -1475,8 +1684,12 @@ mod tests { let messages: Vec<_> = (0..5) .map(|i| { let slot = 10 + i as u64; - let cert_type = CertificateType::Notarize(slot, Hash::new_unique()); + let cert_type = CertificateType::Notarize(Block { + slot, + block_id: Hash::new_unique(), + }); let mut cert = create_signed_certificate_message( + ctx.verifier.cluster_info.my_shred_version(), &ctx.validator_keypairs, cert_type, &(0..7).collect::>(), @@ -1489,9 +1702,19 @@ mod tests { .collect(); ctx.verifier - .verify_and_send_batches(messages_to_batches_with_remote_pubkeys(&messages)) + .verify_and_send_batches(messages_to_batches_with_remote_pubkeys( + &messages, + ctx.verifier.cluster_info.my_shred_version(), + )) .unwrap(); - assert_eq!(ctx.pool_receiver.try_iter().flatten().count(), 3); + let batches = ctx.pool_receiver.try_iter().collect::>(); + assert_eq!(batches.len(), 1); + match &batches[0] { + SigVerifiedBatch::Certificates(certs) => { + assert_eq!(certs.len(), 3); + } + rest => panic!("unexpected type: {rest:?}"), + } for (i, (_, sender)) in messages.iter().enumerate() { if invalid_indexes.contains(&i) { @@ -1515,16 +1738,20 @@ mod tests { let cert_type = CertificateType::Skip(slot); ctx.generated_cert_types.insert_cert(cert_type); let cert = create_signed_certificate_message( + ctx.verifier.cluster_info.my_shred_version(), &ctx.validator_keypairs, cert_type, &(0..ctx.validator_keypairs.len()).collect::>(), ); let consensus_message = ConsensusMessage::Certificate(cert); - let packet_batches = messages_to_batches(&[consensus_message]); + let packet_batches = messages_to_batches( + &[consensus_message], + ctx.verifier.cluster_info.my_shred_version(), + ); ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!(ctx.verifier.stats.num_generated_certs_received, 1); + assert_eq!(ctx.verifier.stats.num_generated_certs_received.0, 1); } #[test] @@ -1533,6 +1760,7 @@ mod tests { let slot = ctx.verifier.sharable_banks.root().slot() + NUM_SLOTS_FOR_VERIFY + 1; let cert_type = CertificateType::Skip(slot); let cert = create_signed_certificate_message( + ctx.verifier.cluster_info.my_shred_version(), &ctx.validator_keypairs, cert_type, &(0..ctx.validator_keypairs.len()).collect::>(), @@ -1540,33 +1768,38 @@ mod tests { let cert = ConsensusMessage::Certificate(cert); let vote = ConsensusMessage::Vote(create_signed_vote_message( &ctx.validator_keypairs, + ctx.verifier.cluster_info.my_shred_version(), Vote::new_skip_vote(slot), 0, )); - let packet_batches = messages_to_batches(&[cert, vote]); + let packet_batches = + messages_to_batches(&[cert, vote], ctx.verifier.cluster_info.my_shred_version()); ctx.verifier .verify_and_send_batches(packet_batches) .unwrap(); - assert_eq!(ctx.verifier.stats.cert_stats.too_far_in_future, 1); - assert_eq!(ctx.verifier.stats.vote_stats.too_far_in_future, 1); + assert_eq!(ctx.verifier.stats.cert_stats.too_far_in_future.0, 1); + assert_eq!(ctx.verifier.stats.vote_stats.too_far_in_future.0, 1); } - fn messages_to_batches(messages: &[ConsensusMessage]) -> Vec { + fn messages_to_batches(messages: &[ConsensusMessage], shred_version: u16) -> Vec { let messages_with_remote_pubkeys: Vec<_> = messages .iter() .cloned() .map(|message| (message, Pubkey::new_unique())) .collect(); - messages_to_batches_with_remote_pubkeys(&messages_with_remote_pubkeys) + messages_to_batches_with_remote_pubkeys(&messages_with_remote_pubkeys, shred_version) } fn messages_to_batches_with_remote_pubkeys( messages: &[(ConsensusMessage, Pubkey)], + shred_version: u16, ) -> Vec { let packets: Vec<_> = messages .iter() - .map(|(message, remote_pubkey)| message_to_packet(message, *remote_pubkey)) + .map(|(message, remote_pubkey)| { + message_to_packet(message, shred_version, *remote_pubkey) + }) .collect(); - vec![RecycledPacketBatch::new(packets).into()] + vec![BytesPacketBatch::from(packets).into()] } } diff --git a/bls-sigverify/src/bls_vote_sigverify.rs b/bls-sigverify/src/bls_vote_sigverify.rs new file mode 100644 index 00000000000..57031d4f764 --- /dev/null +++ b/bls-sigverify/src/bls_vote_sigverify.rs @@ -0,0 +1,257 @@ +#[cfg(feature = "dev-context-only-utils")] +use qualifier_attr::qualifiers; +use { + crate::{ + bls_sigverifier::{BAN_TIMEOUT, NUM_SLOTS_FOR_VERIFY, SigVerifierChannels}, + errors::SigVerifyVoteError, + rewards::rewards_wants_vote, + sig_verified_messages::SigVerifiedBatch, + stats::SigVerifyVoteStats, + utils::{ + send_votes_to_metrics, send_votes_to_pool, send_votes_to_repair, send_votes_to_rewards, + }, + }, + agave_votor_messages::{ + consensus_message::VoteMessage, + metric_types::ConsensusMetricsEvent, + reward_certificate::AddVoteMessage, + unverified_vote_message::UnverifiedVoteMessage, + vote::Vote, + wire::{VotePayloadToSign, get_vote_payload_to_sign}, + }, + log::info, + rayon::{ + ThreadPool, + iter::{Either, IntoParallelIterator, ParallelIterator}, + }, + solana_bls_signatures::pubkey::{ + PopVerified, PubkeyAffine as BlsPubkeyAffine, VerifySignature, + }, + solana_clock::Slot, + solana_gossip::cluster_info::ClusterInfo, + solana_ledger::leader_schedule_cache::LeaderScheduleCache, + solana_measure::{measure::Measure, measure_us}, + solana_pubkey::Pubkey, + solana_runtime::bank::Bank, + solana_streamer::nonblocking::simple_qos::SimpleQosBanlist, + std::collections::HashMap, +}; + +fn into_vote_msg(msg: UnverifiedVoteMessage) -> VoteMessage { + VoteMessage { + vote: msg.vote, + signature: msg.signature, + rank: msg.rank, + } +} + +#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] +struct VerifiedVotePayload { + vote_message: VoteMessage, + sender_vote_account_pubkey: Pubkey, +} + +/// [`VoteMessage`] along with other information needed to sig verify it. +#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] +#[derive(Clone, Debug)] +pub(super) struct UnverifiedVotePayload { + pub vote_message: UnverifiedVoteMessage, + pub sender_bls_pubkey: PopVerified, + pub sender_vote_account_pubkey: Pubkey, + pub sender_identity_pubkey: Pubkey, +} + +impl UnverifiedVotePayload { + fn verify(self) -> Option { + let payload = + get_vote_payload_to_sign(self.vote_message.vote, self.vote_message.shred_version); + let is_verified = self + .sender_bls_pubkey + .verify_signature(&self.vote_message.signature, &payload) + .is_ok(); + is_verified.then_some(VerifiedVotePayload { + vote_message: into_vote_msg(self.vote_message), + sender_vote_account_pubkey: self.sender_vote_account_pubkey, + }) + } +} + +/// Verifies votes and sends the verified votes to the consensus pool; and sends the desired subset +/// to rewards container and repair. +/// +/// Any vote that fails fallback individual signature verification will have its sender banlisted. +pub(super) fn verify_and_send_votes( + unverified_votes: HashMap>, + root_bank: &Bank, + cluster_info: &ClusterInfo, + leader_schedule: &LeaderScheduleCache, + banlist: &SimpleQosBanlist, + thread_pool: &ThreadPool, + channels: &SigVerifierChannels, +) -> Result { + let mut measure = Measure::start("verify_and_send_votes"); + let mut stats = SigVerifyVoteStats::default(); + if unverified_votes.is_empty() { + return Ok(stats); + } + stats + .distinct_votes_stats + .add_sample(unverified_votes.len() as u64); + + for (vote_payload_to_sign, unverified_votes) in unverified_votes { + stats.votes_to_sig_verify += unverified_votes.len() as u64; + let verified_votes = verify_votes( + root_bank, + vote_payload_to_sign, + unverified_votes, + &mut stats, + banlist, + thread_pool, + ); + stats.sig_verified_votes += verified_votes.len() as u64; + + let (votes_for_pool, msgs_for_repair, msg_for_reward, msg_for_metrics) = + process_verified_votes(verified_votes, root_bank, cluster_info, leader_schedule); + + send_votes_to_pool(votes_for_pool, &channels.channel_to_pool, &mut stats)?; + send_votes_to_repair(msgs_for_repair, &channels.channel_to_repair, &mut stats)?; + send_votes_to_rewards(msg_for_reward, &channels.channel_to_reward, &mut stats)?; + send_votes_to_metrics(msg_for_metrics, &channels.channel_to_metrics, &mut stats)?; + } + + measure.stop(); + stats + .fn_verify_and_send_votes_stats + .add_sample(measure.as_us()); + Ok(stats) +} + +/// If the vote is relevant to repair, then adds it to the [`msgs_for_repair`] so it can eventually +/// be sent to repair. +fn inspect_for_repair( + vote: &VerifiedVotePayload, + msgs_for_repair: &mut HashMap>, +) { + let vote_slot = vote.vote_message.vote.slot(); + match vote.vote_message.vote { + Vote::Notarize(_) | Vote::Finalize(_) | Vote::NotarizeFallback(_) => { + msgs_for_repair + .entry(vote.sender_vote_account_pubkey) + .or_default() + .push(vote_slot); + } + Vote::Skip(_) | Vote::SkipFallback(_) | Vote::Genesis(_) => (), + } +} + +/// Processes the verified votes for various downstream services. +/// +/// In particular, collects and returns the relevant messages for the consensus pool; rewards; +/// repair; and metrics; +fn process_verified_votes( + verified_votes: Vec, + root_bank: &Bank, + cluster_info: &ClusterInfo, + leader_schedule: &LeaderScheduleCache, +) -> ( + SigVerifiedBatch, + HashMap>, + AddVoteMessage, + Vec, +) { + let mut votes_for_reward = Vec::with_capacity(verified_votes.len()); + let mut msgs_for_repair = HashMap::new(); + let mut votes_for_pool = Vec::with_capacity(verified_votes.len()); + let mut votes_for_metrics = Vec::with_capacity(verified_votes.len()); + for payload in verified_votes { + if rewards_wants_vote( + cluster_info, + leader_schedule, + root_bank.slot(), + &payload.vote_message.vote, + ) { + votes_for_reward.push(payload.vote_message.clone()); + } + + inspect_for_repair(&payload, &mut msgs_for_repair); + + votes_for_metrics.push(ConsensusMetricsEvent::Vote { + id: payload.sender_vote_account_pubkey, + vote: payload.vote_message.vote, + }); + votes_for_pool.push(payload.vote_message); + } + let msgs_for_repair = msgs_for_repair + .into_iter() + .map(|(pubkey, mut slots)| { + slots.sort_unstable(); + slots.dedup(); + (pubkey, slots) + }) + .collect(); + let votes_for_pool = SigVerifiedBatch::Votes(votes_for_pool); + ( + votes_for_pool, + msgs_for_repair, + AddVoteMessage { + votes: votes_for_reward, + }, + votes_for_metrics, + ) +} + +/// Sig verifies `unverified_votes` and returns a `Vec` of votes that passed verification. +fn verify_votes( + root_bank: &Bank, + vote_payload_to_sign: VotePayloadToSign, + unverified_votes: Vec, + stats: &mut SigVerifyVoteStats, + banlist: &SimpleQosBanlist, + thread_pool: &ThreadPool, +) -> Vec { + // Filter votes too far in the future. + if vote_payload_to_sign.slot() > root_bank.slot().saturating_add(NUM_SLOTS_FOR_VERIFY) { + stats.too_far_in_future += unverified_votes.len() as u64; + return vec![]; + } + + // Fallback to individual verification + let ((verified_votes, invalid_remote_pubkeys), time_us) = + measure_us!(verify_individual_votes(unverified_votes, thread_pool)); + for sender_identity_pubkey in invalid_remote_pubkeys { + if banlist.ban(sender_identity_pubkey, BAN_TIMEOUT) { + stats.already_banned += 1; + } else { + info!( + "bls_vote_sigverify: banned sender={sender_identity_pubkey} due to failed \ + verification" + ); + } + } + stats.fn_verify_individual_votes_stats.add_sample(time_us); + + verified_votes +} + +/// Verifies votes individually on a thread pool. +/// +/// Returns: +/// - `Vec`: votes that passed verification. +/// - `Vec`: senders' identity pubkeys for votes that failed verification. +#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] +fn verify_individual_votes( + unverified_votes: Vec, + thread_pool: &ThreadPool, +) -> (Vec, Vec) { + thread_pool.install(|| { + unverified_votes + .into_par_iter() + .partition_map(|unverified_vote| { + let sender_identity_pubkey = unverified_vote.sender_identity_pubkey; + match unverified_vote.verify() { + Some(vote) => Either::Left(vote), + None => Either::Right(sender_identity_pubkey), + } + }) + }) +} diff --git a/core/src/bls_sigverify/errors.rs b/bls-sigverify/src/errors.rs similarity index 100% rename from core/src/bls_sigverify/errors.rs rename to bls-sigverify/src/errors.rs diff --git a/votor/src/generated_cert_types.rs b/bls-sigverify/src/generated_cert_types.rs similarity index 65% rename from votor/src/generated_cert_types.rs rename to bls-sigverify/src/generated_cert_types.rs index 97f7a073fca..666e5ef6577 100644 --- a/votor/src/generated_cert_types.rs +++ b/bls-sigverify/src/generated_cert_types.rs @@ -1,8 +1,6 @@ -#[cfg(feature = "dev-context-only-utils")] -use qualifier_attr::qualifiers; use { - agave_votor_messages::consensus_message::CertificateType, parking_lot::RwLock, - solana_clock::Slot, std::collections::HashSet, + agave_votor_messages::certificate::CertificateType, parking_lot::RwLock, solana_clock::Slot, + std::collections::HashSet, }; /// A simple container that allows the consensus pool to communicate with the bls sigverifier @@ -17,13 +15,12 @@ impl GeneratedCertTypes { } /// Inserts the `cert_type` into the container. - #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] - pub(crate) fn insert_cert(&self, cert_type: CertificateType) { + pub fn insert_cert(&self, cert_type: CertificateType) { self.0.write().insert(cert_type); } /// Prunes the container, it drops all certs older than `root_slot` as they are no longer needed. - pub(crate) fn prune(&self, root_slot: Slot) { + pub fn prune(&self, root_slot: Slot) { self.0.write().retain(|c| c.slot() >= root_slot); } } diff --git a/core/src/bls_sigverify/mod.rs b/bls-sigverify/src/lib.rs similarity index 50% rename from core/src/bls_sigverify/mod.rs rename to bls-sigverify/src/lib.rs index d214c96dd67..c48cdd8faf7 100644 --- a/core/src/bls_sigverify/mod.rs +++ b/bls-sigverify/src/lib.rs @@ -1,6 +1,11 @@ +#![cfg(feature = "agave-unstable-api")] + pub mod bls_cert_sigverify; pub mod bls_sigverifier; pub mod bls_vote_sigverify; mod errors; +pub mod generated_cert_types; +pub mod rewards; +pub mod sig_verified_messages; pub mod stats; mod utils; diff --git a/bls-sigverify/src/rewards.rs b/bls-sigverify/src/rewards.rs new file mode 100644 index 00000000000..3edb411990a --- /dev/null +++ b/bls-sigverify/src/rewards.rs @@ -0,0 +1,37 @@ +use { + agave_votor_messages::{reward_certificate::NUM_SLOTS_FOR_REWARD, vote::Vote}, + solana_clock::Slot, + solana_gossip::cluster_info::ClusterInfo, + solana_ledger::leader_schedule_cache::LeaderScheduleCache, +}; + +/// Returns [`false`] if the rewards container is not interested in the [`VoteMessage`]. +/// Returns [`true`] if the rewards container might be interested in the [`VoteMessage`]. +pub fn rewards_wants_vote( + cluster_info: &ClusterInfo, + leader_schedule: &LeaderScheduleCache, + root_slot: Slot, + vote: &Vote, +) -> bool { + match vote { + Vote::Notarize(_) | Vote::Skip(_) => (), + Vote::Finalize(_) + | Vote::NotarizeFallback(_) + | Vote::SkipFallback(_) + | Vote::Genesis(_) => return false, + } + let vote_slot = vote.slot(); + if vote_slot.saturating_add(NUM_SLOTS_FOR_REWARD) <= root_slot { + return false; + } + let my_pubkey = cluster_info.id(); + let Some(leader) = + leader_schedule.slot_leader_at(vote_slot.saturating_add(NUM_SLOTS_FOR_REWARD), None) + else { + return false; + }; + if leader.id != my_pubkey { + return false; + } + true +} diff --git a/bls-sigverify/src/sig_verified_messages.rs b/bls-sigverify/src/sig_verified_messages.rs new file mode 100644 index 00000000000..dddf1af12b9 --- /dev/null +++ b/bls-sigverify/src/sig_verified_messages.rs @@ -0,0 +1,29 @@ +use agave_votor_messages::{certificate::Certificate, consensus_message::VoteMessage}; + +/// A batch of vote or cert being sent within the node. They were either sig verified before being +/// sent or they were generated by the node itself so do not need to be sig verified. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SigVerifiedBatch { + /// Batch of votes. + Votes(Vec), + /// Batch of certs. + Certificates(Vec), +} + +impl SigVerifiedBatch { + /// Returns the length of the batch + pub fn len(&self) -> usize { + match self { + Self::Votes(votes) => votes.len(), + Self::Certificates(certs) => certs.len(), + } + } + + /// Returns true if the batch is empty. + pub fn is_empty(&self) -> bool { + match self { + Self::Votes(votes) => votes.is_empty(), + Self::Certificates(certs) => certs.is_empty(), + } + } +} diff --git a/core/src/bls_sigverify/stats.rs b/bls-sigverify/src/stats.rs similarity index 70% rename from core/src/bls_sigverify/stats.rs rename to bls-sigverify/src/stats.rs index 202c1a25d8c..7ad7ebab2fc 100644 --- a/core/src/bls_sigverify/stats.rs +++ b/bls-sigverify/src/stats.rs @@ -3,7 +3,11 @@ use qualifier_attr::qualifiers; use { agave_math_utils::welford_stats::WelfordStats, solana_clock::Slot, - std::time::{Duration, Instant}, + solana_metrics::datapoint_info, + std::{ + num::Saturating, + time::{Duration, Instant}, + }, }; /// Max number of root slots to wait before triggering reporting of stats. @@ -30,7 +34,8 @@ impl Reporting { /// Returns `true` if reporting should be done else `false`. fn should_report(&self, root_slot: Slot) -> bool { - root_slot >= self.slot + SLOTS_INTERVAL || self.time.elapsed() > DURATION_INTERVAL + root_slot >= self.slot.saturating_add(SLOTS_INTERVAL) + || self.time.elapsed() > DURATION_INTERVAL } } @@ -48,21 +53,21 @@ pub(super) struct SigVerifierStats { /// Number of packets received. pub(super) num_pkts: WelfordStats, /// Number of discarded packets received from the streamer. - pub(super) num_discarded_pkts: u64, + pub(super) num_discarded_pkts: Saturating, /// Number of times we failed to deserialize a packet. - pub(super) num_malformed_pkts: u64, + pub(super) num_malformed_pkts: Saturating, /// Number of votes discarded due to an invalid rank. - pub(super) discard_vote_invalid_rank: u64, + pub(super) discard_vote_invalid_rank: Saturating, /// Number of votes discarded due to no epoch stakes. - pub(super) discard_vote_no_epoch_stakes: u64, + pub(super) discard_vote_no_epoch_stakes: Saturating, /// Number of outdated votes received. - pub(super) num_old_votes_received: u64, + pub(super) num_old_votes_received: Saturating, /// Number of outdated certs received. - pub(super) num_old_certs_received: u64, + pub(super) num_old_certs_received: Saturating, /// Number of already verified certs received. - pub(super) num_verified_certs_received: u64, + pub(super) num_verified_certs_received: Saturating, /// Number of certs received that the node has already generated. - pub(super) num_generated_certs_received: u64, + pub(super) num_generated_certs_received: Saturating, /// Last time the stats were reported. pub(super) last_report: Reporting, } @@ -74,14 +79,14 @@ impl SigVerifierStats { cert_stats: SigVerifyCertStats::default(), extract_filter_msgs_us: WelfordStats::default(), num_pkts: WelfordStats::default(), - discard_vote_invalid_rank: 0, - num_discarded_pkts: 0, - num_malformed_pkts: 0, - discard_vote_no_epoch_stakes: 0, - num_old_votes_received: 0, - num_old_certs_received: 0, - num_verified_certs_received: 0, - num_generated_certs_received: 0, + discard_vote_invalid_rank: Saturating(0), + num_discarded_pkts: Saturating(0), + num_malformed_pkts: Saturating(0), + discard_vote_no_epoch_stakes: Saturating(0), + num_old_votes_received: Saturating(0), + num_old_certs_received: Saturating(0), + num_verified_certs_received: Saturating(0), + num_generated_certs_received: Saturating(0), verify_and_send_batch_us: WelfordStats::default(), last_report: Reporting::new(root_slot), } @@ -131,26 +136,30 @@ impl SigVerifierStats { extract_filter_msgs_us.mean().unwrap_or(0), i64 ), - ("discard_vote_invalid_rank", *discard_vote_invalid_rank, i64), - ("num_discarded_pkts", *num_discarded_pkts, i64), - ("num_old_votes_received", *num_old_votes_received, i64), + ( + "discard_vote_invalid_rank", + discard_vote_invalid_rank.0, + i64 + ), + ("num_discarded_pkts", num_discarded_pkts.0, i64), + ("num_old_votes_received", num_old_votes_received.0, i64), ( "num_verified_certs_received", - *num_verified_certs_received, + num_verified_certs_received.0, i64 ), ( "num_generated_certs_received", - *num_generated_certs_received, + num_generated_certs_received.0, i64 ), ( "discard_vote_no_epoch_stakes", - *discard_vote_no_epoch_stakes, + discard_vote_no_epoch_stakes.0, i64 ), - ("num_malformed_pkts", *num_malformed_pkts, i64), - ("num_old_certs_received", *num_old_certs_received, i64), + ("num_malformed_pkts", num_malformed_pkts.0, i64), + ("num_old_certs_received", num_old_certs_received.0, i64), ( "verify_and_send_batch_us_max", verify_and_send_batch_us.maximum().unwrap_or(0), @@ -177,26 +186,26 @@ impl SigVerifierStats { #[derive(Default, Debug)] pub(super) struct SigVerifyCertStats { /// Number of certs [`verify_and_send_certificates`] was requested to verify the signature of. - pub(super) certs_to_sig_verify: u64, + pub(super) certs_to_sig_verify: Saturating, /// Number of certs [`verify_and_send_certificates`] successfully verified the signature of. - pub(super) sig_verified_certs: u64, + pub(super) sig_verified_certs: Saturating, /// Number of certs that were verified unnecessarily because another cert of the same /// `CertificateType` was already verified. - pub(super) unnecessary_certs_verified: u64, + pub(super) unnecessary_certs_verified: Saturating, /// Number of times we are banning a validator that was already banned. - pub(super) already_banned: u64, + pub(super) already_banned: Saturating, - /// Number of times stake verification failed on a cert. - pub(super) stake_verification_failed: u64, - /// Number of times signature verification failed on a cert. - pub(super) signature_verification_failed: u64, + /// Number of times cert verification failed. + pub(super) certificate_verification_failed: Saturating, /// Number of times the cert was too far in the future and discarded. - pub(super) too_far_in_future: u64, + pub(super) too_far_in_future: Saturating, + /// How many messages are outstanding on the channel. + pub(super) pool_outstanding_msgs: Saturating, /// Number of votes sent successfully over the channel to consensus pool. - pub(super) pool_sent: u64, - /// Number of times the channel to consensus pool was full. - pub(super) pool_channel_full: u64, + pub(super) pool_sent: Saturating, + /// Number of times the channel to consensus pool was full and we resorted to blocking send. + pub(super) pool_channel_full: Saturating, /// Stats for [`verify_and_send_certificates`]. pub(super) fn_verify_and_send_certs_stats: WelfordStats, @@ -209,9 +218,9 @@ impl SigVerifyCertStats { sig_verified_certs, unnecessary_certs_verified, already_banned, - stake_verification_failed, - signature_verification_failed, + certificate_verification_failed, too_far_in_future, + pool_outstanding_msgs, pool_sent, pool_channel_full, fn_verify_and_send_certs_stats, @@ -220,9 +229,9 @@ impl SigVerifyCertStats { self.sig_verified_certs += sig_verified_certs; self.unnecessary_certs_verified += unnecessary_certs_verified; self.already_banned += already_banned; - self.stake_verification_failed += stake_verification_failed; - self.signature_verification_failed += signature_verification_failed; + self.certificate_verification_failed += certificate_verification_failed; self.too_far_in_future += too_far_in_future; + self.pool_outstanding_msgs += pool_outstanding_msgs; self.pool_sent += pool_sent; self.pool_channel_full += pool_channel_full; self.fn_verify_and_send_certs_stats @@ -235,32 +244,33 @@ impl SigVerifyCertStats { sig_verified_certs, unnecessary_certs_verified, already_banned, - stake_verification_failed, - signature_verification_failed, + certificate_verification_failed, too_far_in_future, + pool_outstanding_msgs, pool_sent, pool_channel_full, fn_verify_and_send_certs_stats, } = self; + datapoint_info!( "bls_cert_sigverify_stats", - ("certs_to_sig_verify", *certs_to_sig_verify, i64), - ("sig_verified_certs", *sig_verified_certs, i64), + ("certs_to_sig_verify", certs_to_sig_verify.0, i64), + ("sig_verified_certs", sig_verified_certs.0, i64), ( "unnecessary_certs_verified", - *unnecessary_certs_verified, + unnecessary_certs_verified.0, i64 ), - ("already_banned", *already_banned, i64), - ("stake_verification_failed", *stake_verification_failed, i64), + ("already_banned", already_banned.0, i64), ( - "signature_verification_failed", - *signature_verification_failed, + "certificate_verification_failed", + certificate_verification_failed.0, i64 ), - ("too_far_in_future", *too_far_in_future, i64), - ("pool_sent", *pool_sent, i64), - ("pool_channel_full", *pool_channel_full, i64), + ("too_far_in_future", too_far_in_future.0, i64), + ("pool_outstanding_msgs", pool_outstanding_msgs.0, i64), + ("pool_sent", pool_sent.0, i64), + ("pool_channel_full", pool_channel_full.0, i64), ( "fn_verify_and_send_certs_count", fn_verify_and_send_certs_stats.count(), @@ -280,36 +290,36 @@ impl SigVerifyCertStats { #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] pub(super) struct SigVerifyVoteStats { /// Number of votes [`verify_and_send_votes`] was requested to verify the signature of. - pub(super) votes_to_sig_verify: u64, + pub(super) votes_to_sig_verify: Saturating, /// Number of votes [`verify_and_send_votes`] successfully verified the signature of. - pub(super) sig_verified_votes: u64, + pub(super) sig_verified_votes: Saturating, /// Number of times the cert was too far in the future and discarded. - pub(super) too_far_in_future: u64, + pub(super) too_far_in_future: Saturating, /// Number of times we are banning a validator that was already banned. - pub(super) already_banned: u64, + pub(super) already_banned: Saturating, /// Number of votes sent successfully over the channel to metrics. - pub(super) metrics_sent: u64, + pub(super) metrics_sent: Saturating, /// Number of times the channel to metrics was full. - pub(super) metrics_channel_full: u64, + pub(super) metrics_channel_full: Saturating, /// Number of votes sent successfully over the channel to rewards. - pub(super) rewards_sent: u64, + pub(super) rewards_sent: Saturating, /// Number of times the channel to rewards was full. - pub(super) rewards_channel_full: u64, + pub(super) rewards_channel_full: Saturating, + /// How many messages are outstanding on the channel. + pub(super) pool_outstanding_msgs: Saturating, /// Number of votes sent successfully over the channel to consensus pool. - pub(super) pool_sent: u64, - /// Number of times the channel to consensus pool was full. - pub(super) pool_channel_full: u64, + pub(super) pool_sent: Saturating, + /// Number of times the channel to consensus pool was full and we resorted to blocking send. + pub(super) pool_channel_full: Saturating, /// Number of votes sent successfully over the channel to repair. - pub(super) repair_sent: u64, + pub(super) repair_sent: Saturating, /// Number of times the channel to repair was full. - pub(super) repair_channel_full: u64, + pub(super) repair_channel_full: Saturating, /// Stats for [`verify_and_send_votes`]. pub(super) fn_verify_and_send_votes_stats: WelfordStats, - /// Stats for [`verify_votes_optimistic`]. - pub(super) fn_verify_votes_optimistic_stats: WelfordStats, /// Stats for [`verify_individual_votes`]. pub(super) fn_verify_individual_votes_stats: WelfordStats, @@ -330,10 +340,10 @@ impl SigVerifyVoteStats { rewards_channel_full, repair_sent, repair_channel_full, + pool_outstanding_msgs, pool_sent, pool_channel_full, fn_verify_and_send_votes_stats, - fn_verify_votes_optimistic_stats, fn_verify_individual_votes_stats, distinct_votes_stats, } = other; @@ -347,12 +357,11 @@ impl SigVerifyVoteStats { self.rewards_channel_full += rewards_channel_full; self.repair_sent += repair_sent; self.repair_channel_full += repair_channel_full; + self.pool_outstanding_msgs += pool_outstanding_msgs; self.pool_sent += pool_sent; self.pool_channel_full += pool_channel_full; self.fn_verify_and_send_votes_stats .merge(fn_verify_and_send_votes_stats); - self.fn_verify_votes_optimistic_stats - .merge(fn_verify_votes_optimistic_stats); self.fn_verify_individual_votes_stats .merge(fn_verify_individual_votes_stats); self.distinct_votes_stats.merge(distinct_votes_stats); @@ -370,27 +379,28 @@ impl SigVerifyVoteStats { rewards_channel_full, repair_sent, repair_channel_full, + pool_outstanding_msgs, pool_sent, pool_channel_full, fn_verify_and_send_votes_stats, - fn_verify_votes_optimistic_stats, fn_verify_individual_votes_stats, distinct_votes_stats, } = self; datapoint_info!( "bls_vote_sigverify_stats", - ("votes_to_sig_verify", *votes_to_sig_verify, i64), - ("sig_verified_votes", *sig_verified_votes, i64), - ("too_far_in_future", *too_far_in_future, i64), - ("already_banned", *already_banned, i64), - ("metrics_sent", *metrics_sent, i64), - ("metrics_channel_full", *metrics_channel_full, i64), - ("rewards_sent", *rewards_sent, i64), - ("rewards_channel_full", *rewards_channel_full, i64), - ("repair_sent", *repair_sent, i64), - ("repair_channel_full", *repair_channel_full, i64), - ("pool_sent", *pool_sent, i64), - ("pool_channel_full", *pool_channel_full, i64), + ("votes_to_sig_verify", votes_to_sig_verify.0, i64), + ("sig_verified_votes", sig_verified_votes.0, i64), + ("too_far_in_future", too_far_in_future.0, i64), + ("already_banned", already_banned.0, i64), + ("metrics_sent", metrics_sent.0, i64), + ("metrics_channel_full", metrics_channel_full.0, i64), + ("rewards_sent", rewards_sent.0, i64), + ("rewards_channel_full", rewards_channel_full.0, i64), + ("repair_sent", repair_sent.0, i64), + ("repair_channel_full", repair_channel_full.0, i64), + ("pool_outstanding_msgs", pool_outstanding_msgs.0, i64), + ("pool_sent", pool_sent.0, i64), + ("pool_channel_full", pool_channel_full.0, i64), ( "fn_verify_and_send_votes_count", fn_verify_and_send_votes_stats.count(), @@ -401,16 +411,6 @@ impl SigVerifyVoteStats { fn_verify_and_send_votes_stats.mean().unwrap_or(0), i64 ), - ( - "fn_verify_votes_optimistic_count", - fn_verify_votes_optimistic_stats.count(), - i64 - ), - ( - "fn_verify_votes_optimistic_mean", - fn_verify_votes_optimistic_stats.mean().unwrap_or(0), - i64 - ), ( "fn_verify_individual_votes_count", fn_verify_individual_votes_stats.count(), diff --git a/core/src/bls_sigverify/utils.rs b/bls-sigverify/src/utils.rs similarity index 57% rename from core/src/bls_sigverify/utils.rs rename to bls-sigverify/src/utils.rs index 889afe8455a..389de618b5f 100644 --- a/core/src/bls_sigverify/utils.rs +++ b/bls-sigverify/src/utils.rs @@ -1,17 +1,19 @@ use { - super::{errors::SigVerifyVoteError, stats::SigVerifyVoteStats}, crate::{ - bls_sigverify::{errors::SigVerifyCertError, stats::SigVerifyCertStats}, - cluster_info_vote_listener::VerifiedVoterSlotsSender, + errors::{SigVerifyCertError, SigVerifyVoteError}, + sig_verified_messages::SigVerifiedBatch, + stats::{SigVerifyCertStats, SigVerifyVoteStats}, }, - agave_votor::consensus_metrics::{ConsensusMetricsEvent, ConsensusMetricsEventSender}, agave_votor_messages::{ - consensus_message::ConsensusMessage, reward_certificate::AddVoteMessage, + VerifiedVoterSlotsSender, + metric_types::{ConsensusMetricsEvent, ConsensusMetricsEventSender}, + reward_certificate::AddVoteMessage, }, crossbeam_channel::{Sender, TrySendError}, + log::{error, info}, solana_clock::Slot, solana_pubkey::Pubkey, - std::{collections::HashMap, time::Instant}, + std::{collections::HashMap, num::Saturating, time::Instant}, }; pub(super) fn send_votes_to_metrics( @@ -53,23 +55,34 @@ pub(super) fn send_votes_to_rewards( } } +/// Sends the `batch` to the consensus pool. If the channel is full, then does a +/// blocking send. pub(super) fn send_votes_to_pool( - votes: Vec, - channel: &Sender>, + batch: SigVerifiedBatch, + channel: &Sender, stats: &mut SigVerifyVoteStats, ) -> Result<(), SigVerifyVoteError> { - let len = votes.len(); - if len == 0 { + if batch.is_empty() { return Ok(()); } - match channel.try_send(votes) { + let len = batch.len(); + match channel.try_send(batch) { Ok(()) => { stats.pool_sent += len as u64; + stats.pool_outstanding_msgs = Saturating(channel.len() as u64); Ok(()) } - Err(TrySendError::Full(_)) => { + Err(TrySendError::Full(msgs)) => { stats.pool_channel_full += 1; - Ok(()) + error!("votes channel to consensus pool is full. Doing a blocking send."); + match channel.send(msgs) { + Ok(()) => { + info!("votes channel to consensus pool has space again"); + stats.pool_sent += len as u64; + Ok(()) + } + Err(_) => Err(SigVerifyVoteError::ConsensusPoolChannelDisconnected), + } } Err(TrySendError::Disconnected(_)) => { Err(SigVerifyVoteError::ConsensusPoolChannelDisconnected) @@ -98,23 +111,34 @@ pub(super) fn send_votes_to_repair( Ok(()) } +/// Sends the `batch` to the consensus pool. If the channel is bounded and full, then does a +/// blocking send. pub(super) fn send_certs_to_pool( - messages: Vec, - channel_to_pool: &Sender>, + batch: SigVerifiedBatch, + channel_to_pool: &Sender, stats: &mut SigVerifyCertStats, ) -> Result<(), SigVerifyCertError> { - if messages.is_empty() { + if batch.is_empty() { return Ok(()); } - let len = messages.len(); - match channel_to_pool.try_send(messages) { + let len = batch.len(); + match channel_to_pool.try_send(batch) { Ok(()) => { stats.pool_sent += len as u64; + stats.pool_outstanding_msgs = Saturating(channel_to_pool.len() as u64); Ok(()) } - Err(TrySendError::Full(_)) => { + Err(TrySendError::Full(msgs)) => { stats.pool_channel_full += 1; - Ok(()) + error!("certs channel to consensus pool is full. Doing a blocking send."); + match channel_to_pool.send(msgs) { + Ok(()) => { + info!("certs channel to consensus pool has space again"); + stats.pool_sent += len as u64; + Ok(()) + } + Err(_) => Err(SigVerifyCertError::ConsensusPoolChannelDisconnected), + } } Err(TrySendError::Disconnected(_)) => { Err(SigVerifyCertError::ConsensusPoolChannelDisconnected) diff --git a/bucket_map/Cargo.toml b/bucket_map/Cargo.toml index d0bbf74fd73..3c3660fb543 100644 --- a/bucket_map/Cargo.toml +++ b/bucket_map/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [lib] crate-type = ["lib"] name = "solana_bucket_map" @@ -37,3 +42,6 @@ fs_extra = { workspace = true } rayon = { workspace = true } solana-bucket-map = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-pubkey = { workspace = true, features = ["rand"] } + +[lints] +workspace = true diff --git a/bucket_map/src/restart.rs b/bucket_map/src/restart.rs index affb7458a78..09c19193995 100644 --- a/bucket_map/src/restart.rs +++ b/bucket_map/src/restart.rs @@ -95,7 +95,7 @@ impl Debug for RestartableBucket { write!( f, "{:?}", - &self.restart.as_ref().map(|restart| restart.lock().unwrap()) + self.restart.as_ref().map(|restart| restart.lock().unwrap()) )?; Ok(()) } @@ -191,11 +191,10 @@ impl Restart { let dir = fs::read_dir(drive); if let Ok(dir) = dir { for entry in dir.flatten() { - if let Some(name) = entry.path().file_name() { - if let Some(id) = name.to_str().and_then(|str| str.parse::().ok()) - { - result.insert(id, entry.path()); - } + if let Some(name) = entry.path().file_name() + && let Some(id) = name.to_str().and_then(|str| str.parse::().ok()) + { + result.insert(id, entry.path()); } } } diff --git a/builtins-default-costs/Cargo.toml b/builtins-default-costs/Cargo.toml index 2c6e2aefb8d..3a7cde4b23f 100644 --- a/builtins-default-costs/Cargo.toml +++ b/builtins-default-costs/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] # Add additional builtin programs here [lib] @@ -29,6 +31,7 @@ solana-pubkey = { workspace = true } solana-sdk-ids = { workspace = true } [dev-dependencies] +solana-pubkey = { workspace = true, features = ["std"] } static_assertions = { workspace = true } [lints] diff --git a/builtins/Cargo.toml b/builtins/Cargo.toml index 24db4d80d45..5988ae4d481 100644 --- a/builtins/Cargo.toml +++ b/builtins/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] dev-context-only-utils = [] diff --git a/builtins/src/lib.rs b/builtins/src/lib.rs index 8b3182488f2..67605d116d0 100644 --- a/builtins/src/lib.rs +++ b/builtins/src/lib.rs @@ -14,10 +14,7 @@ pub mod core_bpf_migration; pub mod prototype; use { - crate::{ - core_bpf_migration::{CoreBpfMigrationConfig, CoreBpfMigrationTargetType}, - prototype::{BuiltinPrototype, StatelessBuiltinPrototype}, - }, + crate::prototype::{BuiltinPrototype, StatelessBuiltinPrototype}, agave_feature_set as feature_set, solana_program_runtime::solana_sbpf::program::BuiltinFunctionDefinition, solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable}, @@ -111,37 +108,7 @@ pub static BUILTINS: &[BuiltinPrototype] = &[ }, ]; -pub static STATELESS_BUILTINS: &[StatelessBuiltinPrototype] = &[StatelessBuiltinPrototype { - core_bpf_migration_config: Some(CoreBpfMigrationConfig { - source_buffer_address: buffer_accounts::slashing_program::id(), - upgrade_authority_address: None, - feature_id: feature_set::enshrine_slashing_program::id(), - verified_build_hash: Some(buffer_accounts::slashing_program::VERIFIED_BUILD_HASH), - migration_target: CoreBpfMigrationTargetType::Stateless, - datapoint_name: "enshrine_slashing_program", - }), - program_id: buffer_accounts::slashing_program::PROGRAM_ID, - name: "solana_slashing_program", -}]; - -/// Live source buffer accounts for builtin migrations. -mod buffer_accounts { - pub mod slashing_program { - use {solana_hash::Hash, solana_pubkey::Pubkey}; - - solana_pubkey::declare_id!("S1asHs4je6wPb2kWiHqNNdpNRiDaBEDQyfyCThhsrgv"); - - pub(crate) const PROGRAM_ID: Pubkey = - Pubkey::from_str_const("S1ashing11111111111111111111111111111111111"); - // 192ed727334abe822d5accba8b886e25f88b03c76973c2e7290cfb55b9e1115f - const HASH_BYTES: [u8; 32] = [ - 0x19, 0x2e, 0xd7, 0x27, 0x33, 0x4a, 0xbe, 0x82, 0x2d, 0x5a, 0xcc, 0xba, 0x8b, 0x88, - 0x6e, 0x25, 0xf8, 0x8b, 0x03, 0xc7, 0x69, 0x73, 0xc2, 0xe7, 0x29, 0x0c, 0xfb, 0x55, - 0xb9, 0xe1, 0x11, 0x5f, - ]; - pub(crate) const VERIFIED_BUILD_HASH: Hash = Hash::new_from_array(HASH_BYTES); - } -} +pub static STATELESS_BUILTINS: &[StatelessBuiltinPrototype] = &[]; // This module contains a number of arbitrary addresses used for testing Core // BPF migrations. diff --git a/ci/channel-overrides b/ci/channel-overrides index 88579ad7687..da4556621cc 100644 --- a/ci/channel-overrides +++ b/ci/channel-overrides @@ -1,5 +1,5 @@ # Channel override file consumed by ci/channel-info.sh. # Single source of truth: edit on master only, no backports. # Empty = auto-detect. -PINNED_BETA_CHANNEL=v4.0 -PINNED_STABLE_CHANNEL=v3.1 +PINNED_BETA_CHANNEL=v4.1 +PINNED_STABLE_CHANNEL=v4.0 diff --git a/ci/check-channel-version.sh b/ci/check-channel-version.sh deleted file mode 100755 index 6027a9ec73f..00000000000 --- a/ci/check-channel-version.sh +++ /dev/null @@ -1,27 +0,0 @@ - # Ensure the current channel version is not equal ("greater") than -# the version of the latest tag -if [[ -z $CI_TAG ]]; then - echo "--- channel version check" - ( - eval "$(ci/channel-info.sh)" - - if [[ -n $CHANNEL_LATEST_TAG ]]; then - source scripts/read-cargo-variable.sh - - version=$(readCargoVariable version Cargo.toml) - echo "latest channel tag: $CHANNEL_LATEST_TAG" - echo "current version: v$version" - - if [[ $CHANNEL_LATEST_TAG = v$version ]]; then - echo -e "\033[31mError:\033[0m A release has been tagged since your feature branch was last rebased. must be greater than .. - Possible solutions (in the order they should be tried): - 1. rebase your feature branch on the base branch - 2. merge the PR: \"Bump Version to ...\" once it has passed ci/checks, then rebase - 3. ask for help in #devops on discord" - exit 1 - fi - else - echo "Skipped. CHANNEL_LATEST_TAG (CHANNEL=$CHANNEL) unset" - fi - ) -fi diff --git a/ci/check-crates.sh b/ci/check-crates.sh index b83a5606aae..e58bb24fe1a 100755 --- a/ci/check-crates.sh +++ b/ci/check-crates.sh @@ -88,7 +88,9 @@ for file in "${files[@]}"; do exit 1 fi - response=$(curl -s https://crates.io/api/v1/crates/"$crate_name"/owners) + # crates.io API docs state to include a user-agent header: + # https://crates.io/data-access#api + response=$(curl -A 'Anza (https://github.com/anza-xyz/agave)' -s https://crates.io/api/v1/crates/"$crate_name"/owners) errors=$(echo "$response" | jq .errors) if [[ $errors != "null" ]]; then details=$(echo "$response" | jq .errors | jq -r ".[0].detail") @@ -104,14 +106,20 @@ for file in "${files[@]}"; do or -2. make a dummy publication. +2. make a dummy publication to reserve the name (requires your own crates.io token). - example: - scripts/reserve-cratesio-package-name.sh \ - --token \ - lib solana-new-lib-crate + a) publish the placeholder crate: - see also: scripts/reserve-cratesio-package-name.sh --help + scripts/reserve-cratesio-package-name.sh \ + --token \ + lib solana-new-lib-crate + + see also: scripts/reserve-cratesio-package-name.sh --help + + b) send a crate owner invitation to anza-team + + c) once your PR is ready to merge, ping the devops team to accept the + invitation. " else ((error_count++)) diff --git a/ci/coverage/part-1.sh b/ci/coverage/part-1.sh index 5440be089cf..95abc1124be 100755 --- a/ci/coverage/part-1.sh +++ b/ci/coverage/part-1.sh @@ -16,4 +16,5 @@ echo "--- coverage: root (part 1)" --features frozen-abi \ --features dev-context-only-utils \ --lib \ + --bins \ "${packages[@]}" diff --git a/ci/coverage/part-2.sh b/ci/coverage/part-2.sh index 105868e5690..840e3b3a705 100755 --- a/ci/coverage/part-2.sh +++ b/ci/coverage/part-2.sh @@ -15,4 +15,5 @@ echo "--- coverage: root (part 2)" "$git_root"/ci/test-coverage.sh \ --features dev-context-only-utils \ --lib \ + --bins \ "${packages[@]}" diff --git a/ci/coverage/part-3.sh b/ci/coverage/part-3.sh index a43c1708351..45d09c07c1f 100755 --- a/ci/coverage/part-3.sh +++ b/ci/coverage/part-3.sh @@ -21,6 +21,7 @@ echo "--- coverage: coverage (part 3)" --features dev-context-only-utils \ --workspace \ --lib \ + --bins \ "${exclude_packages[@]}" # Clean up @@ -31,7 +32,7 @@ echo "--- coverage: dev-bins" --features dev-context-only-utils \ --manifest-path "$git_root"/dev-bins/Cargo.toml \ --workspace \ - --lib + --bins # Clean up cargo clean diff --git a/ci/docker-run.sh b/ci/docker-run.sh index 4effcbeec8b..9ff494c7947 100755 --- a/ci/docker-run.sh +++ b/ci/docker-run.sh @@ -43,6 +43,12 @@ ARGS=( if [[ -n $CI ]]; then if [[ -n $BUILDKITE ]]; then + ARGS+=( + --env BUILDKITE + --env BUILDKITE_AGENT_ACCESS_TOKEN + --env BUILDKITE_JOB_ID + ) + # I hate buildkite-esque echo is leaking into this generic shell wrapper. # but it's easiest to notify to users, and properly guarded under $BUILDKITE_ env # (2 is chosen for third time's the charm). @@ -112,11 +118,6 @@ fi # Environment variables to propagate into the container ARGS+=( - --env BUILDKITE - --env BUILDKITE_AGENT_ACCESS_TOKEN - --env BUILDKITE_JOB_ID - --env BUILDKITE_PARALLEL_JOB - --env BUILDKITE_PARALLEL_JOB_COUNT --env CI --env CI_BRANCH --env CI_BASE_BRANCH diff --git a/ci/docker/Dockerfile b/ci/docker/Dockerfile index dc939da4398..469d20eca4a 100644 --- a/ci/docker/Dockerfile +++ b/ci/docker/Dockerfile @@ -4,8 +4,8 @@ FROM ${BASE_IMAGE} ARG \ RUST_VERSION= \ RUST_NIGHTLY_VERSION= \ - NODE_MAJOR= \ SCCACHE_VERSION= \ + NEXTEST_VERSION= \ GRCOV_VERSION= \ CODECOV_VERSION= @@ -24,8 +24,8 @@ ENV \ RUN \ if [ -z "$RUST_VERSION" ]; then echo "ERROR: The RUST_VERSION argument is required!" && exit 1; fi && \ if [ -z "$RUST_NIGHTLY_VERSION" ]; then echo "ERROR: The RUST_NIGHTLY_VERSION argument is required!" && exit 1; fi && \ - if [ -z "$NODE_MAJOR" ]; then echo "ERROR: The NODE_MAJOR argument is required!" && exit 1; fi && \ if [ -z "$SCCACHE_VERSION" ]; then echo "ERROR: The SCCACHE_VERSION argument is required!" && exit 1; fi && \ + if [ -z "$NEXTEST_VERSION" ]; then echo "ERROR: The NEXTEST_VERSION argument is required!" && exit 1; fi && \ if [ -z "$GRCOV_VERSION" ]; then echo "ERROR: The GRCOV_VERSION argument is required!" && exit 1; fi && \ if [ -z "$CODECOV_VERSION" ]; then echo "ERROR: The CODECOV_VERSION argument is required!" && exit 1; fi && \ apt-get update && \ @@ -38,14 +38,14 @@ RUN \ git \ vim \ jq \ + iproute2 \ ca-certificates \ curl \ gnupg \ lld \ cmake \ openssh-client \ - # docs - mscgen \ + python3 \ # solana compiling libssl-dev \ libudev-dev \ @@ -82,15 +82,11 @@ RUN \ rustup component add rustfmt --toolchain=$RUST_NIGHTLY_VERSION && \ rustup component add miri --toolchain=$RUST_NIGHTLY_VERSION && \ rustup component add llvm-tools-preview --toolchain=$RUST_NIGHTLY_VERSION && \ - rustup target add wasm32-unknown-unknown && \ cargo install cargo-audit && \ cargo install cargo-hack && \ cargo install cargo-sort@2.0.2 && \ - cargo install mdbook && \ - cargo install mdbook-linkcheck && \ - cargo install svgbob_cli && \ - cargo install wasm-pack && \ cargo install rustfilt && \ + cargo install cargo-build-sbf@4.1.0 --locked && \ rustup show && \ rustc --version && \ cargo --version && \ @@ -98,27 +94,25 @@ RUN \ rm -rf $CARGO_HOME/registry && \ # sccache curl -LOsS "https://github.com/mozilla/sccache/releases/download/$SCCACHE_VERSION/sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl.tar.gz" && \ + echo "$(curl -LsS "https://github.com/mozilla/sccache/releases/download/$SCCACHE_VERSION/sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl.tar.gz.sha256") sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl.tar.gz" | sha256sum -c - && \ tar -xzf "sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl.tar.gz" && \ mv "sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl"/sccache "$CARGO_HOME/bin/" && \ rm "sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl.tar.gz" && \ rm -rf "sccache-$SCCACHE_VERSION-x86_64-unknown-linux-musl" && \ # nextest - curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C "$CARGO_HOME/bin" && \ - # nodejs - sudo mkdir -p /etc/apt/keyrings && \ - curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ - echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list && \ - sudo apt-get update && \ - sudo apt-get install -y nodejs && \ + curl -LOsS "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-$NEXTEST_VERSION/cargo-nextest-$NEXTEST_VERSION-x86_64-unknown-linux-musl.tar.gz" && \ + curl -LsS "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-$NEXTEST_VERSION/cargo-nextest-$NEXTEST_VERSION-x86_64-unknown-linux-musl.sha256" | sha256sum -c - && \ + tar -xzf "cargo-nextest-$NEXTEST_VERSION-x86_64-unknown-linux-musl.tar.gz" && \ + mv ./cargo-nextest "$CARGO_HOME/bin/" && \ + rm "cargo-nextest-$NEXTEST_VERSION-x86_64-unknown-linux-musl.tar.gz" && \ # setup path mkdir /.cache && \ chmod -R a+w /.cache && \ mkdir /.config && \ chmod -R a+w /.config && \ - mkdir /.npm && \ - chmod -R a+w /.npm && \ # grcov curl -LOsS "https://github.com/mozilla/grcov/releases/download/$GRCOV_VERSION/grcov-x86_64-unknown-linux-musl.tar.bz2" && \ + curl -LsS "https://github.com/mozilla/grcov/releases/download/$GRCOV_VERSION/checksums.sha256" | sha256sum -c --ignore-missing - && \ tar -xf grcov-x86_64-unknown-linux-musl.tar.bz2 && \ mv ./grcov $CARGO_HOME/bin && \ rm grcov-x86_64-unknown-linux-musl.tar.bz2 && \ diff --git a/ci/docker/build.sh b/ci/docker/build.sh index 1593d3406a1..f2192fd999d 100755 --- a/ci/docker/build.sh +++ b/ci/docker/build.sh @@ -19,8 +19,8 @@ docker build "${platform[@]}" \ --build-arg "BASE_IMAGE=${CI_DOCKER_ARG_BASE_IMAGE}" \ --build-arg "RUST_VERSION=${CI_DOCKER_ARG_RUST_VERSION}" \ --build-arg "RUST_NIGHTLY_VERSION=${CI_DOCKER_ARG_RUST_NIGHTLY_VERSION}" \ - --build-arg "NODE_MAJOR=${CI_DOCKER_ARG_NODE_MAJOR}" \ --build-arg "SCCACHE_VERSION=${CI_DOCKER_ARG_SCCACHE_VERSION}" \ + --build-arg "NEXTEST_VERSION=${CI_DOCKER_ARG_NEXTEST_VERSION}" \ --build-arg "GRCOV_VERSION=${CI_DOCKER_ARG_GRCOV_VERSION}" \ --build-arg "CODECOV_VERSION=${CI_DOCKER_ARG_CODECOV_VERSION}" \ -t "$CI_DOCKER_IMAGE" . diff --git a/ci/docker/env.sh b/ci/docker/env.sh index fda575ccc85..4974a42d151 100755 --- a/ci/docker/env.sh +++ b/ci/docker/env.sh @@ -13,8 +13,8 @@ fi export CI_DOCKER_ARG_BASE_IMAGE=ubuntu:22.04 export CI_DOCKER_ARG_RUST_VERSION="${rust_stable}" export CI_DOCKER_ARG_RUST_NIGHTLY_VERSION="${rust_nightly}" -export CI_DOCKER_ARG_NODE_MAJOR=24 export CI_DOCKER_ARG_SCCACHE_VERSION=v0.9.1 +export CI_DOCKER_ARG_NEXTEST_VERSION=0.9.137 export CI_DOCKER_ARG_GRCOV_VERSION=v0.8.18 export CI_DOCKER_ARG_CODECOV_VERSION=v0.8.0 @@ -23,8 +23,8 @@ hash_vars=( "${CI_DOCKER_ARG_BASE_IMAGE}" "${CI_DOCKER_ARG_RUST_VERSION}" "${CI_DOCKER_ARG_RUST_NIGHTLY_VERSION}" - "${CI_DOCKER_ARG_NODE_MAJOR}" "${CI_DOCKER_ARG_SCCACHE_VERSION}" + "${CI_DOCKER_ARG_NEXTEST_VERSION}" "${CI_DOCKER_ARG_GRCOV_VERSION}" "${CI_DOCKER_ARG_CODECOV_VERSION}" ) diff --git a/ci/downstream-projects/common.sh b/ci/downstream-projects/common.sh deleted file mode 100755 index 54a7c8c5f0a..00000000000 --- a/ci/downstream-projects/common.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -e - -source ci/_ -source ci/semver_bash/semver.sh -source scripts/patch-crates.sh -source scripts/read-cargo-variable.sh - -SOLANA_VER=$(readCargoVariable version Cargo.toml) -export SOLANA_VER -export SOLANA_DIR=$PWD -export CARGO="$SOLANA_DIR"/cargo -export CARGO_BUILD_SBF="$SOLANA_DIR"/cargo-build-sbf -export CARGO_TEST_SBF="$SOLANA_DIR"/cargo-test-sbf - -mkdir -p target/downstream-projects -cd target/downstream-projects diff --git a/ci/downstream-projects/func-openbook-dex.sh b/ci/downstream-projects/func-openbook-dex.sh deleted file mode 100755 index 71adddf2804..00000000000 --- a/ci/downstream-projects/func-openbook-dex.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -openbook_dex() { - ( - set -x - rm -rf openbook-dex - git clone https://github.com/openbook-dex/program.git openbook-dex - # copy toolchain file to use solana's rust version - cp "$SOLANA_DIR"/rust-toolchain.toml openbook-dex/ - cd openbook-dex || exit 1 - - update_solana_dependencies . "$SOLANA_VER" - patch_crates_io_solana Cargo.toml "$SOLANA_DIR" - cat >> Cargo.toml <> dex/Cargo.toml <=<~]*(.*)\"/\1/p' <"token/program/Cargo.toml") - echo "used solana version: $project_used_solana_version" - if semverGT "$project_used_solana_version" "$SOLANA_VER"; then - echo "skip" - return - fi - - ./patch.crates-io.sh "$SOLANA_DIR" - - for program in "${PROGRAMS[@]}"; do - $CARGO_TEST_SBF --manifest-path "$program"/Cargo.toml - done - - # TODO better: `build.rs` for spl-token-cli doesn't seem to properly build - # the required programs to run the tests, so instead we run the tests - # after we know programs have been built - cargo build - cargo test - ) -} diff --git a/ci/downstream-projects/run-all.sh b/ci/downstream-projects/run-all.sh deleted file mode 100755 index ca33b53084d..00000000000 --- a/ci/downstream-projects/run-all.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -set -e - -here="$(dirname "$0")" - -#shellcheck source=ci/downstream-projects/func-spl.sh -source "$here"/func-spl.sh - -#shellcheck source=ci/downstream-projects/func-openbook-dex.sh -source "$here"/func-openbook-dex.sh - -#shellcheck source=ci/downstream-projects/common.sh -source "$here"/common.sh - -_ spl -_ openbook_dex diff --git a/ci/downstream-projects/run-openbook-dex.sh b/ci/downstream-projects/run-openbook-dex.sh deleted file mode 100755 index d79df546466..00000000000 --- a/ci/downstream-projects/run-openbook-dex.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -e - -here="$(dirname "$0")" - -#shellcheck source=ci/downstream-projects/func-openbook-dex.sh -source "$here"/func-openbook-dex.sh - -#shellcheck source=ci/downstream-projects/common.sh -source "$here"/common.sh - -_ openbook_dex diff --git a/ci/downstream-projects/run-spl.sh b/ci/downstream-projects/run-spl.sh deleted file mode 100755 index 543dca928ea..00000000000 --- a/ci/downstream-projects/run-spl.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -e - -here="$(dirname "$0")" - -#shellcheck source=ci/downstream-projects/func-spl.sh -source "$here"/func-spl.sh - -#shellcheck source=ci/downstream-projects/common.sh -source "$here"/common.sh - -_ spl diff --git a/ci/publish-crate.sh b/ci/publish-crate.sh index 908e69275bc..651405ddf96 100755 --- a/ci/publish-crate.sh +++ b/ci/publish-crate.sh @@ -8,8 +8,10 @@ source ci/rust-version.sh stable is_crate_version_uploaded() { name=$1 version=$2 - curl https://crates.io/api/v1/crates/${name}/${version} | \ - python3 -c "import sys,json; print('version' in json.load(sys.stdin));" + # crates.io API docs state to include a user-agent header: + # https://crates.io/data-access#api + curl --user-agent 'Anza (https://github.com/anza-xyz/agave)' https://crates.io/api/v1/crates/${name}/${version} | + python3 -c "import sys,json; print('version' in json.load(sys.stdin));" } # Only package/publish if this is a tagged release diff --git a/ci/run-local.sh b/ci/run-local.sh index aa1bf1344fa..451d2969b0b 100755 --- a/ci/run-local.sh +++ b/ci/run-local.sh @@ -14,7 +14,6 @@ steps+=(test-coverage) steps+=(test-stable) steps+=(test-stable-sbf) steps+=(test-stable-perf) -steps+=(test-downstream-builds) steps+=(test-bench) steps+=(test-local-cluster) steps+=(test-local-cluster-flakey) diff --git a/ci/rust-version.sh b/ci/rust-version.sh index 1dc16a74575..71826e89376 100644 --- a/ci/rust-version.sh +++ b/ci/rust-version.sh @@ -29,7 +29,7 @@ fi if [[ -n ${RUST_NIGHTLY_VERSION:-} ]]; then nightly_version="$RUST_NIGHTLY_VERSION" else - nightly_version=2026-02-28 + nightly_version=2026-04-11 fi diff --git a/ci/test-channel-info-divergence.sh b/ci/test-channel-info-divergence.sh new file mode 100755 index 00000000000..8d92e481ddb --- /dev/null +++ b/ci/test-channel-info-divergence.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# +# Runs `ci/channel-info.sh` (current authoritative source) and +# `cargo xtask channel-info` (auto-resolver), diffs their output, and +# annotates the build on mismatch. Never blocks the pipeline. + +set -u +cd "$(dirname "$0")/.." || exit 1 + +bash_out=$(ci/channel-info.sh 2>&1) || { + echo "ci/channel-info.sh failed (exit $?); skipping divergence check" >&2 + echo "$bash_out" >&2 + exit 0 +} + +xtask_out=$(cargo run --quiet --manifest-path ci/xtask/Cargo.toml -- channel-info 2>&1) || { + echo "cargo xtask channel-info failed (exit $?); skipping divergence check" >&2 + echo "$xtask_out" >&2 + exit 0 +} + +bash_sorted=$(printf '%s\n' "$bash_out" | LC_ALL=C sort) +xtask_sorted=$(printf '%s\n' "$xtask_out" | LC_ALL=C sort) + +if [[ "$bash_sorted" == "$xtask_sorted" ]]; then + echo "channel-info parity OK" + exit 0 +fi + +echo "channel-info divergence detected" >&2 +echo "--- bash (authoritative)" >&2 +echo "$bash_out" >&2 +echo "--- xtask" >&2 +echo "$xtask_out" >&2 + +if command -v buildkite-agent >/dev/null 2>&1; then + { + echo "**channel-info parity divergence**" + echo + echo '```' + echo "bash (authoritative):" + echo "$bash_out" + echo + echo "xtask:" + echo "$xtask_out" + echo '```' + } | buildkite-agent annotate --style warning --context channel-info-divergence || true +fi + +exit 0 diff --git a/ci/test-downstream-builds.sh b/ci/test-downstream-builds.sh deleted file mode 100755 index 9ddba8755cc..00000000000 --- a/ci/test-downstream-builds.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -cd "$(dirname "$0")/.." - -export CI_LOCAL_RUN=true - -set -ex - -ci/downstream-projects/run-all.sh diff --git a/ci/test-sanity.sh b/ci/test-sanity.sh index 3a1de04025a..c5bfbb05e08 100755 --- a/ci/test-sanity.sh +++ b/ci/test-sanity.sh @@ -50,7 +50,6 @@ fi git diff "$target" --check --oneline ) -_ ci/check-channel-version.sh _ ci/nits.sh scripts/increment-cargo-version.sh check @@ -83,4 +82,22 @@ EOF fi ) +# Disallow adding new files under docs/ — docs now live in a separate repo +( + added_docs=$(git diff --diff-filter=AR --name-only "$target" -- 'docs/*') + if [ -n "$added_docs" ]; then + cat <&2 + +Error: new files added under docs/: +$added_docs + +Documentation has moved to https://github.com/anza-xyz/docs.anza.xyz +Please add your changes there instead. +EOF + exit 1 + fi +) + +./scripts/cargo-install-all.sh --dcou-check-only + echo --- ok diff --git a/ci/test-verify-packets-gossip.sh b/ci/test-verify-packets-gossip.sh deleted file mode 100755 index 64236fe93cd..00000000000 --- a/ci/test-verify-packets-gossip.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -e -here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) - -if ! git lfs --version &>/dev/null; then - echo "Git LFS is not installed. Please install Git LFS to proceed." - exit 1 -fi - -rm -rf "$here"/solana-packets -git clone https://github.com/anza-xyz/solana-packets.git "$here"/solana-packets -GOSSIP_WIRE_FORMAT_PACKETS="$here/solana-packets/GOSSIP_PACKETS" cargo test --package solana-gossip -- wire_format_tests::tests::test_gossip_wire_format --exact --show-output diff --git a/ci/test-xdp.sh b/ci/test-xdp.sh new file mode 100755 index 00000000000..2750a875a8e --- /dev/null +++ b/ci/test-xdp.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -e + +cd "$(dirname "$0")/.." + +# The CI container runs as root for network namespace privileges. Keep +# root-owned Cargo artifacts out of the mounted checkout. +export CARGO_TARGET_DIR=/tmp/agave-xdp-target +cargo xtask xdp-test --release-with-debug diff --git a/ci/xtask/Cargo.lock b/ci/xtask/Cargo.lock index f34d0af04ae..5797d700ea3 100644 --- a/ci/xtask/Cargo.lock +++ b/ci/xtask/Cargo.lock @@ -128,9 +128,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" @@ -159,9 +159,9 @@ checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "camino" @@ -211,11 +211,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -334,7 +340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -478,7 +484,7 @@ dependencies = [ "hkdf", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -519,7 +525,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -656,6 +662,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + [[package]] name = "globset" version = "0.4.18" @@ -676,7 +696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -712,9 +732,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -743,6 +763,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-serde" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f056c8559e3757392c8d091e796416e4649d8e49e88b8d76df6c002f05027fd" +dependencies = [ + "http", + "serde", +] + [[package]] name = "httparse" version = "1.10.1" @@ -786,6 +816,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -807,13 +838,16 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", "futures-channel", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -973,6 +1007,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + [[package]] name = "iri-string" version = "0.7.10" @@ -1037,13 +1077,13 @@ checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "base64", "ed25519-dalek", - "getrandom", + "getrandom 0.2.17", "hmac", "js-sys", "p256", "p384", "pem", - "rand", + "rand 0.8.6", "rsa", "serde", "serde_json", @@ -1090,9 +1130,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "memchr" @@ -1132,7 +1178,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -1175,9 +1221,9 @@ dependencies = [ [[package]] name = "octocrab" -version = "0.51.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2ad8abffe4e2b05f9cdc7e061de63d305a6dca0af81ca1064a7d98e0b78267" +checksum = "fe45bd53ce50c9e85e8a27259675f65d772165fe5eef3e278c1b6168c3f97623" dependencies = [ "arc-swap", "async-trait", @@ -1189,16 +1235,16 @@ dependencies = [ "futures", "futures-core", "futures-util", - "getrandom", + "getrandom 0.2.17", "http", "http-body", "http-body-util", + "http-serde", "hyper", "hyper-rustls", "hyper-timeout", "hyper-util", "jsonwebtoken", - "once_cell", "percent-encoding", "pin-project", "secrecy", @@ -1425,6 +1471,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -1434,6 +1535,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.6" @@ -1441,8 +1548,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1452,7 +1569,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1461,7 +1588,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", ] [[package]] @@ -1475,9 +1611,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -1498,9 +1634,47 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] [[package]] name = "rfc6979" @@ -1520,7 +1694,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -1539,13 +1713,19 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", "zeroize", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1557,9 +1737,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "log", "once_cell", @@ -1588,6 +1768,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] @@ -1726,9 +1907,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -1815,7 +1996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1838,9 +2019,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "snafu" @@ -1923,6 +2104,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -1996,6 +2180,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.52.3" @@ -2047,6 +2246,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -2056,6 +2270,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.24.1+spec-1.1.0" @@ -2065,7 +2288,7 @@ dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow", @@ -2252,6 +2475,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.114" @@ -2265,6 +2497,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.114" @@ -2297,6 +2543,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -2308,6 +2564,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -2541,6 +2806,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" version = "0.6.2" @@ -2559,10 +2830,13 @@ dependencies = [ "octocrab", "pretty_assertions", "regex", + "reqwest", + "semver", "serde", "serde_json", "serde_yaml", "tokio", + "toml", "xtask 0.1.0 (git+https://github.com/anza-xyz/xtask?rev=a9b0cf5facf85eb11458bbefc651132722c18dae)", ] diff --git a/ci/xtask/Cargo.toml b/ci/xtask/Cargo.toml index 9048205ac38..e948165411d 100644 --- a/ci/xtask/Cargo.toml +++ b/ci/xtask/Cargo.toml @@ -24,12 +24,15 @@ clap = { version = "4.6.1", features = ["derive"] } env_logger = "0.11.10" futures-util = "0.3.31" log = "0.4.28" -octocrab = { version = "0.51.0", features = ["stream"] } -regex = "1.12.3" +octocrab = { version = "0.53.1", features = ["stream"] } +regex = "1.12.4" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +semver = { version = "1.0.27", features = ["serde"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" serde_yaml = "0.9.34" tokio = { version = "1.52.3", features = ["full"] } +toml = "1.0" xtask_shared = { package = "xtask", git = "https://github.com/anza-xyz/xtask", rev = "a9b0cf5facf85eb11458bbefc651132722c18dae" } [dev-dependencies] @@ -41,3 +44,15 @@ debug = "line-tables-only" [profile.full-dev] inherits = "dev" debug = "full" + +# Keep in sync with the root [workspace.lints.clippy]. +[lints.clippy] +# Denied lints +arithmetic_side_effects = "deny" +default_trait_access = "deny" +manual_let_else = "deny" +uninlined_format_args = "deny" +used_underscore_binding = "deny" + +# Allowed lints +new_without_default = "allow" diff --git a/ci/xtask/src/commands.rs b/ci/xtask/src/commands.rs index 3645e1472c6..d8febbe2a50 100644 --- a/ci/xtask/src/commands.rs +++ b/ci/xtask/src/commands.rs @@ -1,2 +1,4 @@ +pub mod channel_info; pub mod generate_pipeline; pub mod hello; +pub mod xdp_test; diff --git a/ci/xtask/src/commands/channel_info.rs b/ci/xtask/src/commands/channel_info.rs new file mode 100644 index 00000000000..33f8ca5c337 --- /dev/null +++ b/ci/xtask/src/commands/channel_info.rs @@ -0,0 +1,40 @@ +mod fetch; +mod resolve; + +use { + anyhow::Result, + futures_util::future::try_join_all, + resolve::{BranchVersion, derive_channels, print_channel_info}, + semver::Version, + std::{collections::BTreeMap, env}, +}; + +pub async fn run() -> Result<()> { + let mut heads = fetch::release_heads()?; + let tags = fetch::release_tags()?; + + heads.sort(); + heads.reverse(); + heads.truncate(3); + + let client = reqwest::Client::new(); + let fetched = try_join_all( + heads + .iter() + .copied() + .map(|bv| fetch::workspace_version(&client, bv)), + ) + .await?; + let versions: BTreeMap = heads.into_iter().zip(fetched).collect(); + + let branch = pick_env("CI_BASE_BRANCH").or_else(|| pick_env("CI_BRANCH")); + let channel = pick_env("CHANNEL"); + let info = derive_channels(&versions, &tags, branch.as_deref(), channel.as_deref())?; + print_channel_info(&info); + + Ok(()) +} + +fn pick_env(key: &str) -> Option { + env::var(key).ok().filter(|v| !v.is_empty()) +} diff --git a/ci/xtask/src/commands/channel_info/fetch.rs b/ci/xtask/src/commands/channel_info/fetch.rs new file mode 100644 index 00000000000..0c4bd102fdd --- /dev/null +++ b/ci/xtask/src/commands/channel_info/fetch.rs @@ -0,0 +1,86 @@ +use { + super::resolve::BranchVersion, + anyhow::{Result, anyhow, bail}, + semver::Version, + serde::Deserialize, + std::process::Command, +}; + +const REMOTE: &str = "https://github.com/anza-xyz/agave.git"; +const RAW_BASE: &str = "https://raw.githubusercontent.com/anza-xyz/agave"; + +fn ls_remote(flag: &str) -> Result> { + let output = Command::new("git") + .args(["ls-remote", flag, REMOTE]) + .output() + .map_err(|e| anyhow!("failed to invoke `git ls-remote`: {e}"))?; + if !output.status.success() { + bail!( + "`git ls-remote {flag} {REMOTE}` failed: {}", + String::from_utf8_lossy(&output.stderr).trim(), + ); + } + let stdout = String::from_utf8(output.stdout) + .map_err(|e| anyhow!("`git ls-remote` stdout is not utf-8: {e}"))?; + Ok(stdout.lines().map(str::to_owned).collect()) +} + +fn strip_ref(line: &str, prefix: &str) -> Option { + let (_sha, refname) = line.split_once('\t')?; + refname.strip_prefix(prefix).map(str::to_owned) +} + +pub fn release_heads() -> Result> { + let lines = ls_remote("--heads")?; + Ok(lines + .iter() + .filter_map(|l| strip_ref(l, "refs/heads/")) + .filter_map(|name| name.parse::().ok()) + .collect()) +} + +pub fn release_tags() -> Result> { + let lines = ls_remote("--tags")?; + Ok(lines + .iter() + .filter_map(|l| strip_ref(l, "refs/tags/")) + .filter_map(|name| { + let stripped = name.strip_prefix('v')?; + let v = Version::parse(stripped).ok()?; + (v.pre.is_empty() && v.build.is_empty()).then_some(v) + }) + .collect()) +} + +#[derive(Deserialize)] +struct CargoToml { + workspace: WorkspaceSection, +} + +#[derive(Deserialize)] +struct WorkspaceSection { + package: PackageSection, +} + +#[derive(Deserialize)] +struct PackageSection { + version: Version, +} + +pub async fn workspace_version(client: &reqwest::Client, bv: BranchVersion) -> Result { + let url = format!("{RAW_BASE}/{bv}/Cargo.toml"); + let resp = client + .get(&url) + .send() + .await + .map_err(|e| anyhow!("GET {url}: {e}"))? + .error_for_status() + .map_err(|e| anyhow!("GET {url}: {e}"))?; + let raw = resp + .text() + .await + .map_err(|e| anyhow!("read body for {url}: {e}"))?; + let parsed: CargoToml = + toml::from_str(&raw).map_err(|e| anyhow!("failed to parse Cargo.toml at {url}: {e}"))?; + Ok(parsed.workspace.package.version) +} diff --git a/ci/xtask/src/commands/channel_info/resolve.rs b/ci/xtask/src/commands/channel_info/resolve.rs new file mode 100644 index 00000000000..40cac08db06 --- /dev/null +++ b/ci/xtask/src/commands/channel_info/resolve.rs @@ -0,0 +1,290 @@ +use { + anyhow::{Result, anyhow, bail}, + semver::Version, + std::{collections::BTreeMap, fmt, str::FromStr}, +}; + +/// `vX.Y` release-line identifier. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct BranchVersion { + pub major: u64, + pub minor: u64, +} + +impl FromStr for BranchVersion { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let rest = s + .strip_prefix('v') + .ok_or_else(|| anyhow!("missing `v` prefix in `{s}`"))?; + let (maj, min) = rest + .split_once('.') + .ok_or_else(|| anyhow!("missing `.` separator in `{s}`"))?; + Ok(Self { + major: maj.parse()?, + minor: min.parse()?, + }) + } +} + +impl fmt::Display for BranchVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "v{}.{}", self.major, self.minor) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Stage { + Alpha, + Beta, + Rc, + Ga, +} + +pub fn stage_of(v: &Version) -> Result { + if v.pre.is_empty() { + return Ok(Stage::Ga); + } + + let label = v.pre.as_str().split('.').next().unwrap_or(""); + + match label { + "alpha" => Ok(Stage::Alpha), + "beta" => Ok(Stage::Beta), + "rc" => Ok(Stage::Rc), + other => bail!("unknown prerelease label `{other}` in version {v}"), + } +} + +#[derive(Debug)] +pub struct ChannelInfo { + pub edge_channel: String, + pub beta_channel: String, + pub beta_channel_latest_tag: String, + pub stable_channel: String, + pub stable_channel_latest_tag: String, + pub channel: String, + pub channel_latest_tag: String, +} + +pub fn derive_channels( + versions: &BTreeMap, + tags: &[Version], + branch: Option<&str>, + channel: Option<&str>, +) -> Result { + for (bv, v) in versions { + if v.major != bv.major || v.minor != bv.minor { + bail!("branch {bv} workspace version {v} does not match branch (major, minor)"); + } + } + + let sorted: Vec = versions.keys().rev().copied().collect(); + let h1 = sorted.first().copied(); + let h2 = sorted.get(1).copied(); + let h3 = sorted.get(2).copied(); + + let h1_stage = h1 + .map(|bv| stage_of(versions.get(&bv).expect("h1 is in versions"))) + .transpose()?; + + let (beta, stable) = match h1_stage { + Some(Stage::Alpha) => (h2, h3), + Some(_) => (h1, h2), + None => (None, None), + }; + + let beta = beta.ok_or_else(|| anyhow!("no BETA-eligible vX.Y head"))?; + + if let Some(s) = stable + && s >= beta + { + bail!("STABLE {s} is not less than BETA {beta}"); + } + + let edge_channel = "master".to_string(); + let beta_channel = beta.to_string(); + let stable_channel = stable.map(|s| s.to_string()).unwrap_or_default(); + let beta_channel_latest_tag = latest_tag_for(tags, beta) + .map(|v| format!("v{v}")) + .unwrap_or_default(); + let stable_channel_latest_tag = stable + .and_then(|xy| latest_tag_for(tags, xy)) + .map(|v| format!("v{v}")) + .unwrap_or_default(); + + let channel = channel.map(str::to_owned).unwrap_or_else(|| match branch { + Some(b) if b == stable_channel => "stable".into(), + Some(b) if b == edge_channel => "edge".into(), + Some(b) if b == beta_channel => "beta".into(), + _ => String::new(), + }); + + let channel_latest_tag = match channel.as_str() { + "beta" => beta_channel_latest_tag.clone(), + "stable" => stable_channel_latest_tag.clone(), + _ => String::new(), + }; + + Ok(ChannelInfo { + edge_channel, + beta_channel, + beta_channel_latest_tag, + stable_channel, + stable_channel_latest_tag, + channel, + channel_latest_tag, + }) +} + +fn latest_tag_for(tags: &[Version], bv: BranchVersion) -> Option<&Version> { + tags.iter() + .filter(|t| t.major == bv.major && t.minor == bv.minor) + .max() +} + +pub fn print_channel_info(info: &ChannelInfo) { + println!("EDGE_CHANNEL={}", info.edge_channel); + println!("BETA_CHANNEL={}", info.beta_channel); + println!("BETA_CHANNEL_LATEST_TAG={}", info.beta_channel_latest_tag); + println!("STABLE_CHANNEL={}", info.stable_channel); + println!( + "STABLE_CHANNEL_LATEST_TAG={}", + info.stable_channel_latest_tag + ); + println!("CHANNEL={}", info.channel); + println!("CHANNEL_LATEST_TAG={}", info.channel_latest_tag); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn v(s: &str) -> Version { + Version::parse(s).expect("valid version") + } + + fn bv(major: u64, minor: u64) -> BranchVersion { + BranchVersion { major, minor } + } + + fn versions(pairs: &[(BranchVersion, &str)]) -> BTreeMap { + pairs.iter().map(|(b, s)| (*b, v(s))).collect() + } + + #[test] + fn promotes_top_when_top_is_beta() { + let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-beta.0")]); + + let info = derive_channels(&vs, &[], None, None).unwrap(); + + assert_eq!(info.beta_channel, "v4.1"); + assert_eq!(info.stable_channel, "v4.0"); + } + + #[test] + fn promotes_top_when_top_is_ga() { + let vs = versions(&[(bv(4, 0), "4.0.5"), (bv(4, 1), "4.1.0")]); + + let info = derive_channels(&vs, &[], None, None).unwrap(); + + assert_eq!(info.beta_channel, "v4.1"); + assert_eq!(info.stable_channel, "v4.0"); + } + + #[test] + fn holds_channels_when_top_is_alpha() { + let vs = versions(&[ + (bv(3, 1), "3.1.15"), + (bv(4, 0), "4.0.0"), + (bv(4, 1), "4.1.0-alpha.0"), + ]); + + let info = derive_channels(&vs, &[], None, None).unwrap(); + + assert_eq!(info.beta_channel, "v4.0"); + assert_eq!(info.stable_channel, "v3.1"); + } + + #[test] + fn rc_top_is_promoted() { + let vs = versions(&[(bv(4, 0), "4.0.10"), (bv(4, 1), "4.1.0-rc.2")]); + + let info = derive_channels(&vs, &[], None, None).unwrap(); + + assert_eq!(info.beta_channel, "v4.1"); + assert_eq!(info.stable_channel, "v4.0"); + } + + #[test] + fn rejects_mismatched_workspace_version() { + let vs = versions(&[(bv(4, 0), "5.0.0")]); + + let err = derive_channels(&vs, &[], None, None).unwrap_err(); + + assert!(err.to_string().contains("does not match branch")); + } + + #[test] + fn rejects_unknown_prerelease_label() { + let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-dev.0")]); + + let err = derive_channels(&vs, &[], None, None).unwrap_err(); + + assert!(err.to_string().contains("unknown prerelease label")); + } + + #[test] + fn rejects_when_only_alpha_top_and_nothing_below() { + let vs = versions(&[(bv(4, 1), "4.1.0-alpha.0")]); + + let err = derive_channels(&vs, &[], None, None).unwrap_err(); + + assert!(err.to_string().contains("no BETA-eligible")); + } + + #[test] + fn rejects_empty() { + let err = derive_channels(&BTreeMap::new(), &[], None, None).unwrap_err(); + + assert!(err.to_string().contains("no BETA-eligible")); + } + + #[test] + fn picks_latest_tag_per_channel() { + let vs = versions(&[(bv(3, 0), "3.0.5"), (bv(3, 1), "3.1.0-beta.0")]); + let tags = vec![v("3.0.1"), v("3.0.5"), v("3.0.2"), v("2.9.9")]; + + let info = derive_channels(&vs, &tags, None, None).unwrap(); + + assert_eq!(info.beta_channel_latest_tag, ""); + assert_eq!(info.stable_channel_latest_tag, "v3.0.5"); + } + + #[test] + fn channel_from_branch_match() { + let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-beta.0")]); + + let info = derive_channels(&vs, &[], Some("v4.1"), None).unwrap(); + + assert_eq!(info.channel, "beta"); + } + + #[test] + fn channel_env_var_wins() { + let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-beta.0")]); + + let info = derive_channels(&vs, &[], Some("master"), Some("stable")).unwrap(); + + assert_eq!(info.channel, "stable"); + } + + #[test] + fn stage_of_classifies_known_labels() { + assert_eq!(stage_of(&v("4.0.0")).unwrap(), Stage::Ga); + assert_eq!(stage_of(&v("4.0.0-alpha.0")).unwrap(), Stage::Alpha); + assert_eq!(stage_of(&v("4.0.0-beta.3")).unwrap(), Stage::Beta); + assert_eq!(stage_of(&v("4.0.0-rc.1")).unwrap(), Stage::Rc); + } +} diff --git a/ci/xtask/src/commands/generate_pipeline.rs b/ci/xtask/src/commands/generate_pipeline.rs index ea2af9a598e..aeca426ede0 100644 --- a/ci/xtask/src/commands/generate_pipeline.rs +++ b/ci/xtask/src/commands/generate_pipeline.rs @@ -24,6 +24,54 @@ pub enum Pipeline { Private, } +struct Repo { + owner: String, + name: String, +} + +impl Repo { + /// Resolve the GitHub repo from Buildkite's built-in `BUILDKITE_REPO`, + /// falling back to the agave repo when it is unset or unparseable. + fn from_env() -> Self { + match env::var("BUILDKITE_REPO") + .ok() + .and_then(|url| Self::parse_github_url(&url)) + { + Some(repo) => { + info!( + "Resolved repo {}/{} from `BUILDKITE_REPO`", + repo.owner, repo.name + ); + repo + } + None => { + info!("Falling back to default repo anza-xyz/agave"); + Repo { + owner: String::from("anza-xyz"), + name: String::from("agave"), + } + } + } + } + + /// Extract `owner` and `name` from a GitHub remote URL, handling both the + /// HTTPS (`https://github.com/owner/name.git`) and scp-like SSH + /// (`git@github.com:owner/name.git`) forms. + fn parse_github_url(url: &str) -> Option { + let trimmed = url.trim().trim_end_matches('/'); + let trimmed = trimmed.strip_suffix(".git").unwrap_or(trimmed); + // Both `/` and `:` separate the path, so splitting on either yields the + // trailing `.../owner/name` regardless of URL flavor. + let mut parts = trimmed.rsplit(['/', ':']); + let name = parts.next().filter(|s| !s.is_empty())?; + let owner = parts.next().filter(|s| !s.is_empty())?; + Some(Repo { + owner: owner.to_string(), + name: name.to_string(), + }) + } +} + pub async fn run(args: CommandArgs) -> Result<()> { let pipeline = match args.pipeline { Pipeline::Agave => generate_agave_pipeline().await?, @@ -55,8 +103,9 @@ async fn generate_agave_pipeline() -> Result { .parse::() .map_err(|e| anyhow::anyhow!("failed to parse PR number: {e}"))?; - annotate_pull_request(pr_number)?; - return generate_pull_request_pipeline(pr_number).await; + let repo = Repo::from_env(); + annotate_pull_request(&repo, pr_number)?; + return generate_pull_request_pipeline(&repo, pr_number).await; } info!("failed to get PR number from branch: {branch}, running full pipeline."); @@ -81,6 +130,7 @@ fn generate_private_pipeline() -> Result { ..Default::default() })); + pipeline.add_step(default_channel_info_divergence_step()); pipeline.add_step(default_shellcheck_step()); pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); @@ -93,6 +143,7 @@ fn generate_private_pipeline() -> Result { pipeline.add_step(default_local_cluster_step(10)); pipeline.add_step(default_docs_check_step()); pipeline.add_step(default_localnet_step()); + pipeline.add_step(default_xdp_test_step()); pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); @@ -102,7 +153,7 @@ fn generate_private_pipeline() -> Result { Ok(pipeline) } -fn annotate_pull_request(pr_number: u64) -> Result<()> { +fn annotate_pull_request(repo: &Repo, pr_number: u64) -> Result<()> { let is_ci = env::var("CI") .map(|value| matches!(value.to_ascii_lowercase().as_str(), "1" | "true")) .unwrap_or(false); @@ -110,8 +161,10 @@ fn annotate_pull_request(pr_number: u64) -> Result<()> { return Ok(()); } - let annotation = - format!("Github Pull Request: https://github.com/anza-xyz/agave/pull/{pr_number}"); + let annotation = format!( + "Github Pull Request: https://github.com/{}/{}/pull/{pr_number}", + repo.owner, repo.name + ); let status = Command::new("buildkite-agent") .args([ @@ -136,11 +189,12 @@ fn generate_merge_queue_pipeline() -> Result { let mut pipeline = buildkite::Pipeline::new(); pipeline.set_priority(10); pipeline.add_step(default_sanity_step()); + pipeline.add_step(default_channel_info_divergence_step()); pipeline.add_step(default_checks_step()); Ok(pipeline) } -async fn get_changed_files(pr_number: u64) -> Result> { +async fn get_changed_files(repo: &Repo, pr_number: u64) -> Result> { let mut changed_files = vec![]; let github_client_builder = match env::var("GH_TOKEN") { Ok(token) if !token.trim().is_empty() => { @@ -157,7 +211,7 @@ async fn get_changed_files(pr_number: u64) -> Result> { }; let github_client = github_client_builder.build()?; let stream = github_client - .pulls("anza-xyz", "agave") + .pulls(&repo.owner, &repo.name) .list_files(pr_number) .await? .into_stream(&github_client); @@ -168,51 +222,192 @@ async fn get_changed_files(pr_number: u64) -> Result> { Ok(changed_files) } -pub async fn generate_pull_request_pipeline(pr_number: u64) -> Result { - let changed_files = get_changed_files(pr_number).await?; +struct PullRequestPipelineFlags { + shellcheck: bool, + checks: bool, + feature_check: bool, + miri: bool, + frozen_abi: bool, + stable: bool, + local_cluster: bool, + docs: bool, + localnet: bool, + stable_sbf: bool, + shuttle: bool, + coverage: bool, + xdp_tests: bool, +} + +impl PullRequestPipelineFlags { + fn from_changed_files(changed_files: &[String]) -> Self { + let trigger_all = changed_files.iter().any(|file| { + file.starts_with("ci/xtask/") + || file.ends_with("ci/rust-version.sh") + || file.ends_with("rust-toolchain.toml") + || file.ends_with("ci/docker-run-default-image.sh") + || file.ends_with("ci/docker-run.sh") + || file.ends_with("ci/docker/Dockerfile") + || file.ends_with("ci/docker/env.sh") + }); + + let rust_changed = changed_files.iter().any(|file| { + file.ends_with("Cargo.toml") || file.ends_with("Cargo.lock") || file.ends_with(".rs") + }); + + Self { + shellcheck: changed_files.iter().any(|file| file.ends_with(".sh")), + checks: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("ci/test-checks.sh") + || file.ends_with("scripts/cargo-for-all-lock-files.sh") + || file.ends_with("scripts/check-dev-context-only-utils.sh") + || file.ends_with("scripts/agave-build-lists.sh") + || file.ends_with("ci/order-crates-for-publishing.py") + || file.ends_with("scripts/cargo-clippy.sh") + || file.ends_with("ci/do-audit.sh") + || file.ends_with("ci/check-install-all.sh") + || file.ends_with("scripts/spl-token-cli-version.sh") + || file.ends_with("scripts/cargo-build-sbf-version.sh") + }), + feature_check: trigger_all + || rust_changed + || changed_files + .iter() + .any(|file| file.starts_with("ci/feature-check/")), + miri: trigger_all + || rust_changed + || changed_files + .iter() + .any(|file| file.ends_with("ci/test-miri.sh")), + frozen_abi: trigger_all + || rust_changed + || changed_files + .iter() + .any(|file| file.ends_with("ci/test-frozen-abi.sh")), + stable: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("ci/stable/run-partition.sh") + || file.ends_with("ci/stable/common.sh") + || file.ends_with("ci/common/shared-functions.sh") + || file.ends_with("ci/common/limit-threads.sh") + }), + local_cluster: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("ci/stable/run-local-cluster-partially.sh") + || file.ends_with("ci/stable/common.sh") + || file.ends_with("ci/common/shared-functions.sh") + }), + docs: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("ci/test-docs.sh") + || file.ends_with("ci/test-stable.sh") + || file.ends_with("scripts/ulimit-n.sh") + || file.ends_with("ci/common/limit-threads.sh") + || file.ends_with("ci/common/shared-functions.sh") + }), + localnet: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("ci/stable/run-localnet.sh") + || file.ends_with("ci/localnet-sanity.sh") + || file.ends_with("ci/run-sanity.sh") + || file.ends_with("scripts/wallet-sanity.sh") + || file.ends_with("ci/upload-ci-artifact.sh") + || file.ends_with("scripts/configure-metrics.sh") + || file.ends_with("scripts/run.sh") + }), + stable_sbf: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("ci/test-stable-sbf.sh") + || file.ends_with("ci/test-stable.sh") + || file.ends_with("scripts/ulimit-n.sh") + || file.ends_with("ci/common/limit-threads.sh") + || file.ends_with("ci/common/shared-functions.sh") + || file.ends_with("programs/sbf/install.sh") + }), + shuttle: trigger_all + || rust_changed + || changed_files + .iter() + .any(|file| file.ends_with("ci/test-shuttle.sh")), + coverage: trigger_all + || rust_changed + || changed_files.iter().any(|file| { + file.ends_with("scripts/coverage.sh") + || file.ends_with("ci/test-coverage.sh") + || file.starts_with("ci/coverage/") + }), + xdp_tests: trigger_all + || rust_changed + || changed_files + .iter() + .any(|file| file.starts_with("xdp/") || file.ends_with("ci/test-xdp.sh")), + } + } +} + +async fn generate_pull_request_pipeline( + repo: &Repo, + pr_number: u64, +) -> Result { + let changed_files = get_changed_files(repo, pr_number).await?; + let flags = PullRequestPipelineFlags::from_changed_files(&changed_files); let mut pipeline = buildkite::Pipeline::new(); pipeline.add_step(default_sanity_step()); - if changed_files.iter().any(|file| file.ends_with(".sh")) { + pipeline.add_step(default_channel_info_divergence_step()); + if flags.shellcheck { pipeline.add_step(default_shellcheck_step()); } pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); - let rust_changed = changed_files.iter().any(|file| { - file.ends_with("Cargo.toml") - || file.ends_with("Cargo.lock") - || file.ends_with(".rs") - || file.ends_with("rust-toolchain.toml") - || file.ends_with("ci/rust-version.sh") - || file.ends_with("ci/docker/Dockerfile") - }); - let coverage_scripts_changed = changed_files.iter().any(|file| { - file.ends_with("scripts/coverage.sh") - || file.ends_with("ci/test-coverage.sh") - || file.starts_with("ci/coverage/") - }); - - if rust_changed { + if flags.checks { pipeline.add_step(default_checks_step()); + } + if flags.feature_check { pipeline.add_step(default_feature_check_step(5)); + } + if flags.miri { pipeline.add_step(default_miri_step()); + } + if flags.frozen_abi { pipeline.add_step(default_frozen_abi_step()); + } - pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); + pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); + if flags.stable { pipeline.add_step(default_stable_step(3)); + } + if flags.local_cluster { pipeline.add_step(default_local_cluster_step(10)); + } + if flags.docs { pipeline.add_step(default_docs_check_step()); + } + if flags.localnet { pipeline.add_step(default_localnet_step()); + } + if flags.xdp_tests { + pipeline.add_step(default_xdp_test_step()); + } - pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); + pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); + if flags.stable_sbf { pipeline.add_step(default_stable_sbf_step()); + } + if flags.shuttle { pipeline.add_step(default_shuttle_step()); - pipeline.add_step(default_coverage_step(3)); - } else if coverage_scripts_changed { + } + if flags.coverage { pipeline.add_step(default_coverage_step(3)); } @@ -223,6 +418,7 @@ fn generate_full_pipeline() -> Result { let mut pipeline = buildkite::Pipeline::new(); pipeline.add_step(default_sanity_step()); + pipeline.add_step(default_channel_info_divergence_step()); pipeline.add_step(default_shellcheck_step()); pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); @@ -238,6 +434,7 @@ fn generate_full_pipeline() -> Result { pipeline.add_step(default_local_cluster_step(10)); pipeline.add_step(default_docs_check_step()); pipeline.add_step(default_localnet_step()); + pipeline.add_step(default_xdp_test_step()); pipeline.add_step(buildkite::Step::Wait(buildkite::WaitStep {})); @@ -266,6 +463,20 @@ fn default_sanity_step() -> buildkite::Step { }) } +fn default_channel_info_divergence_step() -> buildkite::Step { + buildkite::Step::Command(buildkite::CommandStep { + name: String::from("channel-info-divergence"), + command: String::from("ci/docker-run-default-image.sh ci/test-channel-info-divergence.sh"), + agents: Some(HashMap::from([( + String::from("queue"), + String::from("default"), + )])), + timeout_in_minutes: Some(10), + soft_fail: Some(true), + ..Default::default() + }) +} + fn default_shellcheck_step() -> buildkite::Step { buildkite::Step::Command(buildkite::CommandStep { name: String::from("shellcheck"), @@ -461,6 +672,32 @@ fn default_localnet_step() -> buildkite::Step { }) } +fn default_xdp_test_step() -> buildkite::Step { + buildkite::Step::Command(buildkite::CommandStep { + name: String::from("xdp-test"), + command: String::from("ci/docker-run-default-image.sh ci/test-xdp.sh"), + agents: Some(HashMap::from([( + String::from("queue"), + String::from("default"), + )])), + timeout_in_minutes: Some(25), + env: Some(HashMap::from([ + ( + String::from("EXTRA_DOCKER_RUN_ARGS"), + String::from( + "--cap-add NET_ADMIN --cap-add NET_RAW --cap-add SYS_ADMIN --security-opt \ + apparmor=unconfined", + ), + ), + ( + String::from("SOLANA_DOCKER_RUN_NOSETUID"), + String::from("1"), + ), + ])), + ..Default::default() + }) +} + fn default_stable_sbf_step() -> buildkite::Step { buildkite::Step::Command(buildkite::CommandStep { name: String::from("stable-sbf"), @@ -551,11 +788,32 @@ fn default_trigger_secondary_step() -> buildkite::Step { mod tests { use {super::*, pretty_assertions::assert_eq}; + #[test] + fn test_parse_github_url() { + for url in [ + "https://github.com/anza-xyz/agave.git", + "https://github.com/anza-xyz/agave", + "git@github.com:anza-xyz/agave.git", + "git@github.com:anza-xyz/agave", + "ssh://git@github.com/anza-xyz/agave.git", + " https://github.com/anza-xyz/agave.git/ ", + ] { + let repo = + Repo::parse_github_url(url).unwrap_or_else(|| panic!("failed to parse url: {url}")); + assert_eq!(repo.owner, "anza-xyz", "url: {url}"); + assert_eq!(repo.name, "agave", "url: {url}"); + } + } + // PR 1850 is a good large PR for testing #[cfg_attr(not(feature = "integration-tests"), ignore = "requires github api")] #[tokio::test] async fn test_get_changed_files_for_pr_1850() { - let changed_files = get_changed_files(1850).await.unwrap(); + let repo = Repo { + owner: String::from("anza-xyz"), + name: String::from("agave"), + }; + let changed_files = get_changed_files(&repo, 1850).await.unwrap(); assert_eq!(changed_files.len(), 68); assert!(changed_files.contains(&String::from("Cargo.lock"))); assert!(changed_files.contains(&String::from("Cargo.toml"))); @@ -634,4 +892,110 @@ mod tests { assert!(changed_files.contains(&String::from("rbpf/tests/elfs/syscall_static.so"))); assert!(changed_files.contains(&String::from("rbpf/tests/elfs/syscalls.rs"))); } + + fn flags(files: &[&str]) -> PullRequestPipelineFlags { + let owned: Vec = files.iter().map(|s| s.to_string()).collect(); + PullRequestPipelineFlags::from_changed_files(&owned) + } + + #[test] + fn test_readme_triggers_nothing() { + let f = flags(&["README.md"]); + assert!(!f.shellcheck); + assert!(!f.checks); + assert!(!f.feature_check); + assert!(!f.miri); + assert!(!f.frozen_abi); + assert!(!f.stable); + assert!(!f.local_cluster); + assert!(!f.docs); + assert!(!f.localnet); + assert!(!f.stable_sbf); + assert!(!f.shuttle); + assert!(!f.coverage); + assert!(!f.xdp_tests); + } + + #[test] + fn test_docker_change_triggers_all() { + let f = flags(&["ci/docker/Dockerfile"]); + assert!(f.checks); + assert!(f.feature_check); + assert!(f.miri); + assert!(f.frozen_abi); + assert!(f.stable); + assert!(f.local_cluster); + assert!(f.docs); + assert!(f.localnet); + assert!(f.stable_sbf); + assert!(f.shuttle); + assert!(f.coverage); + assert!(f.xdp_tests); + } + + #[test] + fn test_rust_change_triggers_all() { + let f = flags(&["core/src/lib.rs"]); + assert!(f.checks); + assert!(f.feature_check); + assert!(f.miri); + assert!(f.frozen_abi); + assert!(f.stable); + assert!(f.local_cluster); + assert!(f.docs); + assert!(f.localnet); + assert!(f.stable_sbf); + assert!(f.shuttle); + assert!(f.coverage); + assert!(f.xdp_tests); + } + + #[test] + fn test_xdp_change_triggers_xdp_tests() { + let f = flags(&["xdp/tests/README.md"]); + assert!(f.xdp_tests); + } + + #[test] + fn test_test_xdp_sh_triggers_xdp_tests_and_shellcheck() { + let f = flags(&["ci/test-xdp.sh"]); + assert!(f.shellcheck); + assert!(f.xdp_tests); + } + + #[test] + fn test_unimportant_shell_triggers_shellcheck_only() { + let f = flags(&["some/random/script.sh"]); + assert!(f.shellcheck); + assert!(!f.checks); + assert!(!f.feature_check); + assert!(!f.miri); + assert!(!f.frozen_abi); + assert!(!f.stable); + assert!(!f.local_cluster); + assert!(!f.docs); + assert!(!f.localnet); + assert!(!f.stable_sbf); + assert!(!f.shuttle); + assert!(!f.coverage); + assert!(!f.xdp_tests); + } + + #[test] + fn test_test_docs_sh_triggers_docs_only() { + let f = flags(&["ci/test-docs.sh"]); + assert!(f.shellcheck); + assert!(f.docs); + assert!(!f.checks); + assert!(!f.feature_check); + assert!(!f.miri); + assert!(!f.frozen_abi); + assert!(!f.stable); + assert!(!f.local_cluster); + assert!(!f.localnet); + assert!(!f.stable_sbf); + assert!(!f.shuttle); + assert!(!f.coverage); + assert!(!f.xdp_tests); + } } diff --git a/ci/xtask/src/commands/xdp_test.rs b/ci/xtask/src/commands/xdp_test.rs new file mode 100644 index 00000000000..d8af5c87daa --- /dev/null +++ b/ci/xtask/src/commands/xdp_test.rs @@ -0,0 +1,204 @@ +use { + anyhow::{Context, Result, bail, ensure}, + clap::Args, + log::info, + serde::Deserialize, + std::{ + collections::HashMap, + env, + ffi::OsString, + io::{self, Write}, + path::{Path, PathBuf}, + process::{Command, Stdio}, + }, +}; + +const DEFAULT_TESTS: &[&str] = &[ + "netlink_snapshot", + "route_monitor", + "router_snapshot", + "transmitter_smoke", +]; + +#[derive(Args)] +pub struct CommandArgs { + #[arg( + long, + help = "Build and run the tests with the release-with-debug profile" + )] + pub release_with_debug: bool, + + #[arg( + long, + help = "Optional command prefix used to run test executables with privileges, for \ + example: sudo -n -E" + )] + runner: Option, + + #[arg(long = "test", value_name = "TEST")] + tests: Vec, + + #[arg(last = true)] + run_args: Vec, +} + +pub fn run(args: CommandArgs) -> Result<()> { + let CommandArgs { + release_with_debug, + runner, + tests, + run_args, + } = args; + let repo_root = repo_root(); + + info!("building local xdp tests from {}", repo_root.display()); + if tests.is_empty() { + let executables = build_tests(&repo_root, DEFAULT_TESTS, release_with_debug)?; + test_executables(&repo_root, executables, runner, run_args) + } else { + let executables = build_tests(&repo_root, &tests, release_with_debug)?; + test_executables(&repo_root, executables, runner, run_args) + } +} + +fn build_tests<'a, S>( + repo_root: &Path, + tests: &'a [S], + release_with_debug: bool, +) -> Result> +where + S: AsRef, +{ + let mut cmd = Command::new(cargo_bin()); + cmd.current_dir(repo_root) + .arg("test") + .arg("-p") + .arg("agave-xdp") + .arg("--features") + .arg("agave-unstable-api") + .arg("--no-run") + .arg("--message-format=json-render-diagnostics") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if release_with_debug { + cmd.arg("--profile").arg("release-with-debug"); + } + for test in tests { + cmd.arg("--test").arg(test.as_ref()); + } + + let output = cmd.output().context("failed to build xdp tests")?; + io::stderr() + .write_all(&output.stderr) + .context("failed to write cargo stderr")?; + if !output.status.success() { + bail!("failed to build xdp tests with {}", output.status); + } + + test_executables_from_cargo_stdout(&output.stdout, tests) +} + +#[derive(Deserialize)] +struct CargoMessage { + reason: String, + target: Option, + executable: Option, +} + +#[derive(Deserialize)] +struct CargoTarget { + name: String, + kind: Vec, + test: bool, +} + +fn test_executables_from_cargo_stdout<'a, S>( + stdout: &[u8], + tests: &'a [S], +) -> Result> +where + S: AsRef, +{ + let mut executables = HashMap::new(); + let stdout = std::str::from_utf8(stdout).context("cargo output is not valid UTF-8")?; + for line in stdout.lines() { + let Ok(message) = serde_json::from_str::(line) else { + continue; + }; + if message.reason != "compiler-artifact" { + continue; + } + let Some(target) = message.target else { + continue; + }; + if !target.test || !target.kind.iter().any(|kind| kind == "test") { + continue; + } + let Some(executable) = message.executable else { + continue; + }; + executables.insert(target.name, executable); + } + + tests + .iter() + .map(|test| { + let executable = executables.remove(test.as_ref()).with_context(|| { + format!("cargo did not report executable for {}", test.as_ref()) + })?; + Ok((test, executable)) + }) + .collect() +} + +fn test_executables( + repo_root: &Path, + executables: I, + runner: Option, + run_args: Vec, +) -> Result<()> +where + I: IntoIterator, + S: AsRef, +{ + for (test, executable) in executables { + info!("running {} from {}", test.as_ref(), executable.display()); + let mut cmd = command_with_runner(runner.as_deref(), &executable)?; + cmd.current_dir(repo_root) + .arg("--include-ignored") + .arg("--test-threads=1"); + for arg in &run_args { + cmd.arg(arg); + } + let status = cmd + .status() + .with_context(|| format!("failed to run {}", test.as_ref()))?; + ensure!(status.success(), "{} failed with {status}", test.as_ref()); + } + + Ok(()) +} + +fn repo_root() -> PathBuf { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + root.canonicalize().unwrap_or(root) +} + +fn command_with_runner(runner: Option<&str>, program: &Path) -> Result { + let Some(runner) = runner else { + return Ok(Command::new(program)); + }; + let mut parts = runner.split_whitespace(); + let Some(runner_program) = parts.next() else { + bail!("runner cannot be empty"); + }; + let mut cmd = Command::new(runner_program); + cmd.args(parts).arg(program); + Ok(cmd) +} + +fn cargo_bin() -> PathBuf { + env::var_os("CARGO") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("cargo")) +} diff --git a/ci/xtask/src/main.rs b/ci/xtask/src/main.rs index 1a66585ae4a..2dfd6a1020a 100644 --- a/ci/xtask/src/main.rs +++ b/ci/xtask/src/main.rs @@ -28,6 +28,10 @@ enum Commands { Publish(xtask_shared::commands::publish::CommandArgs), #[command(about = "Generate Buildkite pipeline")] GeneratePipeline(commands::generate_pipeline::CommandArgs), + #[command(about = "Print release channel info")] + ChannelInfo, + #[command(about = "Run XDP integration tests")] + XdpTest(commands::xdp_test::CommandArgs), } #[derive(Args, Debug)] @@ -78,6 +82,12 @@ async fn try_main(xtask: Xtask) -> Result<()> { Commands::GeneratePipeline(args) => { commands::generate_pipeline::run(args).await?; } + Commands::ChannelInfo => { + commands::channel_info::run().await?; + } + Commands::XdpTest(args) => { + commands::xdp_test::run(args)?; + } } Ok(()) diff --git a/clap-utils/Cargo.toml b/clap-utils/Cargo.toml index d36709a1916..80bf76c39ef 100644 --- a/clap-utils/Cargo.toml +++ b/clap-utils/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] name = "solana_clap_utils" @@ -19,6 +21,8 @@ name = "solana_clap_utils" agave-unstable-api = [] [dependencies] +bip39 = { workspace = true } +bs58 = { workspace = true, features = ["std"] } chrono = { workspace = true, features = ["default"] } clap = { version = "2.33.0", default-features = false, features = ["suggestions"] } rpassword = { workspace = true } @@ -38,7 +42,6 @@ solana-seed-phrase = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } thiserror = { workspace = true } -tiny-bip39 = { workspace = true } uriparse = { workspace = true } url = { workspace = true } @@ -47,3 +50,6 @@ assert_matches = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } solana-system-interface = { workspace = true, features = ["bincode"] } tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/clap-utils/src/input_parsers.rs b/clap-utils/src/input_parsers.rs index fa281c47358..300c7e9adb3 100644 --- a/clap-utils/src/input_parsers.rs +++ b/clap-utils/src/input_parsers.rs @@ -5,7 +5,10 @@ use { }, chrono::DateTime, clap::ArgMatches, - solana_bls_signatures::{Pubkey as BLSPubkey, PubkeyCompressed as BLSPubkeyCompressed}, + solana_bls_signatures::{ + BLS_PUBLIC_KEY_COMPRESSED_SIZE, Pubkey as BLSPubkey, + PubkeyCompressed as BLSPubkeyCompressed, + }, solana_clock::UnixTimestamp, solana_cluster_type::ClusterType, solana_commitment_config::CommitmentConfig, @@ -105,19 +108,45 @@ pub fn pubkeys_of(matches: &ArgMatches<'_>, name: &str) -> Option> { }) } +fn bls_pubkey_compressed_from_base58(value: &str) -> Result { + let bytes = bs58::decode(value) + .into_vec() + .map_err(|err| err.to_string())?; + let bytes: [u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE] = + bytes.try_into().map_err(|bytes: Vec| { + format!( + "expected {BLS_PUBLIC_KEY_COMPRESSED_SIZE} decoded bytes, got {}", + bytes.len() + ) + })?; + Ok(BLSPubkeyCompressed(bytes)) +} + +pub fn bls_pubkey_compressed_from_str(value: &str) -> Result { + if let Ok(bls_pubkey) = BLSPubkeyCompressed::from_str(value) { + return Ok(bls_pubkey); + } + + if let Ok(bls_pubkey) = BLSPubkey::from_str(value) { + return bls_pubkey + .try_into() + .map_err(|err| format!("failed to convert BLS pubkey to compressed form: {err}")); + } + + bls_pubkey_compressed_from_base58(value).map_err(|err| { + format!( + "Failed to parse BLS pubkey as compressed string, uncompressed string, or base58 \ + compressed bytes: {err}" + ) + }) +} + pub fn bls_pubkeys_of(matches: &ArgMatches<'_>, name: &str) -> Option> { matches.values_of(name).map(|values| { values .map(|value| { - // Most of the case it is just a compressed BLS pubkey string - // If the conversion fails, we try to read it as uncompressed BLS pubkey string - BLSPubkeyCompressed::from_str(value).unwrap_or_else(|_| { - let bls_pubkey = BLSPubkey::from_str(value) - .expect("Failed to parse BLS pubkey or compressed BLS pubkey from string"); - bls_pubkey - .try_into() - .expect("Failed to convert to compressed BLS pubkey") - }) + bls_pubkey_compressed_from_str(value) + .expect("Failed to parse BLS pubkey or compressed BLS pubkey from string") }) .collect() }) @@ -466,6 +495,22 @@ mod tests { bls_pubkeys_of(&matches, "multiple"), Some(vec![bls_pubkey1_compressed, bls_pubkey2_compressed]) ); + + // Test that base58-encoded compressed BLS pubkey bytes also work, matching + // the vote-account display format. + let bls_pubkey1_base58 = bs58::encode(bls_pubkey1_compressed.0).into_string(); + let bls_pubkey2_base58 = bs58::encode(bls_pubkey2_compressed.0).into_string(); + let matches = app().get_matches_from(vec![ + "test", + "--multiple", + &bls_pubkey1_base58, + "--multiple", + &bls_pubkey2_base58, + ]); + assert_eq!( + bls_pubkeys_of(&matches, "multiple"), + Some(vec![bls_pubkey1_compressed, bls_pubkey2_compressed]) + ); } #[test] diff --git a/clap-utils/src/keypair.rs b/clap-utils/src/keypair.rs index 835759789ac..d97f0247615 100644 --- a/clap-utils/src/keypair.rs +++ b/clap-utils/src/keypair.rs @@ -15,7 +15,7 @@ use { input_parsers::{STDOUT_OUTFILE_TOKEN, pubkeys_sigs_of}, offline::{SIGN_ONLY_ARG, SIGNER_ARG}, }, - bip39::{Language, Mnemonic, Seed}, + bip39::{Language, Mnemonic}, clap::ArgMatches, rpassword::prompt_password, solana_derivation_path::{DerivationPath, DerivationPathError}, @@ -1065,15 +1065,15 @@ pub fn keypair_from_seed_phrase( let parse_language_fn = || { for language in &[ Language::English, - Language::ChineseSimplified, - Language::ChineseTraditional, + Language::SimplifiedChinese, + Language::TraditionalChinese, Language::Japanese, Language::Spanish, Language::Korean, Language::French, Language::Italian, ] { - if let Ok(mnemonic) = Mnemonic::from_phrase(&sanitized, *language) { + if let Ok(mnemonic) = Mnemonic::parse_in(*language, &sanitized) { return Ok(mnemonic); } } @@ -1081,11 +1081,11 @@ pub fn keypair_from_seed_phrase( }; let mnemonic = parse_language_fn()?; let passphrase = prompt_passphrase(&passphrase_prompt)?; - let seed = Seed::new(&mnemonic, &passphrase); + let seed = mnemonic.to_seed(&passphrase); if legacy { - keypair_from_seed(seed.as_bytes())? + keypair_from_seed(&seed)? } else { - keypair_from_seed_and_derivation_path(seed.as_bytes(), derivation_path)? + keypair_from_seed_and_derivation_path(&seed, derivation_path)? } }; diff --git a/clap-v3-utils/Cargo.toml b/clap-v3-utils/Cargo.toml index 88cccc60b3d..081bb0fffc5 100644 --- a/clap-v3-utils/Cargo.toml +++ b/clap-v3-utils/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] name = "solana_clap_v3_utils" @@ -20,6 +22,7 @@ agave-unstable-api = [] elgamal = ["dep:solana-zk-sdk"] [dependencies] +bip39 = { workspace = true } chrono = { workspace = true, features = ["default"] } clap = { version = "3.2.23", default-features = false, features = ["cargo", "std", "suggestions"] } five8 = { workspace = true } @@ -42,7 +45,6 @@ solana-signature = { workspace = true } solana-signer = { workspace = true } solana-zk-sdk = { workspace = true, optional = true } thiserror = { workspace = true } -tiny-bip39 = { workspace = true } uriparse = { workspace = true } url = { workspace = true } @@ -52,3 +54,6 @@ solana-clap-v3-utils = { path = ".", features = ["agave-unstable-api"] } solana-pubkey = { workspace = true, features = ["rand"] } solana-system-interface = { workspace = true, features = ["bincode"] } tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/clap-v3-utils/src/keygen/mnemonic.rs b/clap-v3-utils/src/keygen/mnemonic.rs index 8cac1972654..35870b5aedf 100644 --- a/clap-v3-utils/src/keygen/mnemonic.rs +++ b/clap-v3-utils/src/keygen/mnemonic.rs @@ -85,8 +85,8 @@ pub fn acquire_language(matches: &ArgMatches) -> Language { let language_name = LANGUAGE_ARG.name; match matches.get_one::(language_name).unwrap().as_str() { "english" => Language::English, - "chinese-simplified" => Language::ChineseSimplified, - "chinese-traditional" => Language::ChineseTraditional, + "chinese-simplified" => Language::SimplifiedChinese, + "chinese-traditional" => Language::TraditionalChinese, "japanese" => Language::Japanese, "spanish" => Language::Spanish, "korean" => Language::Korean, @@ -101,8 +101,8 @@ pub fn try_get_language(matches: &ArgMatches) -> Result, Box(LANGUAGE_ARG.name)? .map(|language| match language.as_str() { "english" => Language::English, - "chinese-simplified" => Language::ChineseSimplified, - "chinese-traditional" => Language::ChineseTraditional, + "chinese-simplified" => Language::SimplifiedChinese, + "chinese-traditional" => Language::TraditionalChinese, "japanese" => Language::Japanese, "spanish" => Language::Spanish, "korean" => Language::Korean, diff --git a/clap-v3-utils/src/keypair.rs b/clap-v3-utils/src/keypair.rs index 0a6972e3b73..27bc4729713 100644 --- a/clap-v3-utils/src/keypair.rs +++ b/clap-v3-utils/src/keypair.rs @@ -17,7 +17,7 @@ use { input_parsers::signer::{SignerSource, SignerSourceKind, try_pubkeys_sigs_of}, offline::{SIGN_ONLY_ARG, SIGNER_ARG}, }, - bip39::{Language, Mnemonic, Seed}, + bip39::{Language, Mnemonic}, clap::ArgMatches, rpassword::prompt_password, solana_derivation_path::DerivationPath, @@ -1186,15 +1186,15 @@ fn encodable_key_from_seed_phrase( let parse_language_fn = || { for language in &[ Language::English, - Language::ChineseSimplified, - Language::ChineseTraditional, + Language::SimplifiedChinese, + Language::TraditionalChinese, Language::Japanese, Language::Spanish, Language::Korean, Language::French, Language::Italian, ] { - if let Ok(mnemonic) = Mnemonic::from_phrase(&sanitized, *language) { + if let Ok(mnemonic) = Mnemonic::parse_in(*language, &sanitized) { return Ok(mnemonic); } } @@ -1202,11 +1202,11 @@ fn encodable_key_from_seed_phrase( }; let mnemonic = parse_language_fn()?; let passphrase = prompt_passphrase(&passphrase_prompt)?; - let seed = Seed::new(&mnemonic, &passphrase); + let seed = mnemonic.to_seed(&passphrase); if legacy { - K::from_seed(seed.as_bytes())? + K::from_seed(&seed)? } else { - K::from_seed_and_derivation_path(seed.as_bytes(), derivation_path)? + K::from_seed_and_derivation_path(&seed, derivation_path)? } }; Ok(key) diff --git a/cli-config/Cargo.toml b/cli-config/Cargo.toml index fec95a467ad..eff06275ece 100644 --- a/cli-config/Cargo.toml +++ b/cli-config/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -25,3 +27,6 @@ url = { workspace = true } [dev-dependencies] anyhow = { workspace = true } + +[lints] +workspace = true diff --git a/cli-output/Cargo.toml b/cli-output/Cargo.toml index 5dddd99d6c4..6a91b700510 100644 --- a/cli-output/Cargo.toml +++ b/cli-output/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -18,7 +20,9 @@ agave-unstable-api = [] [dependencies] Inflector = { workspace = true } agave-reserved-account-keys = { workspace = true } +agave-votor-messages = { workspace = true } base64 = { workspace = true } +bitvec = { workspace = true } chrono = { workspace = true, features = ["default", "serde"] } clap = { version = "2.33.0", default-features = false, features = ["suggestions"] } console = { workspace = true } @@ -31,12 +35,15 @@ serde_json = { workspace = true } solana-account = { workspace = true } solana-account-decoder = { workspace = true } solana-bincode = { workspace = true } +solana-bls-signatures = { workspace = true } solana-clap-utils = { workspace = true } solana-cli-config = { workspace = true } solana-clock = { workspace = true } solana-epoch-info = { workspace = true, features = ["serde"] } +solana-epoch-schedule = { workspace = true } solana-hash = { workspace = true } solana-message = { workspace = true } +solana-native-token = { workspace = true } solana-packet = { workspace = true } solana-pubkey = { workspace = true } solana-rpc-client-api = { workspace = true } @@ -55,3 +62,6 @@ solana-keypair = { workspace = true } solana-seed-derivable = { workspace = true } solana-signer = { workspace = true } solana-transaction-context = { path = "../transaction-context", features = ["agave-unstable-api", "bincode"] } + +[lints] +workspace = true diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 21a9f2d1da6..495626c8bc0 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -10,7 +10,9 @@ use { writeln_transaction, }, }, + agave_votor_messages::{migration::AG_MIGRATION_EPOCH_CREDIT, wire::WireBlockCertMessage}, base64::{Engine, prelude::BASE64_STANDARD}, + bitvec::vec::BitVec, chrono::{Local, TimeZone, Utc}, clap::ArgMatches, console::{Emoji, style}, @@ -22,10 +24,13 @@ use { UiAccountEncoding, UiDataSliceConfig, encode_ui_account, parse_account_data::AccountAdditionalDataV3, parse_token::UiTokenAccount, }, + solana_bls_signatures::Signature as BLSSignature, solana_clap_utils::keypair::SignOnly, solana_clock::{Epoch, Slot, UnixTimestamp}, solana_epoch_info::EpochInfo, + solana_epoch_schedule::EpochSchedule, solana_hash::Hash, + solana_native_token::LAMPORTS_PER_SOL, solana_pubkey::Pubkey, solana_rpc_client_api::response::{ RpcAccountBalance, RpcContactInfo, RpcInflationGovernor, RpcInflationRate, RpcKeyedAccount, @@ -41,12 +46,16 @@ use { EncodedConfirmedBlock, EncodedTransaction, TransactionConfirmationStatus, UiTransactionStatusMeta, }, - solana_transaction_status_client_types::UiTransactionError, + solana_transaction_status_client_types::{Rewards, UiTransactionError}, solana_vote_program::{ authorized_voters::AuthorizedVoters, - vote_state::{BlockTimestamp, LandedVote, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY}, + vote_state::{ + BlockTimestamp, LandedVote, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY, + VOTE_CREDITS_MAXIMUM_PER_SLOT, VoteStateV4, + }, }, std::{ + cmp::Ordering, collections::{BTreeMap, HashMap}, fmt, str::FromStr, @@ -690,7 +699,7 @@ impl fmt::Display for CliValidators { "Current Stake:", &format!( "{} ({:0.2}%)", - &build_balance_message(self.total_current_stake, self.use_lamports_unit, true), + build_balance_message(self.total_current_stake, self.use_lamports_unit, true), 100. * self.total_current_stake as f64 / self.total_active_stake as f64 ), )?; @@ -699,7 +708,7 @@ impl fmt::Display for CliValidators { "Delinquent Stake:", &format!( "{} ({:0.2}%)", - &build_balance_message( + build_balance_message( self.total_delinquent_stake, self.use_lamports_unit, true @@ -1197,7 +1206,7 @@ impl fmt::Display for CliKeyedEpochRewards { } } -fn show_votes_and_credits( +fn show_tower_votes( f: &mut fmt::Formatter, votes: &[CliLandedVote], epoch_voting_history: &[CliEpochVotingHistory], @@ -1211,7 +1220,7 @@ fn show_votes_and_credits( writeln!( f, - "{} Votes (using {}/{} entries):", + "{} Tower votes (using {}/{} entries):", (if newest_history_entry.is_none() { "All" } else { @@ -1233,14 +1242,22 @@ fn show_votes_and_credits( writeln!(f, " (latency {})", vote.latency)?; } } - if let Some(newest) = newest_history_entry { + if let Some(newest) = newest_history_entry + && let CliEpochVotingHistory::Tower(newest) = newest + { writeln!( f, "- ... (truncated {} rooted votes, which have been credited)", newest.credits )?; } + Ok(()) +} +fn show_epoch_credits( + f: &mut fmt::Formatter, + epoch_voting_history: &[CliEpochVotingHistory], +) -> fmt::Result { if !epoch_voting_history.is_empty() { writeln!( f, @@ -1260,39 +1277,76 @@ fn show_votes_and_credits( } for entry in epoch_voting_history.iter().rev() { - writeln!( - f, // tame fmt so that this will be folded like following - "- epoch: {}", - entry.epoch - )?; - writeln!( - f, - " credits range: ({}..{}]", - entry.prev_credits, entry.credits - )?; + match entry { + CliEpochVotingHistory::Tower(tower) => { + writeln!( + f, // tame fmt so that this will be folded like following + "- Tower epoch: {}", + tower.epoch + )?; + writeln!( + f, + " credits range: ({}..{}]", + tower.prev_credits, tower.credits + )?; + writeln!( + f, + " credits/max credits: {}/{}", + tower.credits_earned, + tower.slots_in_epoch * u64::from(tower.max_credits_per_slot) + )?; + } + CliEpochVotingHistory::Alpenglow(ag) => { + writeln!(f, "- Alpenglow epoch: {}", ag.epoch)?; + writeln!( + f, + " lamports range: ({}..{}]", + ag.prev_lamports, ag.lamports + )?; + writeln!( + f, + " lamports earned: {} ({:.4} SOL)", + ag.lamports_earned, + ag.lamports_earned as f64 / LAMPORTS_PER_SOL as f64 + )?; + } + } + } + if let Some(oldest) = epoch_voting_history.iter().next() + && oldest.prev() > 0 + { + // Oldest entry doesn't start with 0. so history must be truncated... + + // count of this combined pseudo credits range: (0..=oldest.prev_credits] like the above + // (or this is just [1..=oldest.prev_credits] for human's simpler minds) + let count = oldest.prev(); + writeln!( f, - " credits/max credits: {}/{}", - entry.credits_earned, - entry.slots_in_epoch * u64::from(entry.max_credits_per_slot) + "- ... (omitting {count} past rooted votes, which have already been credited)" )?; } - if let Some(oldest) = epoch_voting_history.iter().next() { - if oldest.prev_credits > 0 { - // Oldest entry doesn't start with 0. so history must be truncated... - // count of this combined pseudo credits range: (0..=oldest.prev_credits] like the above - // (or this is just [1..=oldest.prev_credits] for human's simpler minds) - let count = oldest.prev_credits; + Ok(()) +} - writeln!( - f, - "- ... (omitting {count} past rooted votes, which have already been credited)" - )?; +fn show_votes_and_credits( + f: &mut fmt::Formatter, + votes: &VotesObserved, + epoch_voting_history: &[CliEpochVotingHistory], +) -> fmt::Result { + match votes { + VotesObserved::Tower(votes) => { + show_tower_votes(f, votes, epoch_voting_history)?; + show_epoch_credits(f, epoch_voting_history) + } + VotesObserved::Alpenglow(vote) => { + if let Some(vote) = vote { + writeln!(f, "Latest Alpenglow vote for slot: {}", vote.slot)?; + } + show_epoch_credits(f, epoch_voting_history) } } - - Ok(()) } enum Format { @@ -1747,6 +1801,36 @@ impl fmt::Display for CliValidatorInfo { } } +#[derive(Serialize, Deserialize)] +pub enum VotesObserved { + Alpenglow(Option), + Tower(Vec), +} + +impl VotesObserved { + pub fn new(vote_state: &VoteStateV4, ag_genesis_cert: &Option) -> Self { + match ag_genesis_cert { + None => Self::Tower(vote_state.votes.iter().map(CliLandedVote::from).collect()), + Some(cert) => match vote_state.votes.iter().last() { + None => Self::Alpenglow(None), + Some(vote) => { + if vote.lockout.slot() <= cert.block.slot { + Self::Tower(vote_state.votes.iter().map(CliLandedVote::from).collect()) + } else { + Self::Alpenglow(Some(CliLandedVote::from(vote))) + } + } + }, + } + } +} + +impl Default for VotesObserved { + fn default() -> Self { + Self::Tower(vec![]) + } +} + #[derive(Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CliVoteAccount { @@ -1759,7 +1843,7 @@ pub struct CliVoteAccount { pub commission: u8, pub root_slot: Option, pub recent_timestamp: BlockTimestamp, - pub votes: Vec, + pub votes_observed: VotesObserved, pub epoch_voting_history: Vec, #[serde(skip_serializing)] pub use_lamports_unit: bool, @@ -1833,7 +1917,7 @@ impl fmt::Display for CliVoteAccount { unix_timestamp_to_string(self.recent_timestamp.timestamp), self.recent_timestamp.slot )?; - show_votes_and_credits(f, &self.votes, &self.epoch_voting_history)?; + show_votes_and_credits(f, &self.votes_observed, &self.epoch_voting_history)?; show_epoch_rewards(f, &self.epoch_rewards, self.use_csv)?; Ok(()) } @@ -1882,9 +1966,25 @@ impl From<&AuthorizedVoters> for CliAuthorizedVoters { } } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum CliEpochVotingHistory { + Alpenglow(AgEpochHistory), + Tower(TowerEpochHistory), +} + +impl CliEpochVotingHistory { + fn prev(&self) -> u64 { + match self { + Self::Tower(t) => t.prev_credits, + Self::Alpenglow(a) => a.prev_lamports, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CliEpochVotingHistory { +pub struct TowerEpochHistory { pub epoch: Epoch, pub slots_in_epoch: u64, pub credits_earned: u64, @@ -1893,7 +1993,94 @@ pub struct CliEpochVotingHistory { pub max_credits_per_slot: u8, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgEpochHistory { + pub epoch: Epoch, + pub slots_in_epoch: u64, + pub lamports_earned: u64, + pub lamports: u64, + pub prev_lamports: u64, +} + +pub fn get_epoch_history( + epoch_schedule: &EpochSchedule, + vote_state: &VoteStateV4, + ag_genesis_cert: &Option, + tvc_activation_epoch: Option, +) -> Vec { + let mut ret = vec![]; + let mut marker_seen = false; + for entry in vote_state.epoch_credits.iter().copied() { + if entry == AG_MIGRATION_EPOCH_CREDIT { + marker_seen = true; + continue; + } + let (epoch, credits, prev_credits) = entry; + let slots_in_epoch = epoch_schedule.get_slots_in_epoch(epoch); + + enum TowerOrAg { + Tower(Slot), + Ag(Slot), + } + + let tower_or_ag = match ag_genesis_cert { + None => TowerOrAg::Tower(slots_in_epoch), + Some(cert) => { + let migration_slot = cert.block.slot; + let migration_epoch = epoch_schedule.get_epoch(migration_slot); + match epoch.cmp(&migration_epoch) { + Ordering::Less => TowerOrAg::Tower(slots_in_epoch), + Ordering::Greater => TowerOrAg::Ag(slots_in_epoch), + Ordering::Equal => { + // +1 because migration slot is still tower + let migration_offset = + migration_slot - epoch_schedule.get_first_slot_in_epoch(epoch) + 1; + if marker_seen { + TowerOrAg::Ag(slots_in_epoch - migration_offset) + } else { + TowerOrAg::Tower(migration_offset) + } + } + } + } + }; + + let entry = match tower_or_ag { + TowerOrAg::Tower(slots_in_epoch) => { + let credits_earned = credits.saturating_sub(prev_credits); + let is_tvc_active = tvc_activation_epoch.map(|e| epoch >= e).unwrap_or_default(); + let max_credits_per_slot = if is_tvc_active { + VOTE_CREDITS_MAXIMUM_PER_SLOT + } else { + 1 + }; + CliEpochVotingHistory::Tower(TowerEpochHistory { + epoch, + slots_in_epoch, + credits_earned, + credits, + prev_credits, + max_credits_per_slot, + }) + } + TowerOrAg::Ag(slots_in_epoch) => { + let lamports_earned = credits.saturating_sub(prev_credits); + CliEpochVotingHistory::Alpenglow(AgEpochHistory { + epoch, + slots_in_epoch, + lamports_earned, + lamports: credits, + prev_lamports: prev_credits, + }) + } + }; + ret.push(entry); + } + ret +} + +#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CliLandedVote { pub latency: u8, @@ -1911,6 +2098,57 @@ impl From<&LandedVote> for CliLandedVote { } } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliAgGenesisInfoPayload { + /// Epoch in which the alpenglow feature was activated. + pub epoch: Epoch, + /// Slot in which migration to alpenglow happened. + pub slot: Slot, + /// Hash of the block in which migration to alpenglow happened. + pub block_id: Hash, + /// Bitvec of validators that signed the block. + pub bitvec: BitVec, + /// The aggregate signature of the validators. + pub signature: BLSSignature, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[allow(clippy::large_enum_variant)] +pub enum CliAgGenesisInfo { + Tower, + Ag(CliAgGenesisInfoPayload), +} + +impl QuietDisplay for CliAgGenesisInfo {} +impl VerboseDisplay for CliAgGenesisInfo {} + +impl fmt::Display for CliAgGenesisInfo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Tower => writeln!(f, "still running tower"), + Self::Ag(payload) => { + let CliAgGenesisInfoPayload { + epoch, + slot, + block_id: block_hash, + bitvec, + signature, + } = payload; + writeln!(f, "Alpenglow genesis information:")?; + writeln!(f, " Feature flag activation: Epoch {epoch}")?; + writeln!(f, " Genesis Block - Slot {slot}, Block ID {block_hash}")?; + writeln!( + f, + " Genesis Vote - {} validators participated, Signature {signature}", + bitvec.count_ones(), + ) + } + } + } +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CliBlockTime { @@ -2547,8 +2785,8 @@ impl fmt::Display for CliUpgradeableProgramClosed { writeln!( f, "Closed Program Id {}, {} reclaimed", - &self.program_id, - &build_balance_message(self.lamports, self.use_lamports_unit, true) + self.program_id, + build_balance_message(self.lamports, self.use_lamports_unit, true) )?; Ok(()) } @@ -2568,7 +2806,7 @@ impl fmt::Display for CliUpgradeableProgramExtended { writeln!( f, "Extended Program Id {} by {} bytes", - &self.program_id, self.additional_bytes, + self.program_id, self.additional_bytes, )?; Ok(()) } @@ -2868,35 +3106,33 @@ pub struct CliBlock { pub slot: Slot, } -impl QuietDisplay for CliBlock {} -impl VerboseDisplay for CliBlock {} - -impl fmt::Display for CliBlock { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "Slot: {}", self.slot)?; - writeln!( - f, - "Parent Slot: {}", - self.encoded_confirmed_block.parent_slot - )?; - writeln!(f, "Blockhash: {}", self.encoded_confirmed_block.blockhash)?; - writeln!( - f, - "Previous Blockhash: {}", - self.encoded_confirmed_block.previous_blockhash - )?; - if let Some(block_time) = self.encoded_confirmed_block.block_time { +impl CliBlock { + pub fn display_block_meta( + f: &mut fmt::Formatter, + slot: Slot, + parent_slot: Slot, + blockhash: &str, + previous_blockhash: &str, + block_time: Option, + block_height: Option, + rewards: &Rewards, + ) -> fmt::Result { + writeln!(f, "Slot: {slot}")?; + writeln!(f, "Parent Slot: {parent_slot}")?; + writeln!(f, "Blockhash: {blockhash}")?; + writeln!(f, "Previous Blockhash: {previous_blockhash}")?; + if let Some(block_time) = block_time { writeln!( f, "Block Time: {:?}", Local.timestamp_opt(block_time, 0).unwrap() )?; } - if let Some(block_height) = self.encoded_confirmed_block.block_height { + if let Some(block_height) = block_height { writeln!(f, "Block Height: {block_height:?}")?; } - if !self.encoded_confirmed_block.rewards.is_empty() { - let mut rewards = self.encoded_confirmed_block.rewards.clone(); + if !rewards.is_empty() { + let mut rewards = rewards.clone(); rewards.sort_by(|a, b| a.pubkey.cmp(&b.pubkey)); let mut total_rewards = 0; writeln!(f, "Rewards:")?; @@ -2951,6 +3187,26 @@ impl fmt::Display for CliBlock { build_balance_message(total_rewards.unsigned_abs(), false, false) )?; } + Ok(()) + } +} + +impl QuietDisplay for CliBlock {} +impl VerboseDisplay for CliBlock {} + +impl fmt::Display for CliBlock { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Self::display_block_meta( + f, + self.slot, + self.encoded_confirmed_block.parent_slot, + &self.encoded_confirmed_block.blockhash, + &self.encoded_confirmed_block.previous_blockhash, + self.encoded_confirmed_block.block_time, + self.encoded_confirmed_block.block_height, + &self.encoded_confirmed_block.rewards, + )?; + for (index, transaction_with_meta) in self.encoded_confirmed_block.transactions.iter().enumerate() { diff --git a/cli-output/src/display.rs b/cli-output/src/display.rs index cd0ea7f096c..0a0d5f1d76b 100644 --- a/cli-output/src/display.rs +++ b/cli-output/src/display.rs @@ -18,7 +18,9 @@ use { Rewards, UiReturnDataEncoding, UiTransactionReturnData, UiTransactionStatusMeta, }, solana_transaction_status_client_types::UiTransactionError, - spl_memo_interface::{v1::id as spl_memo_v1_id, v3::id as spl_memo_v3_id}, + spl_memo_interface::{ + v1::id as spl_memo_v1_id, v3::id as spl_memo_v3_id, v4::id as spl_memo_v4_id, + }, std::{collections::HashMap, fmt, io, time::Duration}, }; @@ -40,7 +42,7 @@ impl Default for BuildBalanceMessageConfig { } fn is_memo_program(k: &Pubkey) -> bool { - *k == spl_memo_v1_id() || *k == spl_memo_v3_id() + *k == spl_memo_v1_id() || *k == spl_memo_v3_id() || *k == spl_memo_v4_id() } pub fn build_balance_message_with_config( @@ -466,11 +468,11 @@ fn write_instruction<'a, W: io::Write>( writeln!(w, "{prefix} {system_instruction:?}")?; raw = false; } - } else if is_memo_program(program_pubkey) { - if let Ok(s) = std::str::from_utf8(&instruction.data) { - writeln!(w, "{prefix} Data: \"{s}\"")?; - raw = false; - } + } else if is_memo_program(program_pubkey) + && let Ok(s) = std::str::from_utf8(&instruction.data) + { + writeln!(w, "{prefix} Data: \"{s}\"")?; + raw = false; } } @@ -510,31 +512,31 @@ fn write_rewards( rewards: Option<&Rewards>, prefix: &str, ) -> io::Result<()> { - if let Some(rewards) = rewards { - if !rewards.is_empty() { - writeln!(w, "{prefix}Rewards:",)?; + if let Some(rewards) = rewards + && !rewards.is_empty() + { + writeln!(w, "{prefix}Rewards:",)?; + writeln!( + w, + "{} {:<44} {:^15} {:<16} {:<20}", + prefix, "Address", "Type", "Amount", "New Balance" + )?; + for reward in rewards { + let sign = if reward.lamports < 0 { "-" } else { "" }; writeln!( w, - "{} {:<44} {:^15} {:<16} {:<20}", - prefix, "Address", "Type", "Amount", "New Balance" + "{} {:<44} {:^15} {}◎{:<14.9} ◎{:<18.9}", + prefix, + reward.pubkey, + if let Some(reward_type) = reward.reward_type { + format!("{reward_type}") + } else { + "-".to_string() + }, + sign, + build_balance_message(reward.lamports.unsigned_abs(), false, false), + build_balance_message(reward.post_balance, false, false) )?; - for reward in rewards { - let sign = if reward.lamports < 0 { "-" } else { "" }; - writeln!( - w, - "{} {:<44} {:^15} {}◎{:<14.9} ◎{:<18.9}", - prefix, - reward.pubkey, - if let Some(reward_type) = reward.reward_type { - format!("{reward_type}") - } else { - "-".to_string() - }, - sign, - build_balance_message(reward.lamports.unsigned_abs(), false, false), - build_balance_message(reward.post_balance, false, false) - )?; - } } } Ok(()) @@ -643,12 +645,12 @@ fn write_log_messages( log_messages: Option<&Vec>, prefix: &str, ) -> io::Result<()> { - if let Some(log_messages) = log_messages { - if !log_messages.is_empty() { - writeln!(w, "{prefix}Log Messages:",)?; - for log_message in log_messages { - writeln!(w, "{prefix} {log_message}")?; - } + if let Some(log_messages) = log_messages + && !log_messages.is_empty() + { + writeln!(w, "{prefix}Log Messages:",)?; + for log_message in log_messages { + writeln!(w, "{prefix} {log_message}")?; } } Ok(()) @@ -672,10 +674,9 @@ pub fn println_transaction( CliTimezone::Local, ) .is_ok() + && let Ok(s) = String::from_utf8(w) { - if let Ok(s) = String::from_utf8(w) { - print!("{s}"); - } + print!("{s}"); } } @@ -698,10 +699,10 @@ pub fn writeln_transaction( CliTimezone::Local, ); - if write_result.is_ok() { - if let Ok(s) = String::from_utf8(w) { - write!(f, "{s}")?; - } + if write_result.is_ok() + && let Ok(s) = String::from_utf8(w) + { + write!(f, "{s}")?; } Ok(()) } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3615acc3c98..25484740e6a 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -11,6 +11,11 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +# `all-features` can't be used: remote-wallet-hidraw and remote-wallet-libusb +# forward to hidapi's mutually-exclusive linux backends. Document the default +# (hidraw) backend plus the unstable API and dev-only utils. +features = ["agave-unstable-api", "dev-context-only-utils"] +rustdoc-args = ["--cfg=docsrs"] [[bin]] name = "solana" @@ -31,6 +36,7 @@ agave-feature-set = { workspace = true } agave-logger = { workspace = true } agave-votor-messages = { workspace = true } bincode = { workspace = true } +bip39 = { workspace = true } bs58 = { workspace = true } clap = { workspace = true } console = { workspace = true } @@ -71,7 +77,7 @@ solana-packet = { workspace = true } solana-program-runtime = { workspace = true } solana-pubkey = { workspace = true } solana-pubsub-client = { workspace = true } -solana-remote-wallet = { workspace = true } +solana-remote-wallet = { workspace = true, features = ["keystone"] } solana-rent = { workspace = true } solana-rpc-client = { workspace = true, features = ["default"] } solana-rpc-client-api = { workspace = true } @@ -80,7 +86,8 @@ solana-sbpf = { workspace = true, features = ["jit"] } solana-sdk-ids = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } -solana-slot-history = { workspace = true } +solana-signer-store = { workspace = true } +solana-slot-history = { workspace = true, features = ["wincode"] } solana-stake-interface = { workspace = true } solana-syscalls = { workspace = true } solana-system-interface = { workspace = true, features = ["bincode"] } @@ -92,8 +99,8 @@ solana-version = { workspace = true } solana-vote-program = { workspace = true } spl-memo-interface = { workspace = true } thiserror = { workspace = true } -tiny-bip39 = { workspace = true } tokio = { workspace = true } +wincode = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } @@ -106,6 +113,9 @@ solana-nonce-account = { workspace = true } solana-presigner = { workspace = true } solana-rpc = { path = "../rpc", features = ["agave-unstable-api"] } solana-sha256-hasher = { workspace = true } -solana-test-validator = { path = "../test-validator" } +solana-test-validator = { path = "../test-validator", features = ["agave-unstable-api", "dev-context-only-utils"] } tempfile = { workspace = true } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/cli/src/cli.rs b/cli/src/cli.rs index a7e0fa3fe3a..59d285984b3 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -449,6 +449,7 @@ pub enum CliCommand { signature: Signature, message: OffchainMessage, }, + GetAgGenesisInfo, } #[derive(Debug, PartialEq)] @@ -490,6 +491,8 @@ pub enum CliError { KeypairFileNotFound(String), #[error("Invalid signature")] InvalidSignature, + #[error("Invalid alpenglow genesis cert")] + InvalidAgGenesisCert, } impl From> for CliError { @@ -617,6 +620,7 @@ pub fn parse_command( } ("epoch", Some(matches)) => parse_get_epoch(matches), ("epoch-info", Some(matches)) => parse_get_epoch_info(matches), + ("alpenglow-genesis-info", Some(matches)) => parse_get_ag_genesis_info(matches), ("feature", Some(matches)) => { parse_feature_subcommand(matches, default_signer, wallet_manager) } @@ -1726,6 +1730,7 @@ pub async fn process_command(config: &CliConfig<'_>) -> ProcessResult { signature, message, } => process_verify_offchain_signature(config, signer_pubkey, signature, message), + CliCommand::GetAgGenesisInfo => process_get_ag_genesis_info(&rpc_client, config).await, } } @@ -1797,10 +1802,10 @@ where match result { Err(err) => { let maybe_tx_err = err.get_transaction_error(); - if let Some(TransactionError::InstructionError(_, ix_error)) = maybe_tx_err { - if let Some(specific_error) = error_adapter(&ix_error) { - return Err(specific_error.into()); - } + if let Some(TransactionError::InstructionError(_, ix_error)) = maybe_tx_err + && let Some(specific_error) = error_adapter(&ix_error) + { + return Err(specific_error.into()); } Err(err.into()) } diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index a19b9578954..e5b9f45cf88 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -3,6 +3,7 @@ use { cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult}, feature::get_feature_activation_epoch, }, + agave_votor_messages::wire::WireBlockCertMessage, clap::{App, AppSettings, Arg, ArgMatches, SubCommand, value_t, value_t_or_exit}, console::style, serde::{Deserialize, Serialize}, @@ -40,8 +41,9 @@ use { }, solana_sdk_ids::sysvar::{self, stake_history}, solana_signature::Signature, + solana_signer_store::{Decoded, decode}, solana_slot_history::{self as slot_history, SlotHistory}, - solana_stake_interface::{self as stake, state::StakeStateV2}, + solana_stake_interface::{self as stake, stake_history::StakeHistory, state::StakeStateV2}, solana_system_interface::MAX_PERMITTED_DATA_LENGTH, solana_transaction_status::{ EncodableWithMeta, EncodedConfirmedTransactionWithStatusMeta, UiTransactionEncoding, @@ -189,6 +191,11 @@ impl ClusterQuerySubCommands for App<'_, '_> { .about("Get information about the current epoch") .alias("get-epoch-info"), ) + .subcommand( + SubCommand::with_name("alpenglow-genesis-info") + .about("Get info about the Alpenglow genesis cert") + .alias("get-alpenglow-genesis-info"), + ) .subcommand( SubCommand::with_name("genesis-hash") .about("Get the genesis hash") @@ -505,6 +512,12 @@ pub fn parse_get_epoch(_matches: &ArgMatches<'_>) -> Result) -> Result { + Ok(CliCommandInfo::without_signers( + CliCommand::GetAgGenesisInfo, + )) +} + pub fn parse_get_epoch_info(_matches: &ArgMatches<'_>) -> Result { Ok(CliCommandInfo::without_signers(CliCommand::GetEpochInfo)) } @@ -1072,6 +1085,34 @@ pub async fn process_get_epoch(rpc_client: &RpcClient, _config: &CliConfig<'_>) Ok(epoch_info.epoch.to_string()) } +pub async fn process_get_ag_genesis_info( + rpc_client: &RpcClient, + config: &CliConfig<'_>, +) -> ProcessResult { + let cert = rpc_client.get_ag_genesis_cert().await?; + let ag_genesis_info = match cert { + None => CliAgGenesisInfo::Tower, + Some(WireBlockCertMessage { block, signature }) => { + let epoch_schedule = rpc_client.get_epoch_schedule().await?; + let epoch = epoch_schedule.get_epoch(block.slot); + const MAX_VALIDATORS: usize = 4096; + let Decoded::Base2(bitvec) = decode(&signature.bitmap, MAX_VALIDATORS) + .map_err(|_| Box::new(CliError::InvalidAgGenesisCert))? + else { + return Err(Box::new(CliError::InvalidAgGenesisCert)); + }; + CliAgGenesisInfo::Ag(CliAgGenesisInfoPayload { + epoch, + slot: block.slot, + block_id: block.block_id, + bitvec, + signature: signature.signature, + }) + } + }; + Ok(config.output_format.formatted_string(&ag_genesis_info)) +} + pub async fn process_get_epoch_info( rpc_client: &RpcClient, config: &CliConfig<'_>, @@ -1212,9 +1253,8 @@ pub async fn process_show_block_production( .value .unwrap(); - let slot_history: SlotHistory = from_account(&slot_history_account).ok_or_else(|| { - CliError::RpcRequestError("Failed to deserialize slot history".to_string()) - })?; + let slot_history: SlotHistory = wincode::deserialize(&slot_history_account.data) + .map_err(|_| CliError::RpcRequestError("Failed to deserialize slot history".to_string()))?; let (confirmed_blocks, start_slot) = if start_slot >= slot_history.oldest() && end_slot <= slot_history.newest() { @@ -1603,8 +1643,11 @@ pub async fn process_show_stakes( .collect(); if !pubkeys.is_empty() { + let mut pubkeys: Vec = pubkeys.into_iter().collect(); + pubkeys.sort(); return Err(CliError::RpcRequestError(format!( - "Failed to retrieve matching vote account for {pubkeys:?}." + "Failed to retrieve matching vote account for {}.", + pubkeys.join(", ") )) .into()); } @@ -1651,17 +1694,27 @@ pub async fn process_show_stakes( .await?; let stake_history_account = rpc_client.get_account(&stake_history::id()).await?; let clock_account = rpc_client.get_account(&sysvar::clock::id()).await?; + let rent_account = rpc_client.get_account(&sysvar::rent::id()).await?; let clock: Clock = from_account(&clock_account).ok_or_else(|| { CliError::RpcRequestError("Failed to deserialize clock sysvar".to_string()) })?; - let stake_history = from_account(&stake_history_account).ok_or_else(|| { - CliError::RpcRequestError("Failed to deserialize stake history".to_string()) - })?; + let rent: Rent = rent_account.deserialize_data()?; + let stake_history: StakeHistory = + bincode::deserialize(&stake_history_account.data).map_err(|_| { + CliError::RpcRequestError("Failed to deserialize stake history".to_string()) + })?; let new_rate_activation_epoch = get_feature_activation_epoch( rpc_client, &agave_feature_set::reduce_stake_warmup_cooldown::id(), ) .await?; + let fixed_point_activation_epoch = get_feature_activation_epoch( + rpc_client, + &agave_feature_set::upgrade_bpf_stake_program_to_v5_1::id(), + ) + .await?; + let use_fixed_point_stake_math = fixed_point_activation_epoch + .is_some_and(|activation_epoch| clock.epoch >= activation_epoch); stake_account_progress_bar.finish_and_clear(); let mut stake_accounts: Vec = vec![]; @@ -1671,9 +1724,7 @@ pub async fn process_show_stakes( Ensure that the account was fetched using a binary encoding.", ); if let Ok(stake_state) = stake_account.state() { - let rent_exempt_balance = rpc_client - .get_minimum_balance_for_rent_exemption(stake_account.data.len()) - .await?; + let rent_exempt_balance = rent.minimum_balance(stake_account.data.len()).max(1); match stake_state { StakeStateV2::Initialized(_) if vote_account_pubkeys.is_empty() => { @@ -1688,6 +1739,7 @@ pub async fn process_show_stakes( new_rate_activation_epoch, rent_exempt_balance, false, + use_fixed_point_stake_math, ), }); } @@ -1706,6 +1758,7 @@ pub async fn process_show_stakes( new_rate_activation_epoch, rent_exempt_balance, false, + use_fixed_point_stake_math, ), }); } diff --git a/cli/src/feature.rs b/cli/src/feature.rs index 17dcb0ec6bd..e2ac7b2e53d 100644 --- a/cli/src/feature.rs +++ b/cli/src/feature.rs @@ -996,10 +996,10 @@ async fn process_activate( .next() .unwrap(); - if let Some(account) = account { - if from_account(&account).is_some() { - return Err(format!("{feature_id} has already been activated").into()); - } + if let Some(account) = account + && from_account(&account).is_some() + { + return Err(format!("{feature_id} has already been activated").into()); } if !feature_activation_allowed(rpc_client, false).await?.0 { diff --git a/cli/src/main.rs b/cli/src/main.rs index cb8dd9b82ef..712bd9a43cd 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -178,12 +178,11 @@ pub fn parse_args<'a>( mut signers, } = parse_command(matches, &default_signer, wallet_manager)?; - if signers.is_empty() { - if let Ok(signer_info) = + if signers.is_empty() + && let Ok(signer_info) = default_signer.generate_unique_signers(vec![None], matches, wallet_manager) - { - signers.extend(signer_info.signers); - } + { + signers.extend(signer_info.signers); } let verbose = matches.is_present("verbose"); diff --git a/cli/src/memo.rs b/cli/src/memo.rs index a6455e4802c..d242e2d39aa 100644 --- a/cli/src/memo.rs +++ b/cli/src/memo.rs @@ -1,4 +1,4 @@ -use {solana_instruction::Instruction, solana_pubkey::Pubkey, spl_memo_interface::v3::id}; +use {solana_instruction::Instruction, solana_pubkey::Pubkey, spl_memo_interface::v4::id}; pub trait WithMemo { fn with_memo>(self, memo: Option) -> Self; diff --git a/cli/src/program.rs b/cli/src/program.rs index cd2942cb3d0..0c075561dc9 100644 --- a/cli/src/program.rs +++ b/cli/src/program.rs @@ -12,7 +12,7 @@ use { feature::{CliFeatureStatus, status_from_account}, }, agave_feature_set::{FEATURE_NAMES, FeatureSet}, - bip39::{Language, Mnemonic, MnemonicType, Seed}, + bip39::{Language, Mnemonic}, clap::{App, AppSettings, Arg, ArgMatches, SubCommand}, log::*, solana_account::state_traits::StateMut, @@ -3128,129 +3128,127 @@ async fn send_deploy_messages( } } - if !write_messages.is_empty() { - if let Some(write_signer) = write_signer { - trace!("Writing program data"); + if !write_messages.is_empty() + && let Some(write_signer) = write_signer + { + trace!("Writing program data"); - // Simulate the first write message to get the number of compute units - // consumed and then reuse that value as the compute unit limit for all - // write messages. + // Simulate the first write message to get the number of compute units + // consumed and then reuse that value as the compute unit limit for all + // write messages. + { + let mut message = write_messages[0].clone(); + if let UpdateComputeUnitLimitResult::UpdatedInstructionIndex(ix_index) = + simulate_and_update_compute_unit_limit( + compute_unit_limit, + &rpc_client, + &mut message, + ) + .await? { - let mut message = write_messages[0].clone(); - if let UpdateComputeUnitLimitResult::UpdatedInstructionIndex(ix_index) = - simulate_and_update_compute_unit_limit( - compute_unit_limit, - &rpc_client, - &mut message, - ) - .await? - { - for msg in &mut write_messages { - // Write messages are all assumed to be identical except - // the program data being written. But just in case that - // assumption is broken, assert that we are only ever - // changing the instruction data for a compute budget - // instruction. - assert_eq!(msg.program_id(ix_index), Some(&compute_budget::id())); - msg.instructions[ix_index] - .data - .clone_from(&message.instructions[ix_index].data); - } + for msg in &mut write_messages { + // Write messages are all assumed to be identical except + // the program data being written. But just in case that + // assumption is broken, assert that we are only ever + // changing the instruction data for a compute budget + // instruction. + assert_eq!(msg.program_id(ix_index), Some(&compute_budget::id())); + msg.instructions[ix_index] + .data + .clone_from(&message.instructions[ix_index].data); } } + } - let connection_cache = { - #[cfg(feature = "dev-context-only-utils")] - let cache = - ConnectionCache::new_quic_for_tests("connection_cache_cli_program_quic", 1); - #[cfg(not(feature = "dev-context-only-utils"))] - let cache = ConnectionCache::new_quic("connection_cache_cli_program_quic", 1); - cache - }; - let transaction_errors = match connection_cache { - ConnectionCache::Udp(cache) => { - solana_tpu_client::nonblocking::tpu_client::TpuClient::new_with_connection_cache( - rpc_client.clone(), - &config.websocket_url, - TpuClientConfig::default(), - cache, - ) - .await? - .send_and_confirm_messages_with_spinner( - &write_messages, - &[fee_payer_signer, write_signer], - ) - .await - } - ConnectionCache::Quic(cache) => { - // `solana_client` type currently required by `send_and_confirm_transactions_in_parallel_v2` - let tpu_client_fut = solana_client::nonblocking::tpu_client::TpuClient::new_with_connection_cache( + let connection_cache = { + #[cfg(feature = "dev-context-only-utils")] + let cache = ConnectionCache::new_quic_for_tests("connection_cache_cli_program_quic", 1); + #[cfg(not(feature = "dev-context-only-utils"))] + let cache = ConnectionCache::new_quic("connection_cache_cli_program_quic", 1); + cache + }; + let transaction_errors = match connection_cache { + ConnectionCache::Udp(cache) => { + solana_tpu_client::nonblocking::tpu_client::TpuClient::new_with_connection_cache( + rpc_client.clone(), + &config.websocket_url, + TpuClientConfig::default(), + cache, + ) + .await? + .send_and_confirm_messages_with_spinner( + &write_messages, + &[fee_payer_signer, write_signer], + ) + .await + } + ConnectionCache::Quic(cache) => { + // `solana_client` type currently required by `send_and_confirm_transactions_in_parallel_v2` + let tpu_client_fut = + solana_client::nonblocking::tpu_client::TpuClient::new_with_connection_cache( rpc_client.clone(), config.websocket_url.as_str(), TpuClientConfig::default(), cache, ); - let tpu_client = if use_rpc { - None - } else { - Some( - tpu_client_fut - .await - .expect("Should return a valid tpu client"), - ) - }; - send_and_confirm_transactions_in_parallel_v2( - rpc_client.clone(), - tpu_client, - &write_messages, - &[fee_payer_signer, write_signer], - SendAndConfirmConfigV2 { - resign_txs_count: Some(max_sign_attempts), - with_spinner: true, - rpc_send_transaction_config: config.send_transaction_config, - }, + let tpu_client = if use_rpc { + None + } else { + Some( + tpu_client_fut + .await + .expect("Should return a valid tpu client"), ) - .await - } + }; + send_and_confirm_transactions_in_parallel_v2( + rpc_client.clone(), + tpu_client, + &write_messages, + &[fee_payer_signer, write_signer], + SendAndConfirmConfigV2 { + resign_txs_count: Some(max_sign_attempts), + with_spinner: true, + rpc_send_transaction_config: config.send_transaction_config, + }, + ) + .await } - .map_err(|err| format!("Data writes to account failed: {err}"))? - .into_iter() - .flatten() - .collect::>(); - - if !transaction_errors.is_empty() { - for transaction_error in &transaction_errors { - error!("{transaction_error:?}"); - } - return Err( - format!("{} write transactions failed", transaction_errors.len()).into(), - ); + } + .map_err(|err| format!("Data writes to account failed: {err}"))? + .into_iter() + .flatten() + .collect::>(); + + if !transaction_errors.is_empty() { + for transaction_error in &transaction_errors { + error!("{transaction_error:?}"); } + return Err(format!("{} write transactions failed", transaction_errors.len()).into()); } } - if let Some(mut message) = final_message { - if let Some(final_signers) = final_signers { - trace!("Deploying program"); + if let Some(mut message) = final_message + && let Some(final_signers) = final_signers + { + trace!("Deploying program"); - simulate_and_update_compute_unit_limit(compute_unit_limit, &rpc_client, &mut message) - .await?; - let mut final_tx = Transaction::new_unsigned(message); - let blockhash = rpc_client.get_latest_blockhash().await?; - let mut signers = final_signers.to_vec(); - signers.push(fee_payer_signer); - final_tx.try_sign(&signers, blockhash)?; - return Ok(Some( - rpc_client - .send_and_confirm_transaction_with_spinner_and_config( - &final_tx, - config.commitment, - config.send_transaction_config, - ) - .await - .map_err(|e| format!("Deploying program failed: {e}"))?, - )); - } + simulate_and_update_compute_unit_limit(compute_unit_limit, &rpc_client, &mut message) + .await?; + let mut final_tx = Transaction::new_unsigned(message); + let blockhash = rpc_client.get_latest_blockhash().await?; + let mut signers = final_signers.to_vec(); + signers.push(fee_payer_signer); + final_tx.try_sign(&signers, blockhash)?; + return Ok(Some( + rpc_client + .send_and_confirm_transaction_with_spinner_and_config( + &final_tx, + config.commitment, + config.send_transaction_config, + ) + .await + .map_err(|e| format!("Deploying program failed: {e}"))?, + )); } Ok(None) @@ -3259,15 +3257,15 @@ async fn send_deploy_messages( fn create_ephemeral_keypair() -> Result<(usize, bip39::Mnemonic, Keypair), Box> { const WORDS: usize = 12; - let mnemonic = Mnemonic::new(MnemonicType::for_word_count(WORDS)?, Language::English); - let seed = Seed::new(&mnemonic, ""); - let new_keypair = keypair_from_seed(seed.as_bytes())?; + let mnemonic = Mnemonic::generate_in(Language::English, WORDS)?; + let seed = mnemonic.to_seed(""); + let new_keypair = keypair_from_seed(&seed)?; Ok((WORDS, mnemonic, new_keypair)) } fn report_ephemeral_mnemonic(words: usize, mnemonic: bip39::Mnemonic, ephemeral_pubkey: &Pubkey) { - let phrase: &str = mnemonic.phrase(); + let phrase = mnemonic.to_string(); let divider = String::from_utf8(vec![b'='; phrase.len()]).unwrap(); eprintln!("{divider}\nRecover the intermediate account's ephemeral keypair file with"); eprintln!("`solana-keygen recover` and the following {words}-word seed phrase:"); diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 7c706be3328..ffc0afe42a0 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -55,7 +55,10 @@ use { error::StakeError, instruction::{self as stake_instruction, LockupArgs}, stake_history::StakeHistory, - state::{Authorized, Lockup, Meta, StakeActivationStatus, StakeAuthorize, StakeStateV2}, + state::{ + Authorized, Delegation, Lockup, Meta, StakeActivationStatus, StakeAuthorize, + StakeStateV2, + }, tools::{acceptable_reference_epoch_credits, eligible_for_deactivate_delinquent}, }, solana_system_interface::{error::SystemError, instruction as system_instruction}, @@ -2235,13 +2238,13 @@ pub async fn process_merge_stake( if !sign_only { for stake_account_address in &[stake_account_pubkey, source_stake_account_pubkey] { - if let Ok(stake_account) = rpc_client.get_account(stake_account_address).await { - if stake_account.owner != stake::program::id() { - return Err(CliError::BadParameter(format!( - "Account {stake_account_address} is not a stake account" - )) - .into()); - } + if let Ok(stake_account) = rpc_client.get_account(stake_account_address).await + && stake_account.owner != stake::program::id() + { + return Err(CliError::BadParameter(format!( + "Account {stake_account_address} is not a stake account" + )) + .into()); } } } @@ -2434,6 +2437,29 @@ fn u64_some_if_not_zero(n: u64) -> Option { if n > 0 { Some(n) } else { None } } +fn stake_activation_status( + delegation: &Delegation, + current_epoch: Epoch, + stake_history: &StakeHistory, + new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, +) -> StakeActivationStatus { + if use_fixed_point_stake_math { + delegation.stake_activating_and_deactivating_v2( + current_epoch, + stake_history, + new_rate_activation_epoch, + ) + } else { + #[allow(deprecated)] + delegation.stake_activating_and_deactivating( + current_epoch, + stake_history, + new_rate_activation_epoch, + ) + } +} + pub fn build_stake_state( account_balance: u64, stake_state: &StakeStateV2, @@ -2443,6 +2469,7 @@ pub fn build_stake_state( new_rate_activation_epoch: Option, rent_exempt_reserve: u64, use_csv: bool, + use_fixed_point_stake_math: bool, ) -> CliStakeState { match stake_state { StakeStateV2::Stake( @@ -2460,10 +2487,12 @@ pub fn build_stake_state( effective, activating, deactivating, - } = stake.delegation.stake_activating_and_deactivating( + } = stake_activation_status( + &stake.delegation, current_epoch, stake_history, new_rate_activation_epoch, + use_fixed_point_stake_math, ); let lockup = if lockup.is_in_force(clock, None) { Some(lockup.into()) @@ -2719,9 +2748,10 @@ pub async fn get_account_stake_state( match stake_account.state() { Ok(stake_state) => { let stake_history_account = rpc_client.get_account(&stake_history::id()).await?; - let stake_history = from_account(&stake_history_account).ok_or_else(|| { - CliError::RpcRequestError("Failed to deserialize stake history".to_string()) - })?; + let stake_history: StakeHistory = bincode::deserialize(&stake_history_account.data) + .map_err(|_| { + CliError::RpcRequestError("Failed to deserialize stake history".to_string()) + })?; let clock_account = rpc_client.get_account(&clock::id()).await?; let clock: Clock = from_account(&clock_account).ok_or_else(|| { CliError::RpcRequestError("Failed to deserialize clock sysvar".to_string()) @@ -2731,6 +2761,13 @@ pub async fn get_account_stake_state( &agave_feature_set::reduce_stake_warmup_cooldown::id(), ) .await?; + let fixed_point_activation_epoch = get_feature_activation_epoch( + rpc_client, + &agave_feature_set::upgrade_bpf_stake_program_to_v5_1::id(), + ) + .await?; + let use_fixed_point_stake_math = fixed_point_activation_epoch + .is_some_and(|activation_epoch| clock.epoch >= activation_epoch); let rent_exempt_balance = rpc_client .get_minimum_balance_for_rent_exemption(stake_account.data.len()) .await?; @@ -2743,25 +2780,27 @@ pub async fn get_account_stake_state( new_rate_activation_epoch, rent_exempt_balance, use_csv, + use_fixed_point_stake_math, ); - if state.stake_type == CliStakeType::Stake && state.activation_epoch.is_some() { - if let Some(num_epochs) = with_rewards { - state.epoch_rewards = match fetch_epoch_rewards( - rpc_client, - stake_account_address, - num_epochs, - starting_epoch, - ) - .await - { - Ok(rewards) => Some(rewards), - Err(error) => { - eprintln!("Failed to fetch epoch rewards: {error:?}"); - None - } - }; - } + if state.stake_type == CliStakeType::Stake + && state.activation_epoch.is_some() + && let Some(num_epochs) = with_rewards + { + state.epoch_rewards = match fetch_epoch_rewards( + rpc_client, + stake_account_address, + num_epochs, + starting_epoch, + ) + .await + { + Ok(rewards) => Some(rewards), + Err(error) => { + eprintln!("Failed to fetch epoch rewards: {error:?}"); + None + } + }; } Ok(state) } @@ -2779,7 +2818,7 @@ pub async fn process_show_stake_history( ) -> ProcessResult { let stake_history_account = rpc_client.get_account(&stake_history::id()).await?; let stake_history = - from_account::(&stake_history_account).ok_or_else(|| { + bincode::deserialize::(&stake_history_account.data).map_err(|_| { CliError::RpcRequestError("Failed to deserialize stake history".to_string()) })?; diff --git a/cli/src/vote.rs b/cli/src/vote.rs index b1d2b180fa7..e39e2a4ef92 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -14,7 +14,9 @@ use { spend_utils::{SpendAmount, resolve_spend_tx_and_check_account_balances}, stake::check_current_authority, }, - agave_feature_set::{bls_pubkey_management_in_vote_account, vote_account_initialize_v2}, + agave_feature_set::{ + alpenglow, bls_pubkey_management_in_vote_account, vote_account_initialize_v2, + }, agave_votor_messages::consensus_message::BLS_KEYPAIR_DERIVE_SEED, clap::{App, Arg, ArgMatches, SubCommand, value_t_or_exit}, solana_account::Account, @@ -30,8 +32,8 @@ use { offline::*, }, solana_cli_output::{ - CliEpochVotingHistory, CliLandedVote, CliVoteAccount, ReturnSignersConfig, - display::build_balance_message, return_signers_with_config, + CliVoteAccount, ReturnSignersConfig, VotesObserved, display::build_balance_message, + get_epoch_history, return_signers_with_config, }, solana_commitment_config::CommitmentConfig, solana_feature_gate_interface::from_account, @@ -47,8 +49,8 @@ use { vote_error::VoteError, vote_instruction::{self, CreateVoteAccountConfig, withdraw}, vote_state::{ - VOTE_CREDITS_MAXIMUM_PER_SLOT, VoteAuthorize, VoteInit, VoteInitV2, VoteStateV4, - VoterWithBLSArgs, create_bls_proof_of_possession, + VoteAuthorize, VoteInit, VoteInitV2, VoteStateV4, VoterWithBLSArgs, + create_bls_proof_of_possession, }, }, std::rc::Rc, @@ -1112,17 +1114,14 @@ pub async fn process_create_vote_account( if let Ok(response) = rpc_client .get_account_with_commitment(&vote_account_address, config.commitment) .await + && let Some(vote_account) = response.value { - if let Some(vote_account) = response.value { - let err_msg = if vote_account.owner == solana_vote_program::id() { - format!("Vote account {vote_account_address} already exists") - } else { - format!( - "Account {vote_account_address} already exists and is not a vote account" - ) - }; - return Err(CliError::BadParameter(err_msg).into()); - } + let err_msg = if vote_account.owner == solana_vote_program::id() { + format!("Vote account {vote_account_address} already exists") + } else { + format!("Account {vote_account_address} already exists and is not a vote account") + }; + return Err(CliError::BadParameter(err_msg).into()); } if let Some(nonce_account) = &nonce_account { @@ -1239,14 +1238,14 @@ pub async fn process_vote_authorize( &[current_authorized_voter, vote_state.authorized_withdrawer], &authorized.pubkey(), )?; - if let Some(signer) = new_authorized_signer { - if signer.is_interactive() { - return Err(CliError::BadParameter(format!( - "invalid new authorized vote signer {new_authorized_pubkey:?}. \ - Interactive vote signers not supported" - )) - .into()); - } + if let Some(signer) = new_authorized_signer + && signer.is_interactive() + { + return Err(CliError::BadParameter(format!( + "invalid new authorized vote signer {new_authorized_pubkey:?}. \ + Interactive vote signers not supported" + )) + .into()); } } } @@ -1616,31 +1615,21 @@ pub async fn process_show_vote_account( .and_then(|feature| feature.activated_at); let tvc_activation_epoch = tvc_activation_slot.map(|s| epoch_schedule.get_epoch(s)); - let mut votes: Vec = vec![]; - let mut epoch_voting_history: Vec = vec![]; - if !vote_state.votes.is_empty() { - for vote in &vote_state.votes { - votes.push(vote.into()); - } - for (epoch, credits, prev_credits) in vote_state.epoch_credits.iter().copied() { - let credits_earned = credits.saturating_sub(prev_credits); - let slots_in_epoch = epoch_schedule.get_slots_in_epoch(epoch); - let is_tvc_active = tvc_activation_epoch.map(|e| epoch >= e).unwrap_or_default(); - let max_credits_per_slot = if is_tvc_active { - VOTE_CREDITS_MAXIMUM_PER_SLOT - } else { - 1 - }; - epoch_voting_history.push(CliEpochVotingHistory { - epoch, - slots_in_epoch, - credits_earned, - credits, - prev_credits, - max_credits_per_slot, - }); - } - } + let ag_is_active = get_feature_is_active(rpc_client, &alpenglow::id()) + .await + .unwrap_or(false); + let ag_genesis_cert = if ag_is_active { + rpc_client.get_ag_genesis_cert().await? + } else { + None + }; + let votes_observed = VotesObserved::new(&vote_state, &ag_genesis_cert); + let epoch_voting_history = get_epoch_history( + &epoch_schedule, + &vote_state, + &ag_genesis_cert, + tvc_activation_epoch, + ); let epoch_rewards = if let Some(num_epochs) = with_rewards { match crate::stake::fetch_epoch_rewards( @@ -1673,7 +1662,7 @@ pub async fn process_show_vote_account( .min(u8::MAX as u16) as u8, root_slot: vote_state.root_slot, recent_timestamp: vote_state.last_timestamp.clone(), - votes, + votes_observed, epoch_voting_history, use_lamports_unit, use_csv, @@ -1839,13 +1828,12 @@ pub async fn process_close_vote_account( .into_iter() .chain(vote_account_status.delinquent) .next() + && vote_account.activated_stake != 0 { - if vote_account.activated_stake != 0 { - return Err(format!( - "Cannot close a vote account with active stake: {vote_account_pubkey}" - ) - .into()); - } + return Err(format!( + "Cannot close a vote account with active stake: {vote_account_pubkey}" + ) + .into()); } let latest_blockhash = rpc_client.get_latest_blockhash().await?; diff --git a/cli/tests/program.rs b/cli/tests/program.rs index 3b4a8023654..3117f075569 100644 --- a/cli/tests/program.rs +++ b/cli/tests/program.rs @@ -59,7 +59,7 @@ fn test_validator_genesis( mint_keypair: &Keypair, features: LoaderV3Features, ) -> TestValidatorGenesis { - let mut genesis = TestValidatorGenesis::default(); + let mut genesis = TestValidatorGenesis::default_for_tests(); genesis .fee_rate_governor(FeeRateGovernor::new(0, 0)) .rent(Rent { @@ -3235,7 +3235,7 @@ async fn test_cli_program_deploy_with_args(compute_unit_price: Option, use_ let mint_keypair = Keypair::new(); let faucet_addr = run_local_faucet_with_unique_port_for_tests(mint_keypair.insecure_clone()); - let test_validator = TestValidatorGenesis::default() + let test_validator = TestValidatorGenesis::default_for_tests() .fee_rate_governor(FeeRateGovernor::new(0, 0)) .rent(Rent { lamports_per_byte: 1, diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 5756f64c814..5bf74b5f5eb 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -40,7 +40,7 @@ async fn test_stake_delegation_force() { let authorized_withdrawer = Keypair::new().pubkey(); let faucet_addr = run_local_faucet_with_unique_port_for_tests(mint_keypair.insecure_clone()); let slots_per_epoch = 32; - let test_validator = TestValidatorGenesis::default() + let test_validator = TestValidatorGenesis::default_for_tests() .fee_rate_governor(FeeRateGovernor::new(0, 0)) .rent(Rent { lamports_per_byte: 1, diff --git a/client-test/Cargo.toml b/client-test/Cargo.toml index bb9a16ba12c..7a6f00e602a 100644 --- a/client-test/Cargo.toml +++ b/client-test/Cargo.toml @@ -35,10 +35,8 @@ solana-runtime = { workspace = true } solana-signer = { workspace = true } solana-system-interface = { workspace = true } solana-system-transaction = { workspace = true } -solana-test-validator = { workspace = true } solana-transaction-status = { workspace = true } solana-version = { workspace = true } -systemstat = { workspace = true } tokio = { workspace = true, features = ["full"] } tungstenite = { workspace = true, features = ["rustls-tls-webpki-roots"] } @@ -47,3 +45,7 @@ agave-logger = { path = "../logger", features = ["agave-unstable-api"] } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api"] } solana-rpc = { path = "../rpc", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } +solana-test-validator = { path = "../test-validator", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/client-test/tests/client.rs b/client-test/tests/client.rs index c58940c4dfd..8d0da4dcdc5 100644 --- a/client-test/tests/client.rs +++ b/client-test/tests/client.rs @@ -38,7 +38,7 @@ use { }, std::{ collections::HashSet, - net::{IpAddr, SocketAddr}, + net::{IpAddr, Ipv4Addr, SocketAddr}, sync::{ Arc, RwLock, atomic::{AtomicBool, AtomicU64, Ordering}, @@ -46,7 +46,6 @@ use { thread::sleep, time::{Duration, Instant}, }, - systemstat::Ipv4Addr, tungstenite::{client::IntoClientRequest, connect}, }; diff --git a/client/Cargo.toml b/client/Cargo.toml index e46f8c0ce4b..0ac76cc726d 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -49,3 +51,6 @@ tokio = { workspace = true, features = ["full"] } [dev-dependencies] crossbeam-channel = { workspace = true } + +[lints] +workspace = true diff --git a/client/src/connection_cache.rs b/client/src/connection_cache.rs index b86d794ddf1..75f5200104d 100644 --- a/client/src/connection_cache.rs +++ b/client/src/connection_cache.rs @@ -1,4 +1,4 @@ -pub use solana_connection_cache::connection_cache::Protocol; +pub use solana_connection_cache::connection_cache::{DEFAULT_MAX_CONNECTIONS, Protocol}; use { solana_connection_cache::{ client_connection::ClientConnection, @@ -92,6 +92,25 @@ impl ConnectionCache { client_socket: Option, cert_info: Option<(&Keypair, IpAddr)>, stake_info: Option<(&Arc>, &Pubkey)>, + ) -> Self { + Self::new_with_max_connections( + name, + connection_pool_size, + DEFAULT_MAX_CONNECTIONS, + client_socket, + cert_info, + stake_info, + ) + } + + /// Create a quic connection_cache with configurable max connections + pub fn new_with_max_connections( + name: &'static str, + connection_pool_size: usize, + max_connections: usize, + client_socket: Option, + cert_info: Option<(&Keypair, IpAddr)>, + stake_info: Option<(&Arc>, &Pubkey)>, ) -> Self { // The minimum pool size is 1. let connection_pool_size = 1.max(connection_pool_size); @@ -106,8 +125,13 @@ impl ConnectionCache { config.set_staked_nodes(stake_info.0, stake_info.1); } let connection_manager = QuicConnectionManager::new_with_connection_config(config); - let cache = - BackendConnectionCache::new(name, connection_manager, connection_pool_size).unwrap(); + let cache = BackendConnectionCache::new_with_max_connections( + name, + connection_manager, + connection_pool_size, + max_connections, + ) + .unwrap(); Self::Quic(Arc::new(cache)) } diff --git a/client/src/send_and_confirm_transactions_in_parallel.rs b/client/src/send_and_confirm_transactions_in_parallel.rs index 4615c119f59..b0517dc6838 100644 --- a/client/src/send_and_confirm_transactions_in_parallel.rs +++ b/client/src/send_and_confirm_transactions_in_parallel.rs @@ -209,8 +209,8 @@ async fn send_transaction_with_rpc_fallback( } else { true }; - if send_over_rpc { - if let Err(e) = rpc_client + if send_over_rpc + && let Err(e) = rpc_client .send_transaction_with_config( &transaction, RpcSendTransactionConfig { @@ -219,41 +219,40 @@ async fn send_transaction_with_rpc_fallback( }, ) .await - { - match e.kind() { - ErrorKind::Io(_) | ErrorKind::Reqwest(_) => { - // fall through on io error, we will retry the transaction - } - ErrorKind::TransactionError(TransactionError::BlockhashNotFound) => { - // fall through so that we will resend with another blockhash - } - ErrorKind::TransactionError(transaction_error) => { - // if we get other than blockhash not found error the transaction is invalid - context.error_map.insert(index, transaction_error.clone()); - } - ErrorKind::RpcError(RpcError::RpcResponseError { - data: - RpcResponseErrorData::SendTransactionPreflightFailure( - RpcSimulateTransactionResult { - err: Some(ui_transaction_error), - .. - }, - ), - .. - }) => { - match TransactionError::from(ui_transaction_error.clone()) { - TransactionError::BlockhashNotFound => { - // fall through so that we will resend with another blockhash - } - err => { - // if we get other than blockhash not found error the transaction is invalid - context.error_map.insert(index, err); - } + { + match e.kind() { + ErrorKind::Io(_) | ErrorKind::Reqwest(_) => { + // fall through on io error, we will retry the transaction + } + ErrorKind::TransactionError(TransactionError::BlockhashNotFound) => { + // fall through so that we will resend with another blockhash + } + ErrorKind::TransactionError(transaction_error) => { + // if we get other than blockhash not found error the transaction is invalid + context.error_map.insert(index, transaction_error.clone()); + } + ErrorKind::RpcError(RpcError::RpcResponseError { + data: + RpcResponseErrorData::SendTransactionPreflightFailure( + RpcSimulateTransactionResult { + err: Some(ui_transaction_error), + .. + }, + ), + .. + }) => { + match TransactionError::from(ui_transaction_error.clone()) { + TransactionError::BlockhashNotFound => { + // fall through so that we will resend with another blockhash + } + err => { + // if we get other than blockhash not found error the transaction is invalid + context.error_map.insert(index, err); } } - _ => { - return Err(TpuSenderError::from(e)); - } + } + _ => { + return Err(TpuSenderError::from(e)); } } } diff --git a/compute-budget-instruction/Cargo.toml b/compute-budget-instruction/Cargo.toml index 1a1dade28d6..90436cc8e41 100644 --- a/compute-budget-instruction/Cargo.toml +++ b/compute-budget-instruction/Cargo.toml @@ -7,10 +7,12 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -42,6 +44,7 @@ solana-compute-budget-instruction = { path = ".", features = ["agave-unstable-ap solana-hash = { workspace = true } solana-keypair = { workspace = true } solana-message = { workspace = true } +solana-pubkey = { workspace = true, features = ["rand"] } solana-signer = { workspace = true } solana-stake-interface = { workspace = true } solana-system-interface = { workspace = true } diff --git a/compute-budget/Cargo.toml b/compute-budget/Cargo.toml index c1e2a982605..d3391b8d2e2 100644 --- a/compute-budget/Cargo.toml +++ b/compute-budget/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] conf-stack-frame-size = ["solana-program-runtime/conf-stack-frame-size"] diff --git a/connection-cache/Cargo.toml b/connection-cache/Cargo.toml index 903bf89568b..2cf013b1dc8 100644 --- a/connection-cache/Cargo.toml +++ b/connection-cache/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] @@ -29,3 +34,6 @@ thiserror = { workspace = true } agave-logger = { path = "../logger", features = ["agave-unstable-api"] } rand_chacha = { workspace = true } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api"] } + +[lints] +workspace = true diff --git a/connection-cache/src/connection_cache.rs b/connection-cache/src/connection_cache.rs index 74dc32e7c8f..c2f003ef6cd 100644 --- a/connection-cache/src/connection_cache.rs +++ b/connection-cache/src/connection_cache.rs @@ -19,8 +19,8 @@ use { thiserror::Error, }; -// Should be non-zero -const MAX_CONNECTIONS: usize = 1024; +/// Default maximum number of connections to keep +pub const DEFAULT_MAX_CONNECTIONS: usize = 1024; /// Default connection pool size per remote address pub const DEFAULT_CONNECTION_POOL_SIZE: usize = 2; @@ -53,6 +53,7 @@ pub struct ConnectionCache< stats: Arc, last_stats: AtomicInterval, connection_pool_size: usize, + max_connections: usize, connection_config: Arc, sender: Sender<(usize, SocketAddr)>, } @@ -68,26 +69,29 @@ where connection_manager: M, connection_pool_size: usize, ) -> Result { - let config = connection_manager.new_connection_config(); - Ok(Self::new_with_config( + Self::new_with_max_connections( name, - connection_pool_size, - config, connection_manager, - )) + connection_pool_size, + DEFAULT_MAX_CONNECTIONS, + ) } - pub fn new_with_config( + pub fn new_with_max_connections( name: &'static str, - connection_pool_size: usize, - connection_config: C, connection_manager: M, - ) -> Self { - info!("Creating ConnectionCache {name}, pool size: {connection_pool_size}"); + connection_pool_size: usize, + max_connections: usize, + ) -> Result { + let config = Arc::new(connection_manager.new_connection_config()); + let max_connections = 1.max(max_connections); // The minimum is 1. + info!( + "Creating ConnectionCache {name}, pool size: {connection_pool_size}, max connections: \ + {max_connections}" + ); let (sender, receiver) = crossbeam_channel::unbounded(); - let map = Arc::new(RwLock::new(IndexMap::with_capacity(MAX_CONNECTIONS))); - let config = Arc::new(connection_config); + let map = Arc::new(RwLock::new(IndexMap::with_capacity(max_connections))); let connection_manager = Arc::new(connection_manager); let connection_pool_size = 1.max(connection_pool_size); // The minimum pool size is 1. @@ -95,16 +99,17 @@ where let _async_connection_thread = Self::create_connection_async_thread(map.clone(), receiver, stats.clone()); - Self { + Ok(Self { name, map, stats, connection_manager, last_stats: AtomicInterval::default(), connection_pool_size, + max_connections, connection_config: config, sender, - } + }) } /// This actually triggers the connection creation by sending empty data @@ -174,6 +179,7 @@ where &mut map, addr, self.connection_pool_size, + self.max_connections, None, ) } else { @@ -189,6 +195,7 @@ where &mut map, addr, self.connection_pool_size, + self.max_connections, Some(&self.sender), ); } @@ -211,6 +218,7 @@ where map: &mut std::sync::RwLockWriteGuard<'_, IndexMap>, addr: &SocketAddr, connection_pool_size: usize, + max_connections: usize, async_connection_sender: Option<&Sender<(usize, SocketAddr)>>, ) -> (bool, u64, u64) { // evict a connection if the cache is reaching upper bounds @@ -218,13 +226,13 @@ where let mut get_connection_cache_eviction_measure = Measure::start("get_connection_cache_eviction_measure"); let existing_index = map.get_index_of(addr); - while map.len() >= MAX_CONNECTIONS { + while map.len() >= max_connections { let mut rng = rng(); - let n = rng.random_range(0..MAX_CONNECTIONS); - if let Some(index) = existing_index { - if n == index { - continue; - } + let n = rng.random_range(0..max_connections); + if let Some(index) = existing_index + && n == index + { + continue; } map.swap_remove_index(n); num_evictions += 1; @@ -304,6 +312,7 @@ where &mut map, addr, self.connection_pool_size, + self.max_connections, Some(&self.sender), ); } @@ -709,7 +718,7 @@ mod tests { DEFAULT_CONNECTION_POOL_SIZE, ) .unwrap(); - let addrs = (0..MAX_CONNECTIONS) + let addrs = (0..DEFAULT_MAX_CONNECTIONS) .map(|_| { let addr = get_addr(&mut rng); connection_cache.get_connection(&addr); @@ -718,7 +727,7 @@ mod tests { .collect::>(); { let map = connection_cache.map.read().unwrap(); - assert!(map.len() == MAX_CONNECTIONS); + assert!(map.len() == DEFAULT_MAX_CONNECTIONS); addrs.iter().for_each(|addr| { let conn = &map.get(addr).expect("Address not found").get(0).unwrap(); let conn = conn.new_blocking_connection(*addr, connection_cache.stats.clone()); @@ -739,7 +748,7 @@ mod tests { let port = addr.port(); let addr_with_quic_port = SocketAddr::new(addr.ip(), port); let map = connection_cache.map.read().unwrap(); - assert!(map.len() == MAX_CONNECTIONS); + assert!(map.len() == DEFAULT_MAX_CONNECTIONS); let _conn = map.get(&addr_with_quic_port).expect("Address not found"); } diff --git a/core/Cargo.toml b/core/Cargo.toml index a18977b66d8..eb12f153589 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -20,6 +22,7 @@ dev-context-only-utils = [ "solana-perf/dev-context-only-utils", "solana-runtime/dev-context-only-utils", "solana-streamer/dev-context-only-utils", + "solana-tpu-client-next/dev-context-only-utils", "agave-votor/dev-context-only-utils", ] frozen-abi = [ @@ -29,9 +32,11 @@ frozen-abi = [ "solana-cost-model/frozen-abi", "solana-frozen-abi/frozen-abi", "solana-gossip/frozen-abi", + "solana-hash/frozen-abi", "solana-ledger/frozen-abi", "solana-packet/frozen-abi", "solana-perf/frozen-abi", + "solana-pubkey/frozen-abi", "solana-runtime/frozen-abi", "solana-signature/frozen-abi", "solana-svm/frozen-abi", @@ -42,6 +47,7 @@ frozen-abi = [ [dependencies] agave-banking-stage-ingress-types = { workspace = true } agave-bls-cert-verify = { workspace = true } +agave-bls-sigverify = { workspace = true } agave-feature-set = { workspace = true } agave-fs = { workspace = true } agave-logger = { workspace = true } @@ -60,6 +66,7 @@ arrayvec = { workspace = true } assert_matches = { workspace = true } async-trait = { workspace = true } bincode = { workspace = true } +bitvec = { workspace = true } bytes = { workspace = true } chrono = { workspace = true, features = ["default", "serde"] } crossbeam-channel = { workspace = true } @@ -85,6 +92,7 @@ serde_bytes = { workspace = true } shaq = { workspace = true } signal-hook = { workspace = true } slab = { workspace = true } +smallvec = { workspace = true } solana-account = { workspace = true } solana-accounts-db = { workspace = true } solana-address-lookup-table-interface = { workspace = true } @@ -139,6 +147,7 @@ solana-sha256-hasher = { workspace = true } solana-shred-version = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } +solana-signer-store = { workspace = true } solana-slot-hashes = { workspace = true } solana-slot-history = { workspace = true } solana-streamer = { workspace = true } @@ -221,10 +230,6 @@ jemallocator = { workspace = true } name = "sigverify_stage" harness = false -[[bench]] -name = "bls_vote_sigverify" -harness = false - [[bench]] name = "shredder" harness = false diff --git a/core/benches/bls_vote_sigverify.rs b/core/benches/bls_vote_sigverify.rs deleted file mode 100644 index 4c9dc565a6a..00000000000 --- a/core/benches/bls_vote_sigverify.rs +++ /dev/null @@ -1,228 +0,0 @@ -/* - To run this benchmark: - `cargo bench --bench bls_vote_sigverify` -*/ - -use { - agave_votor_messages::{consensus_message::VoteMessage, vote::Vote}, - criterion::{BatchSize, Criterion, criterion_group, criterion_main}, - rayon::{ThreadPool, ThreadPoolBuilder}, - solana_bls_signatures::{Keypair as BLSKeypair, PreparedHashedMessage, VerifySignature}, - solana_core::bls_sigverify::{ - bls_vote_sigverify::{ - VotePayload, aggregate_pubkeys_by_payload, aggregate_signatures, - verify_individual_votes, verify_votes_optimistic, - }, - stats::SigVerifyVoteStats, - }, - solana_hash::Hash, - solana_keypair::Keypair, - solana_signer::Signer, - std::{hint::black_box, sync::Arc}, -}; - -static MESSAGE_COUNTS: &[usize] = &[1, 2, 4, 8, 16]; -static BATCH_SIZES: &[usize] = &[8, 16, 32, 64, 128]; - -fn get_thread_pool() -> ThreadPool { - let num_threads = 4; - ThreadPoolBuilder::new() - .num_threads(num_threads) - .build() - .unwrap() -} - -fn get_matrix_params() -> impl Iterator { - BATCH_SIZES.iter().flat_map(|&batch_size| { - MESSAGE_COUNTS.iter().filter_map(move |&num_distinct| { - if num_distinct > batch_size { - None - } else { - Some((batch_size, num_distinct)) - } - }) - }) -} - -fn generate_test_data(num_distinct_messages: usize, batch_size: usize) -> Vec { - assert!( - batch_size >= num_distinct_messages, - "Batch size must be >= distinct messages" - ); - - // Pre-calculate the payloads to ensure exact distinctness - let base_payloads: Vec>> = (0..num_distinct_messages) - .map(|i| { - let slot = (i as u64).saturating_add(100); - let vote = Vote::new_notarization_vote(slot, Hash::new_unique()); - Arc::new(bincode::serialize(&vote).unwrap()) - }) - .collect(); - - let mut votes_to_verify = Vec::with_capacity(batch_size); - - for i in 0..batch_size { - let payload = &base_payloads[i.rem_euclid(num_distinct_messages)]; - - let bls_keypair = BLSKeypair::new(); - let vote: Vote = bincode::deserialize(payload).unwrap(); - - let signature = bls_keypair.sign(payload); - - let vote_message = VoteMessage { - vote, - signature: signature.into(), - rank: 0, - }; - - votes_to_verify.push(VotePayload { - vote_message, - bls_pubkey: bls_keypair.public, - pubkey: Keypair::new().pubkey(), - remote_pubkey: Keypair::new().pubkey(), - prepared_payload: None, - }); - } - - votes_to_verify -} - -// Single Signature Verification -// This is just for reference -fn bench_verify_single_signature(c: &mut Criterion) { - let mut group = c.benchmark_group("verify_single_signature"); - - let keypair = BLSKeypair::new(); - let msg = b"benchmark_message_payload"; - let sig = keypair.sign(msg); - let pubkey = keypair.public; - - group.bench_function("1_item", |b| { - b.iter(|| { - // We use the raw verify method from the underlying library - // to establish the cryptographic floor. - let res = pubkey.verify_signature(black_box(&sig), black_box(msg)); - black_box(res).unwrap(); - }) - }); - group.finish(); -} - -fn bench_verify_single_signature_with_prepared_message(c: &mut Criterion) { - let mut group = c.benchmark_group("verify_single_signature_with_prepared_message"); - - let keypair = BLSKeypair::new(); - let msg = b"benchmark_message_payload"; - let sig = keypair.sign(msg); - let pubkey = keypair.public; - let prepared_msg = PreparedHashedMessage::new(msg); - - group.bench_function("1_item", |b| { - b.iter(|| { - let res = pubkey.verify_signature_prepared(black_box(&sig), black_box(&prepared_msg)); - black_box(res).unwrap(); - }) - }); - group.finish(); -} - -// Optimistic Verification - aggregates the public keys and signatures first before verifying. -// Depends on both batch size and message distinctness due to pairing checks. -fn bench_verify_votes_optimistic(c: &mut Criterion) { - let mut group = c.benchmark_group("verify_votes_optimistic"); - let mut stats = SigVerifyVoteStats::default(); - let thread_pool = get_thread_pool(); - - for (batch_size, num_distinct) in get_matrix_params() { - let votes = generate_test_data(num_distinct, batch_size); - let label = format!("msgs_{num_distinct}/batch_{batch_size}"); - - group.bench_function(&label, |b| { - b.iter(|| { - let res = verify_votes_optimistic(black_box(&votes), &mut stats, &thread_pool); - black_box(res); - }) - }); - } - group.finish(); - black_box(stats); -} - -// Public Key Aggregation -// Depends on message distinctness because keys are grouped by messages. -fn bench_aggregate_pubkeys(c: &mut Criterion) { - let mut group = c.benchmark_group("aggregate_pubkeys"); - let mut stats = SigVerifyVoteStats::default(); - - for (batch_size, num_distinct) in get_matrix_params() { - let votes = generate_test_data(num_distinct, batch_size); - let label = format!("msgs_{num_distinct}/batch_{batch_size}"); - - group.bench_function(&label, |b| { - b.iter(|| { - let res = aggregate_pubkeys_by_payload(black_box(&votes), &mut stats); - black_box(res).2.unwrap(); - }) - }); - } - group.finish(); - black_box(stats); -} - -// Signature Aggregation -// Pure G1 addition - message distinctness is irrelevant. -fn bench_aggregate_signatures(c: &mut Criterion) { - let mut group = c.benchmark_group("aggregate_signatures"); - - for &batch_size in BATCH_SIZES { - // Use 1 distinct message just to generate valid data cheaply. - // It doesn't affect signature aggregation performance. - let votes = generate_test_data(1, batch_size); - let label = format!("batch_{batch_size}"); - - group.bench_function(&label, |b| { - b.iter(|| { - let res = aggregate_signatures(black_box(&votes)); - black_box(res).unwrap(); - }) - }); - } - group.finish(); -} - -// Individual Verification - verifies each signatures in parallel threads -// Message distinctness is irrelevant. -fn bench_verify_individual_votes(c: &mut Criterion) { - let mut group = c.benchmark_group("verify_votes_fallback"); - let thread_pool = get_thread_pool(); - - for &batch_size in BATCH_SIZES { - // Distinctness doesn't affect the cost of N individual verifications. - let votes = generate_test_data(1, batch_size); - let label = format!("batch_{batch_size}"); - - group.bench_function(&label, |b| { - b.iter_batched( - || votes.clone(), - |votes| { - let res = - verify_individual_votes(black_box(votes), vec![], vec![], &thread_pool); - black_box(res); - }, - BatchSize::SmallInput, - ) - }); - } - group.finish(); -} - -criterion_group!( - benches, - bench_verify_single_signature, - bench_verify_single_signature_with_prepared_message, - bench_verify_votes_optimistic, - bench_aggregate_pubkeys, - bench_aggregate_signatures, - bench_verify_individual_votes -); -criterion_main!(benches); diff --git a/core/benches/sigverify_stage.rs b/core/benches/sigverify_stage.rs index 76e9e41d2f0..7c9075d0b0c 100644 --- a/core/benches/sigverify_stage.rs +++ b/core/benches/sigverify_stage.rs @@ -2,7 +2,7 @@ use { bencher::{Bencher, TDynBenchFn, TestDesc, TestDescAndFn, TestFn, benchmark_main}, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, log::*, rand::{ Rng, @@ -74,11 +74,11 @@ fn bench_sigverify_stage(bencher: &mut Bencher, use_same_tx: bool) { let (_bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&create_genesis_config(1).genesis_config); let sharable_banks = bank_forks.read().unwrap().sharable_banks(); - let (packet_s, packet_r) = unbounded(); - let (vote_packet_s, vote_packet_r) = unbounded(); + let (packet_s, packet_r) = bounded(1024); + let (vote_packet_s, vote_packet_r) = bounded(1024); let (verified_s, verified_r) = BankingTracer::channel_for_test(); let (tpu_vote_s, _tpu_vote_r) = BankingTracer::channel_for_test(); - let (forward_stage_s, _forward_stage_r) = unbounded(); + let (forward_stage_s, _forward_stage_r) = bounded(1024); let (stage, gossip_sigverify_handle) = SigVerifyStage::new( packet_r, vote_packet_r, @@ -88,6 +88,7 @@ fn bench_sigverify_stage(bencher: &mut Bencher, use_same_tx: bool) { NonZeroUsize::new(sigverify::threadpool_for_benches().current_num_threads()).unwrap(), false, sharable_banks, + None, ); let packet_s = packet_s; let packet_s_for_bench = packet_s.clone(); diff --git a/core/src/banking_simulation.rs b/core/src/banking_simulation.rs index d1321c0442e..dd6aa5a3c02 100644 --- a/core/src/banking_simulation.rs +++ b/core/src/banking_simulation.rs @@ -12,7 +12,7 @@ use { }, validator::BlockProductionMethod, }, - agave_banking_stage_ingress_types::BankingPacketBatch, + agave_banking_stage_ingress_types::{BankingPacketBatch, SchedulerPriorityFloor}, agave_votor_messages::migration::MigrationStatus, assert_matches::assert_matches, bincode::deserialize_from, @@ -264,18 +264,16 @@ struct SimulatorLoopLogger { } impl SimulatorLoopLogger { - fn bank_costs(bank: &Bank) -> (u64, u64) { - bank.read_cost_tracker() - .map(|t| (t.block_cost(), t.vote_cost())) - .unwrap() + fn bank_cost(bank: &Bank) -> u64 { + bank.read_cost_tracker().map(|t| t.block_cost()).unwrap() } fn log_frozen_bank_cost(&self, bank: &Bank, bank_elapsed: Duration) { info!( - "simulated bank slot+delta: {}+{}ms costs: {:?} fees: {:?} txs: {} (frozen)", + "simulated bank slot+delta: {}+{}ms cost: {} fees: {:?} txs: {} (frozen)", bank.slot(), bank_elapsed.as_millis(), - Self::bank_costs(bank), + Self::bank_cost(bank), bank.get_collector_fee_details(), bank.executed_transaction_count(), ); @@ -283,10 +281,10 @@ impl SimulatorLoopLogger { fn log_ongoing_bank_cost(&self, bank: &Bank, bank_elapsed: Duration) { info!( - "simulated bank slot+delta: {}+{}ms costs: {:?} fees: {:?} txs: {} (ongoing)", + "simulated bank slot+delta: {}+{}ms cost: {} fees: {:?} txs: {} (ongoing)", bank.slot(), bank_elapsed.as_millis(), - Self::bank_costs(bank), + Self::bank_cost(bank), bank.get_collector_fee_details(), bank.executed_transaction_count(), ); @@ -294,26 +292,26 @@ impl SimulatorLoopLogger { fn log_jitter(&self, bank: &Bank) { let old_slot = bank.slot(); - if let Some(event_time) = self.freeze_time_by_slot.get(&old_slot) { - if log_enabled!(log::Level::Info) { - let current_simulation_time = SystemTime::now(); - let elapsed_simulation_time = current_simulation_time - .duration_since(self.base_simulation_time) - .unwrap(); - let elapsed_event_time = event_time.duration_since(self.base_event_time).unwrap(); - info!( - "jitter(parent_slot: {}): {}{:?} (sim: {:?} event: {:?})", - old_slot, - if elapsed_simulation_time > elapsed_event_time { - "+" - } else { - "-" - }, - elapsed_simulation_time.abs_diff(elapsed_event_time), - elapsed_simulation_time, - elapsed_event_time, - ); - } + if let Some(event_time) = self.freeze_time_by_slot.get(&old_slot) + && log_enabled!(log::Level::Info) + { + let current_simulation_time = SystemTime::now(); + let elapsed_simulation_time = current_simulation_time + .duration_since(self.base_simulation_time) + .unwrap(); + let elapsed_event_time = event_time.duration_since(self.base_event_time).unwrap(); + info!( + "jitter(parent_slot: {}): {}{:?} (sim: {:?} event: {:?})", + old_slot, + if elapsed_simulation_time > elapsed_event_time { + "+" + } else { + "-" + }, + elapsed_simulation_time.abs_diff(elapsed_event_time), + elapsed_simulation_time, + elapsed_event_time, + ); } } @@ -855,6 +853,7 @@ impl BankingSimulator { bank_forks.clone(), None, Arc::default(), + Arc::new(SchedulerPriorityFloor::default()), ); let (&_slot, &raw_base_event_time) = freeze_time_by_slot diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index dcc74b7247b..c649b9a2890 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -18,8 +18,8 @@ use { }, validator::BlockProductionMethod, }, - agave_banking_stage_ingress_types::BankingPacketReceiver, - crossbeam_channel::{Receiver, Sender, unbounded}, + agave_banking_stage_ingress_types::{BankingPacketReceiver, SchedulerPriorityFloor}, + crossbeam_channel::{Receiver, Sender, bounded}, futures::{StreamExt, stream::FuturesUnordered}, histogram::Histogram, solana_gossip::{cluster_info::ClusterInfo, contact_info::ContactInfoQuery}, @@ -336,6 +336,7 @@ pub struct BankingStage { committer: Committer, log_messages_bytes_limit: Option, filter_keys: Arc>, + priority_floor: Arc, threads: FuturesUnordered>>, } @@ -357,6 +358,7 @@ impl BankingStage { bank_forks: Arc>, prioritization_fee_cache: Option>, filter_keys: Arc>, + priority_floor: Arc, ) -> BankingStageHandle { let committer = Committer::new( transaction_status_sender, @@ -379,6 +381,7 @@ impl BankingStage { committer, log_messages_bytes_limit, filter_keys, + priority_floor, threads: FuturesUnordered::default(), }; @@ -515,9 +518,12 @@ impl BankingStage { threads.push(self.spawn_vote_worker()); // Create channels for communication between scheduler and workers + const CHANNEL_CAPACITY: usize = 10_000; // unlikely we'll ever hit this given default configuration. + let (work_senders, work_receivers): (Vec>, Vec>) = - (0..num_workers).map(|_| unbounded()).unzip(); - let (finished_work_sender, finished_work_receiver) = unbounded(); + (0..num_workers).map(|_| bounded(CHANNEL_CAPACITY)).unzip(); + let (finished_work_sender, finished_work_receiver) = + bounded(num_workers.saturating_mul(CHANNEL_CAPACITY)); // Spawn the worker threads let decision_maker = DecisionMaker::from(self.poh_recorder.read().unwrap().deref()); @@ -556,6 +562,7 @@ impl BankingStage { finished_work_receiver, GreedySchedulerConfig::default(), ); + let priority_floor = self.priority_floor.clone(); let exit = exit.clone(); let shutdown_signal = self.banking_shutdown_signal.clone(); threads.push( @@ -570,6 +577,7 @@ impl BankingStage { sharable_banks, scheduler, worker_metrics, + priority_floor, ); match scheduler_controller.run() { @@ -825,7 +833,7 @@ mod tests { validator::SchedulerPacing, }, agave_banking_stage_ingress_types::BankingPacketBatch, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, itertools::Itertools, solana_entry::{ entry::{self, EntrySlice}, @@ -891,7 +899,7 @@ mod tests { poh_service, _entry_receiever, ) = create_test_recorder(bank, blockstore, None, None); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let banking_stage = BankingStage::new_num_threads( BlockProductionMethod::CentralSchedulerGreedy, @@ -911,6 +919,7 @@ mod tests { bank_forks, None, Arc::default(), + Arc::new(SchedulerPriorityFloor::new()), ); drop(non_vote_sender); drop(tpu_vote_sender); @@ -952,7 +961,7 @@ mod tests { poh_service, entry_receiver, ) = create_test_recorder(bank, blockstore, None, None); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let banking_stage = BankingStage::new_num_threads( BlockProductionMethod::CentralSchedulerGreedy, @@ -972,6 +981,7 @@ mod tests { bank_forks, // keep a local-copy of bank-forks so worker threads do not lose weak access to bank-forks None, Arc::default(), + Arc::new(SchedulerPriorityFloor::new()), ); // good tx, and no verify @@ -1097,7 +1107,7 @@ mod tests { .expect("Expected to be able to open database ledger"), ); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let entry_receiver = { // start a banking_stage to eat verified receiver let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); @@ -1127,6 +1137,7 @@ mod tests { bank_forks, None, Arc::default(), + Arc::new(SchedulerPriorityFloor::new()), ); // wait for banking_stage to eat the packets @@ -1261,7 +1272,7 @@ mod tests { poh_service, _entry_receiver, ) = create_test_recorder(bank.clone(), blockstore, None, None); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let banking_stage = BankingStage::new_num_threads( BlockProductionMethod::CentralSchedulerGreedy, @@ -1281,6 +1292,7 @@ mod tests { bank_forks, None, Arc::default(), + Arc::new(SchedulerPriorityFloor::new()), ); let keypairs = (0..100).map(|_| Keypair::new()).collect_vec(); @@ -1351,7 +1363,9 @@ mod tests { }) .unwrap() }) - .for_each(|handle| handle.join().unwrap()); + .for_each(|handle| { + handle.join().unwrap(); + }); banking_stage.join().unwrap(); exit.store(true, Ordering::Relaxed); diff --git a/core/src/banking_stage/consume_worker.rs b/core/src/banking_stage/consume_worker.rs index 0a018b93a65..1508c28fc3b 100644 --- a/core/src/banking_stage/consume_worker.rs +++ b/core/src/banking_stage/consume_worker.rs @@ -203,8 +203,10 @@ pub(crate) mod external { }, agave_transaction_view::{ resolved_transaction_view::ResolvedTransactionView, result::TransactionViewError, - transaction_data::TransactionData, transaction_view::SanitizedTransactionView, + sanitize::SanitizeConfig, transaction_data::TransactionData, + transaction_view::SanitizedTransactionView, }, + arrayvec::ArrayVec, solana_account::ReadableAccount, solana_clock::Slot, solana_cost_model::cost_model::CostModel, @@ -214,7 +216,9 @@ pub(crate) mod external { bank::Bank, bank_forks::{BankPair, SharableBanks}, }, - solana_runtime_transaction::runtime_transaction::RuntimeTransaction, + solana_runtime_transaction::{ + runtime_transaction::RuntimeTransaction, sanitize_config::sanitize_config, + }, solana_transaction::TransactionError, std::ptr::NonNull, }; @@ -468,6 +472,10 @@ pub(crate) mod external { root_bank, working_bank, } = self.sharable_banks.load(); + // Prefer the leader bank over the highest working fork when leader + let working_bank = active_leader_state(&self.shared_leader_state) + .and_then(|leader_state| leader_state.working_bank().cloned()) + .unwrap_or(working_bank); if working_bank.slot() > message.max_working_slot { return self.return_unprocessed_message( @@ -771,20 +779,17 @@ pub(crate) mod external { bank: &Bank, responses_ptr: NonNull, ) -> ( - Vec>, - Vec, + ArrayVec, MAX_TRANSACTIONS_PER_MESSAGE>, + ArrayVec, &'a mut [CheckResponse], ) { - let enable_instruction_accounts_limit = - bank.feature_set.snapshot().limit_instruction_accounts; - let mut parsing_results = Vec::with_capacity(MAX_TRANSACTIONS_PER_MESSAGE); - let mut parsed_transactions = Vec::with_capacity(MAX_TRANSACTIONS_PER_MESSAGE); + let sanitize_config = + sanitize_config(bank.feature_set.snapshot().limit_instruction_accounts); + let mut parsing_results = ArrayVec::new(); + let mut parsed_transactions = ArrayVec::new(); for (tx_ptr, _) in batch.iter() { // Parsing and basic sanitization checks - match SanitizedTransactionView::try_new_sanitized( - tx_ptr, - enable_instruction_accounts_limit, - ) { + match SanitizedTransactionView::try_new_sanitized(tx_ptr, &sanitize_config) { Ok(view) => { parsing_results.push(Ok(())); parsed_transactions.push(view); @@ -926,6 +931,7 @@ pub(crate) mod external { &[const { Ok(()) }; MAX_TRANSACTIONS_PER_MESSAGE], working_bank.max_processing_age(), true, + true, &mut error_counters, ); let included_slots = included_slots.expect("requested to collect processed slots"); @@ -966,20 +972,24 @@ pub(crate) mod external { fn translate_transaction_batch( batch: &TransactionPtrBatch, bank: &Bank, - ) -> (Vec>, Vec, Vec) { - let enable_instruction_accounts_limit = - bank.feature_set.snapshot().limit_instruction_accounts; + ) -> ( + ArrayVec, MAX_TRANSACTIONS_PER_MESSAGE>, + ArrayVec, + ArrayVec, + ) { + let sanitize_config = + sanitize_config(bank.feature_set.snapshot().limit_instruction_accounts); let transaction_account_lock_limit = bank.get_transaction_account_lock_limit(); - let mut translation_results = Vec::with_capacity(MAX_TRANSACTIONS_PER_MESSAGE); - let mut transactions = Vec::with_capacity(MAX_TRANSACTIONS_PER_MESSAGE); - let mut max_ages = Vec::with_capacity(MAX_TRANSACTIONS_PER_MESSAGE); + let mut translation_results = ArrayVec::new(); + let mut transactions = ArrayVec::new(); + let mut max_ages = ArrayVec::new(); for (transaction_ptr, _) in batch.iter() { match Self::translate_transaction( transaction_ptr, bank, transaction_account_lock_limit, - enable_instruction_accounts_limit, + &sanitize_config, ) { Ok((tx, max_age)) => { transactions.push(tx); @@ -997,13 +1007,13 @@ pub(crate) mod external { transaction_ptr: TransactionPtr, bank: &Bank, transaction_account_lock_limit: usize, - enable_instruction_accounts_limit: bool, + sanitize_config: &SanitizeConfig, ) -> Result<(Tx, MaxAge), PacketHandlingError> { translate_to_runtime_view( transaction_ptr, bank, transaction_account_lock_limit, - enable_instruction_accounts_limit, + sanitize_config, ) .map(|(view, deactivation_slot)| { ( @@ -1112,7 +1122,7 @@ pub(crate) mod external { handshake::{ClientLogon, client, server::Server}, responses_region::{CheckResponsesPtr, ExecutionResponsesPtr}, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_account::AccountSharedData, solana_genesis_config::GenesisConfig, solana_keypair::Keypair, @@ -1144,7 +1154,7 @@ pub(crate) mod external { mint_keypair: Keypair, genesis_config: GenesisConfig, bank: Arc, - _bank_forks: Arc>, + bank_forks: Arc>, _replay_vote_receiver: ReplayVoteReceiver, record_receiver: RecordReceiver, allocator: rts_alloc::Allocator, @@ -1329,7 +1339,7 @@ pub(crate) mod external { let (record_sender, record_receiver) = record_channels(false); let recorder = TransactionRecorder::new(record_sender); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); let committer = Committer::new(None, replay_vote_sender, None); let consumer = Consumer::new(committer, recorder, None); let shared_leader_state = SharedLeaderState::new(0, None, None); @@ -1349,7 +1359,7 @@ pub(crate) mod external { mint_keypair, genesis_config, bank, - _bank_forks: bank_forks, + bank_forks, _replay_vote_receiver: replay_vote_receiver, record_receiver, allocator: client_session.allocators.pop().unwrap(), @@ -1437,7 +1447,7 @@ pub(crate) mod external { &simple_tx[..], &bank, bank.get_transaction_account_lock_limit(), - true, + &sanitize_config(true), ) .ok() .unwrap() @@ -1625,8 +1635,10 @@ pub(crate) mod external { let parsing_results = [Ok(()), Err(TransactionViewError::ParseError), Ok(())]; let parsed_transactions = [ - SanitizedTransactionView::try_new_sanitized(&tx1[..], true).unwrap(), - SanitizedTransactionView::try_new_sanitized(&tx2[..], true).unwrap(), + SanitizedTransactionView::try_new_sanitized(&tx1[..], &sanitize_config(true)) + .unwrap(), + SanitizedTransactionView::try_new_sanitized(&tx2[..], &sanitize_config(true)) + .unwrap(), ]; bank.store_account( &parsed_transactions[1].static_account_keys()[0], @@ -1676,7 +1688,8 @@ pub(crate) mod external { ) -> RuntimeTransaction> { RuntimeTransaction::>::try_new( RuntimeTransaction::>::try_new( - SanitizedTransactionView::try_new_sanitized(tx, true).unwrap(), + SanitizedTransactionView::try_new_sanitized(tx, &sanitize_config(true)) + .unwrap(), solana_transaction::sanitized::MessageHash::Compute, Some(false), ) @@ -1980,6 +1993,37 @@ pub(crate) mod external { test_frame.free_batch(batch); } + #[test] + fn test_run_check_prefers_active_leader_bank() { + let mut test_frame = setup_external_test_frame(); + let batch = test_frame.allocate_batch(&[test_serialized_transaction( + test_frame.bank.confirmed_last_blockhash(), + )]); + + // Insert a higher fork bank so the highest working bank exceeds + // `max_working_slot` while the active leader bank does not. + let higher_bank = Bank::new_from_parent( + test_frame.bank.clone(), + SlotLeader::new_unique(), + test_frame.bank.slot() + 1, + ); + test_frame.bank_forks.write().unwrap().insert(higher_bank); + test_frame.set_active_bank(); + + test_frame.send_message(PackToWorkerMessage { + flags: pack_message_flags::CHECK | check_flags::STATUS_CHECKS, + max_working_slot: test_frame.bank.slot(), + batch: batch.region, + }); + test_frame.iterate().unwrap(); + let response = test_frame.recv_response(); + assert_eq!(response.processed_code, processed_codes::PROCESSED); + let responses = test_frame.check_responses(&response.responses); + assert_eq!(responses.len(), 1); + + test_frame.free_batch(batch); + } + #[test] fn test_run_check_resolve_without_loaded_addresses() { let mut test_frame = setup_external_test_frame(); @@ -2782,7 +2826,7 @@ mod tests { scheduler_messages::{MaxAge, TransactionBatchId}, tests::{create_slow_genesis_config, sanitize_transactions}, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_clock::Slot, solana_genesis_config::GenesisConfig, solana_keypair::Keypair, @@ -2852,13 +2896,13 @@ mod tests { let (record_sender, record_receiver) = record_channels(false); let recorder = TransactionRecorder::new(record_sender); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); let committer = Committer::new(None, replay_vote_sender, None); let consumer = Consumer::new(committer, recorder, None); let shared_leader_state = SharedLeaderState::new(0, None, None); - let (consume_sender, consume_receiver) = unbounded(); - let (consumed_sender, consumed_receiver) = unbounded(); + let (consume_sender, consume_receiver) = bounded(1024); + let (consumed_sender, consumed_receiver) = bounded(1024); let worker = ConsumeWorker::new( 0, Arc::new(AtomicBool::new(false)), @@ -3310,6 +3354,7 @@ mod tests { &sanitized_txs, &vec![Ok(()); sanitized_txs.len()], bank.max_processing_age(), + true, &mut TransactionErrorMetrics::default(), ) .into_iter() diff --git a/core/src/banking_stage/consumer.rs b/core/src/banking_stage/consumer.rs index fdb814f9734..a0c275a907f 100644 --- a/core/src/banking_stage/consumer.rs +++ b/core/src/banking_stage/consumer.rs @@ -5,7 +5,7 @@ use { qos_service::QosService, scheduler_messages::MaxAge, }, - itertools::Itertools, + smallvec::SmallVec, solana_fee::FeeFeatures, solana_measure::measure_us, solana_poh::{ @@ -128,14 +128,16 @@ impl Consumer { txs: &[impl TransactionWithMeta], ) -> ProcessTransactionBatchOutput { let mut error_counters = TransactionErrorMetrics::default(); - let pre_results = vec![Ok(()); txs.len()]; + let pre_results = + SmallVec::<[_; TARGET_NUM_TRANSACTIONS_PER_BATCH]>::from_elem(Ok(()), txs.len()); let check_results = bank.check_transactions( txs, &pre_results, bank.max_processing_age(), + true, &mut error_counters, ); - let check_results: Vec<_> = check_results + let check_results = check_results .into_iter() .zip(txs.iter()) .map(|(result, tx)| match result { @@ -147,12 +149,12 @@ impl Consumer { } } Err(err) => Err(err), - }) - .collect(); + }); + let mut output = self.process_and_record_transactions_with_pre_results( bank, txs, - check_results.into_iter(), + check_results, ExecutionFlags { drop_on_failure: false, all_or_nothing: false, @@ -331,6 +333,7 @@ impl Consumer { ), drop_on_failure: flags.drop_on_failure, all_or_nothing: flags.all_or_nothing, + strict_nonce_size_check: true, } )); execute_and_commit_timings.load_execute_us = load_execute_us; @@ -349,20 +352,20 @@ impl Consumer { }; let mut entry_bytes = SERIALIZED_ENTRIES_OVERHEAD; - let (processed_transactions, processing_results_to_transactions_us) = measure_us!( - processing_results + let (processed_transactions, processing_results_to_transactions_us) = measure_us!({ + let mut processed_transactions = + Vec::with_capacity(processed_counts.processed_transactions_count as usize); + for (processing_result, tx) in processing_results .iter() .zip(batch.sanitized_transactions()) - .filter_map(|(processing_result, tx)| { - if processing_result.was_processed() { - entry_bytes += tx.serialized_size() as u64; - Some(tx.to_versioned_transaction()) - } else { - None - } - }) - .collect_vec() - ); + { + if processing_result.was_processed() { + entry_bytes += tx.serialized_size() as u64; + processed_transactions.push(tx.to_versioned_transaction()); + } + } + processed_transactions + }); let (freeze_lock, freeze_lock_us) = measure_us!(bank.freeze_lock()); execute_and_commit_timings.freeze_lock_us = freeze_lock_us; @@ -508,13 +511,13 @@ mod tests { super::*, crate::banking_stage::tests::{create_slow_genesis_config, sanitize_transactions}, agave_reserved_account_keys::ReservedAccountKeys, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_account::{AccountSharedData, state_traits::StateMut}, solana_address_lookup_table_interface::{ self as address_lookup_table, state::{AddressLookupTable, LookupTableMeta}, }, - solana_cost_model::{cost_model::CostModel, transaction_cost::TransactionCost}, + solana_cost_model::cost_model::CostModel, solana_fee_calculator::FeeCalculator, solana_hash::Hash, solana_instruction::error::InstructionError, @@ -577,7 +580,7 @@ mod tests { let recorder = TransactionRecorder::new(record_sender); record_receiver.restart(bank.bank_id()); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let committer = Committer::new(transaction_status_sender, replay_vote_sender, None); let consumer = Consumer::new(committer, recorder, None); @@ -600,7 +603,7 @@ mod tests { let recorder = TransactionRecorder::new(record_sender); record_receiver.restart(bank.bank_id()); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let committer = Committer::new(None, replay_vote_sender, None); let consumer = Consumer::new(committer, recorder, None); consumer.process_and_record_transactions(&bank, &transactions) @@ -969,10 +972,9 @@ mod tests { }; let mut cost = CostModel::calculate_cost(&transactions[0], &bank.feature_set); - if let TransactionCost::Transaction(ref mut usage_cost) = cost { - usage_cost.programs_execution_cost = actual_programs_execution_cost; - usage_cost.loaded_accounts_data_size_cost = actual_loaded_accounts_data_size_cost; - } + let usage_cost = cost.usage_cost_details_mut(); + usage_cost.programs_execution_cost = actual_programs_execution_cost; + usage_cost.loaded_accounts_data_size_cost = actual_loaded_accounts_data_size_cost; block_cost + cost.sum() }; @@ -1256,7 +1258,7 @@ mod tests { #[test] fn test_write_persist_transaction_status() { - let (transaction_status_sender, transaction_status_receiver) = unbounded(); + let (transaction_status_sender, transaction_status_receiver) = bounded(1024); let tss = Some(TransactionStatusSender { sender: transaction_status_sender, dependency_tracker: None, @@ -1327,7 +1329,7 @@ mod tests { #[test] fn test_write_persist_loaded_addresses() { - let (transaction_status_sender, transaction_status_receiver) = unbounded(); + let (transaction_status_sender, transaction_status_receiver) = bounded(1024); let tss = Some(TransactionStatusSender { sender: transaction_status_sender, dependency_tracker: None, diff --git a/core/src/banking_stage/latest_validator_vote_packet.rs b/core/src/banking_stage/latest_validator_vote_packet.rs index b24b9307d16..edd00403a97 100644 --- a/core/src/banking_stage/latest_validator_vote_packet.rs +++ b/core/src/banking_stage/latest_validator_vote_packet.rs @@ -108,7 +108,7 @@ impl LatestValidatorVote { let vote = SanitizedTransactionView::try_new_sanitized( std::sync::Arc::new(packet.data(..).unwrap().to_vec()), - true, + &solana_runtime_transaction::sanitize_config::sanitize_config(true), ) .unwrap(); @@ -182,47 +182,38 @@ mod tests { let keypairs = ValidatorVoteKeypairs::new_rand(); let blockhash = Hash::new_unique(); let switch_proof = Hash::new_unique(); - let mut tower_sync = BytesPacket::from_data( + let mut tower_sync = BytesPacket::from_data(new_tower_sync_transaction( + TowerSync::from(vec![(0, 3), (1, 2), (2, 1)]), + blockhash, + &keypairs.node_keypair, + &keypairs.vote_keypair, + &keypairs.vote_keypair, None, - new_tower_sync_transaction( - TowerSync::from(vec![(0, 3), (1, 2), (2, 1)]), - blockhash, - &keypairs.node_keypair, - &keypairs.vote_keypair, - &keypairs.vote_keypair, - None, - ), - ) + )) .unwrap(); tower_sync .meta_mut() .flags .set(PacketFlags::SIMPLE_VOTE_TX, true); - let mut tower_sync_switch = BytesPacket::from_data( - None, - new_tower_sync_transaction( - TowerSync::from(vec![(0, 3), (1, 2), (3, 1)]), - blockhash, - &keypairs.node_keypair, - &keypairs.vote_keypair, - &keypairs.vote_keypair, - Some(switch_proof), - ), - ) + let mut tower_sync_switch = BytesPacket::from_data(new_tower_sync_transaction( + TowerSync::from(vec![(0, 3), (1, 2), (3, 1)]), + blockhash, + &keypairs.node_keypair, + &keypairs.vote_keypair, + &keypairs.vote_keypair, + Some(switch_proof), + )) .unwrap(); tower_sync_switch .meta_mut() .flags .set(PacketFlags::SIMPLE_VOTE_TX, true); - let random_transaction = BytesPacket::from_data( - None, - transfer( - &keypairs.node_keypair, - &Pubkey::new_unique(), - 1000, - blockhash, - ), - ) + let random_transaction = BytesPacket::from_data(transfer( + &keypairs.node_keypair, + &Pubkey::new_unique(), + 1000, + blockhash, + )) .unwrap(); let packet_batch = PacketBatch::from(vec![tower_sync, tower_sync_switch, random_transaction]); diff --git a/core/src/banking_stage/qos_service.rs b/core/src/banking_stage/qos_service.rs index e4920e7baa7..3ee609d917f 100644 --- a/core/src/banking_stage/qos_service.rs +++ b/core/src/banking_stage/qos_service.rs @@ -6,6 +6,7 @@ use { super::committer::CommitTransactionDetails, agave_feature_set::FeatureSet, + smallvec::SmallVec, solana_cost_model::{ cost_model::CostModel, cost_tracker::UpdatedCosts, transaction_cost::TransactionCost, }, @@ -18,6 +19,11 @@ mod transaction { pub use solana_transaction_error::TransactionResult as Result; } +type TransactionCostResults<'a, Tx> = SmallVec< + [transaction::Result>; + super::consumer::TARGET_NUM_TRANSACTIONS_PER_BATCH], +>; + // QosService is local to each banking thread, each instance of QosService provides services to // one banking thread. // Banking thread calls `report_metrics(slot)` at end of `process_and_record_transaction()`, or any time @@ -34,14 +40,11 @@ impl QosService { bank: &Bank, transactions: &'a [Tx], pre_results: impl Iterator>, - ) -> (Vec>>, u64) { + ) -> (TransactionCostResults<'a, Tx>, u64) { let transaction_costs = Self::compute_transaction_costs(&bank.feature_set, transactions.iter(), pre_results); - let (transactions_qos_cost_results, num_included) = Self::select_transactions_per_cost( - transactions.iter(), - transaction_costs.into_iter(), - bank, - ); + let (transactions_qos_cost_results, num_included) = + Self::select_transactions_per_cost(transactions.iter(), transaction_costs, bank); let cost_model_throttled_transactions_count = transactions.len().saturating_sub(num_included) as u64; @@ -57,21 +60,19 @@ impl QosService { feature_set: &FeatureSet, transactions: impl Iterator, pre_results: impl Iterator>, - ) -> Vec>> { + ) -> TransactionCostResults<'a, Tx> { transactions .zip(pre_results) .map(|(tx, pre_result)| { pre_result.map(|()| { let mut reserving_cost = CostModel::calculate_cost(tx, feature_set); - if let TransactionCost::Transaction(ref mut usage_cost_details) = reserving_cost - { - // To maintain cost tracking consistency, reserve at least one page for - // loading the fee payer account in fee-only fallback scenarios. - usage_cost_details.loaded_accounts_data_size_cost = usage_cost_details - .loaded_accounts_data_size_cost - .max(CostModel::calculate_pages_cost(1)); - } + let usage_cost_details = reserving_cost.usage_cost_details_mut(); + // To maintain cost tracking consistency, reserve at least one page for + // loading the fee payer account in fee-only fallback scenarios. + usage_cost_details.loaded_accounts_data_size_cost = usage_cost_details + .loaded_accounts_data_size_cost + .max(CostModel::calculate_pages_cost(1)); reserving_cost }) @@ -84,15 +85,19 @@ impl QosService { /// and a count of the number of transactions that would fit in the block fn select_transactions_per_cost<'a, Tx: TransactionWithMeta>( transactions: impl Iterator, - transactions_costs: impl Iterator>>, + mut transactions_costs: TransactionCostResults<'a, Tx>, bank: &Bank, - ) -> (Vec>>, usize) { + ) -> (TransactionCostResults<'a, Tx>, usize) { let mut cost_tracker = bank.write_cost_tracker().unwrap(); let mut num_included = 0; - let select_results = transactions - .zip(transactions_costs) - .map(|(tx, cost)| match cost { - Ok(cost) => match cost_tracker.try_add(&cost) { + transactions + .zip(transactions_costs.iter_mut()) + .for_each(|(tx, cost_result)| { + let Ok(cost) = cost_result else { + return; + }; + + match cost_tracker.try_add(cost) { Ok(UpdatedCosts { updated_block_cost, updated_costliest_account_cost, @@ -107,7 +112,6 @@ impl QosService { updated_costliest_account_cost ); num_included += 1; - Ok(cost) } Err(e) => { debug!( @@ -118,15 +122,13 @@ impl QosService { cost, e ); - Err(TransactionError::from(e)) + *cost_result = Err(TransactionError::from(e)); } - }, - Err(e) => Err(e), - }) - .collect(); + } + }); cost_tracker.add_transactions_in_flight(num_included); - (select_results, num_included) + (transactions_costs, num_included) } /// Removes transaction costs from the cost tracker if not committed or recorded, or @@ -213,6 +215,7 @@ mod tests { use { super::*, itertools::Itertools, + solana_cost_model::cost_tracker::CostTrackerLimits, solana_hash::Hash, solana_keypair::Keypair, solana_runtime::genesis_utils::{GenesisConfigInfo, create_genesis_config}, @@ -302,9 +305,9 @@ mod tests { let cost_limit = transfer_tx_cost + vote_tx_cost; bank.write_cost_tracker() .unwrap() - .set_limits(cost_limit, cost_limit, cost_limit, 0); + .set_limits(CostTrackerLimits::new(cost_limit, cost_limit, 0)); let (results, num_selected) = - QosService::select_transactions_per_cost(txs.iter(), txs_costs.into_iter(), &bank); + QosService::select_transactions_per_cost(txs.iter(), txs_costs, &bank); assert_eq!(num_selected, 2); // verify that first transfer tx and first vote are allowed @@ -357,7 +360,7 @@ mod tests { .map(|cost| cost.as_ref().unwrap().sum()) .sum(); let (qos_cost_results, _num_included) = - QosService::select_transactions_per_cost(txs.iter(), txs_costs.into_iter(), &bank); + QosService::select_transactions_per_cost(txs.iter(), txs_costs, &bank); assert_eq!( total_txs_cost, bank.read_cost_tracker().unwrap().block_cost() @@ -427,7 +430,7 @@ mod tests { .map(|cost| cost.as_ref().unwrap().sum()) .sum(); let (qos_cost_results, _num_included) = - QosService::select_transactions_per_cost(txs.iter(), txs_costs.into_iter(), &bank); + QosService::select_transactions_per_cost(txs.iter(), txs_costs, &bank); assert_eq!( total_txs_cost, bank.read_cost_tracker().unwrap().block_cost() @@ -480,7 +483,7 @@ mod tests { .map(|cost| cost.as_ref().unwrap().sum()) .sum(); let (qos_cost_results, _num_included) = - QosService::select_transactions_per_cost(txs.iter(), txs_costs.into_iter(), &bank); + QosService::select_transactions_per_cost(txs.iter(), txs_costs, &bank); assert_eq!( total_txs_cost, bank.read_cost_tracker().unwrap().block_cost() @@ -561,9 +564,7 @@ mod tests { .expect("one tx cost") .expect("tx cost should be computed"); - let TransactionCost::Transaction(usage_cost_details) = tx_cost else { - panic!("expected TransactionCost::Transaction"); - }; + let usage_cost_details = tx_cost.usage_cost_details(); assert_eq!( usage_cost_details.loaded_accounts_data_size_cost, @@ -598,7 +599,7 @@ mod tests { .map(|cost| cost.as_ref().unwrap().sum()) .sum(); let (qos_cost_results, _num_included) = - QosService::select_transactions_per_cost(txs.iter(), txs_costs.into_iter(), &bank); + QosService::select_transactions_per_cost(txs.iter(), txs_costs, &bank); // transaction is committed with actual loaded account size == 0. let committed_status: Vec = qos_cost_results .iter() @@ -650,7 +651,7 @@ mod tests { .map(|cost| cost.as_ref().unwrap().sum()) .sum(); let (qos_cost_results, _num_included) = - QosService::select_transactions_per_cost(txs.iter(), txs_costs.into_iter(), &bank); + QosService::select_transactions_per_cost(txs.iter(), txs_costs, &bank); // transaction is committed with actual loaded account size == 1. let committed_status: Vec = qos_cost_results .iter() diff --git a/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs b/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs index 202d4a51807..e1e1f76c862 100644 --- a/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs +++ b/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs @@ -101,8 +101,9 @@ impl Scheduler for GreedyScheduler { let mut schedulable_threads = ThreadSet::any(num_threads); for thread_id in 0..num_threads { - if self.common.in_flight_tracker.cus_in_flight_per_thread()[thread_id] - >= target_cu_per_thread + if self.common.consume_work_senders[thread_id].is_full() + || self.common.in_flight_tracker.cus_in_flight_per_thread()[thread_id] + >= target_cu_per_thread { schedulable_threads.remove(thread_id); } @@ -203,9 +204,10 @@ impl Scheduler for GreedyScheduler { // if the thread is at target_cu_per_thread, remove it from the schedulable threads // if there are no more schedulable threads, stop scheduling. - if self.common.in_flight_tracker.cus_in_flight_per_thread()[thread_id] - + self.common.batches.total_cus()[thread_id] - >= target_cu_per_thread + if self.common.consume_work_senders[thread_id].is_full() + || self.common.in_flight_tracker.cus_in_flight_per_thread()[thread_id] + + self.common.batches.total_cus()[thread_id] + >= target_cu_per_thread { schedulable_threads.remove(thread_id); if schedulable_threads.is_empty() { @@ -292,7 +294,7 @@ mod test { scheduler_messages::{MaxAge, TransactionId}, transaction_scheduler::transaction_state_container::TransactionStateContainer, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, itertools::Itertools, solana_compute_budget_interface::ComputeBudgetInstruction, solana_hash::Hash, @@ -316,8 +318,8 @@ mod test { Sender>>, ) { let (consume_work_senders, consume_work_receivers) = - (0..num_threads).map(|_| unbounded()).unzip(); - let (finished_consume_work_sender, finished_consume_work_receiver) = unbounded(); + (0..num_threads).map(|_| bounded(1024)).unzip(); + let (finished_consume_work_sender, finished_consume_work_receiver) = bounded(1024); let scheduler = GreedyScheduler::new(consume_work_senders, finished_consume_work_receiver, config); ( diff --git a/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs b/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs index 2a95354cf3a..461bb0cdb4c 100644 --- a/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs +++ b/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs @@ -15,8 +15,9 @@ use { }, agave_banking_stage_ingress_types::{BankingPacketBatch, BankingPacketReceiver}, agave_transaction_view::{ - resolved_transaction_view::ResolvedTransactionView, transaction_data::TransactionData, - transaction_version::TransactionVersion, transaction_view::SanitizedTransactionView, + resolved_transaction_view::ResolvedTransactionView, sanitize::SanitizeConfig, + transaction_data::TransactionData, transaction_version::TransactionVersion, + transaction_view::SanitizedTransactionView, }, arrayvec::ArrayVec, core::time::Duration, @@ -31,8 +32,8 @@ use { bank_forks::{BankPair, SharableBanks}, }, solana_runtime_transaction::{ - runtime_transaction::RuntimeTransaction, transaction_meta::TransactionMeta, - transaction_with_meta::TransactionWithMeta, + runtime_transaction::RuntimeTransaction, sanitize_config::sanitize_config, + transaction_meta::TransactionMeta, transaction_with_meta::TransactionWithMeta, }, solana_svm::transaction_error_metrics::TransactionErrorMetrics, solana_svm_transaction::svm_message::SVMMessage, @@ -121,7 +122,8 @@ impl ReceiveAndBuffer for TransactionViewReceiveAndBuffer { } = self.sharable_banks.load(); // Receive packet batches. - const TIMEOUT: Duration = Duration::from_millis(10); + const RECV_TIMEOUT: Duration = Duration::from_millis(10); + const PACKET_BURST_TIMEOUT: Duration = Duration::from_millis(1); const PACKET_BURST_LIMIT: usize = 1000; let start = Instant::now(); @@ -156,7 +158,7 @@ impl ReceiveAndBuffer for TransactionViewReceiveAndBuffer { // overhead for wakers? But then risk not waking up when message // received - as long as sleep is somewhat short, this should be // fine. - match self.receiver.recv_timeout(TIMEOUT) { + match self.receiver.recv_timeout(RECV_TIMEOUT) { Ok(packet_batch_message) => { received_message = true; stats.accumulate(self.handle_packet_batch_message( @@ -177,7 +179,8 @@ impl ReceiveAndBuffer for TransactionViewReceiveAndBuffer { } if !timed_out { - while start.elapsed() < TIMEOUT && stats.num_received < PACKET_BURST_LIMIT { + while start.elapsed() < PACKET_BURST_TIMEOUT && stats.num_received < PACKET_BURST_LIMIT + { let receive_start = Instant::now(); match self.receiver.try_recv() { Ok(packet_batch_message) => { @@ -244,8 +247,8 @@ impl TransactionViewReceiveAndBuffer { // If outside holding window, do not parse. let should_parse = !matches!(decision, BufferedPacketsDecision::Forward); - let enable_instruction_accounts_limit = - root_bank.feature_set.snapshot().limit_instruction_accounts; + let sanitize_config = + sanitize_config(root_bank.feature_set.snapshot().limit_instruction_accounts); let transaction_account_lock_limit = working_bank.get_transaction_account_lock_limit(); // Create temporary batches of transactions to be age-checked. @@ -259,69 +262,72 @@ impl TransactionViewReceiveAndBuffer { let mut num_dropped_on_capacity = 0; let mut num_buffered = 0; - let mut check_and_push_to_queue = - |container: &mut TransactionViewStateContainer, - transaction_priority_ids: &mut ArrayVec| { - // Temporary scope so that transaction references are immediately - // dropped and transactions not passing - let mut check_results = { - let mut transactions = ArrayVec::<_, EXTRA_CAPACITY>::new(); - transactions.extend(transaction_priority_ids.iter().map(|priority_id| { - container - .get_transaction(priority_id.id) - .expect("transaction must exist") - })); - working_bank.check_transactions::>( - &transactions, - &lock_results[..transactions.len()], - working_bank.max_processing_age(), - &mut error_counters, - ) - }; + let mut check_and_push_to_queue = |container: &mut TransactionViewStateContainer, + transaction_priority_ids: &mut ArrayVec< + TransactionPriorityId, + EXTRA_CAPACITY, + >| { + // Temporary scope so that transaction references are immediately + // dropped and transactions not passing + let mut check_results = { + let mut transactions = ArrayVec::<_, EXTRA_CAPACITY>::new(); + transactions.extend(transaction_priority_ids.iter().map(|priority_id| { + container + .get_transaction(priority_id.id) + .expect("transaction must exist") + })); + working_bank.check_transactions::>( + &transactions, + &lock_results[..transactions.len()], + working_bank.max_processing_age(), + true, + &mut error_counters, + ) + }; - // Remove errored transactions - for (result, priority_id) in check_results - .iter_mut() - .zip(transaction_priority_ids.iter()) - { - if let Err(err) = result { - match err { - TransactionError::BlockhashNotFound => { - num_dropped_on_age += 1; - } - TransactionError::AlreadyProcessed => { - num_dropped_on_already_processed += 1; - } - _ => {} + // Remove errored transactions + for (result, priority_id) in check_results + .iter_mut() + .zip(transaction_priority_ids.iter()) + { + if let Err(err) = result { + match err { + TransactionError::BlockhashNotFound => { + num_dropped_on_age += 1; } - container.remove_by_id(priority_id.id); - continue; - } - let transaction = container - .get_transaction(priority_id.id) - .expect("transaction must exist"); - if let Err(err) = Consumer::check_fee_payer_unlocked( - working_bank, - transaction, - &mut error_counters, - ) { - *result = Err(err); - num_dropped_on_fee_payer += 1; - container.remove_by_id(priority_id.id); - continue; + TransactionError::AlreadyProcessed => { + num_dropped_on_already_processed += 1; + } + _ => {} } - - num_buffered += 1; + container.remove_by_id(priority_id.id); + continue; } - // Push non-errored transaction into queue. - num_dropped_on_capacity += container.push_ids_into_queue( - check_results - .into_iter() - .zip(transaction_priority_ids.drain(..)) - .filter(|(r, _)| r.is_ok()) - .map(|(_, id)| id), - ); - }; + let transaction = container + .get_transaction(priority_id.id) + .expect("transaction must exist"); + if let Err(err) = Consumer::check_fee_payer_unlocked( + working_bank, + transaction, + &mut error_counters, + ) { + *result = Err(err); + num_dropped_on_fee_payer += 1; + container.remove_by_id(priority_id.id); + continue; + } + + num_buffered += 1; + } + // Push non-errored transaction into queue. + num_dropped_on_capacity += container.push_ids_into_queue( + check_results + .into_iter() + .zip(transaction_priority_ids.drain(..)) + .filter(|(r, _)| r.is_ok()) + .map(|(_, id)| id), + ); + }; let mut num_received = 0; let mut num_dropped_without_parsing = 0; @@ -349,7 +355,7 @@ impl TransactionViewReceiveAndBuffer { root_bank, working_bank, transaction_account_lock_limit, - enable_instruction_accounts_limit, + &sanitize_config, &self.filter_keys, ) { Ok(state) => Ok(state), @@ -415,14 +421,14 @@ impl TransactionViewReceiveAndBuffer { root_bank: &Bank, working_bank: &Bank, transaction_account_lock_limit: usize, - enable_instruction_accounts_limit: bool, + sanitize_config: &SanitizeConfig, filter_keys: &HashSet, ) -> Result { let (view, deactivation_slot) = translate_to_runtime_view( bytes, root_bank, transaction_account_lock_limit, - enable_instruction_accounts_limit, + sanitize_config, )?; if !filter_keys.is_empty() @@ -455,12 +461,10 @@ pub(crate) fn translate_to_runtime_view( data: D, bank: &Bank, transaction_account_lock_limit: usize, - enable_instruction_accounts_limit: bool, + sanitize_config: &SanitizeConfig, ) -> Result<(RuntimeTransaction>, u64), PacketHandlingError> { // Parsing and basic sanitization checks - let Ok(view) = - SanitizedTransactionView::try_new_sanitized(data, enable_instruction_accounts_limit) - else { + let Ok(view) = SanitizedTransactionView::try_new_sanitized(data, sanitize_config) else { return Err(PacketHandlingError::Sanitization); }; @@ -548,7 +552,7 @@ mod tests { use { super::*, crate::banking_stage::tests::create_slow_genesis_config, - crossbeam_channel::{Receiver, unbounded}, + crossbeam_channel::{Receiver, bounded}, solana_hash::Hash, solana_keypair::Keypair, solana_ledger::genesis_utils::GenesisConfigInfo, @@ -657,7 +661,7 @@ mod tests { #[test] fn test_receive_and_buffer_disconnected_channel() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, _mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks); @@ -670,7 +674,7 @@ mod tests { #[test] fn test_receive_and_buffer_no_hold() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); @@ -720,7 +724,7 @@ mod tests { #[test] fn test_receive_and_buffer_discard() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); @@ -773,7 +777,7 @@ mod tests { #[test] fn test_receive_and_buffer_invalid_transaction_format() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, _mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks); @@ -817,7 +821,7 @@ mod tests { #[test] fn test_receive_and_buffer_invalid_blockhash() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks); @@ -860,7 +864,7 @@ mod tests { #[test] fn test_receive_and_buffer_simple_transfer_unfunded_fee_payer() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, _mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); @@ -908,7 +912,7 @@ mod tests { #[test] fn test_receive_and_buffer_failed_alt_resolve() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); @@ -971,7 +975,7 @@ mod tests { #[test] fn test_receive_and_buffer_simple_transfer() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); @@ -1019,7 +1023,7 @@ mod tests { #[test] fn test_receive_and_buffer_filters_fee_payer() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer_with_filter_keys( @@ -1049,7 +1053,7 @@ mod tests { #[test] fn test_receive_and_buffer_filters_account_key() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let filtered_key = Pubkey::new_unique(); let (mut receive_and_buffer, mut container) = @@ -1080,7 +1084,7 @@ mod tests { #[test] fn test_receive_and_buffer_does_not_filter_unmatched_keys() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer_with_filter_keys( @@ -1110,7 +1114,7 @@ mod tests { #[test] fn test_receive_and_buffer_overfull() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); @@ -1189,7 +1193,7 @@ mod tests { .unwrap() } - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let (bank_forks, mint_keypair) = test_bank_forks(); let (mut receive_and_buffer, mut container) = setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); diff --git a/core/src/banking_stage/transaction_scheduler/scheduler_common.rs b/core/src/banking_stage/transaction_scheduler/scheduler_common.rs index 57ea609acea..b6fd39a6a63 100644 --- a/core/src/banking_stage/transaction_scheduler/scheduler_common.rs +++ b/core/src/banking_stage/transaction_scheduler/scheduler_common.rs @@ -17,10 +17,13 @@ use { solana_runtime_transaction::transaction_with_meta::TransactionWithMeta, }; +const MAX_RECYCLED_BATCHES: usize = 1024; + pub struct Batches { ids: Vec>, transactions: Vec>, max_ages: Vec>, + recycled_batches: Vec<(Vec, Vec, Vec)>, total_cus: Vec, entry_bytes: Vec, target_num_transactions_per_batch: usize, @@ -41,6 +44,7 @@ impl Batches { ids: make_vecs(num_threads, target_num_transactions_per_batch), transactions: make_vecs(num_threads, target_num_transactions_per_batch), max_ages: make_vecs(num_threads, target_num_transactions_per_batch), + recycled_batches: Vec::with_capacity(num_threads), total_cus: vec![0; num_threads], entry_bytes: vec![ENTRY_OVERHEAD_BYTES; num_threads], target_num_transactions_per_batch, @@ -91,20 +95,20 @@ impl Batches { &mut self, thread_id: ThreadId, ) -> (Vec, Vec, Vec, u64) { + let (replacement_ids, replacement_transactions, replacement_max_ages) = + self.recycled_batches.pop().unwrap_or_else(|| { + ( + Vec::with_capacity(self.target_num_transactions_per_batch), + Vec::with_capacity(self.target_num_transactions_per_batch), + Vec::with_capacity(self.target_num_transactions_per_batch), + ) + }); + self.entry_bytes[thread_id] = ENTRY_OVERHEAD_BYTES; ( - core::mem::replace( - &mut self.ids[thread_id], - Vec::with_capacity(self.target_num_transactions_per_batch), - ), - core::mem::replace( - &mut self.transactions[thread_id], - Vec::with_capacity(self.target_num_transactions_per_batch), - ), - core::mem::replace( - &mut self.max_ages[thread_id], - Vec::with_capacity(self.target_num_transactions_per_batch), - ), + core::mem::replace(&mut self.ids[thread_id], replacement_ids), + core::mem::replace(&mut self.transactions[thread_id], replacement_transactions), + core::mem::replace(&mut self.max_ages[thread_id], replacement_max_ages), core::mem::replace(&mut self.total_cus[thread_id], 0), ) } @@ -238,9 +242,9 @@ impl SchedulingCommon { work: ConsumeWork { batch_id, - ids, - transactions, - max_ages: _, + mut ids, + mut transactions, + mut max_ages, }, retryable_indexes, }) => { @@ -252,17 +256,19 @@ impl SchedulingCommon { // Assumption - retryable indexes are in order (sorted by workers). let mut retryable_iter = retryable_indexes.iter().peekable(); - for (index, (id, transaction)) in izip!(ids, transactions).enumerate() { - if let Some(&retryable_index) = retryable_iter.peek() { - if retryable_index.index == index { - container.retry_transaction( - id, - transaction, - retryable_index.immediately_retryable, - ); - retryable_iter.next(); - continue; - } + for (index, (id, transaction)) in + izip!(ids.drain(..), transactions.drain(..)).enumerate() + { + if let Some(&retryable_index) = retryable_iter.peek() + && retryable_index.index == index + { + container.retry_transaction( + id, + transaction, + retryable_index.immediately_retryable, + ); + retryable_iter.next(); + continue; } container.remove_by_id(id); } @@ -276,6 +282,13 @@ impl SchedulingCommon { .collect::>(), ); + max_ages.clear(); + if self.batches.recycled_batches.len() < MAX_RECYCLED_BATCHES { + self.batches + .recycled_batches + .push((ids, transactions, max_ages)); + } + Ok((num_transactions, num_retryable)) } Err(TryRecvError::Empty) => Ok((0, 0)), @@ -313,7 +326,7 @@ mod tests { consumer::RetryableIndex, transaction_scheduler::transaction_state_container::TransactionStateContainer, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_hash::Hash, solana_keypair::Keypair, solana_pubkey::Pubkey, @@ -470,8 +483,8 @@ mod tests { add_transactions_to_container(&mut container, 3); let (work_senders, work_receivers): (Vec>, Vec>) = - (0..NUM_WORKERS).map(|_| unbounded()).unzip(); - let (_finished_work_sender, finished_work_receiver) = unbounded(); + (0..NUM_WORKERS).map(|_| bounded(1024)).unzip(); + let (_finished_work_sender, finished_work_receiver) = bounded(1024); let mut common = SchedulingCommon::new(work_senders, finished_work_receiver, 10); assert_eq!( common.batches.entry_bytes(), @@ -524,8 +537,8 @@ mod tests { add_transactions_to_container(&mut container, 1); let (work_senders, work_receivers): (Vec>, Vec>) = - (0..NUM_WORKERS).map(|_| unbounded()).unzip(); - let (finished_work_sender, finished_work_receiver) = unbounded(); + (0..NUM_WORKERS).map(|_| bounded(1024)).unzip(); + let (finished_work_sender, finished_work_receiver) = bounded(1024); let mut common = SchedulingCommon::new(work_senders, finished_work_receiver, 10); // Send a batch. Return completed work. @@ -579,8 +592,8 @@ mod tests { let mut container = TransactionStateContainer::with_capacity(1024); let (work_senders, work_receivers): (Vec>, Vec>) = - (0..NUM_WORKERS).map(|_| unbounded()).unzip(); - let (finished_work_sender, finished_work_receiver) = unbounded(); + (0..NUM_WORKERS).map(|_| bounded(1024)).unzip(); + let (finished_work_sender, finished_work_receiver) = bounded(1024); let mut common = SchedulingCommon::new(work_senders, finished_work_receiver, 10); // Retryable indexes out-of-order. add_transactions_to_container(&mut container, 2); diff --git a/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs b/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs index ea69e82c44f..c4c52c7a22c 100644 --- a/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs +++ b/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs @@ -20,6 +20,7 @@ use { }, validator::SchedulerPacing, }, + agave_banking_stage_ingress_types::SchedulerPriorityFloor, solana_clock::DEFAULT_MS_PER_SLOT, solana_cost_model::cost_tracker::SharedBlockCost, solana_measure::measure_us, @@ -37,6 +38,13 @@ use { const CHECK_CHUNK: usize = 128; +/// Publish the priority floor once the scheduler's retained transaction buffer is +/// this full. Capacity is enforced on the buffer, not just the priority queue: +/// scheduled transactions still occupy buffer space until workers finish them. +const SATURATION_BUFFER_PCT: u8 = 99; +/// Clear the priority floor once the retained buffer drains below this watermark. +const DESATURATION_BUFFER_PCT: u8 = 95; + #[derive(Clone)] pub struct SchedulerConfig { pub scheduler_pacing: SchedulerPacing, @@ -56,6 +64,56 @@ const DEFAULT_SCHEDULER_PACING_NON_FILL_TIME_MILLIS: u64 = 50; pub(crate) const DEFAULT_SCHEDULER_PACING_FILL_TIME_MILLIS: NonZeroU64 = NonZeroU64::new(DEFAULT_MS_PER_SLOT - DEFAULT_SCHEDULER_PACING_NON_FILL_TIME_MILLIS).unwrap(); +/// Detects saturation and publishes the priority floor for dropping low-priority transactions upstream. +struct SaturationState { + priority_floor: Arc, + saturated: bool, + saturation_watermark: usize, + desaturation_watermark: usize, +} + +impl SaturationState { + fn new(priority_floor: Arc, container_capacity: usize) -> Self { + let saturation_watermark = + container_capacity.saturating_mul(SATURATION_BUFFER_PCT as usize) / 100; + let desaturation_watermark = + container_capacity.saturating_mul(DESATURATION_BUFFER_PCT as usize) / 100; + Self { + priority_floor, + saturated: false, + saturation_watermark, + desaturation_watermark, + } + } + + /// Update the saturation state. + fn update(&mut self, buffer_size: usize, num_dropped_on_capacity: usize) -> bool { + if self.saturated { + if buffer_size < self.desaturation_watermark && num_dropped_on_capacity == 0 { + self.saturated = false; + } + } else if buffer_size >= self.saturation_watermark || num_dropped_on_capacity > 0 { + self.saturated = true; + } + + self.saturated + } + + /// Publish the priority floor. + fn publish_floor(&self, floor: u64) { + self.priority_floor.set(floor); + } +} + +impl Drop for SaturationState { + fn drop(&mut self) { + // The priority floor outlives individual scheduler controllers (it's + // shared with sigverify). Clear it on tear-down so a stale floor + // from this controller can't keep sigverify dropping packets. + self.priority_floor.clear(); + } +} + /// Controls packet and transaction flow into scheduler, and scheduling execution. pub(crate) struct SchedulerController where @@ -88,6 +146,8 @@ where recheck_cursor: Option, /// Recheck IDs scratch space. recheck_chunk: Vec, + /// Saturation detection and priority floor publication. + saturation_state: SaturationState, } impl SchedulerController @@ -103,14 +163,18 @@ where sharable_banks: SharableBanks, scheduler: S, worker_metrics: Vec>, + priority_floor: Arc, ) -> Self { + priority_floor.clear(); + let container_capacity = TOTAL_BUFFERED_PACKETS; + let saturation_state = SaturationState::new(priority_floor, container_capacity); Self { exit, config, decision_maker, receive_and_buffer, sharable_banks, - container: R::Container::with_capacity(TOTAL_BUFFERED_PACKETS), + container: R::Container::with_capacity(container_capacity), scheduler, count_metrics: SchedulerCountMetrics::default(), timing_metrics: SchedulerTimingMetrics::default(), @@ -118,6 +182,7 @@ where scheduling_details: SchedulingDetails::default(), recheck_cursor: None, recheck_chunk: Vec::with_capacity(CHECK_CHUNK), + saturation_state, } } @@ -160,22 +225,21 @@ where // If pacing_fill_time is greater than the bank's slot time, // adjust the pacing_fill_time to be the slot time, and warn. let fill_time = self.config.scheduler_pacing.fill_time(); - if let Some(pacing_fill_time) = fill_time.as_ref() { - if pacing_fill_time.as_nanos() > b.ns_per_slot { - warn!( - "scheduler pacing config pacing_fill_time {:?} is greater than \ - the bank's slot time {}, setting to slot time", - pacing_fill_time, b.ns_per_slot, - ); - self.config.scheduler_pacing = SchedulerPacing::FillTimeMillis( - NonZeroU64::new( - (b.ns_per_slot as u64 / 1_000_000).saturating_sub( - DEFAULT_SCHEDULER_PACING_NON_FILL_TIME_MILLIS, - ), - ) - .unwrap_or(NonZeroU64::new(1).unwrap()), - ); - } + if let Some(pacing_fill_time) = fill_time.as_ref() + && pacing_fill_time.as_nanos() > b.ns_per_slot + { + warn!( + "scheduler pacing config pacing_fill_time {:?} is greater than the \ + bank's slot time {}, setting to slot time", + pacing_fill_time, b.ns_per_slot, + ); + self.config.scheduler_pacing = SchedulerPacing::FillTimeMillis( + NonZeroU64::new( + (b.ns_per_slot as u64 / 1_000_000) + .saturating_sub(DEFAULT_SCHEDULER_PACING_NON_FILL_TIME_MILLIS), + ) + .unwrap_or(NonZeroU64::new(1).unwrap()), + ); } CostPacer { @@ -195,7 +259,7 @@ where timing_metrics.clean_time_us += clean_time_us; }); } - self.receive_and_buffer_packets(&decision).map_err(|_| { + let receiving_stats = self.receive_and_buffer_packets(&decision).map_err(|_| { SchedulerError::DisconnectedRecvChannel("receive and buffer disconnected") })?; // Report metrics only if there is data. @@ -205,6 +269,7 @@ where self.count_metrics.update(|count_metrics| { count_metrics.update_priority_stats(priority_min_max); }); + self.update_scheduler_priority_floor(receiving_stats.num_dropped_on_capacity); self.count_metrics .maybe_report_and_reset_interval(should_report); self.timing_metrics @@ -265,6 +330,27 @@ where Ok(scheduled) } + /// Update the scheduler priority floor. + /// + /// Semantics: when the retained scheduler buffer is nearly full, drop + /// arrivals that are at-or-below the current queue-min priority, i.e. no + /// better than what the bounded scheduler candidate set would evict. + fn update_scheduler_priority_floor(&mut self, num_dropped_on_capacity: usize) { + let buffer_size = self.container.buffer_size(); + let saturated = self + .saturation_state + .update(buffer_size, num_dropped_on_capacity); + let priority_floor = if saturated { + self.container + .get_min_max_priority() + .map_or(0, |(min, _)| min) + } else { + 0 + }; + + self.saturation_state.publish_floor(priority_floor); + } + /// Clears the transaction state container. /// This only clears pending transactions, and does **not** clear in-flight transactions. fn clear_container(&mut self) { @@ -322,12 +408,13 @@ where txs }; - let lock_results = vec![Ok(()); txs.len()]; + let lock_results = [const { Ok(()) }; CHECK_CHUNK]; let mut error_counters = TransactionErrorMetrics::default(); let results = bank.check_transactions::( &txs, - &lock_results, + &lock_results[..txs.len()], bank.max_processing_age(), + true, &mut error_counters, ); @@ -449,7 +536,7 @@ mod tests { transaction_scheduler::greedy_scheduler::{GreedyScheduler, GreedySchedulerConfig}, }, agave_banking_stage_ingress_types::{BankingPacketBatch, BankingPacketReceiver}, - crossbeam_channel::{Receiver, Sender, unbounded}, + crossbeam_channel::{Receiver, Sender, bounded}, itertools::Itertools, solana_compute_budget_interface::ComputeBudgetInstruction, solana_fee_calculator::FeeRateGovernor, @@ -469,7 +556,7 @@ mod tests { }; fn create_channels(num: usize) -> (Vec>, Vec>) { - (0..num).map(|_| unbounded()).unzip() + (0..num).map(|_| bounded(1024)).unzip() } // Helper struct to create tests that hold channels, files, etc. @@ -516,12 +603,12 @@ mod tests { let decision_maker = DecisionMaker::new(shared_leader_state.clone()); - let (banking_packet_sender, banking_packet_receiver) = unbounded(); + let (banking_packet_sender, banking_packet_receiver) = bounded(1024); let receive_and_buffer = create_receive_and_buffer(banking_packet_receiver, bank_forks.clone()); let (consume_work_senders, consume_work_receivers) = create_channels(num_threads); - let (finished_consume_work_sender, finished_consume_work_receiver) = unbounded(); + let (finished_consume_work_sender, finished_consume_work_receiver) = bounded(1024); let test_frame = TestFrame { bank, @@ -547,6 +634,7 @@ mod tests { bank_forks.read().unwrap().sharable_banks(), scheduler, vec![], // no actual workers with metrics to report, this can be empty + Arc::new(SchedulerPriorityFloor::default()), ); (test_frame, scheduler_controller) @@ -995,3 +1083,93 @@ mod tests { assert_eq!(message_hashes, vec![&tx1_hash]); } } + +#[cfg(test)] +mod saturation_state_tests { + use super::*; + + fn saturation_watermark() -> usize { + TOTAL_BUFFERED_PACKETS.saturating_mul(SATURATION_BUFFER_PCT as usize) / 100 + } + + fn desaturation_watermark() -> usize { + TOTAL_BUFFERED_PACKETS.saturating_mul(DESATURATION_BUFFER_PCT as usize) / 100 + } + + fn make_state() -> (SaturationState, Arc) { + let priority_floor = Arc::new(SchedulerPriorityFloor::new()); + let state = SaturationState::new(priority_floor.clone(), TOTAL_BUFFERED_PACKETS); + (state, priority_floor) + } + + #[test] + fn starts_unsaturated() { + let (mut state, floor) = make_state(); + assert!(!state.update(0, 0)); + assert_eq!(floor.get(), 0); + } + + #[test] + fn does_not_enter_when_buffer_below_saturation_watermark() { + let (mut state, floor) = make_state(); + assert!(!state.update(saturation_watermark() - 1, 0)); + assert_eq!(floor.get(), 0); + } + + #[test] + fn enters_when_buffer_reaches_saturation_watermark() { + let (mut state, _floor) = make_state(); + assert!(state.update(saturation_watermark(), 0)); + } + + #[test] + fn enters_on_capacity_drops_even_below_watermark() { + // Direct trigger: any on-capacity drop saturates regardless of + // buffer-watermark — invariant to future changes in the trim policy. + let (mut state, _floor) = make_state(); + assert!(state.update(0, 1)); + } + + #[test] + fn publish_floor_writes_value() { + let (state, floor) = make_state(); + state.publish_floor(42); + assert_eq!(floor.get(), 42); + } + + #[test] + fn stays_saturated_at_desaturation_watermark() { + let (mut state, _floor) = make_state(); + assert!(state.update(saturation_watermark(), 0)); + assert!(state.update(desaturation_watermark(), 0)); + } + + #[test] + fn exits_when_buffer_drops_below_desaturation_watermark() { + let (mut state, _floor) = make_state(); + assert!(state.update(saturation_watermark(), 0)); + assert!(!state.update(desaturation_watermark() - 1, 0)); + } + + #[test] + fn stays_saturated_on_drops_even_below_desaturation_watermark() { + // Direct trigger keeps us saturated even if the buffer has drained, + // because drops in the same tick mean we still need backpressure. + let (mut state, _floor) = make_state(); + assert!(state.update(saturation_watermark(), 0)); + assert!(state.update(desaturation_watermark() - 1, 1)); + } + + #[test] + fn drop_clears_floor() { + // The floor outlives the controller (it's shared with sigverify), so + // tear-down must clear any stale value. + let floor = Arc::new(SchedulerPriorityFloor::new()); + { + let state = SaturationState::new(floor.clone(), TOTAL_BUFFERED_PACKETS); + state.publish_floor(123); + assert_eq!(floor.get(), 123); + } + assert_eq!(floor.get(), 0); + } +} diff --git a/core/src/banking_stage/transaction_scheduler/scheduler_metrics.rs b/core/src/banking_stage/transaction_scheduler/scheduler_metrics.rs index 3883d6868cd..e4b9862f1d6 100644 --- a/core/src/banking_stage/transaction_scheduler/scheduler_metrics.rs +++ b/core/src/banking_stage/transaction_scheduler/scheduler_metrics.rs @@ -188,7 +188,7 @@ impl SchedulerCountMetricsInner { ), ("num_dropped_on_capacity", num_dropped_on_capacity, i64), ("min_priority", self.get_min_priority(), i64), - ("max_priority", self.get_max_priority(), i64) + ("max_priority", self.get_max_priority(), i64), ); if let Some(slot) = slot { datapoint.add_field_i64("slot", slot as i64); diff --git a/core/src/banking_stage/transaction_scheduler/transaction_state_container.rs b/core/src/banking_stage/transaction_scheduler/transaction_state_container.rs index 781170f59a0..e09ac485589 100644 --- a/core/src/banking_stage/transaction_scheduler/transaction_state_container.rs +++ b/core/src/banking_stage/transaction_scheduler/transaction_state_container.rs @@ -396,7 +396,9 @@ mod tests { solana_keypair::Keypair, solana_message::Message, solana_perf::packet::Packet, - solana_runtime_transaction::runtime_transaction::RuntimeTransaction, + solana_runtime_transaction::{ + runtime_transaction::RuntimeTransaction, sanitize_config::sanitize_config, + }, solana_signer::Signer, solana_system_interface::instruction as system_instruction, solana_transaction::{ @@ -484,7 +486,8 @@ mod tests { let reserved_addresses = HashSet::default(); let packet_parser = |data, priority, cost| { - let view = SanitizedTransactionView::try_new_sanitized(data, true).unwrap(); + let view = + SanitizedTransactionView::try_new_sanitized(data, &sanitize_config(true)).unwrap(); let view = RuntimeTransaction::>::try_new( view, MessageHash::Compute, diff --git a/core/src/banking_stage/vote_packet_receiver.rs b/core/src/banking_stage/vote_packet_receiver.rs index 856d270a0a6..15f92de8628 100644 --- a/core/src/banking_stage/vote_packet_receiver.rs +++ b/core/src/banking_stage/vote_packet_receiver.rs @@ -11,6 +11,7 @@ use { crossbeam_channel::RecvTimeoutError, solana_measure::{measure::Measure, measure_us}, solana_pubkey::Pubkey, + solana_runtime_transaction::sanitize_config::sanitize_config, std::{ collections::HashSet, num::Saturating, @@ -115,8 +116,9 @@ impl VotePacketReceiver { match SanitizedTransactionView::try_new_sanitized( Arc::new(pkt.data(..)?.to_vec()), // Vote instructions are created in the validator code, and they are not - // referencing more than 255 accounts, so it is safe to set this to true. - true, + // referencing more than 255 accounts, so it is safe to enforce the + // instruction accounts limit unconditionally. + &sanitize_config(true), ) { Ok(pkt) => { if self.should_filter_packet(&pkt) { @@ -280,7 +282,7 @@ mod tests { leader_slot_metrics::LeaderSlotMetricsTracker, vote_storage::{VoteStorage, tests::packet_from_slots}, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_perf::packet::PacketBatch, solana_runtime::{ bank::Bank, @@ -294,7 +296,7 @@ mod tests { filter_keys: Arc>, ) -> VoteStorage { let vote_packet = packet_from_slots(vec![(1, 1)], keypairs, None); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); sender .send(Arc::new(vec![PacketBatch::from(vec![vote_packet])])) .unwrap(); diff --git a/core/src/banking_stage/vote_storage.rs b/core/src/banking_stage/vote_storage.rs index b1562a6edca..4149b0a4282 100644 --- a/core/src/banking_stage/vote_storage.rs +++ b/core/src/banking_stage/vote_storage.rs @@ -5,7 +5,7 @@ use { ahash::HashMap, itertools::Itertools, rand::{Rng, rng}, - solana_account::from_account, + solana_account::ReadableAccount as _, solana_clock::Epoch, solana_pubkey::Pubkey, solana_runtime::{ @@ -146,7 +146,7 @@ impl VoteStorage { pub fn drain_unprocessed(&mut self, bank: &Bank) -> Vec> { let slot_hashes = bank .get_account(&sysvar::slot_hashes::id()) - .and_then(|account| from_account::(&account)); + .and_then(|account| wincode::deserialize::(account.data()).ok()); if slot_hashes.is_none() { error!( "Slot hashes sysvar doesn't exist on bank {}. Including all votes without \ @@ -391,6 +391,7 @@ pub(crate) mod tests { solana_leader_schedule::SlotLeader, solana_perf::packet::{BytesPacket, PacketFlags}, solana_runtime::genesis_utils::{self, ValidatorVoteKeypairs}, + solana_runtime_transaction::sanitize_config::sanitize_config, solana_signer::Signer, solana_vote::vote_transaction::new_tower_sync_transaction, solana_vote_program::vote_state::TowerSync, @@ -473,7 +474,7 @@ pub(crate) mod tests { &keypairs.vote_keypair, None, ); - let mut packet = BytesPacket::from_data(None, vote_tx).unwrap(); + let mut packet = BytesPacket::from_data(vote_tx).unwrap(); packet .meta_mut() .flags @@ -509,7 +510,7 @@ pub(crate) mod tests { authorized_voter, None, ); - let mut packet = BytesPacket::from_data(None, vote_tx).unwrap(); + let mut packet = BytesPacket::from_data(vote_tx).unwrap(); packet .meta_mut() .flags @@ -519,8 +520,11 @@ pub(crate) mod tests { } fn to_sanitized_view(packet: BytesPacket) -> SanitizedTransactionView { - SanitizedTransactionView::try_new_sanitized(Arc::new(packet.buffer().to_vec()), true) - .unwrap() + SanitizedTransactionView::try_new_sanitized( + Arc::new(packet.buffer().to_vec()), + &sanitize_config(true), + ) + .unwrap() } #[test] diff --git a/core/src/banking_stage/vote_worker.rs b/core/src/banking_stage/vote_worker.rs index 3c9060a0de0..b7c05af0a43 100644 --- a/core/src/banking_stage/vote_worker.rs +++ b/core/src/banking_stage/vote_worker.rs @@ -17,9 +17,7 @@ use { agave_transaction_view::{ transaction_version::TransactionVersion, transaction_view::SanitizedTransactionView, }, - arrayvec::ArrayVec, crossbeam_channel::RecvTimeoutError, - itertools::Itertools, solana_accounts_db::account_locks::validate_account_locks, solana_clock::FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET, solana_measure::{measure::Measure, measure_us}, @@ -50,11 +48,6 @@ mod transaction { pub use solana_transaction_error::TransactionResult as Result; } -// This vote batch size was selected to balance the following two things: -// 1. Amortize execution overhead (Larger is better) -// 2. Constrain max entry size for FEC set packing (Smaller is better) -pub const UNPROCESSED_BUFFER_STEP_SIZE: usize = 16; - pub struct VoteWorker { exit: Arc, shutdown_signal: CancellationToken, @@ -244,109 +237,55 @@ impl VoteWorker { // Based on the stake distribution present in the supplied bank, drain the unprocessed votes // from each validator using a weighted random ordering. Votes from validators with // 0 stake are ignored. - let all_vote_packets = self.storage.drain_unprocessed(bank); - - let mut reached_end_of_slot = false; + let mut all_vote_packets = self.storage.drain_unprocessed(bank).into_iter(); let mut error_counters: TransactionErrorMetrics = TransactionErrorMetrics::default(); - let mut resolved_txs = ArrayVec::<_, UNPROCESSED_BUFFER_STEP_SIZE>::new(); - for chunk in Itertools::chunks(all_vote_packets.into_iter(), UNPROCESSED_BUFFER_STEP_SIZE) - .into_iter() - { - debug_assert!(resolved_txs.is_empty()); - - // Short circuit if we've reached the end of slot. - if reached_end_of_slot { - self.storage.reinsert_packets(chunk.into_iter()); - + // Process one vote at a time to avoid over-reserving block CUs during packing. + // This also keeps each recorded vote batch small, which favors entry/FEC-set packing. + while let Some(packet) = all_vote_packets.next() { + let Some(sanitized_transaction) = + consume_scan_should_process_packet(bank, packet, &mut error_counters) + else { continue; - } + }; - // Sanitize & resolve our chunk. - for packet in chunk.into_iter() { - if let Some(tx) = - consume_scan_should_process_packet(bank, packet, &mut error_counters) - { - resolved_txs.push(tx); - } - } + let (process_transactions_summary, process_packets_transactions_us) = + measure_us!(self.process_packets_transactions( + bank, + std::slice::from_ref(&sanitized_transaction), + banking_stage_stats, + slot_metrics_tracker, + )); + slot_metrics_tracker + .increment_process_packets_transactions_us(process_packets_transactions_us); - if let Some(retryable_vote_indices) = self.do_process_packets( - bank, - &mut reached_end_of_slot, - &resolved_txs, - banking_stage_stats, - consumed_buffered_packets_count, - rebuffered_packet_count, - slot_metrics_tracker, - ) { - self.storage.reinsert_packets( - Self::extract_retryable(&mut resolved_txs, retryable_vote_indices) - .map(|tx| tx.into_inner_transaction().into_view()), - ); - } else { - self.storage.reinsert_packets( - resolved_txs - .drain(..) - .map(|tx| tx.into_inner_transaction().into_view()), - ); - } - } + let ProcessTransactionsSummary { + reached_max_poh_height, + retryable_transaction_indexes: retryable_vote_indices, + .. + } = process_transactions_summary; - reached_end_of_slot - } + let retryable_vote_count = retryable_vote_indices.len(); + *consumed_buffered_packets_count += usize::from(retryable_vote_count == 0); + *rebuffered_packet_count += retryable_vote_count; - fn do_process_packets( - &self, - bank: &Bank, - reached_end_of_slot: &mut bool, - sanitized_transactions: &[RuntimeTransactionView], - banking_stage_stats: &BankingStageStats, - consumed_buffered_packets_count: &mut usize, - rebuffered_packet_count: &mut usize, - slot_metrics_tracker: &mut LeaderSlotMetricsTracker, - ) -> Option> { - if *reached_end_of_slot { - return None; - } - - let (process_transactions_summary, process_packets_transactions_us) = - measure_us!(self.process_packets_transactions( - bank, - sanitized_transactions, - banking_stage_stats, - slot_metrics_tracker, - )); - - slot_metrics_tracker - .increment_process_packets_transactions_us(process_packets_transactions_us); + slot_metrics_tracker.increment_retryable_packets_count(retryable_vote_count as u64); - let ProcessTransactionsSummary { - reached_max_poh_height, - retryable_transaction_indexes, - .. - } = process_transactions_summary; + if retryable_vote_count != 0 { + // vote is processed one at a time, so the only valid retryable index is 0 + assert_eq!(retryable_vote_indices.as_slice(), &[0]); - *reached_end_of_slot = has_reached_end_of_slot(reached_max_poh_height, bank); - - // The difference between all transactions passed to execution and the ones that - // are retryable were the ones that were either: - // 1) Committed into the block - // 2) Dropped without being committed because they had some fatal error (too old, - // duplicate signature, etc.) - // - // Note: This assumes that every packet deserializes into one transaction! - *consumed_buffered_packets_count += sanitized_transactions - .len() - .saturating_sub(retryable_transaction_indexes.len()); - - // Out of the buffered packets just retried, collect any still unprocessed - // transactions in this batch - *rebuffered_packet_count += retryable_transaction_indexes.len(); + self.storage.reinsert_packets(std::iter::once( + sanitized_transaction.into_inner_transaction().into_view(), + )); + } - slot_metrics_tracker - .increment_retryable_packets_count(retryable_transaction_indexes.len() as u64); + if has_reached_end_of_slot(reached_max_poh_height, bank) { + self.storage.reinsert_packets(all_vote_packets); + return true; + } + } - Some(retryable_transaction_indexes) + false } fn process_packets_transactions( @@ -500,25 +439,6 @@ impl VoteWorker { .filter_map(|(index, res)| res.as_ref().ok().map(|_| index)) .collect() } - - fn extract_retryable( - vote_packets: &mut ArrayVec, - retryable_vote_indices: Vec, - ) -> impl Iterator + '_ { - debug_assert!(retryable_vote_indices.is_sorted()); - let mut retryable_vote_indices = retryable_vote_indices.into_iter().peekable(); - - vote_packets - .drain(..) - .enumerate() - .filter_map(move |(i, packet)| { - (Some(&i) == retryable_vote_indices.peek()).then(|| { - retryable_vote_indices.next(); - - packet - }) - }) - } } fn consume_scan_should_process_packet( @@ -575,33 +495,14 @@ mod tests { crate::banking_stage::{ committer::Committer, tests::{create_slow_genesis_config, sanitize_transactions}, - vote_storage::tests::packet_from_slots, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_ledger::genesis_utils::GenesisConfigInfo, - solana_perf::packet::BytesPacket, solana_poh::record_channels::record_channels, - solana_runtime::genesis_utils::ValidatorVoteKeypairs, - solana_runtime_transaction::transaction_meta::TransactionMeta, solana_svm::account_loader::CheckedTransactionDetails, solana_system_transaction as system_transaction, - std::collections::HashSet, }; - fn to_runtime_transaction_view(packet: BytesPacket) -> RuntimeTransactionView { - let tx = - SanitizedTransactionView::try_new_sanitized(Arc::new(packet.buffer().to_vec()), true) - .unwrap(); - let tx = RuntimeTransaction::>::try_new( - tx, - MessageHash::Compute, - None, - ) - .unwrap(); - - RuntimeTransactionView::try_new(tx, None, &HashSet::default()).unwrap() - } - #[test] fn test_bank_prepare_filter_for_pending_transaction() { assert_eq!( @@ -656,82 +557,6 @@ mod tests { ); } - #[test] - fn extract_retryable_one_all_retryable() { - let keypair_a = ValidatorVoteKeypairs::new_rand(); - let mut packets = ArrayVec::from_iter([to_runtime_transaction_view(packet_from_slots( - vec![(1, 1)], - &keypair_a, - None, - ))]); - let retryable_indices = vec![0]; - - // Assert - Able to extract exactly one packet. - let expected = *packets[0].message_hash(); - let mut extracted = VoteWorker::extract_retryable(&mut packets, retryable_indices); - assert_eq!(extracted.next().unwrap().message_hash(), &expected); - assert!(extracted.next().is_none()); - } - - #[test] - fn extract_retryable_one_none_retryable() { - let keypair_a = ValidatorVoteKeypairs::new_rand(); - let mut packets = ArrayVec::from_iter([to_runtime_transaction_view(packet_from_slots( - vec![(1, 1)], - &keypair_a, - None, - ))]); - let retryable_indices = vec![]; - - // Assert - Able to extract exactly zero packets. - let mut extracted = VoteWorker::extract_retryable(&mut packets, retryable_indices); - assert!(extracted.next().is_none()); - } - - #[test] - fn extract_retryable_three_last_retryable() { - let keypair_a = ValidatorVoteKeypairs::new_rand(); - let mut packets = ArrayVec::from_iter( - [ - packet_from_slots(vec![(5, 3)], &keypair_a, None), - packet_from_slots(vec![(6, 2)], &keypair_a, None), - packet_from_slots(vec![(7, 1)], &keypair_a, None), - ] - .into_iter() - .map(to_runtime_transaction_view), - ); - let retryable_indices = vec![2]; - - // Assert - Able to extract exactly one packet. - let expected = *packets[2].message_hash(); - let mut extracted = VoteWorker::extract_retryable(&mut packets, retryable_indices); - assert_eq!(extracted.next().unwrap().message_hash(), &expected); - assert!(extracted.next().is_none()); - } - - #[test] - fn extract_retryable_three_first_last_retryable() { - let keypair_a = ValidatorVoteKeypairs::new_rand(); - let mut packets = ArrayVec::from_iter( - [ - packet_from_slots(vec![(5, 3)], &keypair_a, None), - packet_from_slots(vec![(6, 2)], &keypair_a, None), - packet_from_slots(vec![(7, 1)], &keypair_a, None), - ] - .into_iter() - .map(to_runtime_transaction_view), - ); - let retryable_indices = vec![0, 2]; - - // Assert - Able to extract exactly one packet. - let expected0 = *packets[0].message_hash(); - let expected1 = *packets[2].message_hash(); - let mut extracted = VoteWorker::extract_retryable(&mut packets, retryable_indices); - assert_eq!(extracted.next().unwrap().message_hash(), &expected0); - assert_eq!(extracted.next().unwrap().message_hash(), &expected1); - assert!(extracted.next().is_none()); - } - #[test] fn test_has_reached_end_of_slot() { let GenesisConfigInfo { genesis_config, .. } = create_slow_genesis_config(10_000); @@ -764,7 +589,7 @@ mod tests { let recorder = solana_poh::transaction_recorder::TransactionRecorder::new(record_sender); record_receiver.restart(bank.bank_id()); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let committer = Committer::new(None, replay_vote_sender, None); let consumer = Consumer::new(committer, recorder, None); diff --git a/core/src/banking_trace.rs b/core/src/banking_trace.rs index 211e4231f50..448b95d059e 100644 --- a/core/src/banking_trace.rs +++ b/core/src/banking_trace.rs @@ -2,11 +2,12 @@ use { agave_banking_stage_ingress_types::{BankingPacketBatch, BankingPacketReceiver}, bincode::serialize_into, chrono::{DateTime, Local}, - crossbeam_channel::{Receiver, SendError, Sender, TryRecvError, unbounded}, + crossbeam_channel::{Receiver, SendError, TryRecvError, TrySendError, bounded}, rolling_file::{RollingCondition, RollingConditionBasic, RollingFileAppender}, serde::{Deserialize, Serialize}, solana_clock::Slot, solana_hash::Hash, + solana_streamer::{evicting_sender::EvictingSender, streamer::ChannelSend}, std::{ fs::{create_dir_all, remove_dir_all}, io::{self, Write}, @@ -21,6 +22,14 @@ use { thiserror::Error, }; +/// Capacity of the vote channel between sigverify and the banking-stage. +/// Sized to fit all votes from a reasonably sized cluster for 1 slot, + margin. +const VOTE_CHANNEL_CAPACITY: usize = 1024 * 8; + +/// Capacity of the non-vote (transaction) channel between sigverify and the banking-stage. +/// Larger than the vote channel to absorb bursty TPU load. +const NON_VOTE_CHANNEL_CAPACITY: usize = 1024 * 16; + pub type BankingPacketSender = TracedSender; pub type TracerThreadResult = Result<(), TraceError>; pub type TracerThread = Option>; @@ -50,13 +59,12 @@ pub const DISABLED_BAKING_TRACE_DIR: DirByteLimit = 0; pub const BANKING_TRACE_DIR_DEFAULT_BYTE_LIMIT: DirByteLimit = TRACE_FILE_DEFAULT_ROTATE_BYTE_THRESHOLD * TRACE_FILE_ROTATE_COUNT; -#[derive(Clone, Debug)] +#[derive(Clone)] struct ActiveTracer { - trace_sender: Sender, + trace_sender: EvictingSender, exit: Arc, } -#[derive(Debug)] pub struct BankingTracer { active_tracer: Option, } @@ -76,7 +84,10 @@ pub enum TracedEvent { BlockAndBankHash(Slot, Hash, Hash), } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, AbiEnumVisitor, StableAbi, StableAbiSample) +)] #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub enum ChannelLabel { NonVote, @@ -203,7 +214,9 @@ impl BankingTracer { )); } - let (trace_sender, trace_receiver) = unbounded(); + const TRACING_CHANNEL_CAPACITY: usize = 50_000; + let (trace_sender, trace_receiver) = + EvictingSender::new_bounded(TRACING_CHANNEL_CAPACITY); let file_appender = Self::create_file_appender(path, rotate_threshold_size)?; @@ -240,22 +253,25 @@ impl BankingTracer { } fn trace_event(&self, on_trace: impl Fn() -> TimedTracedEvent) { - if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer { - if !exit.load(Ordering::Relaxed) { - trace_sender - .send(on_trace()) - .expect("active tracer thread unless exited"); - } + if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer + && !exit.load(Ordering::Relaxed) + { + // Ignore errors in sending to tracer - it is a non-critical component. + let _ = trace_sender.try_send(on_trace()); } } pub fn create_channels(&self) -> Channels { let (non_vote_sender, non_vote_receiver) = self.create_channel_non_vote(); - let (tpu_vote_sender, tpu_vote_receiver) = - Self::channel(ChannelLabel::TpuVote, self.active_tracer.as_ref().cloned()); + let (tpu_vote_sender, tpu_vote_receiver) = Self::channel( + ChannelLabel::TpuVote, + VOTE_CHANNEL_CAPACITY, + self.active_tracer.as_ref().cloned(), + ); let (gossip_vote_sender, gossip_vote_receiver) = Self::channel( ChannelLabel::GossipVote, + VOTE_CHANNEL_CAPACITY, self.active_tracer.as_ref().cloned(), ); @@ -270,20 +286,26 @@ impl BankingTracer { } pub fn create_channel_non_vote(&self) -> (BankingPacketSender, BankingPacketReceiver) { - Self::channel(ChannelLabel::NonVote, self.active_tracer.as_ref().cloned()) + Self::channel( + ChannelLabel::NonVote, + NON_VOTE_CHANNEL_CAPACITY, + self.active_tracer.as_ref().cloned(), + ) } pub fn channel_for_test() -> (TracedSender, Receiver) { - Self::channel(ChannelLabel::Dummy, None) + Self::channel(ChannelLabel::Dummy, VOTE_CHANNEL_CAPACITY, None) } fn channel( label: ChannelLabel, + capacity: usize, active_tracer: Option, ) -> (TracedSender, Receiver) { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(capacity); + let evicting = EvictingSender::new(sender, receiver.clone()); - (TracedSender::new(label, sender, active_tracer), receiver) + (TracedSender::new(label, evicting, active_tracer), receiver) } pub fn ensure_cleanup_path(path: &PathBuf) -> Result<(), io::Error> { @@ -357,14 +379,14 @@ impl BankingTracer { #[derive(Clone)] pub struct TracedSender { label: ChannelLabel, - sender: Sender, + sender: EvictingSender, active_tracer: Option, } impl TracedSender { fn new( label: ChannelLabel, - sender: Sender, + sender: EvictingSender, active_tracer: Option, ) -> Self { Self { @@ -374,21 +396,24 @@ impl TracedSender { } } - pub fn send(&self, batch: BankingPacketBatch) -> Result<(), SendError> { - if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer { - if !exit.load(Ordering::Relaxed) { - trace_sender - .send(TimedTracedEvent( - SystemTime::now(), - TracedEvent::PacketBatch(self.label, BankingPacketBatch::clone(&batch)), - )) - .map_err(|err| { - error!("unexpected error when tracing a banking event...: {err:?}"); - SendError(BankingPacketBatch::clone(&batch)) - })?; - } + /// Send a batch on the channel. This may evict an existing batch to make + /// room; in that case `Ok(n)` is returned where `n` is the number of + /// evicted packets. On channel disconnect returns `Err(SendError)`. + pub fn send(&self, batch: BankingPacketBatch) -> Result> { + if let Some(ActiveTracer { trace_sender, exit }) = &self.active_tracer + && !exit.load(Ordering::Relaxed) + { + // Ignore errors in sending to tracer - it is a non-critical component. + let _ = trace_sender.try_send(TimedTracedEvent( + SystemTime::now(), + TracedEvent::PacketBatch(self.label, BankingPacketBatch::clone(&batch)), + )); + } + match self.sender.try_send(batch) { + Ok(()) => Ok(0), + Err(TrySendError::Full(b)) => Ok(b.iter().map(|pb| pb.len()).sum()), + Err(TrySendError::Disconnected(b)) => Err(SendError(b)), } - self.sender.send(batch) } pub fn len(&self) -> usize { diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index 618bddb7c07..5d69dfbc59f 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -7,20 +7,21 @@ use { crate::{ banking_trace::{BankingPacketSender, BankingTracer}, + block_creation_loop::rewards::{ + certs_requestor::CertsRequestor, msg_types::RewardRespSucc, + reward_certs_service::RewardCertsService, + }, replay_stage::{Finalizer, ReplayStage}, }, agave_votor::event::LeaderWindowInfo, agave_votor_messages::{ consensus_message::Block, - reward_certificate::{ - BuildRewardCertsRequest, BuildRewardCertsRespSucc, BuildRewardCertsResponse, - NotarRewardCertificate, SkipRewardCertificate, - }, + reward_certificate::{AddVoteMessage, NotarRewardCertificate, SkipRewardCertificate}, }, - crossbeam_channel::{Receiver, RecvTimeoutError, Sender, select_biased}, + crossbeam_channel::{Receiver, Sender, select_biased}, solana_clock::Slot, solana_entry::block_component::{ - BlockFooterV1, GenesisCertificate, UpdateParentV1, VersionedBlockMarker, + BlockFooterV1, GenesisCertBlockMarker, UpdateParentV1, VersionedBlockMarker, }, solana_gossip::cluster_info::ClusterInfo, solana_hash::Hash, @@ -35,7 +36,7 @@ use { solana_rpc::{rpc_subscriptions::RpcSubscriptions, slot_status_notifier::SlotStatusNotifier}, solana_runtime::{ bank::{Bank, NewBankOptions}, - bank_forks::BankForks, + bank_forks::{BankForks, SharableBanks}, bank_forks_controller::{BankForksController, BankForksControllerError}, block_component_processor::BlockComponentProcessor, leader_schedule_utils::{last_of_consecutive_leader_slots, leader_slot_index}, @@ -56,8 +57,20 @@ use { thiserror::Error, }; +pub(crate) mod rewards; mod stats; +// Empirically derived value estimating the time to +// - drain and record the final batch of transactions, +// - produce the block footer, +// - produce the 'alpentick', +// - freeze the bank, +// - shred the final batches of the block, +// - broadcast. +// Recording stops this much before the slot timeout so block completion has time to finish before +// the leader window deadline. +const TIME_TO_COMPLETE_BLOCK_BROADCAST: Duration = Duration::from_millis(6); + /// Source of a leader-window notification consumed by BCL. enum ParentSource { /// Parent from ParentReady event for this leader window is already known. @@ -67,25 +80,39 @@ enum ParentSource { } pub struct BlockCreationLoop { - thread: JoinHandle<()>, + t_block_creation_loop: JoinHandle<()>, + reward_certs_service: RewardCertsService, } impl BlockCreationLoop { - pub fn new(config: BlockCreationLoopConfig) -> Self { - let thread = Builder::new() + pub fn new(config: BlockCreationLoopConfig) -> (Self, Sender) { + let (reward_certs_service, certs_requestor, votes_sender) = RewardCertsService::new( + config.cluster_info.clone(), + config.leader_schedule_cache.clone(), + config.sharable_banks.clone(), + config.exit.clone(), + ); + let t_block_creation_loop = Builder::new() .name("solBlkCreatLoop".to_string()) .spawn(move || { info!("BlockCreationLoop has started"); - start_loop(config); + start_loop(config, certs_requestor); info!("BlockCreationLoop has stopped"); }) .unwrap(); - Self { thread } + ( + Self { + t_block_creation_loop, + reward_certs_service, + }, + votes_sender, + ) } pub fn join(self) -> thread::Result<()> { - self.thread.join() + self.t_block_creation_loop.join()?; + self.reward_certs_service.join() } } @@ -94,6 +121,7 @@ pub struct BlockCreationLoopConfig { // Shared state pub bank_forks: Arc>, + pub sharable_banks: SharableBanks, pub bank_forks_controller: Arc, pub blockstore: Arc, pub cluster_info: Arc, @@ -107,7 +135,7 @@ pub struct BlockCreationLoopConfig { // Receivers / notifications from banking stage / replay / votor pub leader_window_info_receiver: Receiver, - pub highest_parent_ready: Arc>, + pub highest_parent_ready: Arc>, pub replay_highest_frozen: Arc, pub highest_finalized: Arc>>, @@ -115,11 +143,6 @@ pub struct BlockCreationLoopConfig { pub record_receiver_receiver: Receiver, pub optimistic_parent_receiver: Receiver, - /// Channel to send the request to build reward certs. - pub build_reward_certs_sender: Sender, - /// Channel to receive the built reward certs. - pub reward_certs_receiver: Receiver, - /// Sender for packets to banking stage (used to re-inject transactions after sad leader handover). pub banking_stage_sender: BankingPacketSender, } @@ -132,7 +155,7 @@ struct LeaderContext { /// ParentReady for a future window observed while producing the current one. pending_parent_ready: Option, /// Highest ParentReady event observed by Votor, used to abandon stale windows. - highest_parent_ready: Arc>, + highest_parent_ready: Arc>, highest_finalized: Arc>>, blockstore: Arc, @@ -145,8 +168,7 @@ struct LeaderContext { slot_status_notifier: Option, banking_tracer: Arc, replay_highest_frozen: Arc, - build_reward_certs_sender: Sender, - reward_certs_receiver: Receiver, + reward_certs_requestor: CertsRequestor, /// Banking-stage ingress used to reschedule transactions after sad handover. banking_stage_sender: BankingPacketSender, @@ -155,7 +177,7 @@ struct LeaderContext { slot_metrics: SlotMetrics, // Migration information - genesis_cert: GenesisCertificate, + genesis_cert_block_marker: GenesisCertBlockMarker, } #[derive(Default)] @@ -209,7 +231,7 @@ enum StartLeaderError { /// The `votor::consensus_pool_service` tracks when it is our leader window, and /// communicates the skip timer and parent slot for our window. This loop takes the responsibility /// of creating our `NUM_CONSECUTIVE_LEADER_SLOTS` blocks and finishing them within the required timeout. -fn start_loop(config: BlockCreationLoopConfig) { +fn start_loop(config: BlockCreationLoopConfig, reward_certs_requestor: CertsRequestor) { let BlockCreationLoopConfig { exit, bank_forks, @@ -226,10 +248,9 @@ fn start_loop(config: BlockCreationLoopConfig) { replay_highest_frozen, highest_parent_ready, optimistic_parent_receiver, - build_reward_certs_sender, - reward_certs_receiver, highest_finalized, banking_stage_sender, + sharable_banks: _, } = config; // Similar to Votor, if this loop dies kill the validator @@ -255,7 +276,7 @@ fn start_loop(config: BlockCreationLoopConfig) { .migration_status() .genesis_certificate() .expect("Migration complete, genesis certificate must exist"); - let genesis_cert = GenesisCertificate::try_from((*genesis_cert).clone()) + let genesis_cert_block_marker = GenesisCertBlockMarker::try_from((*genesis_cert).clone()) .expect("Genesis certificate must be valid"); info!("{my_pubkey}: PohService has shutdown, BlockCreationLoop is enabled"); @@ -276,13 +297,12 @@ fn start_loop(config: BlockCreationLoopConfig) { slot_status_notifier, banking_tracer, replay_highest_frozen, - build_reward_certs_sender, - reward_certs_receiver, + reward_certs_requestor, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), highest_finalized, - genesis_cert, + genesis_cert_block_marker, }; // Setup poh @@ -355,12 +375,12 @@ fn start_loop(config: BlockCreationLoopConfig) { let LeaderWindowInfo { start_slot, end_slot, - parent_block: (parent_slot, parent_hash), + parent_block, block_timer, } = info; trace!( - "{my_pubkey}: window {start_slot}-{end_slot} parent {parent_slot} \ + "{my_pubkey}: window {start_slot}-{end_slot} parent {parent_block:?} \ flh={fast_leader_handover}" ); @@ -373,8 +393,7 @@ fn start_loop(config: BlockCreationLoopConfig) { fast_leader_handover, start_slot, end_slot, - parent_slot, - parent_hash, + parent_block, block_timer, &mut ctx, ) { @@ -410,13 +429,11 @@ fn reset_poh_recorder(bank: &Arc, ctx: &LeaderContext) { .reset(bank.clone(), next_leader_slot); } -/// From the passed in bank (used for determining slot times) and leader block -/// index within the leader window, returns the duration after which we should -/// publish the final shred for the block with starting point being the start of -/// the leader window. -fn block_timeout(bank: &Bank, leader_block_index: usize) -> Duration { - Duration::from_nanos_u128(bank.ns_per_slot) - .saturating_mul((leader_block_index as u32).saturating_add(1)) +/// Returns the elapsed leader-window time at which `slot` must be completed. +fn block_timeout(bank: &Bank, slot: Slot) -> Duration { + Duration::from_nanos_u128(bank.ns_per_slot_at_slot(slot)) + .saturating_mul((leader_slot_index(slot) as u32).saturating_add(1)) + .saturating_sub(TIME_TO_COMPLETE_BLOCK_BROADCAST) } /// Select the freshest leader-window notification within one source. @@ -479,18 +496,14 @@ fn select_freshest_window( /// Clamps the block producer timestamp to ensure that the leader produces a timestamp that conforms /// to Alpenglow clock bounds. fn skew_block_producer_time_nanos( - parent_slot: Slot, parent_time_nanos: i64, - working_bank_slot: Slot, working_bank_time_nanos: i64, - ns_per_slot: u64, + elapsed_slot_duration_nanos: u128, ) -> i64 { let (min_working_bank_time, max_working_bank_time) = BlockComponentProcessor::nanosecond_time_bounds( - parent_slot, parent_time_nanos, - working_bank_slot, - ns_per_slot, + elapsed_slot_duration_nanos, ); working_bank_time_nanos @@ -515,29 +528,29 @@ fn produce_block_footer( if let Some(parent_bank) = bank.parent() { // Get parent time from alpenglow clock (nanoseconds) or fall back to clock sysvar (seconds -> nanoseconds) - let parent_time_nanos = bank + let parent_time_nanos = parent_bank .get_nanosecond_clock() .unwrap_or_else(|| bank.clock().unix_timestamp.saturating_mul(1_000_000_000)); let parent_slot = parent_bank.slot(); - let ns_per_slot = u64::try_from(bank.ns_per_slot).unwrap_or(u64::MAX); + let elapsed_slot_duration_nanos = + bank.slot_range_duration_nanos(parent_slot.saturating_add(1), slot); block_producer_time_nanos = skew_block_producer_time_nanos( - parent_slot, parent_time_nanos, - slot, block_producer_time_nanos, - ns_per_slot, + elapsed_slot_duration_nanos, ); } // Convert finalization certs into block marker - let final_cert = highest_finalized.map(ValidatedBlockFinalizationCert::to_final_certificate); + let block_final_cert = + highest_finalized.map(ValidatedBlockFinalizationCert::to_block_final_cert); BlockFooterV1 { bank_hash: Hash::default(), block_producer_time_nanos: block_producer_time_nanos as u64, block_user_agent: format!("agave/{}", version!()).into_bytes(), - final_cert, + block_final_cert, skip_reward_cert, notar_reward_cert, } @@ -549,16 +562,15 @@ fn produce_window( fast_leader_handover: bool, start_slot: Slot, end_slot: Slot, - parent_slot: Slot, - parent_hash: Hash, + parent_block: Block, mut block_timer: Instant, ctx: &mut LeaderContext, ) -> Result<(), StartLeaderError> { // Insert the first bank let mut working_bank = start_leader_wait_for_parent_replay( start_slot, - parent_slot, - Some(parent_hash), + parent_block.slot, + Some(parent_block.block_id), block_timer, ctx, )?; @@ -571,7 +583,7 @@ fn produce_window( let mut slot = start_slot; while !ctx.exit.load(Ordering::Relaxed) && slot <= end_slot { - let timeout = block_timeout(&working_bank, leader_slot_index(slot)); + let timeout = block_timeout(&working_bank, slot); trace!( "{my_pubkey}: waiting for leader bank {slot} to finish, remaining time: {}ms", timeout.saturating_sub(block_timer.elapsed()).as_millis() @@ -579,7 +591,7 @@ fn produce_window( let mut bank_completion_measure = Measure::start("bank_completion"); let optimistic_parent = - (fast_leader_handover && slot == start_slot).then_some((parent_slot, parent_hash)); + (fast_leader_handover && slot == start_slot).then_some(parent_block); if let Err(e) = record_and_complete_block(ctx, slot, optimistic_parent, &mut block_timer, timeout) { @@ -628,14 +640,14 @@ fn produce_window( fn record_and_complete_block( ctx: &mut LeaderContext, bank_slot: Slot, - mut optimistic_parent: Option<(Slot, Hash)>, + mut optimistic_parent: Option, block_timer: &mut Instant, block_timeout: Duration, ) -> Result<(), PohRecorderError> { - drain_stale_reward_certs(ctx, bank_slot); - ctx.build_reward_certs_sender - .send(BuildRewardCertsRequest { bank_slot }) - .map_err(|_| PohRecorderError::ChannelDisconnected)?; + let reward_cert_request = ctx + .reward_certs_requestor + .request_reward_certs(ctx.my_pubkey, bank_slot) + .map_err(|()| PohRecorderError::ChannelDisconnected)?; let mut accumulated_txs = vec![]; let mut records_shutdown = false; let window_has_moved_on = loop { @@ -736,7 +748,7 @@ fn record_and_complete_block( "optimistic_parent should be None after receiving ParentReady" ); - // Alpentick and clear bank + // Alpentick, produce the footer, and clear bank let mut w_poh_recorder = ctx.poh_recorder.write().unwrap(); let bank = w_poh_recorder .bank() @@ -754,22 +766,30 @@ fn record_and_complete_block( bank.set_tick_height(max_tick_height - 1); let footer = { - let reward_certs = recv_reward_certs(ctx, bank_slot)?; - let BuildRewardCertsRespSucc { + let reward_certs = ctx + .reward_certs_requestor + .recv_reward_certs(ctx.my_pubkey, reward_cert_request) + .map_err(|()| PohRecorderError::ChannelDisconnected)?; + let RewardRespSucc { skip, notar, - validators: _, + validators, } = reward_certs; - let reward_cert = ValidatedRewardCert::try_new(&bank, &skip, ¬ar)?; + let reward_cert = + ValidatedRewardCert::try_new_for_leader(bank.slot(), &skip, ¬ar, validators)?; let guard = ctx.highest_finalized.read().unwrap(); let footer = produce_block_footer(&bank, skip, notar, guard.as_ref()); let final_cert_input = guard.as_ref().map(|c| c.vote_rewards_input()); + // BankingStage may still be executing batches that were already recorded. + // Footer processing mutates vote accounts directly, so wait for execution to complete first. + bank.wait_for_inflight_commits(); + BlockComponentProcessor::update_bank_with_footer_fields( &bank, i64::try_from(footer.block_producer_time_nanos) .expect("locally produced block timestamp must fit in i64"), - Hash::default(), // Banks we produce do not need the bank hash mismatch check + None, // Banks we produce do not need the bank hash mismatch check reward_cert, final_cert_input, )?; @@ -791,7 +811,7 @@ fn process_parent_ready( ctx: &mut LeaderContext, info: LeaderWindowInfo, bank_slot: Slot, - optimistic_parent: &mut Option<(Slot, Hash)>, + optimistic_parent: &mut Option, accumulated_txs: &mut Vec, block_timer: &mut Instant, records_shutdown: &mut bool, @@ -803,8 +823,8 @@ fn process_parent_ready( } if info.start_slot == bank_slot { - if let Some(optimistic_parent_block) = optimistic_parent.take() { - if handle_parent_ready( + if let Some(optimistic_parent_block) = optimistic_parent.take() + && handle_parent_ready( ctx, info, optimistic_parent_block, @@ -812,9 +832,8 @@ fn process_parent_ready( block_timer, )? .is_some() - { - *records_shutdown = false; - } + { + *records_shutdown = false; } return Ok(false); } @@ -826,49 +845,6 @@ fn process_parent_ready( Ok(false) } -/// Drop reward-certificate responses left over from an older bank slot. -fn drain_stale_reward_certs(ctx: &LeaderContext, bank_slot: Slot) { - for reward_cert in ctx.reward_certs_receiver.try_iter() { - warn!( - "{}: dropping stale reward cert response for bank slot {}, before starting {bank_slot}", - ctx.my_pubkey, reward_cert.bank_slot - ); - } -} - -/// Wait for the reward-certificate response for `bank_slot`, ignoring stale -/// responses generated for banks that have already been abandoned. -fn recv_reward_certs( - ctx: &LeaderContext, - bank_slot: Slot, -) -> Result { - loop { - if ctx.exit.load(Ordering::Relaxed) { - return Err(PohRecorderError::ChannelDisconnected); - } - - match ctx - .reward_certs_receiver - .recv_timeout(Duration::from_millis(100)) - { - Ok(reward_certs) if reward_certs.bank_slot == bank_slot => { - break reward_certs.result.map_err(PohRecorderError::from); - } - Ok(reward_certs) => { - warn!( - "{}: ignoring stale reward cert response for bank slot {}, expected \ - {bank_slot}", - ctx.my_pubkey, reward_certs.bank_slot - ); - } - Err(RecvTimeoutError::Timeout) => continue, - Err(RecvTimeoutError::Disconnected) => { - return Err(PohRecorderError::ChannelDisconnected); - } - } - } -} - /// Stop record intake, drain any already-reserved records, and reset PoH after /// failing to complete the working bank. fn abort_failed_working_bank( @@ -903,12 +879,11 @@ fn send_update_parent( poh_recorder: &RwLock, new_parent_block: Block, ) -> Result<(), PohRecorderError> { - let (new_parent_slot, new_parent_block_id) = new_parent_block; let update_parent = UpdateParentV1 { - new_parent_slot, - new_parent_block_id, + new_parent_slot: new_parent_block.slot, + new_parent_block_id: new_parent_block.block_id, }; - let marker = VersionedBlockMarker::new_update_parent(update_parent); + let marker = VersionedBlockMarker::from_update_parent(update_parent); poh_recorder.write().unwrap().send_marker(marker)?; Ok(()) } @@ -927,7 +902,7 @@ fn send_update_parent( fn handle_parent_ready( ctx: &mut LeaderContext, leader_window_info: LeaderWindowInfo, - optimistic_parent_block: (Slot, Hash), + optimistic_parent_block: Block, mut accumulated_txs: Vec, block_timer: &mut Instant, ) -> Result>, PohRecorderError> { @@ -958,8 +933,11 @@ fn handle_parent_ready( send_update_parent(&ctx.poh_recorder, leader_window_info.parent_block)?; let slot = leader_window_info.start_slot; - let (old_parent_slot, _) = optimistic_parent_block; - let (new_parent_slot, new_parent_hash) = leader_window_info.parent_block; + let old_parent_slot = optimistic_parent_block.slot; + let Block { + slot: new_parent_slot, + block_id: new_parent_hash, + } = leader_window_info.parent_block; let bank = ctx .poh_recorder @@ -1014,6 +992,9 @@ fn handle_parent_ready( let batch: PacketBatch = packets.into(); let banking_packet_batch = Arc::new(vec![batch]); ctx.banking_stage_sender + // technically this send can evict to make room (which may drop a few packets) + // but this should (hopefully) not be significant amounts since we are evicting + // at most 1 batch. .send(banking_packet_batch) .map_err(|_| PohRecorderError::RescheduleTransactionsError(slot))?; } @@ -1068,10 +1049,7 @@ fn start_leader_wait_for_parent_replay( ctx.my_pubkey ); let my_pubkey = ctx.my_pubkey; - let timeout = block_timeout( - &ctx.bank_forks.read().unwrap().root_bank(), - leader_slot_index(slot), - ); + let timeout = block_timeout(&ctx.bank_forks.read().unwrap().root_bank(), slot); let end_slot = last_of_consecutive_leader_slots(slot); let mut slot_delay_start = Measure::start("slot_delay"); @@ -1299,6 +1277,13 @@ fn create_and_insert_leader_bank( &parent_bank.hash(), ); + // If this is the first alpenglow block, set PoH to low power mode. + // This is persisted in the recorder when we set bank. + // Replayers will update hashes_per_tick when they process the genesis certificate block marker. + if should_include_genesis_certificate(parent_slot, &ctx.genesis_cert_block_marker) { + tpu_bank.set_hashes_per_tick(None); + } + // Insert the bank let tpu_bank = ctx.bank_forks_controller.insert_bank(tpu_bank)?; @@ -1332,36 +1317,43 @@ fn maybe_include_genesis_certificate( parent_slot: Slot, ctx: &LeaderContext, ) -> Result<(), PohRecorderError> { - if parent_slot != ctx.genesis_cert.slot || parent_slot == 0 { + if !should_include_genesis_certificate(parent_slot, &ctx.genesis_cert_block_marker) { return Ok(()); } // Send the genesis certificate - let genesis_marker = VersionedBlockMarker::new_genesis_certificate(ctx.genesis_cert.clone()); + let block_marker = + VersionedBlockMarker::from_genesis_cert_block_marker(ctx.genesis_cert_block_marker.clone()); let mut poh_recorder = ctx.poh_recorder.write().unwrap(); - poh_recorder.send_marker(genesis_marker)?; + poh_recorder.send_marker(block_marker)?; // Process the genesis certificate let bank = poh_recorder.bank().expect("Bank cannot have been cleared"); - let processor = bank.block_component_processor.read().unwrap(); + let mut processor = bank.block_component_processor.write().unwrap(); processor - .on_genesis_certificate( + .on_genesis_cert_block_marker_leader( bank.clone(), - ctx.genesis_cert.clone(), + ctx.genesis_cert_block_marker.clone(), &ctx.bank_forks.read().unwrap().migration_status(), ) .expect("Recording genesis certificate should not fail"); Ok(()) } +fn should_include_genesis_certificate( + parent_slot: Slot, + genesis_cert_block_marker: &GenesisCertBlockMarker, +) -> bool { + parent_slot == genesis_cert_block_marker.slot && parent_slot != 0 +} + #[cfg(test)] mod tests { use { super::*, crate::banking_trace::BankingTracer, agave_banking_stage_ingress_types::BankingPacketReceiver, - agave_votor_messages::reward_certificate::{BuildRewardCertsRespError, RewardCertError}, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_entry::{block_component::VersionedUpdateParent, entry_or_marker::EntryOrMarker}, solana_keypair::Keypair, @@ -1390,8 +1382,8 @@ mod tests { )) } - fn test_genesis_certificate() -> GenesisCertificate { - GenesisCertificate { + fn test_genesis_cert_block_marker() -> GenesisCertBlockMarker { + GenesisCertBlockMarker { slot: Slot::MAX, block_id: Hash::default(), bls_signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), @@ -1421,19 +1413,17 @@ mod tests { Ok(self.bank_forks.write().unwrap().insert(bank)) } - fn set_root( + fn enqueue_set_root( &self, - _my_pubkey: Pubkey, _parent_slot: Slot, new_root: Slot, highest_super_majority_root: Option, - ) -> Result<(), BankForksControllerError> { + ) { // Test code only so we allow writing bank forks directly. self.bank_forks .write() .unwrap() .set_root(new_root, None, highest_super_majority_root); - Ok(()) } fn clear_bank(&self, slot: Slot) -> Result<(), BankForksControllerError> { @@ -1456,7 +1446,10 @@ mod tests { LeaderWindowInfo { start_slot, end_slot: last_of_consecutive_leader_slots(start_slot), - parent_block: (parent_slot, Hash::new_unique()), + parent_block: Block { + slot: parent_slot, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), } } @@ -1474,7 +1467,7 @@ mod tests { .unwrap(); assert_eq!(selected.start_slot, 16); - assert_eq!(selected.parent_block.0, 15); + assert_eq!(selected.parent_block.slot, 15); } #[test] @@ -1484,7 +1477,7 @@ mod tests { .unwrap(); assert_eq!(selected.start_slot, 8); - assert_eq!(selected.parent_block.0, 6); + assert_eq!(selected.parent_block.slot, 6); } fn recv_update_parent_marker( @@ -1554,18 +1547,23 @@ mod tests { let poh_recorder = Arc::new(RwLock::new(poh_recorder)); let (_record_sender, record_receiver) = record_channels(false); - let (_leader_window_info_sender, leader_window_info_receiver) = unbounded(); - let (build_reward_certs_sender, _build_reward_certs_receiver) = unbounded(); - let (reward_certs_sender, reward_certs_receiver) = unbounded(); + let (_leader_window_info_sender, leader_window_info_receiver) = bounded(1024); let (banking_stage_sender, _banking_stage_receiver) = BankingTracer::channel_for_test(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); + let (reward_certs_requestor, _receiver) = CertsRequestor::new(); let mut ctx = LeaderContext { exit, my_pubkey, leader_window_info_receiver, pending_parent_ready: None, - highest_parent_ready: Arc::new(RwLock::new((0, (0, Hash::default())))), + highest_parent_ready: Arc::new(RwLock::new(( + 0, + Block { + slot: 0, + block_id: Hash::default(), + }, + ))), highest_finalized: Arc::new(RwLock::new(None)), blockstore, record_receiver, @@ -1577,12 +1575,11 @@ mod tests { slot_status_notifier: None, banking_tracer: BankingTracer::new_disabled(), replay_highest_frozen: Arc::new(ReplayHighestFrozen::default()), - build_reward_certs_sender, - reward_certs_receiver, + reward_certs_requestor, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), - genesis_cert: test_genesis_certificate(), + genesis_cert_block_marker: test_genesis_cert_block_marker(), }; create_and_insert_leader_bank(1, root_bank.clone(), &mut ctx).unwrap(); @@ -1593,8 +1590,8 @@ mod tests { let in_flight_commit = bank.freeze_lock(); ctx.record_receiver.shutdown(); for _ in ctx.record_receiver.drain_after_shutdown() {} - let (abort_started_sender, abort_started_receiver) = unbounded(); - let (abort_done_sender, abort_done_receiver) = unbounded(); + let (abort_started_sender, abort_started_receiver) = bounded(1024); + let (abort_done_sender, abort_done_receiver) = bounded(1024); std::thread::scope(|scope| { scope.spawn(|| { abort_started_sender.send(()).unwrap(); @@ -1623,27 +1620,6 @@ mod tests { assert!(ctx.bank_forks.read().unwrap().get(1).is_none()); assert!(ctx.record_receiver.is_shutdown()); assert!(ctx.record_receiver.is_safe_to_restart()); - - reward_certs_sender - .send(BuildRewardCertsResponse { - bank_slot: 1, - result: Err(BuildRewardCertsRespError::RewardCertTryNew( - RewardCertError::InvalidBitmap, - )), - }) - .unwrap(); - reward_certs_sender - .send(BuildRewardCertsResponse { - bank_slot: 2, - result: Ok(BuildRewardCertsRespSucc { - validators: vec![my_pubkey], - ..BuildRewardCertsRespSucc::default() - }), - }) - .unwrap(); - - let reward_certs = recv_reward_certs(&ctx, 2).unwrap(); - assert_eq!(reward_certs.validators, vec![my_pubkey]); } #[test] @@ -1687,20 +1663,25 @@ mod tests { let poh_recorder = Arc::new(RwLock::new(poh_recorder)); let (_record_sender, record_receiver) = record_channels(false); - let (_leader_window_info_sender, leader_window_info_receiver) = unbounded(); - let (build_reward_certs_sender, _build_reward_certs_receiver) = unbounded(); - let (_reward_certs_sender, reward_certs_receiver) = unbounded(); + let (_leader_window_info_sender, leader_window_info_receiver) = bounded(1024); let (banking_stage_sender, _banking_stage_receiver) = BankingTracer::channel_for_test(); - let mut genesis_cert = test_genesis_certificate(); - genesis_cert.slot = parent_bank.slot(); + let mut genesis_cert_block_marker = test_genesis_cert_block_marker(); + genesis_cert_block_marker.slot = parent_bank.slot(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); + let (reward_certs_requestor, _receiver) = CertsRequestor::new(); let mut ctx = LeaderContext { exit, my_pubkey, leader_window_info_receiver, pending_parent_ready: None, - highest_parent_ready: Arc::new(RwLock::new((0, (0, Hash::default())))), + highest_parent_ready: Arc::new(RwLock::new(( + 0, + Block { + slot: 0, + block_id: Hash::default(), + }, + ))), highest_finalized: Arc::new(RwLock::new(None)), blockstore, record_receiver, @@ -1712,12 +1693,11 @@ mod tests { slot_status_notifier: None, banking_tracer: BankingTracer::new_disabled(), replay_highest_frozen: Arc::new(ReplayHighestFrozen::default()), - build_reward_certs_sender, - reward_certs_receiver, + reward_certs_requestor, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), - genesis_cert, + genesis_cert_block_marker, }; let err = create_and_insert_leader_bank(2, parent_bank, &mut ctx).unwrap_err(); @@ -1760,18 +1740,23 @@ mod tests { let poh_recorder = Arc::new(RwLock::new(poh_recorder)); let (record_sender, record_receiver) = record_channels(false); - let (_leader_window_info_sender, leader_window_info_receiver) = unbounded(); - let (build_reward_certs_sender, build_reward_certs_receiver) = unbounded(); - let (reward_certs_sender, reward_certs_receiver) = unbounded(); + let (_leader_window_info_sender, leader_window_info_receiver) = bounded(1024); let (banking_stage_sender, _banking_stage_receiver) = BankingTracer::channel_for_test(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); + let (reward_certs_requestor, _reward_request_receiver) = CertsRequestor::new(); let mut ctx = LeaderContext { exit, my_pubkey, leader_window_info_receiver, pending_parent_ready: None, - highest_parent_ready: Arc::new(RwLock::new((2, (0, Hash::default())))), + highest_parent_ready: Arc::new(RwLock::new(( + 2, + Block { + slot: 0, + block_id: Hash::default(), + }, + ))), highest_finalized: Arc::new(RwLock::new(None)), blockstore, record_receiver, @@ -1783,12 +1768,11 @@ mod tests { slot_status_notifier: None, banking_tracer: BankingTracer::new_disabled(), replay_highest_frozen: Arc::new(ReplayHighestFrozen::default()), - build_reward_certs_sender, - reward_certs_receiver, + reward_certs_requestor, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), - genesis_cert: test_genesis_certificate(), + genesis_cert_block_marker: test_genesis_cert_block_marker(), }; create_and_insert_leader_bank(1, root_bank, &mut ctx).unwrap(); @@ -1801,14 +1785,6 @@ mod tests { )) .unwrap(); - let delayed_reward = std::thread::spawn(move || { - std::thread::sleep(Duration::from_millis(300)); - let _ = reward_certs_sender.send(BuildRewardCertsResponse { - bank_slot: 1, - result: Ok(BuildRewardCertsRespSucc::default()), - }); - }); - let start = Instant::now(); let result = record_and_complete_block(&mut ctx, 1, None, &mut Instant::now(), Duration::ZERO); @@ -1818,15 +1794,6 @@ mod tests { assert!(ctx.bank_forks.read().unwrap().get(1).is_none()); assert!(ctx.record_receiver.is_shutdown()); assert!(ctx.record_receiver.is_safe_to_restart()); - assert_eq!( - build_reward_certs_receiver - .recv_timeout(Duration::from_secs(1)) - .unwrap() - .bank_slot, - 1 - ); - - delayed_reward.join().unwrap(); } #[test] @@ -1882,18 +1849,23 @@ mod tests { let poh_recorder = Arc::new(RwLock::new(poh_recorder)); let (record_sender, record_receiver) = record_channels(false); - let (leader_window_info_sender, leader_window_info_receiver) = unbounded(); - let (build_reward_certs_sender, _build_reward_certs_receiver) = unbounded(); - let (_reward_certs_sender, reward_certs_receiver) = unbounded(); + let (leader_window_info_sender, leader_window_info_receiver) = bounded(1024); let (banking_stage_sender, banking_stage_receiver) = BankingTracer::channel_for_test(); let bank_forks_controller = test_bank_forks_controller(bank_forks.clone()); + let (reward_certs_requestor, _receiver) = CertsRequestor::new(); let mut ctx = LeaderContext { exit, my_pubkey, leader_window_info_receiver, pending_parent_ready: None, - highest_parent_ready: Arc::new(RwLock::new((4, (new_parent_slot, new_parent_hash)))), + highest_parent_ready: Arc::new(RwLock::new(( + 4, + Block { + slot: new_parent_slot, + block_id: new_parent_hash, + }, + ))), highest_finalized: Arc::new(RwLock::new(None)), blockstore, record_receiver, @@ -1905,12 +1877,11 @@ mod tests { slot_status_notifier: None, banking_tracer: BankingTracer::new_disabled(), replay_highest_frozen: Arc::new(ReplayHighestFrozen::default()), - build_reward_certs_sender, - reward_certs_receiver, + reward_certs_requestor, banking_stage_sender, metrics: LoopMetrics::default(), slot_metrics: SlotMetrics::default(), - genesis_cert: test_genesis_certificate(), + genesis_cert_block_marker: test_genesis_cert_block_marker(), }; let leader_slot = 4; @@ -1930,13 +1901,19 @@ mod tests { let parent_ready = LeaderWindowInfo { start_slot: leader_slot, end_slot: 7, - parent_block: (new_parent_slot, new_parent_hash), + parent_block: Block { + slot: new_parent_slot, + block_id: new_parent_hash, + }, block_timer: Instant::now(), }; let new_bank = handle_parent_ready( &mut ctx, parent_ready, - (optimistic_parent_slot, optimistic_parent_hash), + Block { + slot: optimistic_parent_slot, + block_id: optimistic_parent_hash, + }, vec![accumulated_tx.clone()], &mut Instant::now(), ) diff --git a/core/src/block_creation_loop/rewards/certs_builder.rs b/core/src/block_creation_loop/rewards/certs_builder.rs new file mode 100644 index 00000000000..3884aed583d --- /dev/null +++ b/core/src/block_creation_loop/rewards/certs_builder.rs @@ -0,0 +1,138 @@ +use { + crate::block_creation_loop::rewards::msg_types::{ + RewardRequest, RewardRespSucc, RewardResponse, + }, + agave_bls_sigverify::rewards::rewards_wants_vote, + agave_votor_messages::{ + consensus_message::VoteMessage, + reward_certificate::{BuildRewardCertsRespError, NUM_SLOTS_FOR_REWARD}, + }, + crossbeam_channel::RecvError, + entry::Entry, + solana_clock::Slot, + solana_gossip::cluster_info::ClusterInfo, + solana_ledger::leader_schedule_cache::LeaderScheduleCache, + solana_runtime::bank::Bank, + std::{collections::BTreeMap, sync::Arc}, +}; + +mod entry; + +/// Container to store state needed to generate reward certificates. +pub(super) struct CertsBuilder { + /// Per [`Slot`], stores skip and notar votes. + votes: BTreeMap, + /// Stores the latest pubkey for the current node. + cluster_info: Arc, + /// Stores the leader schedules. + leader_schedule: Arc, +} + +impl CertsBuilder { + /// Constructs a new instance of [`CertsBuilder`]. + pub(super) fn new( + cluster_info: Arc, + leader_schedule: Arc, + ) -> Self { + Self { + votes: BTreeMap::default(), + cluster_info, + leader_schedule, + } + } + + /// Builds reward certificates. + fn build_certs( + &mut self, + bank_slot: Slot, + ) -> Result { + let Some(reward_slot) = bank_slot.checked_sub(NUM_SLOTS_FOR_REWARD) else { + return Ok(RewardRespSucc::default()); + }; + // we assume that the block creation loop will only ever request to build reward certs in a + // strictly increasing order so we can drop older state + self.votes = self.votes.split_off(&reward_slot); + match self.votes.remove(&reward_slot) { + None => Ok(RewardRespSucc::default()), + Some(entry) => entry.build_certs(reward_slot), + } + } + + pub(super) fn build_request( + &mut self, + request: Result, + ) -> Result<(), ()> { + let my_pubkey = self.cluster_info.id(); + match request { + Ok(RewardRequest { + bank_slot, + reply_sender, + }) => { + let resp = RewardResponse { + result: self.build_certs(bank_slot), + }; + let _ = reply_sender.send(resp).inspect_err(|_| { + info!( + "{my_pubkey}: channel to send reply for bank_slot={bank_slot} disconnected" + ); + }); + Ok(()) + } + Err(_) => { + error!("{my_pubkey}: build reward certs channel is disconnected; exiting."); + Err(()) + } + } + } + + /// Returns [`true`] if the rewards container is interested in this vote else [`false`]. + fn wants_vote(&self, root_slot: Slot, vote: &VoteMessage) -> bool { + if !rewards_wants_vote( + &self.cluster_info, + &self.leader_schedule, + root_slot, + &vote.vote, + ) { + return false; + } + let Some(entry) = self.votes.get(&vote.vote.slot()) else { + return true; + }; + entry.wants_vote(vote) + } + + /// Adds received [`VoteMessage`] from other validators. + pub(super) fn add_vote(&mut self, root_bank: &Bank, vote: &VoteMessage) { + let slot = vote.vote.slot(); + let Some(rank_map) = root_bank.get_rank_map(slot) else { + warn!( + "failed to look up rank_map for slot {slot} using bank for slot {}", + root_bank.slot() + ); + return; + }; + let max_validators = rank_map.len(); + let root_slot = root_bank.slot(); + // drop state that is too old based on how the root slot has progressed + // TODO: if this actually purges state, that probably indicates that the leader missed its + // window. We should have a metric for this. + self.votes = self + .votes + .split_off(&root_slot.saturating_sub(NUM_SLOTS_FOR_REWARD)); + + if !self.wants_vote(root_slot, vote) { + return; + } + match self + .votes + .entry(vote.vote.slot()) + .or_insert(Entry::new(max_validators)) + .add_vote(rank_map, vote) + { + Ok(()) => (), + Err(e) => { + warn!("Adding vote {vote:?} failed with {e}"); + } + } + } +} diff --git a/votor/src/consensus_rewards/entry.rs b/core/src/block_creation_loop/rewards/certs_builder/entry.rs similarity index 84% rename from votor/src/consensus_rewards/entry.rs rename to core/src/block_creation_loop/rewards/certs_builder/entry.rs index 711cc1beda5..aa891a17817 100644 --- a/votor/src/consensus_rewards/entry.rs +++ b/core/src/block_creation_loop/rewards/certs_builder/entry.rs @@ -1,5 +1,6 @@ use { - super::{BuildRewardCertsRespError, BuildRewardCertsRespSucc}, + super::BuildRewardCertsRespError, + crate::block_creation_loop::rewards::msg_types::RewardRespSucc, agave_votor_messages::{ consensus_message::VoteMessage, reward_certificate::SkipRewardCertificate, vote::Vote, }, @@ -69,7 +70,7 @@ impl Entry { rank_map, vote.rank, &vote.signature, - notar.block_id, + notar.block.block_id, self.max_validators, ), Vote::Skip(_) => self.skip.add_vote(rank_map, vote.rank, &vote.signature), @@ -81,7 +82,7 @@ impl Entry { pub(super) fn build_certs( self, reward_slot: Slot, - ) -> Result { + ) -> Result { let notar = self.notar.build_cert(reward_slot)?; let skip = match self.skip.build_sig_bitmap() { Err(e) => match e { @@ -107,7 +108,7 @@ impl Entry { } }; - Ok(BuildRewardCertsRespSucc { + Ok(RewardRespSucc { skip, notar, validators, @@ -119,6 +120,8 @@ impl Entry { mod tests { use { super::*, + agave_votor_messages::{consensus_message::Block, wire::get_vote_payload_to_sign}, + rand::Rng, solana_bls_signatures::{Keypair as BlsKeypair, PubkeyCompressed as BlsPubkeyCompressed}, solana_epoch_schedule::EpochSchedule, solana_hash::Hash, @@ -140,8 +143,13 @@ mod tests { } } - pub(crate) fn new_vote(vote: Vote, rank: usize, keypairs: &[BlsKeypair]) -> VoteMessage { - let serialized = bincode::serialize(&vote).unwrap(); + pub(crate) fn new_vote( + vote: Vote, + rank: usize, + keypairs: &[BlsKeypair], + shred_version: u16, + ) -> VoteMessage { + let serialized = get_vote_payload_to_sign(vote, shred_version); let signature = keypairs[rank].sign(&serialized).into(); VoteMessage { vote, @@ -154,6 +162,14 @@ mod tests { max_validators: usize, slot: Slot, ) -> (Arc, Vec) { + get_rank_map_keypairs_with_stakes(vec![100; max_validators], slot) + } + + pub(crate) fn get_rank_map_keypairs_with_stakes( + stakes: Vec, + slot: Slot, + ) -> (Arc, Vec) { + let max_validators = stakes.len(); let validator_keypairs = (0..max_validators) .map(|_| ValidatorVoteKeypairs::new_rand()) .collect::>(); @@ -169,7 +185,7 @@ mod tests { let mut genesis_config = create_genesis_config_with_alpenglow_vote_accounts( 1_000_000_000, &validator_keypairs, - vec![100; validator_keypairs.len()], + stakes, ) .genesis_config; genesis_config.epoch_schedule = EpochSchedule::without_warmup(); @@ -199,13 +215,14 @@ mod tests { let slot = 123; let max_validators = 5; let (rank_map, keypairs) = get_rank_map_keypairs(max_validators, slot); + let shred_version = rand::rng().random(); let mut entry = Entry::new(max_validators); let resp = entry.clone().build_certs(slot).unwrap(); assert_eq!(resp.skip, None); assert_eq!(resp.notar, None); let skip = Vote::new_skip_vote(7); - let vote = new_vote(skip, 0, &keypairs); + let vote = new_vote(skip, 0, &keypairs, shred_version); entry.add_vote(&rank_map, &vote).unwrap(); let resp = entry.build_certs(slot).unwrap(); assert_eq!(resp.notar, None); @@ -218,6 +235,7 @@ mod tests { fn validate_build_notar_cert() { let slot = 123; let max_validators = 5; + let shred_version = rand::rng().random(); let (rank_map, keypairs) = get_rank_map_keypairs(max_validators, slot); let mut entry = Entry::new(max_validators); @@ -229,13 +247,19 @@ mod tests { let blockid1 = Hash::new_unique(); for rank in 0..2 { - let notar = Vote::new_notarization_vote(slot, blockid0); - let vote = new_vote(notar, rank, &keypairs); + let notar = Vote::new_notarization_vote(Block { + slot, + block_id: blockid0, + }); + let vote = new_vote(notar, rank, &keypairs, shred_version); entry.add_vote(&rank_map, &vote).unwrap(); } for rank in 2..5 { - let notar = Vote::new_notarization_vote(slot, blockid1); - let vote = new_vote(notar, rank, &keypairs); + let notar = Vote::new_notarization_vote(Block { + slot, + block_id: blockid1, + }); + let vote = new_vote(notar, rank, &keypairs, shred_version); entry.add_vote(&rank_map, &vote).unwrap(); } let resp = entry.build_certs(slot).unwrap(); diff --git a/votor/src/consensus_rewards/entry/notar_entry.rs b/core/src/block_creation_loop/rewards/certs_builder/entry/notar_entry.rs similarity index 75% rename from votor/src/consensus_rewards/entry/notar_entry.rs rename to core/src/block_creation_loop/rewards/certs_builder/entry/notar_entry.rs index 9315e6d25dc..2d383fb288e 100644 --- a/votor/src/consensus_rewards/entry/notar_entry.rs +++ b/core/src/block_creation_loop/rewards/certs_builder/entry/notar_entry.rs @@ -3,8 +3,7 @@ use { super::{AddVoteError, BuildSigBitmapError, partial_cert::PartialCert}, - crate::consensus_rewards::BuildRewardCertsRespError, - agave_votor_messages::reward_certificate::NotarRewardCertificate, + agave_votor_messages::reward_certificate::{BuildRewardCertsRespError, NotarRewardCertificate}, solana_bls_signatures::Signature as BLSSignature, solana_clock::Slot, solana_hash::Hash, @@ -66,15 +65,13 @@ impl NotarEntry { self, reward_slot: Slot, ) -> Result)>, BuildRewardCertsRespError> { - // we can only submit one notar rewards certificate but different validators may vote for different blocks and we cannot combine notar votes for different blocks together in one cert. - // ideally we should pick the block_id with the most stake to maximum leader rewards. - // we expect this to be rare enough that picking the block_id with the most votes should be fine in most cases. - - let res = self + // We can only submit one notar rewards certificate, but different validators may vote for + // different block ids. Pick the block id with the most stake to maximize leader rewards. + let selected = self .partials .into_iter() - .max_by_key(|(_block_id, partial)| partial.votes_seen()); - let Some((block_id, partial)) = res else { + .max_by_key(|(_block_id, partial)| partial.stake()); + let Some((block_id, partial)) = selected else { return Ok(None); }; match partial.build_sig_bitmap() { @@ -95,10 +92,14 @@ impl NotarEntry { mod tests { use { super::*, - crate::consensus_rewards::entry::tests::{ - get_rank_map_keypairs, new_vote, validate_bitmap, + crate::block_creation_loop::rewards::certs_builder::entry::tests::{ + get_rank_map_keypairs, get_rank_map_keypairs_with_stakes, new_vote, validate_bitmap, + }, + agave_votor_messages::{ + consensus_message::{Block, VoteMessage}, + vote::Vote, }, - agave_votor_messages::{consensus_message::VoteMessage, vote::Vote}, + rand::Rng, solana_bls_signatures::signature::BLS_SIGNATURE_AFFINE_SIZE, solana_hash::Hash, }; @@ -107,12 +108,16 @@ mod tests { fn validator_add_vote() { let slot = 123; let max_validators = 5; + let shred_version = rand::rng().random(); let (rank_map, keypairs) = get_rank_map_keypairs(max_validators, slot); let rank = 0; let mut entry = NotarEntry::new(max_validators); let blockid0 = Hash::new_unique(); - let notar = Vote::new_notarization_vote(slot, blockid0); + let notar = Vote::new_notarization_vote(Block { + slot, + block_id: blockid0, + }); let invalid_vote = VoteMessage { vote: notar, signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), @@ -128,7 +133,7 @@ mod tests { ) .unwrap_err(); - let vote = new_vote(notar, rank as usize, &keypairs); + let vote = new_vote(notar, rank as usize, &keypairs, shred_version); entry .add_vote( &rank_map, @@ -154,7 +159,9 @@ mod tests { fn validate_build_cert() { let slot = 123; let max_validators = 5; - let (rank_map, keypairs) = get_rank_map_keypairs(max_validators, slot); + let (rank_map, keypairs) = + get_rank_map_keypairs_with_stakes(vec![1_000, 900, 10, 10, 10], slot); + let shred_version = rand::rng().random(); let mut entry = NotarEntry::new(max_validators); assert_eq!(entry.clone().build_cert(slot).unwrap(), None); @@ -163,8 +170,11 @@ mod tests { let blockid1 = Hash::new_unique(); for rank in 0..2 { - let notar = Vote::new_notarization_vote(slot, blockid0); - let vote = new_vote(notar, rank, &keypairs); + let notar = Vote::new_notarization_vote(Block { + slot, + block_id: blockid0, + }); + let vote = new_vote(notar, rank, &keypairs, shred_version); entry .add_vote( &rank_map, @@ -176,8 +186,11 @@ mod tests { .unwrap(); } for rank in 2..5 { - let notar = Vote::new_notarization_vote(slot, blockid1); - let vote = new_vote(notar, rank, &keypairs); + let notar = Vote::new_notarization_vote(Block { + slot, + block_id: blockid1, + }); + let vote = new_vote(notar, rank, &keypairs, shred_version); entry .add_vote( &rank_map, @@ -190,7 +203,8 @@ mod tests { } let (notar_cert, _) = entry.build_cert(slot).unwrap().unwrap(); assert_eq!(notar_cert.slot, slot); - assert_eq!(notar_cert.block_id, blockid1); - validate_bitmap(notar_cert.bitmap(), 3, 5); + // We should pick the block id with the most stake (not the most votes) + assert_eq!(notar_cert.block_id, blockid0); + validate_bitmap(notar_cert.bitmap(), 2, 5); } } diff --git a/votor/src/consensus_rewards/entry/partial_cert.rs b/core/src/block_creation_loop/rewards/certs_builder/entry/partial_cert.rs similarity index 80% rename from votor/src/consensus_rewards/entry/partial_cert.rs rename to core/src/block_creation_loop/rewards/certs_builder/entry/partial_cert.rs index c4be4b65279..0d4a055fd86 100644 --- a/votor/src/consensus_rewards/entry/partial_cert.rs +++ b/core/src/block_creation_loop/rewards/certs_builder/entry/partial_cert.rs @@ -27,8 +27,8 @@ pub(super) struct PartialCert { signature: SignatureProjective, /// bitvec of ranks whose signatures is included in the aggregate above. bitvec: BitVec, - /// number of signatures in the aggregate above. - cnt: usize, + /// total stake represented by the signatures in the aggregate above. + stake: u64, validators: Vec, } @@ -38,7 +38,7 @@ impl PartialCert { Self { signature: SignatureProjective::identity(), bitvec: BitVec::repeat(false, max_validators), - cnt: 0, + stake: 0, validators: Vec::with_capacity(max_validators), } } @@ -64,16 +64,15 @@ impl PartialCert { if *ind { return Err(AddVoteError::Duplicate); } - let pubkey = rank_map + let entry = rank_map .get_pubkey_stake_entry(rank.into()) - .unwrap() - .vote_account_pubkey; - self.validators.push(pubkey); + .ok_or(AddVoteError::InvalidRank)?; self.signature.aggregate_with(std::iter::once(signature))?; + self.validators.push(entry.vote_account_pubkey); + self.stake = self.stake.saturating_add(entry.stake.get()); *ind = true; } } - self.cnt = self.cnt.saturating_add(1); Ok(()) } @@ -83,7 +82,7 @@ impl PartialCert { pub(super) fn build_sig_bitmap( self, ) -> Result<(BLSSignatureCompressed, Vec, Vec), BuildSigBitmapError> { - if self.cnt == 0 { + if self.validators.is_empty() { return Err(BuildSigBitmapError::Empty); } let mut bitvec = self.bitvec.clone(); @@ -94,9 +93,9 @@ impl PartialCert { Ok((signature, bitmap, self.validators)) } - /// Returns how many votes have been observed. - pub(super) fn votes_seen(&self) -> usize { - self.cnt + /// Returns how much stake has been observed. + pub(super) fn stake(&self) -> u64 { + self.stake } } @@ -104,15 +103,18 @@ impl PartialCert { mod tests { use { super::*, - crate::consensus_rewards::entry::tests::{ + crate::block_creation_loop::rewards::certs_builder::entry::tests::{ get_rank_map_keypairs, new_vote, validate_bitmap, }, - agave_votor_messages::{consensus_message::VoteMessage, vote::Vote}, + agave_votor_messages::{ + consensus_message::VoteMessage, vote::Vote, wire::get_vote_payload_to_sign, + }, + rand::Rng, solana_bls_signatures::Keypair as BlsKeypair, }; fn new_invalid_vote(vote: Vote, rank: usize) -> VoteMessage { - let serialized = bincode::serialize(&vote).unwrap(); + let serialized = get_vote_payload_to_sign(vote, 0); let keypair = BlsKeypair::new(); let signature = keypair.sign(&serialized).into(); VoteMessage { @@ -122,26 +124,11 @@ mod tests { } } - #[test] - fn validate_votes_seen() { - let slot = 123; - let max_validators = 2; - let (rank_map, keypairs) = get_rank_map_keypairs(max_validators, slot); - let skip = Vote::new_skip_vote(7); - let mut partial_cert = PartialCert::new(max_validators); - for rank in 0..max_validators { - let vote = new_vote(skip, rank, &keypairs); - partial_cert - .add_vote(&rank_map, vote.rank, &vote.signature) - .unwrap(); - assert_eq!(partial_cert.votes_seen(), rank + 1); - } - } - #[test] fn validate_build_sig_bitmap() { let slot = 123; let max_validators = 2; + let shred_version = rand::rng().random(); let (rank_map, keypairs) = get_rank_map_keypairs(max_validators, slot); let mut partial_cert = PartialCert::new(max_validators); assert!(matches!( @@ -150,7 +137,7 @@ mod tests { )); let skip = Vote::new_skip_vote(slot); for rank in 0..max_validators { - let vote = new_vote(skip, rank, &keypairs); + let vote = new_vote(skip, rank, &keypairs, shred_version); partial_cert .add_vote(&rank_map, vote.rank, &vote.signature) .unwrap(); @@ -163,6 +150,7 @@ mod tests { fn validate_add_vote() { let slot = 123; let max_validators = 2; + let shred_version = rand::rng().random(); let (rank_map, keypairs) = get_rank_map_keypairs(max_validators, slot); let mut partial_cert = PartialCert::new(max_validators); let skip = Vote::new_skip_vote(slot); @@ -171,7 +159,7 @@ mod tests { partial_cert.add_vote(&rank_map, vote.rank, &vote.signature), Err(AddVoteError::InvalidRank) )); - let vote = new_vote(skip, 0, &keypairs); + let vote = new_vote(skip, 0, &keypairs, shred_version); partial_cert .add_vote(&rank_map, vote.rank, &vote.signature) .unwrap(); @@ -179,11 +167,11 @@ mod tests { partial_cert.add_vote(&rank_map, vote.rank, &vote.signature), Err(AddVoteError::Duplicate) )); - let vote = new_vote(skip, 1, &keypairs); + let vote = new_vote(skip, 1, &keypairs, shred_version); partial_cert .add_vote(&rank_map, vote.rank, &vote.signature) .unwrap(); - let vote = new_vote(skip, 0, &keypairs); + let vote = new_vote(skip, 0, &keypairs, shred_version); assert!(matches!( partial_cert.add_vote(&rank_map, vote.rank, &vote.signature), Err(AddVoteError::Duplicate) @@ -194,18 +182,19 @@ mod tests { fn validate_wants_vote() { let slot = 123; let max_validators = 2; + let shred_version = rand::rng().random(); let (rank_map, keypairs) = get_rank_map_keypairs(max_validators, slot); let skip = Vote::new_skip_vote(slot); let mut partial_cert = PartialCert::new(max_validators); let vote = new_invalid_vote(skip, 2); assert!(!partial_cert.wants_vote(vote.rank)); - let vote = new_vote(skip, 0, &keypairs); + let vote = new_vote(skip, 0, &keypairs, shred_version); assert!(partial_cert.wants_vote(vote.rank)); partial_cert .add_vote(&rank_map, vote.rank, &vote.signature) .unwrap(); assert!(!partial_cert.wants_vote(vote.rank)); - let vote = new_vote(skip, 1, &keypairs); + let vote = new_vote(skip, 1, &keypairs, shred_version); partial_cert .add_vote(&rank_map, vote.rank, &vote.signature) .unwrap(); diff --git a/core/src/block_creation_loop/rewards/certs_requestor.rs b/core/src/block_creation_loop/rewards/certs_requestor.rs new file mode 100644 index 00000000000..f62dc46ada2 --- /dev/null +++ b/core/src/block_creation_loop/rewards/certs_requestor.rs @@ -0,0 +1,141 @@ +use { + crate::block_creation_loop::rewards::msg_types::{ + RewardRequest, RewardRequestToken, RewardRespSucc, + }, + crossbeam_channel::{Receiver, Sender, TryRecvError, TrySendError, bounded}, + solana_clock::Slot, + solana_pubkey::Pubkey, +}; + +/// Struct to handle requests to build rewards certs and receive the produced certs. +pub(crate) struct CertsRequestor { + req_sender: Sender, +} + +impl CertsRequestor { + pub(crate) fn new() -> (Self, Receiver) { + // If the rewards container is keeping up, we should only ever expect there to be 1 msg in + // flight. So using a fairly small capacity to allow the rewards container to catch up if + // it ever falls behind. + const CAPACITY: usize = 16; + + let (tx, rx) = bounded(CAPACITY); + (Self { req_sender: tx }, rx) + } + + /// Tries to send a message requesting a reward cert to be built. + /// + /// Does not block trying to send a message as we have a tight deadline to produce the block + /// and if the rewards container is not keeping up, then we would rather not include a rewards + /// cert than wait. + /// + /// Returns: + /// - `Ok(Some(RewardCertRequest))`` if sending succeeded. The `RewardCertRequest` can be + /// used to receive a response to the build the reward cert. + /// - `Ok(None)` if sending failed because the channel is full i.e. the rewards container is + /// not keeping up. + /// - `Err(())` if the channel to the rewards container is disconnected. + pub(crate) fn request_reward_certs( + &mut self, + my_pubkey: Pubkey, + bank_slot: Slot, + ) -> Result, ()> { + let (reply_sender, reply_receiver) = bounded(1); + let request = RewardRequest { + bank_slot, + reply_sender, + }; + match self.req_sender.try_send(request) { + Ok(()) => Ok(Some(RewardRequestToken { reply_receiver })), + Err(TrySendError::Full(_)) => { + warn!( + "{my_pubkey} sending request to build reward cert for bank_slot={bank_slot} \ + failed, channel is full" + ); + Ok(None) + } + Err(TrySendError::Disconnected(_)) => Err(()), + } + } + + /// Tries to receive a response for an earlier request to build reward certs. + /// + /// Does not block trying to receive the response as we have a tight deadline to produce + /// the block and if the rewards container is not keeping up, we will rather not include any + /// reward certs than wait for it. + pub(crate) fn recv_reward_certs( + &mut self, + my_pubkey: Pubkey, + request: Option, + ) -> Result { + match request { + None => Ok(RewardRespSucc::default()), + Some(request) => { + let msg = request.reply_receiver.try_recv(); + match msg { + Err(TryRecvError::Empty) => { + warn!("{my_pubkey} trying to receive cert failed, channel is empty"); + Ok(RewardRespSucc::default()) + } + Err(TryRecvError::Disconnected) => Err(()), + Ok(resp) => match resp.result { + Ok(res) => Ok(res), + Err(err) => { + error!("{my_pubkey} building reward cert failed with {err:?}"); + Ok(RewardRespSucc::default()) + } + }, + } + } + } + } +} + +#[cfg(test)] +mod tests { + + use {super::*, crate::block_creation_loop::rewards::msg_types::RewardResponse}; + + #[test] + fn test_does_not_block() { + let my_pubkey = Pubkey::default(); + let (mut handler, _receiver) = CertsRequestor::new(); + handler.request_reward_certs(my_pubkey, 1).unwrap().unwrap(); + + let resp = handler.recv_reward_certs(my_pubkey, None).unwrap(); + assert_eq!(resp, RewardRespSucc::default()); + + { + let (_reply_sender, reply_receiver) = bounded(0); + let request = RewardRequestToken { reply_receiver }; + let resp = handler.recv_reward_certs(my_pubkey, Some(request)).unwrap(); + assert_eq!(resp, RewardRespSucc::default()); + } + } + + #[test] + fn test_basic_functionality() { + let my_pubkey = Pubkey::default(); + let (mut handler, receiver) = CertsRequestor::new(); + let request = handler.request_reward_certs(my_pubkey, 1).unwrap().unwrap(); + let validators = vec![Pubkey::new_unique(); 10]; + + let RewardRequest { + bank_slot: _, + reply_sender, + } = receiver.recv().unwrap(); + reply_sender + .send(RewardResponse { + result: Ok(RewardRespSucc { + skip: None, + notar: None, + validators: validators.clone(), + }), + }) + .unwrap(); + let resp = handler.recv_reward_certs(my_pubkey, Some(request)).unwrap(); + assert!(resp.skip.is_none()); + assert!(resp.notar.is_none()); + assert_eq!(resp.validators, validators); + } +} diff --git a/core/src/block_creation_loop/rewards/mod.rs b/core/src/block_creation_loop/rewards/mod.rs new file mode 100644 index 00000000000..a7941cb856d --- /dev/null +++ b/core/src/block_creation_loop/rewards/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod certs_builder; +pub(super) mod certs_requestor; +pub(crate) mod msg_types; +pub(crate) mod reward_certs_service; diff --git a/core/src/block_creation_loop/rewards/msg_types.rs b/core/src/block_creation_loop/rewards/msg_types.rs new file mode 100644 index 00000000000..901e4ebb600 --- /dev/null +++ b/core/src/block_creation_loop/rewards/msg_types.rs @@ -0,0 +1,39 @@ +use { + agave_votor_messages::reward_certificate::{ + BuildRewardCertsRespError, NotarRewardCertificate, SkipRewardCertificate, + }, + crossbeam_channel::{Receiver, Sender}, + solana_clock::Slot, + solana_pubkey::Pubkey, +}; + +/// Request to build reward certificates. +pub(crate) struct RewardRequest { + /// The bank slot which will include the built reward certs. + pub(crate) bank_slot: Slot, + /// The channel on which to send the reply. + pub(super) reply_sender: Sender, +} + +/// Response when the reward certs are built successfully. +#[derive(Default, Debug, PartialEq, Eq)] +pub(crate) struct RewardRespSucc { + /// Skip reward certificate. None if no skip votes were registered. + pub(crate) skip: Option, + /// Notar reward certificate. None if no notar votes were registered. + pub(crate) notar: Option, + /// If at least one of the certs above is present, then this contains the slot for which the + /// reward certs were built and the list of validators in the certs. + pub(crate) validators: Vec, +} + +/// Response to a [`RewardRequest`]. +pub(crate) struct RewardResponse { + /// The result of building reward certs for `bank_slot`. + pub(crate) result: Result, +} + +/// The request token to be used to receive responses on previously sent requests to build reward certs. +pub(crate) struct RewardRequestToken { + pub(super) reply_receiver: Receiver, +} diff --git a/core/src/block_creation_loop/rewards/reward_certs_service.rs b/core/src/block_creation_loop/rewards/reward_certs_service.rs new file mode 100644 index 00000000000..23d028a365b --- /dev/null +++ b/core/src/block_creation_loop/rewards/reward_certs_service.rs @@ -0,0 +1,117 @@ +use { + crate::{ + block_creation_loop::rewards::{ + certs_builder::CertsBuilder, certs_requestor::CertsRequestor, msg_types::RewardRequest, + }, + tvu::MAX_ALPENGLOW_PACKET_NUM, + }, + agave_votor_messages::reward_certificate::AddVoteMessage, + crossbeam_channel::{Receiver, Sender, bounded, select_biased}, + solana_gossip::cluster_info::ClusterInfo, + solana_ledger::leader_schedule_cache::LeaderScheduleCache, + solana_runtime::bank_forks::SharableBanks, + std::{ + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + thread::{self, Builder, JoinHandle}, + time::Duration, + }, +}; + +pub(crate) struct RewardCertsService { + handler: JoinHandle<()>, +} + +impl RewardCertsService { + pub(crate) fn new( + cluster_info: Arc, + leader_schedule: Arc, + sharable_banks: SharableBanks, + exit: Arc, + ) -> (Self, CertsRequestor, Sender) { + let (votes_sender, votes_receiver) = bounded(MAX_ALPENGLOW_PACKET_NUM); + let (certs_requestor, req_receiver) = CertsRequestor::new(); + let builder = CertsBuilder::new(cluster_info.clone(), leader_schedule); + let ctx = Context::new( + exit, + cluster_info, + votes_receiver, + req_receiver, + sharable_banks, + builder, + ); + let handler = Builder::new() + .name("solConsRew".to_string()) + .spawn(move || { + ctx.run(); + }) + .unwrap(); + (Self { handler }, certs_requestor, votes_sender) + } + + pub(crate) fn join(self) -> thread::Result<()> { + self.handler.join() + } +} + +struct Context { + exit: Arc, + cluster_info: Arc, + votes_receiver: Receiver, + req_receiver: Receiver, + sharable_banks: SharableBanks, + builder: CertsBuilder, +} + +impl Context { + fn new( + exit: Arc, + cluster_info: Arc, + votes_receiver: Receiver, + req_receiver: Receiver, + sharable_banks: SharableBanks, + builder: CertsBuilder, + ) -> Self { + Self { + exit, + cluster_info, + votes_receiver, + req_receiver, + sharable_banks, + builder, + } + } + + fn run(mut self) { + while !self.exit.load(Ordering::Relaxed) { + let my_pubkey = self.cluster_info.id(); + // bias messages to build certificates as that is on the critical path + select_biased! { + recv(self.req_receiver) -> msg => { + if let Err(()) = self.builder.build_request(msg) { + break; + } + } + recv(self.votes_receiver) -> msg => { + match msg { + Ok(msg) => { + let bank = self.sharable_banks.root(); + for vote in msg.votes { + self.builder.add_vote(&bank, &vote); + } + } + Err(_) => { + error!("{my_pubkey}: votes receiver channel is disconnected; exiting."); + break; + } + } + } + default(Duration::from_secs(1)) => { + continue; + } + } + } + } +} diff --git a/core/src/bls_sigverify/bls_vote_sigverify.rs b/core/src/bls_sigverify/bls_vote_sigverify.rs deleted file mode 100644 index 8b1b8c73f90..00000000000 --- a/core/src/bls_sigverify/bls_vote_sigverify.rs +++ /dev/null @@ -1,456 +0,0 @@ -#[cfg(feature = "dev-context-only-utils")] -use qualifier_attr::qualifiers; -use { - super::{errors::SigVerifyVoteError, stats::SigVerifyVoteStats}, - crate::bls_sigverify::{ - bls_sigverifier::{BAN_TIMEOUT, NUM_SLOTS_FOR_VERIFY, SigVerifierChannels}, - utils::{ - send_votes_to_metrics, send_votes_to_pool, send_votes_to_repair, send_votes_to_rewards, - }, - }, - agave_votor::{consensus_metrics::ConsensusMetricsEvent, consensus_rewards}, - agave_votor_messages::{ - consensus_message::{ConsensusMessage, VoteMessage}, - reward_certificate::AddVoteMessage, - vote::Vote, - }, - rayon::{ - ThreadPool, current_thread_index, - iter::{Either, IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}, - }, - solana_bls_signatures::{ - BlsError, PreparedHashedMessage, - pubkey::{PopVerified, PubkeyAffine as BlsPubkeyAffine, PubkeyProjective, VerifySignature}, - signature::SignatureProjective, - }, - solana_clock::Slot, - solana_gossip::cluster_info::ClusterInfo, - solana_ledger::leader_schedule_cache::LeaderScheduleCache, - solana_measure::{measure::Measure, measure_us}, - solana_pubkey::Pubkey, - solana_runtime::bank::Bank, - solana_streamer::nonblocking::simple_qos::SimpleQosBanlist, - std::{collections::HashMap, sync::Arc}, -}; - -/// This is the percentage threshold of distinct votes among the total votes under which we will prepare a cache of -/// prepared payloads for individual verification. -const PREPARED_PAYLOAD_CACHE_DISTINCT_VOTE_THRESHOLD_PERCENT: usize = 90; - -/// [`VoteMessage`] along with other information needed to sig verify it. -#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] -#[derive(Clone, Debug)] -pub(super) struct VotePayload { - pub vote_message: VoteMessage, - pub bls_pubkey: PopVerified, - pub pubkey: Pubkey, - pub remote_pubkey: Pubkey, - pub prepared_payload: Option>, -} - -impl VotePayload { - fn verify(self) -> Option { - let is_verified = if let Some(prepared_payload) = self.prepared_payload.as_deref() { - self.bls_pubkey - .verify_signature_prepared(&self.vote_message.signature, prepared_payload) - .is_ok() - } else { - let payload = wincode::serialize(&self.vote_message.vote).ok()?; - self.bls_pubkey - .verify_signature(&self.vote_message.signature, &payload) - .is_ok() - }; - is_verified.then_some(self) - } -} - -/// Verifies votes and sends the verified votes to the consensus pool; and sends the desired subset -/// to rewards container and repair. -/// -/// Any vote that fails fallback individual signature verification will have its sender banlisted. -pub(super) fn verify_and_send_votes( - votes_to_verify: Vec, - root_bank: &Bank, - cluster_info: &ClusterInfo, - leader_schedule: &LeaderScheduleCache, - banlist: &SimpleQosBanlist, - thread_pool: &ThreadPool, - channels: &SigVerifierChannels, -) -> Result { - let mut measure = Measure::start("verify_and_send_votes"); - let mut stats = SigVerifyVoteStats::default(); - if votes_to_verify.is_empty() { - return Ok(stats); - } - stats.votes_to_sig_verify += votes_to_verify.len() as u64; - let verified_votes = verify_votes(root_bank, votes_to_verify, &mut stats, banlist, thread_pool); - stats.sig_verified_votes += verified_votes.len() as u64; - - let (votes_for_pool, msgs_for_repair, msg_for_reward, msg_for_metrics) = - process_verified_votes(&verified_votes, root_bank, cluster_info, leader_schedule); - - send_votes_to_pool(votes_for_pool, &channels.channel_to_pool, &mut stats)?; - send_votes_to_repair(msgs_for_repair, &channels.channel_to_repair, &mut stats)?; - send_votes_to_rewards(msg_for_reward, &channels.channel_to_reward, &mut stats)?; - send_votes_to_metrics(msg_for_metrics, &channels.channel_to_metrics, &mut stats)?; - - measure.stop(); - stats - .fn_verify_and_send_votes_stats - .add_sample(measure.as_us()); - Ok(stats) -} - -/// If the vote is relevant to repair, then adds it to the [`msgs_for_repair`] so it can eventually -/// be sent to repair. -fn inspect_for_repair(vote: &VotePayload, msgs_for_repair: &mut HashMap>) { - let vote_slot = vote.vote_message.vote.slot(); - match vote.vote_message.vote { - Vote::Notarize(_) | Vote::Finalize(_) | Vote::NotarizeFallback(_) => { - msgs_for_repair - .entry(vote.pubkey) - .or_default() - .push(vote_slot); - } - Vote::Skip(_) | Vote::SkipFallback(_) | Vote::Genesis(_) => (), - } -} - -/// Processes the verified votes for various downstream services. -/// -/// In particular, collects and returns the relevant messages for the consensus pool; rewards; -/// repair; and metrics; -fn process_verified_votes( - verified_votes: &[VotePayload], - root_bank: &Bank, - cluster_info: &ClusterInfo, - leader_schedule: &LeaderScheduleCache, -) -> ( - Vec, - HashMap>, - AddVoteMessage, - Vec, -) { - let mut votes_for_reward = Vec::with_capacity(verified_votes.len()); - let mut msgs_for_repair = HashMap::new(); - let mut votes_for_pool = Vec::with_capacity(verified_votes.len()); - let mut votes_for_metrics = Vec::with_capacity(verified_votes.len()); - for vote in verified_votes { - let vote_message = vote.vote_message; - if consensus_rewards::wants_vote( - cluster_info, - leader_schedule, - root_bank.slot(), - &vote_message, - ) { - votes_for_reward.push(vote_message); - } - - inspect_for_repair(vote, &mut msgs_for_repair); - - votes_for_pool.push(ConsensusMessage::Vote(vote_message)); - - votes_for_metrics.push(ConsensusMetricsEvent::Vote { - id: vote.pubkey, - vote: vote.vote_message.vote, - }); - } - let msgs_for_repair = msgs_for_repair - .into_iter() - .map(|(pubkey, mut slots)| { - slots.sort_unstable(); - slots.dedup(); - (pubkey, slots) - }) - .collect(); - ( - votes_for_pool, - msgs_for_repair, - AddVoteMessage { - votes: votes_for_reward, - }, - votes_for_metrics, - ) -} - -/// Sig verifies `votes_to_verify` and returns a `Vec` of votes that passed verification. -fn verify_votes( - root_bank: &Bank, - votes_to_verify: Vec, - stats: &mut SigVerifyVoteStats, - banlist: &SimpleQosBanlist, - thread_pool: &ThreadPool, -) -> Vec { - // Filter votes too far in the future. - let len_before = votes_to_verify.len(); - let votes_to_verify = votes_to_verify - .into_iter() - .filter(|v| { - v.vote_message.vote.slot() <= root_bank.slot().saturating_add(NUM_SLOTS_FOR_VERIFY) - }) - .collect::>(); - let num_discarded = len_before - votes_to_verify.len(); - stats.too_far_in_future = stats.too_far_in_future.saturating_add(num_discarded as u64); - - // Try optimistic verification - fast to verify, but cannot identify invalid votes - let (optimistic_result, distinct_votes, distinct_payloads) = - verify_votes_optimistic(&votes_to_verify, stats, thread_pool); - if matches!(optimistic_result, OptimisticVerificationResult::Verified) { - return votes_to_verify; - } - - // Fallback to individual verification - let ((verified_votes, invalid_remote_pubkeys), time_us) = measure_us!(verify_individual_votes( - votes_to_verify, - distinct_votes, - distinct_payloads, - thread_pool, - )); - for remote_pubkey in invalid_remote_pubkeys { - if banlist.ban(remote_pubkey, BAN_TIMEOUT) { - stats.already_banned += 1; - } else { - info!("bls_vote_sigverify: banned sender={remote_pubkey} due to failed verification"); - } - } - stats.fn_verify_individual_votes_stats.add_sample(time_us); - - verified_votes -} - -/// Outcome of optimistic aggregate vote verification. -/// -/// `Failed` carries the number of distinct vote messages seen before falling -/// back to individual verification. -#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum OptimisticVerificationResult { - Verified, - Failed { num_distinct_messages: usize }, -} - -#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] -/// Attempts aggregate BLS verification across the full vote set. -/// -/// This fast path aggregates all vote signatures and the public keys for each -/// distinct vote payload, minimizing the number of pairing operations needed -/// for verification. When aggregation or aggregate verification fails, the -/// caller falls back to individual vote verification so invalid votes can be -/// identified precisely. -/// -/// Returns the optimistic verification outcome together with the distinct vote -/// messages and their prepared payloads, which can be reused by the fallback -/// path. -fn verify_votes_optimistic( - votes: &[VotePayload], - stats: &mut SigVerifyVoteStats, - thread_pool: &ThreadPool, -) -> ( - OptimisticVerificationResult, - Vec, - Vec, -) { - let mut measure = Measure::start("verify_votes_optimistic"); - - // For BLS verification, minimizing the expensive pairing operation is key. - // Each BLS signature verification requires two pairings. - // - // However, the BLS verification formula allows us to: - // 1. Aggregate all signatures into a single signature. - // 2. Aggregate public keys for each unique message. - // - // By verifying the aggregated signature against the aggregated public keys, - // the number of pairings required is reduced to (1 + number of distinct messages). - let (signature_result, (distinct_votes, distinct_payloads, pubkeys_result)) = thread_pool.join( - || aggregate_signatures(votes), - || aggregate_pubkeys_by_payload(votes, stats), - ); - - let Ok(aggregate_signature) = signature_result else { - return ( - OptimisticVerificationResult::Failed { - num_distinct_messages: 0, - }, - Vec::new(), - Vec::new(), - ); - }; - - let Ok(aggregate_pubkeys) = pubkeys_result else { - return ( - OptimisticVerificationResult::Failed { - num_distinct_messages: distinct_payloads.len(), - }, - distinct_votes, - distinct_payloads, - ); - }; - - let verified = if distinct_payloads.len() == 1 { - // if one unique payload, just verify the aggregate signature for the single payload - // this requires (2 pairings) - aggregate_pubkeys[0] - .verify_signature_prepared(&aggregate_signature, &distinct_payloads[0]) - .is_ok() - } else { - // if non-unique payload, we need to apply a pairing for each distinct message, - // which is done inside `par_verify_distinct_aggregated_prepared`. - thread_pool.install(|| { - SignatureProjective::par_verify_distinct_aggregated_prepared( - &aggregate_pubkeys, - &aggregate_signature, - &distinct_payloads, - ) - .is_ok() - }) - }; - - measure.stop(); - stats - .fn_verify_votes_optimistic_stats - .add_sample(measure.as_us()); - let result = if verified { - OptimisticVerificationResult::Verified - } else { - OptimisticVerificationResult::Failed { - num_distinct_messages: distinct_payloads.len(), - } - }; - (result, distinct_votes, distinct_payloads) -} - -#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] -fn aggregate_signatures(votes: &[VotePayload]) -> Result { - debug_assert!(current_thread_index().is_some()); - let signatures = votes.par_iter().map(|v| &v.vote_message.signature); - // TODO(sam): Currently, `par_aggregate` performs full validation - // (on-curve + subgroup check) for every signature. Since the subgroup - // check is expensive, we can use an `unchecked` deserialization here - // (performing only the cheap on-curve check) and rely on a single subgroup - // check on the final aggregated signature. This should save more than 80% - // of the time for signature aggregation. - SignatureProjective::par_aggregate(signatures) -} - -#[allow(clippy::type_complexity)] -#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] -fn aggregate_pubkeys_by_payload( - votes: &[VotePayload], - stats: &mut SigVerifyVoteStats, -) -> ( - Vec, - Vec, - Result>, BlsError>, -) { - debug_assert!(current_thread_index().is_some()); - let mut grouped_votes: HashMap<&Vote, Vec<&PopVerified>> = HashMap::new(); - - for v in votes { - grouped_votes - .entry(&v.vote_message.vote) - .or_default() - .push(&v.bls_pubkey); - } - - stats - .distinct_votes_stats - .add_sample(grouped_votes.len() as u64); - - let distinct_grouped_votes = grouped_votes - .into_par_iter() - .filter_map(|(vote, pubkeys)| { - wincode::serialize(vote).ok().map(|serialized_vote| { - // converting aggregate pubkey to `PopVerified` is safe here - // since the pubkeys are all PoP verified in the vote account - let pubkey = PubkeyProjective::par_aggregate(pubkeys.into_par_iter()) - .map(|agg| unsafe { PopVerified::new_unchecked(*agg) }); - (*vote, PreparedHashedMessage::new(&serialized_vote), pubkey) - }) - }) - .collect::>(); - let (distinct_votes, distinct_payloads, distinct_pubkeys_results): (Vec<_>, Vec<_>, Vec<_>) = - distinct_grouped_votes.into_iter().fold( - (Vec::new(), Vec::new(), Vec::new()), - |mut acc, (vote, payload, pubkey)| { - acc.0.push(vote); - acc.1.push(payload); - acc.2.push(pubkey); - acc - }, - ); - let aggregate_pubkeys_result = distinct_pubkeys_results.into_iter().collect(); - - (distinct_votes, distinct_payloads, aggregate_pubkeys_result) -} - -/// Verifies votes individually on a thread pool. -/// -/// Returns: -/// - `Vec`: votes that passed verification. -/// - `Vec`: remote pubkeys for votes that failed verification. -#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] -fn verify_individual_votes( - mut votes_to_verify: Vec, - distinct_votes: Vec, - distinct_payloads: Vec, - thread_pool: &ThreadPool, -) -> (Vec, Vec) { - if should_prepare_payload_cache(distinct_votes.len(), votes_to_verify.len()) { - let prepared_payloads: HashMap<_, _> = distinct_votes - .into_iter() - .zip(distinct_payloads) - .map(|(vote, payload)| (vote, Arc::new(payload))) - .collect(); - for vote in &mut votes_to_verify { - vote.prepared_payload = prepared_payloads.get(&vote.vote_message.vote).cloned(); - } - } - - thread_pool.install(|| { - votes_to_verify.into_par_iter().partition_map(|vote| { - let remote_pubkey = vote.remote_pubkey; - match vote.verify() { - Some(vote) => Either::Left(vote), - None => Either::Right(remote_pubkey), - } - }) - }) -} - -fn should_prepare_payload_cache(distinct_vote_count: usize, total_vote_count: usize) -> bool { - distinct_vote_count.saturating_mul(100) - <= total_vote_count.saturating_mul(PREPARED_PAYLOAD_CACHE_DISTINCT_VOTE_THRESHOLD_PERCENT) -} - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn test_should_prepare_payload_cache() { - assert!(should_prepare_payload_cache(9, 10)); - assert!(should_prepare_payload_cache(0, 0)); - assert!(!should_prepare_payload_cache(1, 1)); - assert!(!should_prepare_payload_cache(10, 10)); - assert!(!should_prepare_payload_cache(91, 100)); - assert!(should_prepare_payload_cache(90, 100)); - } - - #[test] - #[should_panic] - fn ensure_aggregate_signatures_runs_on_thread_pool() { - let votes = vec![]; - // calling without a rayon thread pool should trigger a debug assert. - aggregate_signatures(&votes).unwrap(); - } - - #[test] - #[should_panic] - fn ensure_aggregate_pubkeys_by_payload_runs_on_thread_pool() { - let votes = vec![]; - let mut stats = SigVerifyVoteStats::default(); - // calling without a rayon thread pool should trigger a debug assert. - aggregate_pubkeys_by_payload(&votes, &mut stats).2.unwrap(); - } -} diff --git a/core/src/cluster_info_vote_listener.rs b/core/src/cluster_info_vote_listener.rs index 8845029334c..8d66d87ee8e 100644 --- a/core/src/cluster_info_vote_listener.rs +++ b/core/src/cluster_info_vote_listener.rs @@ -8,7 +8,7 @@ use { sigverify_stage::GossipSigVerifyHandle, }, agave_banking_stage_ingress_types::BankingPacketBatch, - agave_votor_messages::migration::MigrationStatus, + agave_votor_messages::{VerifiedVoterSlotsSender, migration::MigrationStatus}, crossbeam_channel::{Receiver, RecvTimeoutError, Select, Sender, unbounded}, log::*, solana_clock::{BankId, Slot}, @@ -19,7 +19,6 @@ use { solana_hash::Hash, solana_ledger::blockstore::Blockstore, solana_measure::measure::Measure, - solana_metrics::inc_new_counter_debug, solana_perf::packet::{self, PacketBatch}, solana_pubkey::Pubkey, solana_rpc::{ @@ -57,11 +56,6 @@ use { pub type ThresholdConfirmedSlots = Vec<(Slot, Hash)>; pub type VerifiedVoteTransactionsSender = Sender>; pub type VerifiedVoteTransactionsReceiver = Receiver>; -// Send side of verified voter channel. -// Each message contains the Pubkey of the voter and the slots in last verified vote. -pub type VerifiedVoterSlotsSender = Sender<(Pubkey, Vec)>; -// Receive side of verified voter channel. -pub type VerifiedVoterSlotsReceiver = Receiver<(Pubkey, Vec)>; pub type GossipVerifiedVoteHashSender = Sender<(Pubkey, Slot, Hash)>; pub type GossipVerifiedVoteHashReceiver = Receiver<(Pubkey, Slot, Hash)>; pub type DuplicateConfirmedSlotsSender = Sender; @@ -497,15 +491,47 @@ impl ClusterInfoVoteListener { verified_packets_sender: BankingPacketSender, verified_vote_transactions_sender: VerifiedVoteTransactionsSender, ) -> Result<()> { + #[derive(Default)] + struct Stats { + received_count: usize, + banking_channel_max_len: usize, + banking_channel_eviction_drops: usize, + } + const STATS_REPORT_INTERVAL: Duration = Duration::from_secs(1); let mut cursor = Cursor::default(); + let mut last_report = Instant::now(); + let mut stats = Stats::default(); while !exit.load(Ordering::Relaxed) { let votes = cluster_info.get_votes(&mut cursor); - inc_new_counter_debug!("cluster_info_vote_listener-recv_count", votes.len()); if !votes.is_empty() { + stats.received_count += votes.len(); let (vote_txs, packets) = Self::verify_votes(votes, &mut gossip_sigverify_handle, &sharable_banks)?; verified_vote_transactions_sender.send(vote_txs)?; - verified_packets_sender.send(BankingPacketBatch::new(packets))?; + // Sample backlog before the push. + stats.banking_channel_max_len = stats + .banking_channel_max_len + .max(verified_packets_sender.len()); + stats.banking_channel_eviction_drops += + verified_packets_sender.send(BankingPacketBatch::new(packets))?; + } + if last_report.elapsed() >= STATS_REPORT_INTERVAL { + datapoint_info!( + "cluster_info_vote_listener", + ("received_count", stats.received_count as i64, i64), + ( + "banking_channel_max_len", + stats.banking_channel_max_len as i64, + i64 + ), + ( + "banking_channel_eviction_drops", + stats.banking_channel_eviction_drops as i64, + i64 + ), + ); + stats = Stats::default(); + last_report = Instant::now(); } sleep(Duration::from_millis(GOSSIP_SLEEP_MILLIS)); } @@ -722,31 +748,30 @@ impl ClusterInfoVoteListener { let reached_duplicate_confirmed = reached_threshold_results[0]; let reached_optimistic_confirmed = reached_threshold_results[1]; - if reached_duplicate_confirmed { - if let Some(ref sender) = notifiers.duplicate_confirmed_slot_sender { - let _ = sender.send(vec![(last_vote_slot, last_vote_hash)]); - } + if reached_duplicate_confirmed + && let Some(ref sender) = notifiers.duplicate_confirmed_slot_sender + { + let _ = sender.send(vec![(last_vote_slot, last_vote_hash)]); } if reached_optimistic_confirmed { new_optimistic_confirmed_slots.push((last_vote_slot, last_vote_hash)); - if let Some(ref sender) = notifiers.bank_notification_sender { - if notifiers + if let Some(ref sender) = notifiers.bank_notification_sender + && notifiers .migration_status .should_report_commitment_or_root(last_vote_slot) - { - let dependency_work = sender - .dependency_tracker - .as_ref() - .map(|s| s.get_current_declared_work()); - sender - .sender - .send(( - BankNotification::OptimisticallyConfirmed(last_vote_slot), - dependency_work, - )) - .unwrap_or_else(|err| warn!("bank_notification_sender failed: {err:?}")); - } + { + let dependency_work = sender + .dependency_tracker + .as_ref() + .map(|s| s.get_current_declared_work()); + sender + .sender + .send(( + BankNotification::OptimisticallyConfirmed(last_vote_slot), + dependency_work, + )) + .unwrap_or_else(|err| warn!("bank_notification_sender failed: {err:?}")); } } @@ -966,6 +991,7 @@ mod tests { use { super::*, crate::sigverify::GossipVerifiedVoteBatch, + crossbeam_channel::bounded, itertools::Itertools, solana_hash::Hash, solana_keypair::Keypair, @@ -1022,8 +1048,8 @@ mod tests { votes: Vec, sharable_banks: &SharableBanks, ) -> (Vec, Vec) { - let (worker_sender, _worker_receiver) = unbounded(); - let (verified_vote_sender, verified_vote_receiver) = unbounded(); + let (worker_sender, _worker_receiver) = bounded(1024); + let (verified_vote_sender, verified_vote_receiver) = bounded(1024); let mut gossip_sigverify_handle = GossipSigVerifyHandle::new_for_tests(worker_sender, verified_vote_receiver); @@ -1129,10 +1155,10 @@ mod tests { subscriptions, .. } = setup(); - let (votes_sender, votes_receiver) = unbounded(); - let (verified_voter_slots_sender, _verified_voter_slots_receiver) = unbounded(); - let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = unbounded(); - let (replay_votes_sender, replay_votes_receiver) = unbounded(); + let (votes_sender, votes_receiver) = bounded(1024); + let (verified_voter_slots_sender, _verified_voter_slots_receiver) = bounded(1024); + let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = bounded(1024); + let (replay_votes_sender, replay_votes_receiver) = bounded(1024); let mut latest_vote_slot_per_validator = HashMap::new(); let GenesisConfigInfo { genesis_config, .. } = @@ -1257,10 +1283,10 @@ mod tests { bank: bank0, .. } = setup(); - let (votes_txs_sender, votes_txs_receiver) = unbounded(); - let (replay_votes_sender, replay_votes_receiver) = unbounded(); - let (gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = unbounded(); - let (verified_voter_slots_sender, verified_voter_slots_receiver) = unbounded(); + let (votes_txs_sender, votes_txs_receiver) = bounded(1024); + let (replay_votes_sender, replay_votes_receiver) = bounded(1024); + let (gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = bounded(1024); + let (verified_voter_slots_sender, verified_voter_slots_receiver) = bounded(1024); let mut latest_vote_slot_per_validator = HashMap::new(); let gossip_vote_slots = vec![1, 2]; @@ -1406,10 +1432,10 @@ mod tests { } = setup(); // Send some votes to process - let (votes_txs_sender, votes_txs_receiver) = unbounded(); - let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = unbounded(); - let (verified_voter_slots_sender, verified_voter_slots_receiver) = unbounded(); - let (_replay_votes_sender, replay_votes_receiver) = unbounded(); + let (votes_txs_sender, votes_txs_receiver) = bounded(1024); + let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = bounded(1024); + let (verified_voter_slots_sender, verified_voter_slots_receiver) = bounded(1024); + let (_replay_votes_sender, replay_votes_receiver) = bounded(1024); let mut latest_vote_slot_per_validator = HashMap::new(); let mut expected_voter_slots = vec![]; @@ -1499,11 +1525,11 @@ mod tests { } fn run_test_process_votes3(switch_proof_hash: Option) { - let (votes_sender, votes_receiver) = unbounded(); - let (verified_voter_slots_sender, _verified_voter_slots_receiver) = unbounded(); - let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = unbounded(); + let (votes_sender, votes_receiver) = bounded(1024); + let (verified_voter_slots_sender, _verified_voter_slots_receiver) = bounded(1024); + let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = bounded(1024); let (replay_votes_sender, replay_votes_receiver): (ReplayVoteSender, ReplayVoteReceiver) = - unbounded(); + bounded(1024); let mut latest_vote_slot_per_validator = HashMap::new(); let vote_slot = 1; @@ -1902,8 +1928,8 @@ mod tests { None, )]; - let (verified_voter_slots_sender, _verified_voter_slots_receiver) = unbounded(); - let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = unbounded(); + let (verified_voter_slots_sender, _verified_voter_slots_receiver) = bounded(1024); + let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = bounded(1024); let notifiers = ConfirmationNotifiers { gossip_verified_vote_hash_sender: gossip_verified_vote_hash_sender.clone(), verified_voter_slots_sender: verified_voter_slots_sender.clone(), @@ -2152,8 +2178,8 @@ mod tests { )); let mut latest_vote_slot_per_validator = HashMap::new(); - let (verified_voter_slots_sender, _verified_voter_slots_receiver) = unbounded(); - let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = unbounded(); + let (verified_voter_slots_sender, _verified_voter_slots_receiver) = bounded(1024); + let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = bounded(1024); let mut diff = HashMap::default(); let mut new_optimistic_confirmed_slots = vec![]; diff --git a/core/src/cluster_slots_service/cluster_slots.rs b/core/src/cluster_slots_service/cluster_slots.rs index 907b72edc99..ccd7639867f 100644 --- a/core/src/cluster_slots_service/cluster_slots.rs +++ b/core/src/cluster_slots_service/cluster_slots.rs @@ -390,7 +390,7 @@ impl ClusterSlots { .set_support_by_pubkey(&epoch_slots.from, sender_stake) .is_err() { - error!("Unexpected pubkey {} for slot {}!", &epoch_slots.from, slot); + error!("Unexpected pubkey {} for slot {}!", epoch_slots.from, slot); break; } } @@ -502,11 +502,11 @@ impl ClusterSlots { let mut indices = Vec::with_capacity(repair_peers.len()); for (index, peer) in repair_peers.iter().enumerate() { - if let Some(stake) = slot_peers.get_support_by_pubkey(peer.pubkey()) { - if stake > 0 { - weights.push(stake.max(1)); - indices.push(index); - } + if let Some(stake) = slot_peers.get_support_by_pubkey(peer.pubkey()) + && stake > 0 + { + weights.push(stake.max(1)); + indices.push(index); } } (weights, indices) diff --git a/core/src/cluster_slots_service/slot_supporters.rs b/core/src/cluster_slots_service/slot_supporters.rs index 76f11fa2c3e..50aa75729a3 100644 --- a/core/src/cluster_slots_service/slot_supporters.rs +++ b/core/src/cluster_slots_service/slot_supporters.rs @@ -123,7 +123,7 @@ impl SlotSupporters { if !same_epoch { let old_len = self.supporting_stakes.len(); let new_len = index_map.len(); - if new_len < old_len * 2 { + if new_len * 2 < old_len { // if new length is much less than allocation, reallocate self.supporting_stakes = Vec::from_iter(repeat_atomic_u64(new_len)); } else { diff --git a/core/src/commitment_service.rs b/core/src/commitment_service.rs index 356d96a3995..2c0170c84ae 100644 --- a/core/src/commitment_service.rs +++ b/core/src/commitment_service.rs @@ -193,9 +193,9 @@ impl AggregateCommitmentService { // Notarize (our first round vote in favor of a block) satisfies the Processed commitment level w_block_commitment_cache.set_slot(slot); } - AlpenglowCommitmentType::Finalized => { + AlpenglowCommitmentType::Rooted => { // There is no distinction of OC, root, or finalized in Alpengow commitment. - // When receiving a finalilzation certificate we set all of these values. + // Once votor selects a finalized bank as root, set all of these values. w_block_commitment_cache.set_highest_confirmed_slot(slot); w_block_commitment_cache.set_root(slot); w_block_commitment_cache.set_highest_super_majority_root(slot); diff --git a/core/src/completed_data_sets_service.rs b/core/src/completed_data_sets_service.rs index a35aff8e0bb..c3f581ffe13 100644 --- a/core/src/completed_data_sets_service.rs +++ b/core/src/completed_data_sets_service.rs @@ -104,7 +104,9 @@ impl CompletedDataSetsService { pub fn new( completed_sets_receiver: CompletedDataSetsReceiver, blockstore: Arc, - rpc_subscriptions: Arc, + // None when the node has no RPC subscriptions (e.g. a geyser node without --rpc-port); the + // deshred-transaction sink still runs, only the signatureSubscribe path is skipped. + rpc_subscriptions: Option>, deshred_transaction_notifier: Option, exit: Arc, max_slots: Arc, @@ -121,7 +123,7 @@ impl CompletedDataSetsService { if let Err(RecvTimeoutError::Disconnected) = Self::recv_completed_data_sets( &completed_sets_receiver, &blockstore, - &rpc_subscriptions, + rpc_subscriptions.as_deref(), &deshred_transaction_notifier, &max_slots, &bank_forks, @@ -138,7 +140,7 @@ impl CompletedDataSetsService { fn recv_completed_data_sets( completed_sets_receiver: &CompletedDataSetsReceiver, blockstore: &Blockstore, - rpc_subscriptions: &RpcSubscriptions, + rpc_subscriptions: Option<&RpcSubscriptions>, deshred_transaction_notifier: &Option, max_slots: &Arc, bank_forks: &RwLock, @@ -174,9 +176,11 @@ impl CompletedDataSetsService { &mut stats, ); - let transactions = Self::get_transaction_signatures(entries); - if !transactions.is_empty() { - rpc_subscriptions.notify_signatures_received((slot, transactions)); + if let Some(rpc_subscriptions) = rpc_subscriptions { + let transactions = Self::get_transaction_signatures(entries); + if !transactions.is_empty() { + rpc_subscriptions.notify_signatures_received((slot, transactions)); + } } } Err(e) => warn!("completed-data-set-service deserialize error: {e:?}"), @@ -551,7 +555,7 @@ pub mod test { ))], )]; let shreds = blockstore::entries_to_test_shreds(&entries, 11, 10, true, 0); - let completed_data_sets = blockstore.insert_shreds(shreds, None, true).unwrap(); + let completed_data_sets = blockstore.insert_shreds(shreds, true).unwrap(); assert_eq!(completed_data_sets.len(), 1); let completed_data_set = completed_data_sets[0].clone(); sender.send(completed_data_sets).unwrap(); @@ -559,7 +563,7 @@ pub mod test { CompletedDataSetsService::recv_completed_data_sets( &receiver, &blockstore, - &rpc_subscriptions, + Some(&rpc_subscriptions), ¬ifier, &max_slots, &bank_forks, @@ -617,7 +621,7 @@ pub mod test { .collect(); let shreds = blockstore::entries_to_test_shreds(&entries, 12, 11, true, 0); assert!(shreds.len() > 1); - let completed_data_sets = blockstore.insert_shreds(shreds, None, true).unwrap(); + let completed_data_sets = blockstore.insert_shreds(shreds, true).unwrap(); assert_eq!(completed_data_sets.len(), 1); let completed_data_set = completed_data_sets[0].clone(); sender.send(completed_data_sets).unwrap(); @@ -625,7 +629,7 @@ pub mod test { CompletedDataSetsService::recv_completed_data_sets( &receiver, &blockstore, - &rpc_subscriptions, + Some(&rpc_subscriptions), ¬ifier, &max_slots, &bank_forks, diff --git a/core/src/consensus.rs b/core/src/consensus.rs index 81be5122aa6..39d3683c314 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -61,7 +61,7 @@ impl ThresholdDecision { } } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(PartialEq, Eq, Clone, Debug)] pub enum SwitchForkDecision { SwitchProof(Hash), @@ -200,7 +200,7 @@ impl TowerVersions { } } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(PartialEq, Eq, Debug, Default, Clone, Copy)] pub(crate) enum BlockhashStatus { /// No vote since restart @@ -498,14 +498,14 @@ impl Tower { vote_slots.insert(slot); } - if start_root != vote_state.root_slot { - if let Some(root) = start_root { - // The account's prior root can be older than this fork's root; clamp to - // the same range for the same reason as above. - if root > root_slot { - trace!("ROOT: {root}"); - vote_slots.insert(root); - } + if start_root != vote_state.root_slot + && let Some(root) = start_root + { + // The account's prior root can be older than this fork's root; clamp to + // the same range for the same reason as above. + if root > root_slot { + trace!("ROOT: {root}"); + vote_slots.insert(root); } } if let Some(root) = vote_state.root_slot { @@ -703,15 +703,15 @@ impl Tower { vote_hash: Hash, block_id: Hash, ) -> Option { - if let Some(last_voted_slot) = self.vote_state.last_voted_slot() { - if vote_slot <= last_voted_slot { - panic!( - "Error while recording vote {} {} in local tower {:?}", - vote_slot, - vote_hash, - VoteError::VoteTooOld - ); - } + if let Some(last_voted_slot) = self.vote_state.last_voted_slot() + && vote_slot <= last_voted_slot + { + panic!( + "Error while recording vote {} {} in local tower {:?}", + vote_slot, + vote_hash, + VoteError::VoteTooOld + ); } trace!("{} record_vote for {}", self.node_pubkey, vote_slot); @@ -807,10 +807,10 @@ impl Tower { if slot <= last_voted_slot { return false; } - } else if let Some(root) = self.vote_state.root_slot { - if slot <= root { - return false; - } + } else if let Some(root) = self.vote_state.root_slot + && slot <= root + { + return false; } true } @@ -841,15 +841,15 @@ impl Tower { } } - if let Some(root_slot) = vote_state.root_slot { - if slot != root_slot { - // This case should never happen because bank forks purges all - // non-descendants of the root every time root is set - assert!( - ancestors.contains(&root_slot), - "ancestors: {ancestors:?}, slot: {slot} root: {root_slot}" - ); - } + if let Some(root_slot) = vote_state.root_slot + && slot != root_slot + { + // This case should never happen because bank forks purges all + // non-descendants of the root every time root is set + assert!( + ancestors.contains(&root_slot), + "ancestors: {ancestors:?}, slot: {slot} root: {root_slot}" + ); } false @@ -1061,9 +1061,11 @@ impl Tower { .unwrap_or(true) { // Our last vote slot was purged because it was on a duplicate fork, don't continue below - // where checks may panic. We allow a freebie vote here that may violate switching - // thresholds - // TODO: Properly handle this case + // where checks may panic. We allow a freebie vote here without checking the switch + // threshold as it is trivially satisfied: + // - Freebie can only occur because our last vote block was dumped & repaired + // - Dump & repair only triggers due to another version reaching duplicate confirmation (52%) + // - 52% > 38% so the switching threshold is implicitely satisifed info!( "Allowing switch vote on {:?} because last vote {:?} was rolled back", (switch_slot, switch_hash), @@ -1733,6 +1735,7 @@ impl TowerError { #[derive(Debug)] pub enum ExternalRootSource { Tower(Slot), + VoteHistory(Slot), HardFork(Slot), } @@ -1740,6 +1743,7 @@ impl ExternalRootSource { fn root(&self) -> Slot { match self { ExternalRootSource::Tower(slot) => *slot, + ExternalRootSource::VoteHistory(slot) => *slot, ExternalRootSource::HardFork(slot) => *slot, } } @@ -1996,7 +2000,7 @@ pub mod test { // Fill the BankForks according to the above fork structure vote_simulator.fill_bank_forks(forks, &HashMap::new(), true); - for (_, fork_progress) in vote_simulator.progress.iter_mut() { + for fork_progress in vote_simulator.progress.values_mut() { fork_progress.fork_stats.computed = true; } @@ -3091,7 +3095,7 @@ pub mod test { // Fill the BankForks according to the above fork structure vote_simulator.fill_bank_forks(forks, &HashMap::new(), true); - for (_, fork_progress) in vote_simulator.progress.iter_mut() { + for fork_progress in vote_simulator.progress.values_mut() { fork_progress.fork_stats.computed = true; } @@ -3180,7 +3184,7 @@ pub mod test { // Fill the BankForks according to the above fork structure vote_simulator.fill_bank_forks(forks, &HashMap::new(), true); - for (_, fork_progress) in vote_simulator.progress.iter_mut() { + for fork_progress in vote_simulator.progress.values_mut() { fork_progress.fork_stats.computed = true; } @@ -3329,11 +3333,11 @@ pub mod test { let blockstore = Blockstore::open(ledger_path.path()).unwrap(); let (shreds, _) = make_slot_entries(1, 0, 42); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let (shreds, _) = make_slot_entries(3, 1, 42); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let (shreds, _) = make_slot_entries(4, 1, 42); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert!(!blockstore.is_root(0)); assert!(!blockstore.is_root(1)); assert!(!blockstore.is_root(3)); @@ -3365,11 +3369,11 @@ pub mod test { let blockstore = Blockstore::open(ledger_path.path()).unwrap(); let (shreds, _) = make_slot_entries(1, 0, 42); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let (shreds, _) = make_slot_entries(3, 1, 42); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let (shreds, _) = make_slot_entries(4, 1, 42); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); blockstore.set_roots(std::iter::once(&3)).unwrap(); assert!(!blockstore.is_root(0)); assert!(!blockstore.is_root(1)); @@ -3393,9 +3397,9 @@ pub mod test { let blockstore = Blockstore::open(ledger_path.path()).unwrap(); let (shreds, _) = make_slot_entries(1, 0, 42); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let (shreds, _) = make_slot_entries(3, 1, 42); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert!(!blockstore.is_root(0)); assert!(!blockstore.is_root(1)); assert!(!blockstore.is_root(3)); @@ -3818,7 +3822,7 @@ pub mod test { // Fill the BankForks according to the above fork structure vote_simulator.fill_bank_forks(forks, &HashMap::new(), true); - for (_, fork_progress) in vote_simulator.progress.iter_mut() { + for fork_progress in vote_simulator.progress.values_mut() { fork_progress.fork_stats.computed = true; } diff --git a/core/src/consensus/heaviest_subtree_fork_choice.rs b/core/src/consensus/heaviest_subtree_fork_choice.rs index ee90e9b5ed1..ae04153cb0c 100644 --- a/core/src/consensus/heaviest_subtree_fork_choice.rs +++ b/core/src/consensus/heaviest_subtree_fork_choice.rs @@ -6,6 +6,7 @@ use { latest_validator_votes_for_frozen_banks::LatestValidatorVotesForFrozenBanks, progress_map::ProgressMap, tree_diff::TreeDiff, }, + ahash::AHashMap, solana_clock::{Epoch, Slot}, solana_epoch_schedule::EpochSchedule, solana_hash::Hash, @@ -140,15 +141,15 @@ impl ForkInfo { my_key: &SlotHashKey, newly_valid_ancestor: Slot, ) { - if let Some(latest_invalid_ancestor) = self.latest_invalid_ancestor { - if latest_invalid_ancestor <= newly_valid_ancestor { - info!( - "Fork choice for {my_key:?} clearing latest invalid ancestor \ - {latest_invalid_ancestor:?} because {newly_valid_ancestor:?} was duplicate \ - confirmed" - ); - self.latest_invalid_ancestor = None; - } + if let Some(latest_invalid_ancestor) = self.latest_invalid_ancestor + && latest_invalid_ancestor <= newly_valid_ancestor + { + info!( + "Fork choice for {my_key:?} clearing latest invalid ancestor \ + {latest_invalid_ancestor:?} because {newly_valid_ancestor:?} was duplicate \ + confirmed" + ); + self.latest_invalid_ancestor = None; } } @@ -183,7 +184,7 @@ impl PartialEq for ForkInfo { #[derive(Debug, Clone)] pub struct HeaviestSubtreeForkChoice { - fork_infos: HashMap, + fork_infos: AHashMap, latest_votes: HashMap, tree_root: SlotHashKey, last_root_time: Instant, @@ -220,7 +221,7 @@ impl HeaviestSubtreeForkChoice { tree_root, // Doesn't implement default because `root` must // exist in all the fields - fork_infos: HashMap::new(), + fork_infos: AHashMap::new(), latest_votes: HashMap::new(), last_root_time: Instant::now(), }; @@ -608,7 +609,7 @@ impl HeaviestSubtreeForkChoice { self.process_update_operations(update_operations); // Remove node + all children and add to new tree - let mut split_tree_fork_infos = HashMap::new(); + let mut split_tree_fork_infos = AHashMap::new(); let mut to_visit = vec![*slot_hash_key]; while let Some(current_node) = to_visit.pop() { let current_fork_info = self @@ -1381,13 +1382,13 @@ impl ForkChoice for HeaviestSubtreeForkChoice { struct AncestorIterator<'a> { current_slot_hash_key: SlotHashKey, - fork_infos: &'a HashMap, + fork_infos: &'a AHashMap, } impl<'a> AncestorIterator<'a> { fn new( start_slot_hash_key: SlotHashKey, - fork_infos: &'a HashMap, + fork_infos: &'a AHashMap, ) -> Self { Self { current_slot_hash_key: start_slot_hash_key, diff --git a/core/src/consensus/progress_map.rs b/core/src/consensus/progress_map.rs index 1054a47fdaa..c919b72750f 100644 --- a/core/src/consensus/progress_map.rs +++ b/core/src/consensus/progress_map.rs @@ -287,6 +287,9 @@ impl PropagatedStats { #[derive(Default)] pub struct ProgressMap { progress_map: HashMap, + /// Tracks the number of times a slot was switched from an alternate location. + /// This persists even if the slot is removed from the progress_map due to a switch. + bank_switch_counts: HashMap, } impl std::ops::Deref for ProgressMap { @@ -304,6 +307,12 @@ impl std::ops::DerefMut for ProgressMap { impl ProgressMap { pub fn insert(&mut self, slot: Slot, fork_progress: ForkProgress) { + let num_bank_switches = self.get_num_bank_switches(slot); + fork_progress + .replay_stats + .write() + .unwrap() + .num_bank_switches = num_bank_switches; self.progress_map.insert(slot, fork_progress); } @@ -330,6 +339,15 @@ impl ProgressMap { .map(|fork_progress| &fork_progress.fork_stats) } + pub fn increment_num_bank_switches(&mut self, slot: Slot) { + let count = self.bank_switch_counts.entry(slot).or_insert(0); + *count = count.saturating_add(1); + } + + pub fn get_num_bank_switches(&self, slot: Slot) -> u64 { + self.bank_switch_counts.get(&slot).cloned().unwrap_or(0) + } + pub fn get_fork_stats_mut(&mut self, slot: Slot) -> Option<&mut ForkStats> { self.progress_map .get_mut(&slot) @@ -431,6 +449,8 @@ impl ProgressMap { pub fn handle_new_root(&mut self, bank_forks: &BankForks) { self.progress_map .retain(|k, _| bank_forks.get(*k).is_some()); + self.bank_switch_counts + .retain(|k, _| bank_forks.get(*k).is_some()); } pub fn log_propagated_stats(&self, slot: Slot, bank_forks: &RwLock) { diff --git a/core/src/consensus/tower1_14_11.rs b/core/src/consensus/tower1_14_11.rs index 3c943a9ceec..f184023c238 100644 --- a/core/src/consensus/tower1_14_11.rs +++ b/core/src/consensus/tower1_14_11.rs @@ -9,8 +9,11 @@ use { #[cfg_attr( feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "A5VjnPLPpApf7kjBxMX89crYHxsk6XotSqpG6qXuuwS1") + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + api_digest = "B1L3iQKWwktCs1jEZhvgwn8h4fk6zMVJM2hXbKA1TuZ8", + abi_digest = "DsTqqTxSXRHpGKM12HvSqKaYr18D4WGPGEbYV1cs2W3n" + ) )] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] pub struct Tower1_14_11 { diff --git a/core/src/consensus/tower1_7_14.rs b/core/src/consensus/tower1_7_14.rs index f473730ea9b..0a769840cae 100644 --- a/core/src/consensus/tower1_7_14.rs +++ b/core/src/consensus/tower1_7_14.rs @@ -10,8 +10,11 @@ use { #[cfg_attr( feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "FJeuD6UBJe9D8s6iWwianu6KcnJxSozHxDpPQMTrkNJK") + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + api_digest = "FJeuD6UBJe9D8s6iWwianu6KcnJxSozHxDpPQMTrkNJK", + abi_digest = "D6KFhXxjZNqELNLwVMc1nJYoKDetqTm46U78LUCD5ZAz" + ) )] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] pub struct Tower1_7_14 { @@ -41,8 +44,11 @@ pub struct Tower1_7_14 { #[cfg_attr( feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "82njBGFDS9sGdbSuqdeANPe8rmZW1zsrPRpdgnSmZkpY") + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + api_digest = "82njBGFDS9sGdbSuqdeANPe8rmZW1zsrPRpdgnSmZkpY", + abi_digest = "6U5RkdsuzLHyWpjGWKCHj9AzGFreginb6cXUiXodbgRv" + ) )] #[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct SavedTower1_7_14 { diff --git a/core/src/consensus/tower_storage.rs b/core/src/consensus/tower_storage.rs index fcb93a987b7..21f43d4efc3 100644 --- a/core/src/consensus/tower_storage.rs +++ b/core/src/consensus/tower_storage.rs @@ -14,7 +14,7 @@ use { }, }; -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub enum SavedTowerVersions { V1_17_14(SavedTower1_7_14), @@ -78,8 +78,11 @@ impl From for SavedTowerVersions { #[cfg_attr( feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "2Ne3NmHSeLpPfv38wn7ZRsuq4i56kqYzJeFLYmz6bw3Z") + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + api_digest = "2Ne3NmHSeLpPfv38wn7ZRsuq4i56kqYzJeFLYmz6bw3Z", + abi_digest = "6U5RkdsuzLHyWpjGWKCHj9AzGFreginb6cXUiXodbgRv" + ) )] #[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct SavedTower { diff --git a/core/src/fetch_stage.rs b/core/src/fetch_stage.rs index 3772677c675..a686bbf78fd 100644 --- a/core/src/fetch_stage.rs +++ b/core/src/fetch_stage.rs @@ -4,15 +4,15 @@ use { crate::result::{Error, Result}, crossbeam_channel::{RecvTimeoutError, TrySendError, unbounded}, solana_clock::{DEFAULT_TICKS_PER_SLOT, HOLD_TRANSACTIONS_SLOT_OFFSET}, - solana_metrics::{inc_new_counter_debug, inc_new_counter_info}, solana_packet::PacketFlags, solana_perf::{ - packet::{PacketBatchRecycler, PacketRefMut}, + packet::{PacketBatch, PacketBatchRecycler, PacketRefMut}, recycler::Recycler, }, solana_poh::poh_recorder::PohRecorder, - solana_streamer::streamer::{ - self, PacketBatchReceiver, PacketBatchSender, StreamerReceiveStats, + solana_streamer::{ + evicting_sender::EvictingSender, + streamer::{self, PacketBatchReceiver, PacketBatchSender, StreamerReceiveStats}, }, std::{ net::UdpSocket, @@ -21,10 +21,54 @@ use { atomic::{AtomicBool, Ordering}, }, thread::{self, Builder, JoinHandle, sleep}, - time::Duration, + time::{Duration, Instant}, }, }; +struct ForwardingStats { + last_report: Instant, + /// The number of packets sent along to the next stage + num_packets_sent: usize, + /// The number of packets discarded because we are not leader soon + num_packets_discarded: usize, + /// The number of packets dropped because the channel is already full + num_packets_dropped: usize, +} + +impl ForwardingStats { + const REPORT_INTERVAL: Duration = Duration::from_secs(2); + + fn new() -> Self { + Self { + last_report: Instant::now(), + num_packets_sent: 0, + num_packets_discarded: 0, + num_packets_dropped: 0, + } + } + + fn maybe_report_and_reset(&mut self) { + if self.last_report.elapsed() < Self::REPORT_INTERVAL { + return; + } + + if !self.is_empty() { + datapoint_info!( + "fetch_stage-forwards", + ("num_packets_sent", self.num_packets_sent, i64), + ("num_packets_discarded", self.num_packets_discarded, i64), + ("num_packets_dropped", self.num_packets_dropped, i64) + ); + } + + *self = Self::new(); + } + + fn is_empty(&self) -> bool { + (self.num_packets_sent + self.num_packets_discarded + self.num_packets_dropped) == 0 + } +} + pub struct FetchStage { thread_hdls: Vec>, } @@ -37,7 +81,8 @@ impl FetchStage { coalesce: Option, ) -> (Self, PacketBatchReceiver, PacketBatchReceiver) { let (sender, receiver) = unbounded(); - let (vote_sender, vote_receiver) = unbounded(); + let (vote_sender, vote_receiver) = + EvictingSender::new_bounded(crate::tpu::TPU_VOTE_CHANNEL_SIZE); let (_forward_sender, forward_receiver) = unbounded(); ( Self::new_with_sender( @@ -58,7 +103,7 @@ impl FetchStage { tpu_vote_sockets: Vec, exit: Arc, sender: &PacketBatchSender, - vote_sender: &PacketBatchSender, + vote_sender: &EvictingSender, forward_receiver: PacketBatchReceiver, poh_recorder: &Arc>, coalesce: Option, @@ -79,6 +124,7 @@ impl FetchStage { recvr: &PacketBatchReceiver, sendr: &PacketBatchSender, poh_recorder: &Arc>, + stats: &mut ForwardingStats, ) -> Result<()> { let mark_forwarded = |mut packet: PacketRefMut| { packet.meta_mut().flags |= PacketFlags::FORWARDED; @@ -103,26 +149,20 @@ impl FetchStage { .unwrap() .would_be_leader(HOLD_TRANSACTIONS_SLOT_OFFSET.saturating_mul(DEFAULT_TICKS_PER_SLOT)) { - let mut packets_sent = 0usize; - let mut packets_dropped = 0usize; for packet_batch in packet_batches { let packets_in_batch = packet_batch.len(); match sendr.try_send(packet_batch) { Ok(()) => { - packets_sent += packets_in_batch; + stats.num_packets_sent += packets_in_batch; } Err(TrySendError::Full(_)) => { - packets_dropped += packets_in_batch; + stats.num_packets_dropped += packets_in_batch; } Err(TrySendError::Disconnected(_)) => return Err(Error::Send), }; } - inc_new_counter_debug!("fetch_stage-honor_forwards", packets_sent); - if packets_dropped > 0 { - inc_new_counter_error!("fetch_stage-dropped_forwards", packets_dropped); - } } else { - inc_new_counter_info!("fetch_stage-discard_forwards", num_packets); + stats.num_packets_discarded += num_packets; } Ok(()) @@ -132,7 +172,7 @@ impl FetchStage { tpu_vote_sockets: Vec>, exit: Arc, sender: &PacketBatchSender, - vote_sender: &PacketBatchSender, + vote_sender: &EvictingSender, forward_receiver: PacketBatchReceiver, poh_recorder: &Arc>, coalesce: Option, @@ -164,10 +204,15 @@ impl FetchStage { let fwd_thread_hdl = Builder::new() .name("solFetchStgFwRx".to_string()) .spawn(move || { + let mut stats = ForwardingStats::new(); + loop { - if let Err(e) = - Self::handle_forwarded_packets(&forward_receiver, &sender, &poh_recorder) - { + if let Err(e) = Self::handle_forwarded_packets( + &forward_receiver, + &sender, + &poh_recorder, + &mut stats, + ) { match e { Error::RecvTimeout(RecvTimeoutError::Disconnected) => break, Error::RecvTimeout(RecvTimeoutError::Timeout) => (), @@ -176,6 +221,7 @@ impl FetchStage { _ => error!("{e:?}"), } } + stats.maybe_report_and_reset(); } }) .unwrap(); diff --git a/core/src/forwarding_stage.rs b/core/src/forwarding_stage.rs index 59f99145e43..2c1bf365cdf 100644 --- a/core/src/forwarding_stage.rs +++ b/core/src/forwarding_stage.rs @@ -20,7 +20,8 @@ use { bank_forks::SharableBanks, }, solana_runtime_transaction::{ - runtime_transaction::RuntimeTransaction, transaction_meta::TransactionMeta, + runtime_transaction::RuntimeTransaction, sanitize_config::sanitize_config, + transaction_meta::TransactionMeta, }, solana_streamer::sendmmsg::{SendPktsError, batch_send}, solana_tls_utils::NotifyKeyUpdate, @@ -265,8 +266,8 @@ impl is_tpu_vote_batch: bool, bank: &Bank, ) { - let enable_instruction_accounts_limit = - bank.feature_set.snapshot().limit_instruction_accounts; + let sanitize_config = + sanitize_config(bank.feature_set.snapshot().limit_instruction_accounts); for batch in packet_batches.iter() { for packet in batch .iter() @@ -287,21 +288,20 @@ impl // Perform basic sanitization checks and calculate priority. // If any steps fail, drop the packet. - let Some(priority) = SanitizedTransactionView::try_new_sanitized( - packet_data, - enable_instruction_accounts_limit, - ) - .map_err(|_| ()) - .and_then(|transaction| { - RuntimeTransaction::>::try_new( - transaction, - MessageHash::Compute, - Some(packet.meta().is_simple_vote_tx()), - ) - .map_err(|_| ()) - }) - .ok() - .and_then(|transaction| calculate_priority(&transaction, bank)) else { + let Some(priority) = + SanitizedTransactionView::try_new_sanitized(packet_data, &sanitize_config) + .map_err(|_| ()) + .and_then(|transaction| { + RuntimeTransaction::>::try_new( + transaction, + MessageHash::Compute, + Some(packet.meta().is_simple_vote_tx()), + ) + .map_err(|_| ()) + }) + .ok() + .and_then(|transaction| calculate_priority(&transaction, bank)) + else { self.metrics.votes_dropped_on_receive += vote_count; self.metrics.non_votes_dropped_on_receive += non_vote_count; continue; @@ -765,7 +765,7 @@ fn initial_packet_meta_filter(meta: &packet::Meta) -> bool { mod tests { use { super::*, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, packet::PacketFlags, solana_hash::Hash, solana_keypair::Keypair, @@ -842,7 +842,7 @@ mod tests { #[test] fn test_forwarding() { - let (packet_batch_sender, packet_batch_receiver) = unbounded(); + let (packet_batch_sender, packet_batch_receiver) = bounded(1024); let (_bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&create_genesis_config(1).genesis_config); diff --git a/core/src/lib.rs b/core/src/lib.rs index b0ec144ed7b..51801573b35 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -12,8 +12,7 @@ pub mod admin_rpc_post_init; pub mod banking_simulation; pub mod banking_stage; pub mod banking_trace; -mod block_creation_loop; -pub mod bls_sigverify; +pub(crate) mod block_creation_loop; pub mod cluster_info_vote_listener; pub mod cluster_slots_service; pub mod commitment_service; diff --git a/core/src/next_leader.rs b/core/src/next_leader.rs index 90187ef8a10..2fb409ba8d0 100644 --- a/core/src/next_leader.rs +++ b/core/src/next_leader.rs @@ -1,11 +1,12 @@ use { crate::banking_stage::LikeClusterInfo, itertools::Itertools, - solana_clock::{FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET, NUM_CONSECUTIVE_LEADER_SLOTS}, + solana_clock::FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET, solana_gossip::{ cluster_info::ClusterInfo, contact_info::{ContactInfoQuery, Protocol}, }, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_poh::poh_recorder::PohRecorder, std::{net::SocketAddr, sync::RwLock}, }; @@ -46,7 +47,8 @@ pub(crate) fn next_leaders( let leader_pubkeys: Vec<_> = (0..max_count) .filter_map(|i| { recorder.leader_after_n_slots( - FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET + i * NUM_CONSECUTIVE_LEADER_SLOTS, + FORWARD_TRANSACTIONS_TO_LEADER_AT_SLOT_OFFSET + + i * NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64, ) }) .collect(); diff --git a/core/src/optimistic_confirmation_verifier.rs b/core/src/optimistic_confirmation_verifier.rs index d898bcba492..f9bab6139a0 100644 --- a/core/src/optimistic_confirmation_verifier.rs +++ b/core/src/optimistic_confirmation_verifier.rs @@ -73,8 +73,8 @@ impl OptimisticConfirmationVerifier { timestamp().try_into().unwrap(), ) { error!( - "failed to record optimistic slot in blockstore: slot={}: {:?}", - new_optimistic_slot, &e + "failed to record optimistic slot in blockstore: \ + slot={new_optimistic_slot}: {e:?}" ); } datapoint_info!("optimistic_slot", ("slot", new_optimistic_slot, i64),); diff --git a/core/src/repair/ancestor_hashes_service.rs b/core/src/repair/ancestor_hashes_service.rs index c17d607b07e..84ef3fe8bc6 100644 --- a/core/src/repair/ancestor_hashes_service.rs +++ b/core/src/repair/ancestor_hashes_service.rs @@ -23,7 +23,7 @@ use { solana_keypair::{Keypair, Signer, signable::Signable}, solana_ledger::blockstore::Blockstore, solana_perf::{ - packet::{PacketBatch, PacketRef, deserialize_from_with_limit}, + packet::{PacketBatch, PacketRef, packet_config}, recycler::Recycler, }, solana_pubkey::Pubkey, @@ -375,7 +375,7 @@ impl AncestorHashesService { return None; }; let mut cursor = Cursor::new(packet_data); - let Ok(response) = deserialize_from_with_limit(&mut cursor) else { + let Ok(response) = wincode::config::deserialize_from(&mut cursor, packet_config()) else { stats.invalid_packets += 1; return None; }; @@ -383,7 +383,8 @@ impl AncestorHashesService { match response { AncestorHashesResponse::Hashes(ref hashes) => { // deserialize trailing nonce - let Ok(nonce) = deserialize_from_with_limit(&mut cursor) else { + let Ok(nonce) = wincode::config::deserialize_from(&mut cursor, packet_config()) + else { stats.invalid_packets += 1; return None; }; @@ -453,7 +454,7 @@ impl AncestorHashesService { } stats.ping_count += 1; let pong = RepairProtocol::Pong(Pong::new(&ping, keypair)); - if let Ok(pong) = bincode::serialize(&pong) { + if let Ok(pong) = wincode::serialize(&pong) { let _ = ancestor_socket.send_to(&pong, from_addr); } None @@ -915,7 +916,7 @@ mod test { #[test] pub fn test_ancestor_hashes_service_process_replay_updates() { let (ancestor_hashes_replay_update_sender, ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); let ancestor_hashes_request_statuses = DashMap::new(); let mut dead_slot_pool = HashSet::new(); let mut repairable_dead_slot_pool = HashSet::new(); @@ -1056,7 +1057,7 @@ mod test { #[test] pub fn test_ancestor_hashes_service_process_pruned_replay_updates() { let (ancestor_hashes_replay_update_sender, ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); let ancestor_hashes_request_statuses = DashMap::new(); let mut dead_slot_pool = HashSet::new(); let mut repairable_dead_slot_pool = HashSet::new(); @@ -1259,8 +1260,8 @@ mod test { // Set up thread to give us responses let exit = Arc::new(AtomicBool::new(false)); - let (requests_sender, requests_receiver) = unbounded(); - let (response_sender, response_receiver) = unbounded(); + let (requests_sender, requests_receiver) = bounded(1024); + let (response_sender, response_receiver) = bounded(1024); // Create slots [slot - MAX_ANCESTOR_RESPONSES, slot) with 5 shreds apiece let (shreds, _) = make_many_slot_entries( @@ -1269,7 +1270,7 @@ mod test { 5, ); blockstore - .insert_shreds(shreds, None, false) + .insert_shreds(shreds, false) .expect("Expect successful ledger write"); let mut correct_bank_hashes = HashMap::new(); for duplicate_confirmed_slot in @@ -1354,7 +1355,8 @@ mod test { bank_forks_r.migration_status(), ) }; - let (ancestor_duplicate_slots_sender, _ancestor_duplicate_slots_receiver) = unbounded(); + let (ancestor_duplicate_slots_sender, _ancestor_duplicate_slots_receiver) = + bounded(1024); let repair_info = RepairInfo { bank_forks, cluster_info: requester_cluster_info, @@ -1366,8 +1368,8 @@ mod test { }; let (ancestor_hashes_replay_update_sender, ancestor_hashes_replay_update_receiver) = - unbounded(); - let (retryable_slots_sender, retryable_slots_receiver) = unbounded(); + bounded(1024); + let (retryable_slots_sender, retryable_slots_receiver) = bounded(1024); Self { ancestor_hashes_request_statuses, ancestor_hashes_request_socket, @@ -1421,7 +1423,7 @@ mod test { // Create slots [slot, slot + num_ancestors) with 5 shreds apiece let (shreds, _) = make_many_slot_entries(dead_slot, dead_slot, 5); blockstore - .insert_shreds(shreds, None, false) + .insert_shreds(shreds, false) .expect("Expect successful ledger write"); for duplicate_confirmed_slot in 0..(dead_slot - 1) { let bank_hash = correct_bank_hashes @@ -1978,7 +1980,7 @@ mod test { ref cluster_slots, .. } = repair_info; - let (dumped_slots_sender, _dumped_slots_receiver) = unbounded(); + let (dumped_slots_sender, _dumped_slots_receiver) = bounded(1024); // Add the responder to the eligible list for requests let responder_id = *responder_info.pubkey(); @@ -2204,7 +2206,7 @@ mod test { #[test] fn test_process_replay_updates_continue_after_skipped_update() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let ancestor_hashes_request_statuses = DashMap::new(); let mut dead_slot_pool = HashSet::new(); let mut repairable_dead_slot_pool = HashSet::new(); diff --git a/core/src/repair/block_id_repair_service.rs b/core/src/repair/block_id_repair_service.rs index 28a3c099d6e..ba17dbf45a2 100644 --- a/core/src/repair/block_id_repair_service.rs +++ b/core/src/repair/block_id_repair_service.rs @@ -33,7 +33,6 @@ use { log::{debug, info}, solana_clock::Slot, solana_gossip::ping_pong::{Ping, Pong}, - solana_hash::Hash, solana_keypair::signable::Signable, solana_ledger::{ blockstore::{Blockstore, BlockstoreError, CompletedSlotsReceiver}, @@ -41,7 +40,7 @@ use { shred::DATA_SHREDS_PER_FEC_BLOCK, }, solana_perf::{ - packet::{PacketBatch, PacketRef, deserialize_from_with_limit}, + packet::{PacketBatch, PacketRef, packet_config}, recycler::Recycler, }, solana_pubkey::Pubkey, @@ -157,7 +156,7 @@ enum PendingRepairDecision { /// Action to perform as a result of a repair event enum RepairAction { - StartRepair { slot: Slot, block_id: Hash }, + StartRepair { block: Block }, QueueParent { slot: Slot, location: BlockLocation }, } @@ -408,7 +407,7 @@ impl BlockIdRepairService { let mut first_error = None; // Clean up old request tracking - state.requested_blocks.retain(|(slot, _)| *slot > root); + state.requested_blocks.retain(|block| block.slot > root); state.prune_expected_ping_responses(timestamp()); // Process responses, generate new requests / repair events @@ -544,7 +543,7 @@ impl BlockIdRepairService { return; }; let mut cursor = Cursor::new(packet_data); - let Ok(response) = deserialize_from_with_limit::<_, BlockIdRepairResponse>(&mut cursor) + let Ok(response) = wincode::config::deserialize_from(&mut cursor, packet_config()) .inspect_err(|e| { debug!("Failed to deserialize response: {e:?}"); }) @@ -566,7 +565,7 @@ impl BlockIdRepairService { return; } - let nonce: u32 = match deserialize_from_with_limit(&mut cursor) { + let nonce: u32 = match wincode::config::deserialize_from(&mut cursor, packet_config()) { Ok(n) => n, Err(e) => { debug!("{my_pubkey}: Failed to deserialize nonce: {e:?}"); @@ -601,7 +600,7 @@ impl BlockIdRepairService { .sent_requests .remove(&OutgoingMessage::Metadata(request)); - let (slot, block_id) = request.block(); + let Block { slot, block_id } = request.block(); match response { BlockIdRepairResponse::ParentFecSetCount { @@ -611,8 +610,10 @@ impl BlockIdRepairService { } => { // Queue a request to repair the parent (filtered out later if we already have the parent) state.push_pending_repair_event(RepairEvent::FetchBlock { - slot: p_slot, - block_id: p_block_id, + block: Block { + slot: p_slot, + block_id: p_block_id, + }, }); // Queue FecSetRoot requests @@ -688,7 +689,7 @@ impl BlockIdRepairService { return; } let pong = RepairProtocol::Pong(Pong::new(ping, keypair)); - let pong_bytes = bincode::serialize(&pong).expect("Pong serialization should not fail"); + let pong_bytes = wincode::serialize(&pong).expect("Pong serialization should not fail"); match block_id_repair_socket.send_to(&pong_bytes, addr) { Ok(bytes_sent) if bytes_sent == pong_bytes.len() => { @@ -725,16 +726,16 @@ impl BlockIdRepairService { } match event { - RepairEvent::FetchBlock { slot, block_id } => { - if requested_blocks.contains(&(slot, block_id)) { + RepairEvent::FetchBlock { block } => { + if requested_blocks.contains(&block) { return Ok(PendingRepairDecision::Drop); } // Check if we already have the block, if so queue fetching the parent // Note: when a block becomes full in blockstore -> we atomically calculate the DMR and populate location - if let Some(location) = blockstore.get_block_location(slot, block_id)? { + if let Some(location) = blockstore.get_block_location(block.slot, block.block_id)? { return Ok(PendingRepairDecision::Act(RepairAction::QueueParent { - slot, + slot: block.slot, location, })); } @@ -743,48 +744,47 @@ impl BlockIdRepairService { // Note: we require the invariant that Turbine + Eager repair will either: // - Eventually fill in all shreds for a slot (slot_meta.is_full()) resulting in the DMR calculation // - Mark the slot as dead - if blockstore.is_dead(slot) { + if blockstore.is_dead(block.slot) { info!( - "{my_pubkey}: FetchBlock: slot {slot} is dead, starting repair for \ - block_id={block_id:?}" + "{my_pubkey}: FetchBlock: slot {} is dead, starting repair for \ + block_id={:?}", + block.slot, block.block_id ); return Ok(PendingRepairDecision::Act(RepairAction::StartRepair { - slot, - block_id, + block, })); } // Turbine did not fail, check the progress - match blockstore.get_double_merkle_root(slot, BlockLocation::Original)? { + match blockstore.get_double_merkle_root(block.slot, BlockLocation::Original)? { None => { // Turbine has not completed, defer and check again later debug!( - "{my_pubkey}: FetchBlock: Turbine not complete for slot {slot}, \ - deferring" + "{my_pubkey}: FetchBlock: Turbine not complete for slot {}, deferring", + block.slot ); Ok(PendingRepairDecision::KeepPending) } - Some(turbine_block_id) if turbine_block_id != block_id => { + Some(turbine_block_id) if turbine_block_id != block.block_id => { // Turbine has a different block warn!( "{my_pubkey}: FetchBlock: Turbine has different block \ - {turbine_block_id:?} vs requested {block_id:?} for slot {slot}, \ - starting repair" + {turbine_block_id:?} vs requested block {block:?}, starting repair" ); Ok(PendingRepairDecision::Act(RepairAction::StartRepair { - slot, - block_id, + block, })) } Some(_) => { // Turbine completed between when we checked for the block above and here // Queue the parent debug!( - "{my_pubkey}: FetchBlock: Turbine has correct block for slot {slot}, \ - fetching parent" + "{my_pubkey}: FetchBlock: Turbine has correct block for slot {}, \ + fetching parent", + block.slot ); Ok(PendingRepairDecision::Act(RepairAction::QueueParent { - slot, + slot: block.slot, location: BlockLocation::Original, })) } @@ -801,8 +801,8 @@ impl BlockIdRepairService { state: &mut RepairState, ) -> Result<(), BlockstoreError> { match action { - RepairAction::StartRepair { slot, block_id } => { - if state.requested_blocks.contains(&(slot, block_id)) { + RepairAction::StartRepair { block } => { + if state.requested_blocks.contains(&block) { return Ok(()); } @@ -810,19 +810,20 @@ impl BlockIdRepairService { let alternate_blocks: Vec<_> = state .requested_blocks .iter() - .filter(|(s, _)| *s == slot) - .map(|(_, b)| *b) + .filter(|b| b.slot == block.slot) + .map(|b| b.block_id) .collect(); if alternate_blocks.len() >= MAX_ALTERNATE_BLOCKS_PER_SLOT { error!( - "{my_pubkey}: Too many alternate blocks for slot {slot}, ignoring request \ - for {block_id:?}, requested_blocks: {alternate_blocks:?}" + "{my_pubkey}: Too many alternate blocks for slot {}, ignoring request for \ + {:?}, requested_blocks: {alternate_blocks:?}", + block.slot, block.block_id ); datapoint_error!( "block_id_repair_service-too_many_alternate_blocks", - ("slot", slot, i64), - ("block_id", block_id.to_string(), String), + ("slot", block.slot, i64), + ("block_id", block.block_id.to_string(), String), ); return Ok(()); } @@ -830,9 +831,12 @@ impl BlockIdRepairService { state .pending_repair_requests .push(OutgoingMessage::Metadata( - BlockIdRepairType::ParentAndFecSetCount { slot, block_id }, + BlockIdRepairType::ParentAndFecSetCount { + slot: block.slot, + block_id: block.block_id, + }, )); - state.requested_blocks.insert((slot, block_id)); + state.requested_blocks.insert(block); Ok(()) } RepairAction::QueueParent { slot, location } => { @@ -860,8 +864,10 @@ impl BlockIdRepairService { .expect("SlotMeta must be populated for full slots"); state.push_pending_repair_event(RepairEvent::FetchBlock { - slot: meta.parent_slot.expect("Parent must exist for full slots"), - block_id: meta.parent_block_id, + block: Block { + slot: meta.parent_slot.expect("Parent must exist for full slots"), + block_id: meta.parent_block_id, + }, }); Ok(()) @@ -999,9 +1005,10 @@ impl BlockIdRepairService { ); }) else { - state - .requested_blocks - .remove(&(shred_request.slot(), shred_request.block_id().unwrap())); + state.requested_blocks.remove(&Block { + slot: shred_request.slot(), + block_id: shred_request.block_id().unwrap(), + }); continue; }; @@ -1059,7 +1066,6 @@ impl BlockIdRepairService { mod tests { use { super::*, - bincode::Options, solana_gossip::{cluster_info::ClusterInfo, contact_info::ContactInfo, ping_pong::Ping}, solana_hash::Hash, solana_keypair::{Keypair, Signer}, @@ -1072,7 +1078,7 @@ mod tests { solana_perf::packet::Packet, solana_runtime::{bank::Bank, bank_forks::BankForks, genesis_utils::create_genesis_config}, solana_sha256_hasher::hashv, - std::{io::Cursor, sync::RwLock}, + std::sync::RwLock, }; /// Helper to build a merkle tree from leaf hashes and return the root and proofs @@ -1096,11 +1102,7 @@ mod tests { /// Serialize a response and nonce into packet format fn serialize_response(response: &BlockIdRepairResponse, nonce: u32) -> Vec { - bincode::options() - .with_fixint_encoding() - .allow_trailing_bytes() - .serialize(&(response, nonce)) - .unwrap() + wincode::serialize(&(response, nonce)).unwrap() } /// Create a packet from serialized data @@ -1183,19 +1185,25 @@ mod tests { for slot in base_slot..base_slot + MAX_PENDING_REPAIR_EVENTS as Slot { state.push_pending_repair_event(RepairEvent::FetchBlock { - slot, - block_id: Hash::new_unique(), + block: Block { + slot, + block_id: Hash::new_unique(), + }, }); assert!(state.pending_repair_events.len() <= MAX_PENDING_REPAIR_EVENTS); } state.push_pending_repair_event(RepairEvent::FetchBlock { - slot: 1, - block_id: Hash::new_unique(), + block: Block { + slot: 1, + block_id: Hash::new_unique(), + }, }); state.push_pending_repair_event(RepairEvent::FetchBlock { - slot: base_slot + MAX_PENDING_REPAIR_EVENTS as Slot, - block_id: Hash::new_unique(), + block: Block { + slot: base_slot + MAX_PENDING_REPAIR_EVENTS as Slot, + block_id: Hash::new_unique(), + }, }); assert_eq!(state.pending_repair_events.len(), MAX_PENDING_REPAIR_EVENTS); @@ -1225,12 +1233,12 @@ mod tests { parent_proof: parent_proof.clone(), }; - let data = bincode::serialize(&response).unwrap(); + let data = wincode::serialize(&response).unwrap(); let packet = make_packet(&data); let packet_data = packet.data(..).unwrap(); let deser_response: BlockIdRepairResponse = - deserialize_from_with_limit(&mut Cursor::new(packet_data)).unwrap(); + wincode::config::deserialize(packet_data, packet_config()).unwrap(); match deser_response { BlockIdRepairResponse::ParentFecSetCount { @@ -1257,12 +1265,11 @@ mod tests { fec_set_proof: fec_set_proof.clone(), }; - let data = bincode::serialize(&response).unwrap(); + let data = wincode::serialize(&response).unwrap(); let packet = make_packet(&data); let packet_data = packet.data(..).unwrap(); - let deser_response: BlockIdRepairResponse = - deserialize_from_with_limit(&mut Cursor::new(packet_data)).unwrap(); + let deser_response: BlockIdRepairResponse = wincode::deserialize(packet_data).unwrap(); match deser_response { BlockIdRepairResponse::FecSetRoot { @@ -1281,18 +1288,14 @@ mod tests { // Empty packet let packet = make_packet(&[]); let packet_data = packet.data(..).unwrap(); - assert!( - deserialize_from_with_limit::<_, BlockIdRepairResponse>(&mut Cursor::new(packet_data)) - .is_err() - ); + wincode::config::deserialize::(packet_data, packet_config()) + .unwrap_err(); // Garbage data let packet = make_packet(&[0xff, 0xff, 0xff, 0xff]); let packet_data = packet.data(..).unwrap(); - assert!( - deserialize_from_with_limit::<_, BlockIdRepairResponse>(&mut Cursor::new(packet_data)) - .is_err() - ); + wincode::config::deserialize::(packet_data, packet_config()) + .unwrap_err(); } #[test] @@ -1404,7 +1407,11 @@ mod tests { // Create valid merkle tree for the response let fec_set_roots: Vec = (0..fec_set_count).map(|_| Hash::new_unique()).collect(); - let parent_info_leaf = hashv(&[&parent_slot.to_le_bytes(), parent_block_id.as_ref()]); + let parent_info_leaf = hashv(&[ + &parent_slot.to_le_bytes(), + parent_block_id.as_ref(), + &fec_set_count.to_le_bytes(), + ]); let mut leaves = fec_set_roots.clone(); leaves.push(parent_info_leaf); let (block_id, proofs) = build_merkle_tree(&leaves); @@ -1442,12 +1449,9 @@ mod tests { // Verify: FetchBlock event for parent was added to pending_repair_events assert_eq!(state.pending_repair_events.len(), 1); - let RepairEvent::FetchBlock { - slot: s, - block_id: b, - } = state.pending_repair_events.first().unwrap(); - assert_eq!(*s, parent_slot); - assert_eq!(*b, parent_block_id); + let RepairEvent::FetchBlock { block } = state.pending_repair_events.first().unwrap(); + assert_eq!(block.slot, parent_slot); + assert_eq!(block.block_id, parent_block_id); // Verify: FecSetRoot requests were added to pending assert_eq!(state.pending_repair_requests.len(), fec_set_count_usize); @@ -1603,7 +1607,7 @@ mod tests { let ping = Ping::new([7u8; 32], &ping_keypair); state.expect_ping_response(ping_keypair.pubkey(), from_addr, timestamp()); let response = BlockIdRepairResponse::Ping { ping }; - let data = bincode::serialize(&response).unwrap(); + let data = wincode::serialize(&response).unwrap(); let mut packet = make_packet(&data); packet.meta_mut().set_socket_addr(&from_addr); @@ -1625,7 +1629,7 @@ mod tests { .unwrap(); let mut buffer = vec![0; 2048]; let (size, _) = pong_receiver.recv_from(&mut buffer).unwrap(); - match bincode::deserialize(&buffer[..size]).unwrap() { + match wincode::deserialize(&buffer[..size]).unwrap() { RepairProtocol::Pong(pong) => assert!(pong.verify()), request => panic!("Expected Pong response, got {request:?}"), } @@ -1643,7 +1647,7 @@ mod tests { let first_ping = Ping::new([1u8; 32], &ping_keypair); let response = BlockIdRepairResponse::Ping { ping: first_ping }; - let data = bincode::serialize(&response).unwrap(); + let data = wincode::serialize(&response).unwrap(); let mut packet = make_packet(&data); packet.meta_mut().set_socket_addr(&from_addr); BlockIdRepairService::process_block_id_repair_response( @@ -1656,7 +1660,7 @@ mod tests { let second_ping = Ping::new([2u8; 32], &ping_keypair); let response = BlockIdRepairResponse::Ping { ping: second_ping }; - let data = bincode::serialize(&response).unwrap(); + let data = wincode::serialize(&response).unwrap(); let mut packet = make_packet(&data); packet.meta_mut().set_socket_addr(&from_addr); BlockIdRepairService::process_block_id_repair_response( @@ -1681,7 +1685,7 @@ mod tests { let from_addr = SocketAddr::from(([127, 0, 0, 1], 1234)); let ping = Ping::new([7u8; 32], &ping_keypair); let response = BlockIdRepairResponse::Ping { ping }; - let data = bincode::serialize(&response).unwrap(); + let data = wincode::serialize(&response).unwrap(); let mut packet = make_packet(&data); packet.meta_mut().set_socket_addr(&from_addr); @@ -1710,7 +1714,9 @@ mod tests { // Mark the slot as dead (Turbine failed) blockstore.set_dead_slot(slot).unwrap(); - let event = RepairEvent::FetchBlock { slot, block_id }; + let event = RepairEvent::FetchBlock { + block: Block { slot, block_id }, + }; process_repair_event_for_test(Pubkey::new_unique(), event, 0, &blockstore, &mut state) .unwrap(); @@ -1729,7 +1735,7 @@ mod tests { } // Verify: block was added to requested_blocks - assert!(state.requested_blocks.contains(&(slot, block_id))); + assert!(state.requested_blocks.contains(&Block { slot, block_id })); // Verify: no deferred events assert!(state.pending_repair_events.is_empty()); @@ -1744,7 +1750,9 @@ mod tests { let slot = 100u64; let block_id = Hash::new_unique(); - let event = RepairEvent::FetchBlock { slot, block_id }; + let event = RepairEvent::FetchBlock { + block: Block { slot, block_id }, + }; process_repair_event_for_test(Pubkey::new_unique(), event, 0, &blockstore, &mut state) .unwrap(); @@ -1754,15 +1762,12 @@ mod tests { // Verify: Event was deferred assert_eq!(state.pending_repair_events.len(), 1); - let RepairEvent::FetchBlock { - slot: s, - block_id: b, - } = state.pending_repair_events.first().unwrap(); - assert_eq!(*s, slot); - assert_eq!(*b, block_id); + let RepairEvent::FetchBlock { block } = state.pending_repair_events.first().unwrap(); + assert_eq!(block.slot, slot); + assert_eq!(block.block_id, block_id); // Verify: block was NOT added to requested_blocks (so it can be re-added when reprocessed) - assert!(!state.requested_blocks.contains(&(slot, block_id))); + assert!(!state.requested_blocks.contains(&Block { slot, block_id })); } #[test] @@ -1782,8 +1787,10 @@ mod tests { .unwrap(); let event = RepairEvent::FetchBlock { - slot, - block_id: requested_block_id, + block: Block { + slot, + block_id: requested_block_id, + }, }; process_repair_event_for_test(Pubkey::new_unique(), event, 0, &blockstore, &mut state) @@ -1803,7 +1810,10 @@ mod tests { } // Verify: block was added to requested_blocks - assert!(state.requested_blocks.contains(&(slot, requested_block_id))); + assert!(state.requested_blocks.contains(&Block { + slot, + block_id: requested_block_id + })); // Verify: no deferred events assert!(state.pending_repair_events.is_empty()); @@ -1819,9 +1829,11 @@ mod tests { let block_id = Hash::new_unique(); // Pre-add block to requested_blocks - state.requested_blocks.insert((slot, block_id)); + state.requested_blocks.insert(Block { slot, block_id }); - let event = RepairEvent::FetchBlock { slot, block_id }; + let event = RepairEvent::FetchBlock { + block: Block { slot, block_id }, + }; process_repair_event_for_test(Pubkey::new_unique(), event, 0, &blockstore, &mut state) .unwrap(); @@ -1839,14 +1851,16 @@ mod tests { // Use slot 0 which is at root let slot = 0u64; let block_id = Hash::new_unique(); - let event = RepairEvent::FetchBlock { slot, block_id }; + let event = RepairEvent::FetchBlock { + block: Block { slot, block_id }, + }; process_repair_event_for_test(Pubkey::new_unique(), event, 0, &blockstore, &mut state) .unwrap(); // Verify: No request was added (slot at root is ignored) assert!(state.pending_repair_requests.is_empty()); - assert!(!state.requested_blocks.contains(&(slot, block_id))); + assert!(!state.requested_blocks.contains(&Block { slot, block_id })); } #[test] @@ -1859,13 +1873,18 @@ mod tests { // Fill up requested_blocks with MAX_ALTERNATE_BLOCKS_PER_SLOT blocks for this slot for _ in 0..MAX_ALTERNATE_BLOCKS_PER_SLOT { - state.requested_blocks.insert((slot, Hash::new_unique())); + state.requested_blocks.insert(Block { + slot, + block_id: Hash::new_unique(), + }); } let new_block_id = Hash::new_unique(); let event = RepairEvent::FetchBlock { - slot, - block_id: new_block_id, + block: Block { + slot, + block_id: new_block_id, + }, }; process_repair_event_for_test(Pubkey::new_unique(), event, 0, &blockstore, &mut state) @@ -1874,7 +1893,10 @@ mod tests { // Verify: No new request was added assert!(state.pending_repair_requests.is_empty()); // Verify: new block was NOT added to requested_blocks - assert!(!state.requested_blocks.contains(&(slot, new_block_id))); + assert!(!state.requested_blocks.contains(&Block { + slot, + block_id: new_block_id + })); } #[test] @@ -1894,8 +1916,10 @@ mod tests { .iter() .map(|block_id| { let event = RepairEvent::FetchBlock { - slot, - block_id: *block_id, + block: Block { + slot, + block_id: *block_id, + }, }; match BlockIdRepairService::decide_pending_repair_event( &my_pubkey, @@ -1930,12 +1954,14 @@ mod tests { assert!( block_ids[..MAX_ALTERNATE_BLOCKS_PER_SLOT] .iter() - .all(|block_id| state.requested_blocks.contains(&(slot, *block_id))) - ); - assert!( - !state - .requested_blocks - .contains(&(slot, block_ids[MAX_ALTERNATE_BLOCKS_PER_SLOT])) + .all(|block_id| state.requested_blocks.contains(&Block { + slot, + block_id: *block_id + })) ); + assert!(!state.requested_blocks.contains(&Block { + slot, + block_id: block_ids[MAX_ALTERNATE_BLOCKS_PER_SLOT] + })); } } diff --git a/core/src/repair/cluster_slot_state_verifier.rs b/core/src/repair/cluster_slot_state_verifier.rs index 63b0de897ed..63f8ebcd751 100644 --- a/core/src/repair/cluster_slot_state_verifier.rs +++ b/core/src/repair/cluster_slot_state_verifier.rs @@ -640,17 +640,14 @@ fn on_epoch_slots_frozen( // // Thus if we have a duplicate confirmation, but `slot` is pruned, we continue // processing it as `epoch_slots_frozen`. - if !is_popular_pruned { - if let Some(duplicate_confirmed_hash) = duplicate_confirmed_hash { - if epoch_slots_frozen_hash != duplicate_confirmed_hash { - warn!( - "EpochSlots sample returned slot {slot} with hash {epoch_slots_frozen_hash}, \ - but we already saw duplicate confirmation on hash: \ - {duplicate_confirmed_hash:?}", - ); - } - return vec![]; + if !is_popular_pruned && let Some(duplicate_confirmed_hash) = duplicate_confirmed_hash { + if epoch_slots_frozen_hash != duplicate_confirmed_hash { + warn!( + "EpochSlots sample returned slot {slot} with hash {epoch_slots_frozen_hash}, but \ + we already saw duplicate confirmation on hash: {duplicate_confirmed_hash:?}", + ); } + return vec![]; } match bank_status { @@ -900,10 +897,10 @@ pub(crate) fn check_slot_agrees_with_cluster( // Avoid duplicate work from multiple of the same DuplicateConfirmed signal. This can // happen if we get duplicate confirmed from gossip and from local replay. if let SlotStateUpdate::DuplicateConfirmed(state) = &slot_state_update { - if let Some(bank_hash) = state.bank_status.bank_hash() { - if let Some(true) = fork_choice.is_duplicate_confirmed(&(slot, bank_hash)) { - return; - } + if let Some(bank_hash) = state.bank_status.bank_hash() + && let Some(true) = fork_choice.is_duplicate_confirmed(&(slot, bank_hash)) + { + return; } datapoint_info!( @@ -926,15 +923,13 @@ pub(crate) fn check_slot_agrees_with_cluster( ); } - if let SlotStateUpdate::EpochSlotsFrozen(epoch_slots_frozen_state) = &slot_state_update { - if let Some(old_epoch_slots_frozen_hash) = + if let SlotStateUpdate::EpochSlotsFrozen(epoch_slots_frozen_state) = &slot_state_update + && let Some(old_epoch_slots_frozen_hash) = epoch_slots_frozen_slots.insert(slot, epoch_slots_frozen_state.epoch_slots_frozen_hash) - { - if old_epoch_slots_frozen_hash == epoch_slots_frozen_state.epoch_slots_frozen_hash { - // If EpochSlots has already told us this same hash was frozen, return - return; - } - } + && old_epoch_slots_frozen_hash == epoch_slots_frozen_state.epoch_slots_frozen_hash + { + // If EpochSlots has already told us this same hash was frozen, return + return; } let state_changes = slot_state_update.into_state_changes(slot); @@ -954,7 +949,7 @@ mod test { use { super::*, crate::{consensus::progress_map::ProgressMap, replay_stage::tests::setup_forks_from_tree}, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_runtime::bank_forks::BankForks, std::{ collections::{HashMap, HashSet}, @@ -1616,7 +1611,7 @@ mod test { .unwrap() .hash(); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); apply_state_changes( duplicate_slot, &mut heaviest_subtree_fork_choice, @@ -1699,7 +1694,7 @@ mod test { // BankFrozen should mark it down in Blockstore. assert!(blockstore.get_bank_hash(duplicate_slot).is_none()); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); apply_state_changes( duplicate_slot, &mut heaviest_subtree_fork_choice, @@ -1780,7 +1775,7 @@ mod test { )]; modify_state_changes(our_duplicate_slot_hash, &mut state_changes); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); apply_state_changes( duplicate_slot, &mut heaviest_subtree_fork_choice, @@ -1864,7 +1859,7 @@ mod test { || initial_bank_hash, ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( duplicate_slot, root, @@ -1976,7 +1971,7 @@ mod test { || Some(slot2_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 2, root, @@ -2098,7 +2093,7 @@ mod test { || Some(slot2_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 2, root, @@ -2222,7 +2217,7 @@ mod test { || Some(slot3_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 3, root, @@ -2308,7 +2303,7 @@ mod test { false, ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 3, root, @@ -2338,7 +2333,7 @@ mod test { || Some(slot3_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 3, root, @@ -2402,7 +2397,7 @@ mod test { false, ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 3, root, diff --git a/core/src/repair/duplicate_repair_status.rs b/core/src/repair/duplicate_repair_status.rs index 5eff267d4a3..60ce6450c96 100644 --- a/core/src/repair/duplicate_repair_status.rs +++ b/core/src/repair/duplicate_repair_status.rs @@ -206,18 +206,15 @@ impl AncestorRequestStatus { response_slot_hashes: Vec<(Slot, Hash)>, blockstore: &Blockstore, ) -> Option { - if let Some(did_get_response) = self.sampled_validators.get_mut(from_addr) { - if *did_get_response { - // If we've already received a response from this validator, return. - return None; - } - // Mark we got a response from this validator already - *did_get_response = true; - self.num_responses += 1; - } else { - // If this is not a response from one of the sampled validators, return. + // If this is not a response from one of the sampled validators, return. + let did_get_response = self.sampled_validators.get_mut(from_addr)?; + if *did_get_response { + // If we've already received a response from this validator, return. return None; } + // Mark we got a response from this validator already + *did_get_response = true; + self.num_responses += 1; let validators_with_same_response = self .ancestor_request_responses diff --git a/core/src/repair/malicious_repair_handler.rs b/core/src/repair/malicious_repair_handler.rs index 27a225ed881..d0b878f739a 100644 --- a/core/src/repair/malicious_repair_handler.rs +++ b/core/src/repair/malicious_repair_handler.rs @@ -56,10 +56,10 @@ impl MaliciousRepairHandler { /// Check if we should respond maliciously for this slot and shred index fn should_respond_maliciously(&self, slot: Slot, shred_index: u64) -> bool { - if let Some((start, end)) = self.config.slot_range { - if slot < start || slot > end { - return false; - } + if let Some((start, end)) = self.config.slot_range + && (slot < start || slot > end) + { + return false; } let slot_matches = self @@ -162,16 +162,14 @@ impl RepairHandler for MaliciousRepairHandler { // Parse the original shred to get its metadata if let Ok(original_shred) = Shred::new_from_serialized_shred(original_shred_bytes.clone()) - { - if let Some(equivocating_shred) = + && let Some(equivocating_shred) = self.generate_equivocating_shred(&original_shred, shred_index) - { - info!( - "Responding with equivocating shred in slot {slot} index {shred_index} to \ - {dest}" - ); - return repair_response_packet_from_bytes(equivocating_shred, dest, nonce); - } + { + info!( + "Responding with equivocating shred in slot {slot} index {shred_index} to \ + {dest}" + ); + return repair_response_packet_from_bytes(equivocating_shred, dest, nonce); } } diff --git a/core/src/repair/repair_generic_traversal.rs b/core/src/repair/repair_generic_traversal.rs index b7090c04faa..2117955de94 100644 --- a/core/src/repair/repair_generic_traversal.rs +++ b/core/src/repair/repair_generic_traversal.rs @@ -6,10 +6,11 @@ use { serve_repair::ShredRepairType, }, }, + ahash::{AHashMap, AHashSet}, solana_clock::Slot, solana_hash::Hash, - solana_ledger::{blockstore::Blockstore, blockstore_meta::SlotMeta}, - std::collections::{HashMap, HashSet}, + solana_ledger::{blockstore::Blockstore, blockstore_meta::SlotMetaRepair}, + std::collections::HashMap, }; struct GenericTraversal<'a> { @@ -31,12 +32,11 @@ impl Iterator for GenericTraversal<'_> { fn next(&mut self) -> Option { let next = self.pending.pop(); if let Some(slot) = next { - let children: Vec<_> = self + let children = self .tree .children(&(slot, Hash::default())) .unwrap() - .map(|(child_slot, _)| *child_slot) - .collect(); + .map(|(child_slot, _)| *child_slot); self.pending.extend(children); } next @@ -52,8 +52,8 @@ impl Iterator for GenericTraversal<'_> { pub fn get_unknown_last_index( tree: &HeaviestSubtreeForkChoice, blockstore: &Blockstore, - slot_meta_cache: &mut HashMap>, - processed_slots: &mut HashSet, + slot_meta_cache: &mut AHashMap>, + processed_slots: &mut AHashSet, limit: usize, outstanding_repairs: &mut HashMap, ) -> Vec { @@ -65,18 +65,18 @@ pub fn get_unknown_last_index( } let slot_meta = slot_meta_cache .entry(slot) - .or_insert_with(|| blockstore.meta(slot).unwrap()); - if let Some(slot_meta) = slot_meta { - if slot_meta.last_index.is_none() { - let shred_index = blockstore.get_index(slot).unwrap(); - let num_processed_shreds = if let Some(shred_index) = shred_index { - shred_index.data().num_shreds() as u64 - } else { - slot_meta.consumed - }; - unknown_last.push((slot, slot_meta.received, num_processed_shreds)); - processed_slots.insert(slot); - } + .or_insert_with(|| blockstore.meta_repair(slot).unwrap()); + if let Some(slot_meta) = slot_meta + && slot_meta.last_index.is_none() + { + let shred_index = blockstore.get_index(slot).unwrap(); + let num_processed_shreds = if let Some(shred_index) = shred_index { + shred_index.data().num_shreds() as u64 + } else { + slot_meta.consumed + }; + unknown_last.push((slot, slot_meta.received, num_processed_shreds)); + processed_slots.insert(slot); } } // Prioritize slots with more data shreds currently present in blockstore. @@ -98,21 +98,21 @@ pub fn get_unknown_last_index( fn get_unrepaired_path( start_slot: Slot, blockstore: &Blockstore, - slot_meta_cache: &mut HashMap>, - visited: &mut HashSet, + slot_meta_cache: &mut AHashMap>, + visited: &mut AHashSet, ) -> Vec { let mut path = Vec::new(); let mut slot = start_slot; while visited.insert(slot) { let slot_meta = slot_meta_cache .entry(slot) - .or_insert_with(|| blockstore.meta(slot).unwrap()); - if let Some(slot_meta) = slot_meta { - if !slot_meta.is_full() { - path.push(slot); - if let Some(parent_slot) = slot_meta.parent_slot { - slot = parent_slot - } + .or_insert_with(|| blockstore.meta_repair(slot).unwrap()); + if let Some(slot_meta) = slot_meta + && !slot_meta.is_full() + { + path.push(slot); + if let Some(parent_slot) = slot_meta.parent_slot { + slot = parent_slot } } } @@ -127,8 +127,8 @@ pub fn get_closest_completion( tree: &HeaviestSubtreeForkChoice, blockstore: &Blockstore, root_slot: Slot, - slot_meta_cache: &mut HashMap>, - processed_slots: &mut HashSet, + slot_meta_cache: &mut AHashMap>, + processed_slots: &mut AHashSet, limit: usize, repair_eligibility: &mut RepairEligibility, outstanding_repairs: &mut HashMap, @@ -141,7 +141,7 @@ pub fn get_closest_completion( } let slot_meta = slot_meta_cache .entry(slot) - .or_insert_with(|| blockstore.meta(slot).unwrap()); + .or_insert_with(|| blockstore.meta_repair(slot).unwrap()); if let Some(slot_meta) = slot_meta { if slot_meta.is_full() { continue; @@ -187,7 +187,7 @@ pub fn get_closest_completion( } slot_dists.sort_by_key(|(_, d)| *d); - let mut visited = HashSet::from([root_slot]); + let mut visited = AHashSet::from([root_slot]); let mut repairs = Vec::new(); let mut total_processed_slots = 0; for (slot, _) in slot_dists { @@ -233,8 +233,8 @@ pub mod test { fn test_get_unknown_last_index() { let (blockstore, heaviest_subtree_fork_choice) = setup_forks(); let last_shred = blockstore.meta(0).unwrap().unwrap().received; - let mut slot_meta_cache = HashMap::default(); - let mut processed_slots = HashSet::default(); + let mut slot_meta_cache = AHashMap::default(); + let mut processed_slots = AHashSet::default(); let mut outstanding_requests = HashMap::new(); let repairs = get_unknown_last_index( &heaviest_subtree_fork_choice, @@ -268,8 +268,8 @@ pub mod test { #[test] fn test_get_closest_completion() { let (blockstore, heaviest_subtree_fork_choice) = setup_forks(); - let mut slot_meta_cache = HashMap::default(); - let mut processed_slots = HashSet::default(); + let mut slot_meta_cache = AHashMap::default(); + let mut processed_slots = AHashSet::default(); let mut outstanding_requests = HashMap::new(); let (repairs, _) = get_closest_completion( &heaviest_subtree_fork_choice, @@ -296,8 +296,8 @@ pub mod test { Hash::default(), ); let heaviest_subtree_fork_choice = HeaviestSubtreeForkChoice::new_from_tree(forks); - let mut slot_meta_cache = HashMap::default(); - let mut processed_slots = HashSet::default(); + let mut slot_meta_cache = AHashMap::default(); + let mut processed_slots = AHashSet::default(); outstanding_requests = HashMap::new(); let mut repair_eligibility = RepairEligibility::elapsed_for_slots_for_tests(&blockstore, 0..=5); @@ -388,7 +388,7 @@ pub mod test { shreds.pop().unwrap(); shreds.push(shred); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); } walk.forward(); } diff --git a/core/src/repair/repair_handler.rs b/core/src/repair/repair_handler.rs index 9ea511fe229..dbb6c501db8 100644 --- a/core/src/repair/repair_handler.rs +++ b/core/src/repair/repair_handler.rs @@ -10,7 +10,6 @@ use { serve_repair::{AncestorHashesResponse, BlockIdRepairResponse, MAX_ANCESTOR_RESPONSES}, }, agave_votor_messages::migration::MigrationStatus, - bincode::serialize, solana_clock::Slot, solana_gossip::cluster_info::ClusterInfo, solana_hash::Hash, @@ -30,16 +29,20 @@ use { net::SocketAddr, sync::{Arc, RwLock}, }, + wincode::{SchemaWrite, serialize}, }; /// Helper function to create a PacketBatch from a serializable response -fn create_response_packet_batch( +fn create_response_packet_batch( recycler: &PacketBatchRecycler, response: &T, from_addr: &SocketAddr, nonce: Nonce, debug_label: &'static str, -) -> Option { +) -> Option +where + T: SchemaWrite, +{ let serialized_response = serialize(response).ok()?; let packet = repair_response::repair_response_packet_from_bytes(serialized_response, from_addr, nonce)?; @@ -333,7 +336,6 @@ mod tests { .into_iter() .chain(parent_coding_shreds) .collect::>(), - None, true, ) .unwrap(); @@ -356,7 +358,6 @@ mod tests { .into_iter() .chain(coding_shreds) .collect::>(), - None, true, // is_trusted ) .unwrap(); @@ -423,7 +424,7 @@ mod tests { let packet = packet_batch.iter().next().unwrap(); let (response, response_nonce): (BlockIdRepairResponse, Nonce) = - bincode::deserialize(packet.data(..packet.meta().size).unwrap()).unwrap(); + wincode::deserialize(packet.data(..packet.meta().size).unwrap()).unwrap(); assert_eq!(response_nonce, nonce); match response { @@ -490,7 +491,7 @@ mod tests { let packet = packet_batch.iter().next().unwrap(); let (response, response_nonce): (BlockIdRepairResponse, Nonce) = - bincode::deserialize(packet.data(..packet.meta().size).unwrap()).unwrap(); + wincode::deserialize(packet.data(..packet.meta().size).unwrap()).unwrap(); assert_eq!(response_nonce, nonce); match response { diff --git a/core/src/repair/repair_response.rs b/core/src/repair/repair_response.rs index ae53a792f69..6c9fa310d07 100644 --- a/core/src/repair/repair_response.rs +++ b/core/src/repair/repair_response.rs @@ -38,7 +38,7 @@ pub fn repair_response_packet_from_bytes( packet.meta_mut().set_socket_addr(dest); packet.buffer_mut()[..bytes.len()].copy_from_slice(bytes); let mut wr = io::Cursor::new(&mut packet.buffer_mut()[bytes.len()..]); - bincode::serialize_into(&mut wr, &nonce).expect("Buffer not large enough to fit nonce"); + wincode::serialize_into(&mut wr, &nonce).expect("Buffer not large enough to fit nonce"); Some(packet) } diff --git a/core/src/repair/repair_service.rs b/core/src/repair/repair_service.rs index e83885cee68..af160ab2e56 100644 --- a/core/src/repair/repair_service.rs +++ b/core/src/repair/repair_service.rs @@ -3,7 +3,6 @@ use { super::standard_repair_handler::StandardRepairHandler, crate::{ - cluster_info_vote_listener::VerifiedVoterSlotsReceiver, cluster_slots_service::cluster_slots::ClusterSlots, repair::{ ancestor_hashes_service::{ @@ -18,6 +17,9 @@ use { }, }, }, + agave_votor_messages::{VerifiedVoterSlotsReceiver, migration::MigrationStatus}, + ahash::AHashMap, + bytes::Bytes, crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender}, lazy_lru::LruCache, rand::prelude::IndexedRandom as _, @@ -28,11 +30,12 @@ use { solana_hash::Hash, solana_keypair::Signer, solana_ledger::{ - blockstore::{Blockstore, SlotMeta}, - blockstore_meta::BlockLocation, + blockstore::Blockstore, + blockstore_meta::{BlockLocation, SlotMetaRepair}, shred, }, solana_measure::measure::Measure, + solana_net_utils::PinnedXdpSender, solana_pubkey::Pubkey, solana_runtime::{ bank::Bank, @@ -137,7 +140,7 @@ impl SlotRepairFecTimes { /// probing. #[derive(Debug, Default)] pub struct RepairEligibility { - slots: HashMap, + slots: AHashMap, } impl RepairEligibility { @@ -153,7 +156,7 @@ impl RepairEligibility { /// highest received shred falls in an older FEC, that FEC was already /// timestamped; if it falls in a future FEC, resizing the vector backfills /// any skipped FECs with the same timestamp. - fn observe_slot(&mut self, slot: Slot, slot_meta: &SlotMeta) { + fn observe_slot(&mut self, slot: Slot, slot_meta: &SlotMetaRepair) { let Some(last_received_index) = slot_meta.received.checked_sub(1) else { // Have not observed any shreds for this slot yet. return; @@ -207,7 +210,7 @@ impl RepairEligibility { pub(crate) fn is_highest_shred_eligible( &self, slot: Slot, - slot_meta: &SlotMeta, + slot_meta: &SlotMetaRepair, now_ms: u64, ) -> bool { if slot_meta.last_index.is_some() { @@ -230,7 +233,11 @@ impl RepairEligibility { /// This keeps traversal tests focused on ordering and duplicate suppression /// without sleeping in every setup path. #[cfg(test)] - pub(crate) fn observe_slot_as_elapsed_for_tests(&mut self, slot: Slot, slot_meta: &SlotMeta) { + pub(crate) fn observe_slot_as_elapsed_for_tests( + &mut self, + slot: Slot, + slot_meta: &SlotMetaRepair, + ) { self.observe_slot(slot, slot_meta); if let Some(slot_repair) = self.slots.get_mut(&slot) { let elapsed_time = timestamp().saturating_sub(FEC_REPAIR_DELAY.as_millis() as u64); @@ -250,7 +257,7 @@ impl RepairEligibility { ) -> Self { let mut repair_eligibility = Self::default(); for slot in slots { - if let Some(slot_meta) = blockstore.meta(slot).unwrap() { + if let Some(slot_meta) = blockstore.meta_repair(slot).unwrap() { repair_eligibility.observe_slot_as_elapsed_for_tests(slot, &slot_meta); } } @@ -384,66 +391,52 @@ impl RepairStats { #[derive(Default, Debug)] pub struct RepairTiming { - pub set_root_elapsed: u64, - pub dump_slots_elapsed: u64, - pub get_votes_elapsed: u64, - pub add_voters_elapsed: u64, - pub purge_outstanding_repairs: u64, - pub handle_popular_pruned_forks: u64, - pub get_best_orphans_elapsed: u64, - pub get_best_shreds_elapsed: u64, - pub get_unknown_last_index_elapsed: u64, - pub get_closest_completion_elapsed: u64, - pub send_repairs_elapsed: u64, - pub build_repairs_batch_elapsed: u64, - pub batch_send_repairs_elapsed: u64, + pub set_root_us: u64, + pub dump_slots_us: u64, + pub get_votes_us: u64, + pub add_voters_us: u64, + pub purge_outstanding_repairs_us: u64, + pub handle_popular_pruned_forks_us: u64, + pub get_best_orphans_us: u64, + pub get_best_shreds_us: u64, + pub get_unknown_last_index_us: u64, + pub get_closest_completion_us: u64, + pub build_batch_us: u64, + pub send_batch_us: u64, } impl RepairTiming { fn report(&self) { datapoint_info!( "repair_service-repair_timing", - ("set-root-elapsed", self.set_root_elapsed, i64), - ("dump-slots-elapsed", self.dump_slots_elapsed, i64), - ("get-votes-elapsed", self.get_votes_elapsed, i64), - ("add-voters-elapsed", self.add_voters_elapsed, i64), - ( - "purge-outstanding-repairs", - self.purge_outstanding_repairs, - i64 - ), + ("set-root-us", self.set_root_us, i64), + ("dump-slots-us", self.dump_slots_us, i64), + ("get-votes-us", self.get_votes_us, i64), + ("add-voters-us", self.add_voters_us, i64), ( - "handle-popular-pruned-forks", - self.handle_popular_pruned_forks, + "purge-outstanding-repairs-us", + self.purge_outstanding_repairs_us, i64 ), ( - "get-best-orphans-elapsed", - self.get_best_orphans_elapsed, + "handle-popular-pruned-forks-us", + self.handle_popular_pruned_forks_us, i64 ), - ("get-best-shreds-elapsed", self.get_best_shreds_elapsed, i64), + ("get-best-orphans-us", self.get_best_orphans_us, i64), + ("get-best-shreds-us", self.get_best_shreds_us, i64), ( - "get-unknown-last-index-elapsed", - self.get_unknown_last_index_elapsed, + "get-unknown-last-index-us", + self.get_unknown_last_index_us, i64 ), ( - "get-closest-completion-elapsed", - self.get_closest_completion_elapsed, - i64 - ), - ("send-repairs-elapsed", self.send_repairs_elapsed, i64), - ( - "build-repairs-batch-elapsed", - self.build_repairs_batch_elapsed, - i64 - ), - ( - "batch-send-repairs-elapsed", - self.batch_send_repairs_elapsed, + "get-closest-completion-us", + self.get_closest_completion_us, i64 ), + ("build-batch-us", self.build_batch_us, i64), + ("send-batch-us", self.send_batch_us, i64), ); } } @@ -621,6 +614,7 @@ impl RepairService { repair_info: RepairInfo, outstanding_requests: Arc>, repair_service_channels: RepairServiceChannels, + xdp_sender: Option, ) -> Self { let t_repair = { let blockstore = blockstore.clone(); @@ -636,6 +630,7 @@ impl RepairService { repair_service_channels.repair_channels, repair_info, &outstanding_requests, + xdp_sender, ) }) .unwrap() @@ -665,12 +660,12 @@ impl RepairService { repair_metrics: &mut RepairMetrics, ) { // Purge outdated slots from the weighting heuristic - let mut set_root_elapsed = Measure::start("set_root_elapsed"); + let mut set_root_us = Measure::start("set_root_us"); repair_weight.set_root(root_bank.slot()); - set_root_elapsed.stop(); + set_root_us.stop(); // Remove dumped slots from the weighting heuristic - let mut dump_slots_elapsed = Measure::start("dump_slots_elapsed"); + let mut dump_slots_us = Measure::start("dump_slots_us"); dumped_slots_receiver .try_iter() .for_each(|slot_hash_keys_to_dump| { @@ -697,10 +692,10 @@ impl RepairService { } } }); - dump_slots_elapsed.stop(); + dump_slots_us.stop(); // Add new votes to the weighting heuristic - let mut get_votes_elapsed = Measure::start("get_votes_elapsed"); + let mut get_votes_us = Measure::start("get_votes_us"); let mut slot_to_vote_pubkeys: HashMap> = HashMap::new(); verified_voter_slots_receiver .try_iter() @@ -712,21 +707,21 @@ impl RepairService { .push(vote_pubkey); } }); - get_votes_elapsed.stop(); + get_votes_us.stop(); - let mut add_voters_elapsed = Measure::start("add_voters"); + let mut add_voters_us = Measure::start("add_voters_us"); repair_weight.add_voters( blockstore, slot_to_vote_pubkeys.into_iter(), root_bank.epoch_stakes_map(), root_bank.epoch_schedule(), ); - add_voters_elapsed.stop(); + add_voters_us.stop(); - repair_metrics.timing.set_root_elapsed += set_root_elapsed.as_us(); - repair_metrics.timing.dump_slots_elapsed += dump_slots_elapsed.as_us(); - repair_metrics.timing.get_votes_elapsed += get_votes_elapsed.as_us(); - repair_metrics.timing.add_voters_elapsed += add_voters_elapsed.as_us(); + repair_metrics.timing.set_root_us += set_root_us.as_us(); + repair_metrics.timing.dump_slots_us += dump_slots_us.as_us(); + repair_metrics.timing.get_votes_us += get_votes_us.as_us(); + repair_metrics.timing.add_voters_us += add_voters_us.as_us(); } fn identify_repairs( @@ -738,13 +733,13 @@ impl RepairService { outstanding_repairs: &mut HashMap, repair_metrics: &mut RepairMetrics, ) -> Vec { - let mut purge_outstanding_repairs = Measure::start("purge_outstanding_repairs"); + let mut purge_outstanding_repairs_us = Measure::start("purge_outstanding_repairs_us"); // Purge old entries. They've either completed or need to be retried. outstanding_repairs.retain(|_repair_request, time| { timestamp().saturating_sub(*time) < REPAIR_REQUEST_TIMEOUT_MS }); - purge_outstanding_repairs.stop(); - repair_metrics.timing.purge_outstanding_repairs = purge_outstanding_repairs.as_us(); + purge_outstanding_repairs_us.stop(); + repair_metrics.timing.purge_outstanding_repairs_us += purge_outstanding_repairs_us.as_us(); repair_eligibility.set_root(root_bank.slot()); repair_weight.get_best_weighted_repairs( @@ -768,7 +763,7 @@ impl RepairService { popular_pruned_forks_sender: &PopularPrunedForksSender, repair_metrics: &mut RepairMetrics, ) { - let mut handle_popular_pruned_forks = Measure::start("handle_popular_pruned_forks"); + let mut handle_popular_pruned_forks_us = Measure::start("handle_popular_pruned_forks_us"); let mut popular_pruned_forks = repair_weight .get_popular_pruned_forks(root_bank.epoch_stakes_map(), root_bank.epoch_schedule()); // Check if we've already sent a request along this pruned fork @@ -789,8 +784,9 @@ impl RepairService { .send(popular_pruned_forks) .unwrap_or_else(|err| error!("failed to send popular pruned forks {err}")); } - handle_popular_pruned_forks.stop(); - repair_metrics.timing.handle_popular_pruned_forks = handle_popular_pruned_forks.as_us(); + handle_popular_pruned_forks_us.stop(); + repair_metrics.timing.handle_popular_pruned_forks_us += + handle_popular_pruned_forks_us.as_us(); } fn build_and_send_repair_batch( @@ -800,9 +796,10 @@ impl RepairService { repair_info: &RepairInfo, outstanding_requests: &RwLock, repair_socket: &UdpSocket, + xdp_sender: Option<&PinnedXdpSender>, repair_metrics: &mut RepairMetrics, ) { - let mut build_repairs_batch_elapsed = Measure::start("build_repairs_batch_elapsed"); + let mut build_batch_us = Measure::start("build_batch_us"); let batch: Vec<(Vec, SocketAddr)> = { let mut outstanding_requests = outstanding_requests.write().unwrap(); repairs @@ -821,27 +818,35 @@ impl RepairService { }) .collect() }; - build_repairs_batch_elapsed.stop(); + build_batch_us.stop(); - let mut batch_send_repairs_elapsed = Measure::start("batch_send_repairs_elapsed"); + let mut send_batch_us = Measure::start("send_batch_us"); if !batch.is_empty() { let num_pkts = batch.len(); - let batch = batch.iter().map(|(bytes, addr)| (bytes, addr)); - match batch_send(repair_socket, batch) { - Ok(()) => (), - Err(SendPktsError::IoError(err, num_failed)) => { - error!( - "{} batch_send failed to send {num_failed}/{num_pkts} packets first error \ - {err:?}", - repair_info.cluster_info.id() - ); + if let Some(xdp) = xdp_sender { + for (i, (bytes, addr)) in batch.into_iter().enumerate() { + if let Err(e) = xdp.try_send(i, addr, Bytes::from(bytes)) { + warn!("repair xdp send failed: {e:?}"); + } + } + } else { + let batch = batch.iter().map(|(bytes, addr)| (bytes, addr)); + match batch_send(repair_socket, batch) { + Ok(()) => (), + Err(SendPktsError::IoError(err, num_failed)) => { + error!( + "{} batch_send failed to send {num_failed}/{num_pkts} packets first \ + error {err:?}", + repair_info.cluster_info.id() + ); + } } } } - batch_send_repairs_elapsed.stop(); + send_batch_us.stop(); - repair_metrics.timing.build_repairs_batch_elapsed = build_repairs_batch_elapsed.as_us(); - repair_metrics.timing.batch_send_repairs_elapsed = batch_send_repairs_elapsed.as_us(); + repair_metrics.timing.build_batch_us += build_batch_us.as_us(); + repair_metrics.timing.send_batch_us += send_batch_us.as_us(); } fn run_repair_iteration( @@ -851,6 +856,8 @@ impl RepairService { repair_tracker: &mut RepairTracker, outstanding_requests: &RwLock, repair_socket: &UdpSocket, + xdp_sender: Option<&PinnedXdpSender>, + migration_status: &MigrationStatus, ) { let RepairChannels { verified_voter_slots_receiver, @@ -889,13 +896,15 @@ impl RepairService { repair_metrics, ); - Self::handle_popular_pruned_forks( - root_bank.clone(), - repair_weight, - popular_pruned_forks_requests, - popular_pruned_forks_sender, - repair_metrics, - ); + if !migration_status.is_alpenglow_enabled() { + Self::handle_popular_pruned_forks( + root_bank.clone(), + repair_weight, + popular_pruned_forks_requests, + popular_pruned_forks_sender, + repair_metrics, + ); + } Self::build_and_send_repair_batch( serve_repair, @@ -904,6 +913,7 @@ impl RepairService { repair_info, outstanding_requests, repair_socket, + xdp_sender, repair_metrics, ); } @@ -915,6 +925,7 @@ impl RepairService { repair_channels: RepairChannels, repair_info: RepairInfo, outstanding_requests: &RwLock, + xdp_sender: Option, ) { let (sharable_banks, migration_status) = { let bank_forks_r = repair_info.bank_forks.read().unwrap(); @@ -933,7 +944,7 @@ impl RepairService { sharable_banks, repair_info.repair_whitelist.clone(), Box::new(StandardRepairHandler::new(blockstore.clone())), - migration_status, + migration_status.clone(), ) }, repair_metrics: RepairMetrics::default(), @@ -951,6 +962,8 @@ impl RepairService { &mut repair_tracker, outstanding_requests, repair_socket, + xdp_sender.as_ref(), + migration_status.as_ref(), ); repair_tracker.repair_metrics.maybe_report(); sleep(Duration::from_millis(REPAIR_MS)); @@ -962,7 +975,7 @@ impl RepairService { pub(crate) fn generate_repairs_for_slot( blockstore: &Blockstore, slot: Slot, - slot_meta: &SlotMeta, + slot_meta: &SlotMetaRepair, repair_eligibility: &mut RepairEligibility, max_repairs: usize, outstanding_repairs: &mut HashMap, @@ -1018,7 +1031,7 @@ impl RepairService { let mut pending_slots = vec![slot]; while repairs.len() < max_repairs && !pending_slots.is_empty() { let slot = pending_slots.pop().unwrap(); - if let Some(slot_meta) = blockstore.meta(slot).unwrap() { + if let Some(slot_meta) = blockstore.meta_repair(slot).unwrap() { let new_repairs = Self::generate_repairs_for_slot( blockstore, slot, @@ -1198,11 +1211,11 @@ impl RepairService { } let meta = blockstore - .meta(slot) + .meta_repair(slot) .expect("Unable to lookup slot meta") - .unwrap_or(SlotMeta { + .unwrap_or(SlotMetaRepair { slot, - ..SlotMeta::default() + ..SlotMetaRepair::default() }); repair_eligibility.observe_slot_as_elapsed_for_tests(slot, &meta); @@ -1225,7 +1238,7 @@ impl RepairService { blockstore: &Blockstore, slot: Slot, ) -> Option> { - if let Some(slot_meta) = blockstore.meta(slot).unwrap() { + if let Some(slot_meta) = blockstore.meta_repair(slot).unwrap() { if slot_meta.is_full() { // If the slot is full, no further need to repair this slot None @@ -1467,7 +1480,7 @@ mod test { let (mut shreds, _) = make_slot_entries(1, 0, 1); let (shreds2, _) = make_slot_entries(5, 2, 1); shreds.extend(shreds2); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let mut repair_weight = RepairWeight::new(0); assert_eq!( repair_weight.get_best_weighted_repairs( @@ -1498,7 +1511,7 @@ mod test { // Write this shred to slot 2, should chain to slot 0, which we haven't received // any shreds for - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let mut repair_weight = RepairWeight::new(0); // Check that repair tries to patch the empty slot @@ -1547,9 +1560,7 @@ mod test { missing_indexes_per_slot.insert(0, index); } } - blockstore - .insert_shreds(shreds_to_write, None, false) - .unwrap(); + blockstore.insert_shreds(shreds_to_write, false).unwrap(); let expected: Vec = (0..num_slots) .flat_map(|slot| { missing_indexes_per_slot @@ -1639,11 +1650,10 @@ mod test { shreds_by_index.get(&0).unwrap().clone(), shreds_by_index.get(&(missing_index + 1)).unwrap().clone(), ], - None, false, ) .unwrap(); - let slot_meta = blockstore.meta(slot).unwrap().unwrap(); + let slot_meta = blockstore.meta_repair(slot).unwrap().unwrap(); let mut repair_eligibility = RepairEligibility::default(); assert_eq!( RepairService::generate_repairs_for_slot( @@ -1678,11 +1688,9 @@ mod test { .chain(std::iter::once(future_fec_start)) .map(|index| shreds_by_index.get(&index).unwrap().clone()) .collect(); - blockstore - .insert_shreds(shreds_to_insert, None, false) - .unwrap(); + blockstore.insert_shreds(shreds_to_insert, false).unwrap(); - let slot_meta = blockstore.meta(slot).unwrap().unwrap(); + let slot_meta = blockstore.meta_repair(slot).unwrap().unwrap(); let mut repair_eligibility = RepairEligibility::default(); assert_eq!( RepairService::generate_repairs_for_slot( @@ -1728,7 +1736,7 @@ mod test { // Remove last shred (which is also last in slot) so that slot is not complete shreds.pop(); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // We didn't get the last shred for this slot, so ask for the highest shred for that slot let expected: Vec = @@ -1781,10 +1789,10 @@ mod test { .collect(); blockstore - .insert_shreds(vec![shreds_by_index.get(&0).unwrap().clone()], None, false) + .insert_shreds(vec![shreds_by_index.get(&0).unwrap().clone()], false) .unwrap(); let mut repair_eligibility = RepairEligibility::default(); - let slot_meta = blockstore.meta(0).unwrap().unwrap(); + let slot_meta = blockstore.meta_repair(0).unwrap().unwrap(); assert_eq!( RepairService::generate_repairs_for_slot( &blockstore, @@ -1798,9 +1806,9 @@ mod test { ); blockstore - .insert_shreds(vec![shreds_by_index.get(&1).unwrap().clone()], None, false) + .insert_shreds(vec![shreds_by_index.get(&1).unwrap().clone()], false) .unwrap(); - let slot_meta = blockstore.meta(0).unwrap().unwrap(); + let slot_meta = blockstore.meta_repair(0).unwrap().unwrap(); assert_eq!( RepairService::generate_repairs_for_slot( &blockstore, @@ -1838,7 +1846,7 @@ mod test { let shreds = make_chaining_slot_entries(&slots, num_entries_per_slot, 0); for (mut slot_shreds, _) in shreds.into_iter() { slot_shreds.remove(0); - blockstore.insert_shreds(slot_shreds, None, false).unwrap(); + blockstore.insert_shreds(slot_shreds, false).unwrap(); } // Iterate through all possible combinations of start..end (inclusive on both @@ -1892,7 +1900,7 @@ mod test { num_entries_per_slot as u64, ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); } let end = 4; @@ -1929,7 +1937,7 @@ mod test { num_entries_per_slot, ); blockstore - .insert_shreds(shreds[..shreds.len() - 1].to_vec(), None, false) + .insert_shreds(shreds[..shreds.len() - 1].to_vec(), false) .unwrap(); assert!( RepairService::generate_duplicate_repairs_for_slot(&blockstore, dead_slot,).is_some() @@ -1937,7 +1945,7 @@ mod test { // SlotMeta is full, should make no repairs blockstore - .insert_shreds(vec![shreds.pop().unwrap()], None, false) + .insert_shreds(vec![shreds.pop().unwrap()], false) .unwrap(); assert!( RepairService::generate_duplicate_repairs_for_slot(&blockstore, dead_slot,).is_none() @@ -1977,7 +1985,7 @@ mod test { let num_entries_per_slot = max_ticks_per_n_shreds(1, None) + 1; let (mut shreds, _) = make_slot_entries(dead_slot, dead_slot - 1, num_entries_per_slot); blockstore - .insert_shreds(shreds[..shreds.len() - 1].to_vec(), None, false) + .insert_shreds(shreds[..shreds.len() - 1].to_vec(), false) .unwrap(); duplicate_slot_repair_statuses.insert(dead_slot, duplicate_status); @@ -2029,7 +2037,7 @@ mod test { // Insert rest of shreds. Slot is full, should get filtered from // `duplicate_slot_repair_statuses` blockstore - .insert_shreds(vec![shreds.pop().unwrap()], None, false) + .insert_shreds(vec![shreds.pop().unwrap()], false) .unwrap(); RepairService::generate_and_send_duplicate_repairs( &mut duplicate_slot_repair_statuses, diff --git a/core/src/repair/repair_weight.rs b/core/src/repair/repair_weight.rs index 635071f7852..92698c85389 100644 --- a/core/src/repair/repair_weight.rs +++ b/core/src/repair/repair_weight.rs @@ -9,11 +9,13 @@ use { }, replay_stage::DUPLICATE_THRESHOLD, }, + ahash::{AHashMap, AHashSet}, solana_clock::{Epoch, Slot}, solana_epoch_schedule::EpochSchedule, solana_hash::Hash, solana_ledger::{ - ancestor_iterator::AncestorIterator, blockstore::Blockstore, blockstore_meta::SlotMeta, + ancestor_iterator::AncestorIterator, blockstore::Blockstore, + blockstore_meta::SlotMetaRepair, }, solana_measure::measure::Measure, solana_pubkey::Pubkey, @@ -213,10 +215,10 @@ impl RepairWeight { outstanding_repairs: &mut HashMap, ) -> Vec { let mut repairs = vec![]; - let mut processed_slots = HashSet::from([self.root]); - let mut slot_meta_cache = HashMap::default(); + let mut processed_slots = AHashSet::from([self.root]); + let mut slot_meta_cache = AHashMap::default(); - let mut get_best_orphans_elapsed = Measure::start("get_best_orphans"); + let mut get_best_orphans_us = Measure::start("get_best_orphans_us"); // Find the best orphans in order from heaviest stake to least heavy self.get_best_orphans( blockstore, @@ -230,9 +232,9 @@ impl RepairWeight { // Subtract 1 because the root is not processed as an orphan let num_orphan_slots = processed_slots.len() - 1; let num_orphan_repairs = repairs.len(); - get_best_orphans_elapsed.stop(); + get_best_orphans_us.stop(); - let mut get_best_shreds_elapsed = Measure::start("get_best_shreds"); + let mut get_best_shreds_us = Measure::start("get_best_shreds_us"); let mut best_shreds_repairs = Vec::default(); // Find the best incomplete slots in rooted subtree self.get_best_shreds( @@ -249,14 +251,14 @@ impl RepairWeight { let num_best_shreds_slots = repair_slots_set.len(); processed_slots.extend(repair_slots_set); repairs.extend(best_shreds_repairs); - get_best_shreds_elapsed.stop(); + get_best_shreds_us.stop(); // Although we have generated repairs for orphan roots and slots in the rooted subtree, // if we have space we should generate repairs for slots in orphan trees in preparation for // when they are no longer rooted. Here we generate repairs for slots with unknown last // indices as well as slots that are close to completion. - let mut get_unknown_last_index_elapsed = Measure::start("get_unknown_last_index"); + let mut get_unknown_last_index_us = Measure::start("get_unknown_last_index_us"); let pre_num_slots = processed_slots.len(); let unknown_last_index_repairs = self.get_best_unknown_last_index( blockstore, @@ -268,9 +270,9 @@ impl RepairWeight { let num_unknown_last_index_repairs = unknown_last_index_repairs.len(); let num_unknown_last_index_slots = processed_slots.len() - pre_num_slots; repairs.extend(unknown_last_index_repairs); - get_unknown_last_index_elapsed.stop(); + get_unknown_last_index_us.stop(); - let mut get_closest_completion_elapsed = Measure::start("get_closest_completion"); + let mut get_closest_completion_us = Measure::start("get_closest_completion_us"); let pre_num_slots = processed_slots.len(); let (closest_completion_repairs, total_slots_processed) = self.get_best_closest_completion( blockstore, @@ -285,7 +287,7 @@ impl RepairWeight { let num_closest_completion_slots_path = total_slots_processed.saturating_sub(num_closest_completion_slots); repairs.extend(closest_completion_repairs); - get_closest_completion_elapsed.stop(); + get_closest_completion_us.stop(); repair_metrics.best_repairs_stats.update( num_orphan_slots as u64, @@ -299,12 +301,10 @@ impl RepairWeight { num_closest_completion_repairs as u64, self.trees.len() as u64, ); - repair_metrics.timing.get_best_orphans_elapsed += get_best_orphans_elapsed.as_us(); - repair_metrics.timing.get_best_shreds_elapsed += get_best_shreds_elapsed.as_us(); - repair_metrics.timing.get_unknown_last_index_elapsed += - get_unknown_last_index_elapsed.as_us(); - repair_metrics.timing.get_closest_completion_elapsed += - get_closest_completion_elapsed.as_us(); + repair_metrics.timing.get_best_orphans_us += get_best_orphans_us.as_us(); + repair_metrics.timing.get_best_shreds_us += get_best_shreds_us.as_us(); + repair_metrics.timing.get_unknown_last_index_us += get_unknown_last_index_us.as_us(); + repair_metrics.timing.get_closest_completion_us += get_closest_completion_us.as_us(); repairs } @@ -502,7 +502,7 @@ impl RepairWeight { fn get_best_shreds( &mut self, blockstore: &Blockstore, - slot_meta_cache: &mut HashMap>, + slot_meta_cache: &mut AHashMap>, repairs: &mut Vec, max_new_shreds: usize, repair_eligibility: &mut RepairEligibility, @@ -523,7 +523,7 @@ impl RepairWeight { fn get_best_orphans( &mut self, blockstore: &Blockstore, - processed_slots: &mut HashSet, + processed_slots: &mut AHashSet, repairs: &mut Vec, epoch_stakes: &HashMap, epoch_schedule: &EpochSchedule, @@ -563,17 +563,16 @@ impl RepairWeight { epoch_stakes, epoch_schedule, ); - if let Some(new_orphan_root) = new_orphan_root { - if new_orphan_root != self.root { - if let Some(repair_request) = RepairService::request_repair_if_needed( - outstanding_repairs, - ShredRepairType::Orphan(new_orphan_root), - ) { - repairs.push(repair_request); - processed_slots.insert(new_orphan_root); - new_best_orphan_requests += 1; - } - } + if let Some(new_orphan_root) = new_orphan_root + && new_orphan_root != self.root + && let Some(repair_request) = RepairService::request_repair_if_needed( + outstanding_repairs, + ShredRepairType::Orphan(new_orphan_root), + ) + { + repairs.push(repair_request); + processed_slots.insert(new_orphan_root); + new_best_orphan_requests += 1; } } } @@ -600,13 +599,13 @@ impl RepairWeight { fn get_best_unknown_last_index( &mut self, blockstore: &Blockstore, - slot_meta_cache: &mut HashMap>, - processed_slots: &mut HashSet, + slot_meta_cache: &mut AHashMap>, + processed_slots: &mut AHashSet, max_new_repairs: usize, outstanding_repairs: &mut HashMap, ) -> Vec { let mut repairs = Vec::default(); - for (_slot, tree) in self.trees.iter() { + for tree in self.trees.values() { if repairs.len() >= max_new_repairs { break; } @@ -630,15 +629,15 @@ impl RepairWeight { fn get_best_closest_completion( &mut self, blockstore: &Blockstore, - slot_meta_cache: &mut HashMap>, - processed_slots: &mut HashSet, + slot_meta_cache: &mut AHashMap>, + processed_slots: &mut AHashSet, max_new_repairs: usize, repair_eligibility: &mut RepairEligibility, outstanding_repairs: &mut HashMap, ) -> (Vec, /* processed slots */ usize) { let mut repairs = Vec::default(); let mut total_processed_slots = 0; - for (_slot, tree) in self.trees.iter() { + for tree in self.trees.values() { if repairs.len() >= max_new_repairs { break; } @@ -1509,7 +1508,7 @@ mod test { // should prioritize smaller orphan first let mut repairs = vec![]; let mut outstanding_repairs = HashMap::new(); - let mut processed_slots: HashSet = vec![repair_weight.root].into_iter().collect(); + let mut processed_slots: AHashSet = vec![repair_weight.root].into_iter().collect(); repair_weight.get_best_orphans( &blockstore, &mut processed_slots, @@ -1662,7 +1661,7 @@ mod test { // exactly one more of the remaining two let mut repairs = vec![]; let mut outstanding_repairs = HashMap::new(); - let mut processed_slots: HashSet = vec![repair_weight.root].into_iter().collect(); + let mut processed_slots: AHashSet = vec![repair_weight.root].into_iter().collect(); blockstore.add_tree(tr(100) / (tr(101)), true, true, 2, Hash::default()); repair_weight.get_best_orphans( &blockstore, @@ -2446,7 +2445,7 @@ mod test { // Simulate repair on 6 and 5 for (shreds, _) in make_chaining_slot_entries(&[5, 6], 100, 0) { - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); } // Verify orphans properly updated and chained diff --git a/core/src/repair/repair_weighted_traversal.rs b/core/src/repair/repair_weighted_traversal.rs index 4c6a7234a53..fbc3b63b02f 100644 --- a/core/src/repair/repair_weighted_traversal.rs +++ b/core/src/repair/repair_weighted_traversal.rs @@ -6,10 +6,11 @@ use { serve_repair::ShredRepairType, }, }, + ahash::{AHashMap, AHashSet}, solana_clock::Slot, solana_hash::Hash, - solana_ledger::{blockstore::Blockstore, blockstore_meta::SlotMeta}, - std::collections::{HashMap, HashSet}, + solana_ledger::{blockstore::Blockstore, blockstore_meta::SlotMetaRepair}, + std::collections::HashMap, }; #[derive(Debug, PartialEq, Eq)] @@ -31,6 +32,11 @@ impl Visit { struct RepairWeightTraversal<'a> { tree: &'a HeaviestSubtreeForkChoice, pending: Vec, + // Reusable scratch buffer for collecting and sorting a slot's + // children before appending them to `pending`. + // + // This allows us to avoid allocating a new vector for each slot. + scratch: Vec, } impl<'a> RepairWeightTraversal<'a> { @@ -38,6 +44,7 @@ impl<'a> RepairWeightTraversal<'a> { Self { tree, pending: vec![Visit::Unvisited(tree.tree_root().0)], + scratch: vec![], } } } @@ -51,22 +58,22 @@ impl Iterator for RepairWeightTraversal<'_> { // Add a bookmark to communicate all child // slots have been visited self.pending.push(Visit::Visited(slot)); - let mut children: Vec<_> = self - .tree - .children(&(slot, Hash::default())) - .unwrap() - .map(|(child_slot, _)| Visit::Unvisited(*child_slot)) - .collect(); + self.scratch.extend( + self.tree + .children(&(slot, Hash::default())) + .unwrap() + .map(|(child_slot, _)| Visit::Unvisited(*child_slot)), + ); // Sort children by weight to prioritize visiting the heaviest // ones first - children.sort_by(|slot1, slot2| { + self.scratch.sort_by(|slot1, slot2| { self.tree.max_by_weight( (slot1.slot(), Hash::default()), (slot2.slot(), Hash::default()), ) }); - self.pending.extend(children); + self.pending.append(&mut self.scratch); } next }) @@ -79,7 +86,7 @@ impl Iterator for RepairWeightTraversal<'_> { pub fn get_best_repair_shreds( tree: &HeaviestSubtreeForkChoice, blockstore: &Blockstore, - slot_meta_cache: &mut HashMap>, + slot_meta_cache: &mut AHashMap>, repairs: &mut Vec, max_new_shreds: usize, repair_eligibility: &mut RepairEligibility, @@ -91,7 +98,7 @@ pub fn get_best_repair_shreds( return; } let weighted_iter = RepairWeightTraversal::new(tree); - let mut visited_set = HashSet::new(); + let mut visited_set = AHashSet::new(); for next in weighted_iter { if repairs.len() >= max_repairs { break; @@ -99,7 +106,7 @@ pub fn get_best_repair_shreds( let slot_meta = slot_meta_cache .entry(next.slot()) - .or_insert_with(|| blockstore.meta(next.slot()).unwrap()); + .or_insert_with(|| blockstore.meta_repair(next.slot()).unwrap()); // May not exist if blockstore purged the SlotMeta due to something // like duplicate slots. TODO: Account for duplicate slot may be in orphans, especially @@ -233,7 +240,7 @@ pub mod test { // return repairs for all slots (none are completed) in order of traversal let mut repairs = vec![]; let mut outstanding_repairs = HashMap::new(); - let mut slot_meta_cache = HashMap::default(); + let mut slot_meta_cache = AHashMap::default(); let last_shred = blockstore.meta(0).unwrap().unwrap().received; let mut repair_eligibility = RepairEligibility::elapsed_for_slots_for_tests(&blockstore, 0..=5); @@ -260,7 +267,7 @@ pub mod test { // repairing those new leaves before trying other branches repairs = vec![]; outstanding_repairs = HashMap::new(); - slot_meta_cache = HashMap::default(); + slot_meta_cache = AHashMap::default(); let best_overall_slot = heaviest_subtree_fork_choice.best_overall_slot().0; assert_eq!(best_overall_slot, 4); blockstore.add_tree( @@ -293,7 +300,7 @@ pub mod test { // Completing slots should remove them from the repair list repairs = vec![]; outstanding_repairs = HashMap::new(); - slot_meta_cache = HashMap::default(); + slot_meta_cache = AHashMap::default(); let keypair = Keypair::new(); let reed_solomon_cache = ReedSolomonCache::default(); @@ -314,9 +321,7 @@ pub mod test { shreds }) .collect(); - blockstore - .insert_shreds(completed_shreds, None, false) - .unwrap(); + blockstore.insert_shreds(completed_shreds, false).unwrap(); let mut repair_eligibility = RepairEligibility::elapsed_for_slots_for_tests(&blockstore, 0..=7); get_best_repair_shreds( @@ -341,7 +346,7 @@ pub mod test { // the parents are complete should still be repaired repairs = vec![]; outstanding_repairs = HashMap::new(); - slot_meta_cache = HashMap::default(); + slot_meta_cache = AHashMap::default(); blockstore.add_tree(tr(2) / (tr(8)), true, false, 2, Hash::default()); let mut repair_eligibility = RepairEligibility::elapsed_for_slots_for_tests(&blockstore, 0..=8); @@ -384,7 +389,7 @@ pub mod test { let mut repairs = vec![]; let mut outstanding_repairs = HashMap::new(); - let mut slot_meta_cache = HashMap::default(); + let mut slot_meta_cache = AHashMap::default(); let mut repair_eligibility = RepairEligibility::elapsed_for_slots_for_tests(&blockstore, 0..=7); get_best_repair_shreds( @@ -412,7 +417,7 @@ pub mod test { let (blockstore, heaviest_subtree_fork_choice) = setup_forks(); let mut repairs = vec![]; let mut outstanding_repairs = HashMap::new(); - let mut slot_meta_cache = HashMap::default(); + let mut slot_meta_cache = AHashMap::default(); let mut repair_eligibility = RepairEligibility::elapsed_for_slots_for_tests(&blockstore, 0..=5); diff --git a/core/src/repair/result.rs b/core/src/repair/result.rs index ea90e376e2b..0fb78cd14f4 100644 --- a/core/src/repair/result.rs +++ b/core/src/repair/result.rs @@ -30,7 +30,9 @@ pub enum Error { #[error("Send Error")] SendError, #[error(transparent)] - Serialize(#[from] std::boxed::Box), + Serialize(#[from] wincode::WriteError), + #[error(transparent)] + Deserialize(#[from] wincode::ReadError), #[error(transparent)] WeightedIndex(#[from] rand::distr::weighted::Error), } diff --git a/core/src/repair/serve_repair.rs b/core/src/repair/serve_repair.rs index 9dec36b39c0..81447388c1f 100644 --- a/core/src/repair/serve_repair.rs +++ b/core/src/repair/serve_repair.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "frozen-abi")] +use serde::{Deserialize, Serialize}; #[cfg(test)] use { crate::repair::standard_repair_handler::StandardRepairHandler, @@ -17,7 +19,6 @@ use { }, }, agave_votor_messages::{consensus_message::Block, migration::MigrationStatus}, - bincode::{Options, serialize}, crossbeam_channel::{Receiver, RecvTimeoutError}, lazy_lru::LruCache, rand::{ @@ -27,7 +28,6 @@ use { weighted::{Error as WeightedError, WeightedIndex}, }, }, - serde::{Deserialize, Serialize}, solana_clock::Slot, solana_gossip::{ cluster_info::{ClusterInfo, ClusterInfoError}, @@ -47,7 +47,8 @@ use { solana_net_utils::{SocketAddrSpace, token_bucket::TokenBucket}, solana_packet::PACKET_DATA_SIZE, solana_perf::packet::{ - BytesPacket, Packet, PacketBatch, PacketBatchRecycler, PacketRef, RecycledPacketBatch, + BytesPacket, Packet, PacketBatch, PacketBatchRecycler, PacketConfig, PacketRef, + RecycledPacketBatch, packet_from_data, }, solana_poh::poh_recorder::SharedLeaderState, solana_pubkey::{PUBKEY_BYTES, Pubkey}, @@ -71,6 +72,7 @@ use { thread::{Builder, JoinHandle}, time::{Duration, Instant}, }, + wincode::{SchemaRead, SchemaWrite, serialize}, }; /// the number of slots to respond with when responding to `Orphan` requests @@ -102,7 +104,7 @@ const SIGNED_REPAIR_TIME_WINDOW: Duration = Duration::from_secs(60 * 10); // 10 #[cfg(test)] static_assertions::const_assert_eq!(MAX_ANCESTOR_RESPONSES, 30); -#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum ShredRepairType { /// Requesting `MAX_ORPHAN_REPAIR_RESPONSES ` parent shreds Orphan(Slot), @@ -189,7 +191,16 @@ impl AncestorHashesRepairType { } } -#[derive(Debug, Deserialize, Serialize)] +#[cfg_attr( + feature = "frozen-abi", + derive(StableAbi, StableAbiSample, Deserialize, Serialize, PartialEq), + frozen_abi( + abi_digest = "DhEfFPRMwZSyPVCX3wqoK3u7LvrWaK6SE7q6uLXSJ5ph", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Debug, SchemaRead, SchemaWrite)] pub enum AncestorHashesResponse { Hashes(Vec<(Slot, Hash)>), Ping(Ping), @@ -224,18 +235,27 @@ pub enum BlockIdRepairType { impl BlockIdRepairType { pub(crate) fn block(&self) -> Block { - match self { - BlockIdRepairType::ParentAndFecSetCount { slot, block_id } => (*slot, *block_id), - BlockIdRepairType::FecSetRoot { slot, block_id, .. } => (*slot, *block_id), + match *self { + BlockIdRepairType::ParentAndFecSetCount { slot, block_id } => Block { slot, block_id }, + BlockIdRepairType::FecSetRoot { slot, block_id, .. } => Block { slot, block_id }, } } pub(crate) fn slot(&self) -> Slot { - self.block().0 + self.block().slot } } -#[derive(Debug, Deserialize, Serialize)] +#[cfg_attr( + feature = "frozen-abi", + derive(StableAbi, StableAbiSample, Deserialize, Serialize, PartialEq), + frozen_abi( + abi_digest = "4UwjM1HevzQRxGkh6L9vXhf1db7y2ioQYTXRUaKZ4iNo", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Debug, SchemaRead, SchemaWrite)] pub enum BlockIdRepairResponse { ParentFecSetCount { fec_set_count: u32, @@ -289,8 +309,11 @@ impl RequestResponse for BlockIdRepairType { return false; } - let parent_info_leaf = - hashv(&[&parent_slot.to_le_bytes(), parent_block_id.as_ref()]); + let parent_info_leaf = hashv(&[ + &parent_slot.to_le_bytes(), + parent_block_id.as_ref(), + &fec_set_count.to_le_bytes(), + ]); merkle_tree::verify_merkle_proof( parent_info_leaf, *fec_set_count as usize, @@ -367,8 +390,11 @@ struct ServeRepairStats { err_id_mismatch: usize, } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi))] -#[derive(Debug, Deserialize, Serialize)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, Deserialize, Serialize, PartialEq) +)] +#[derive(Debug, SchemaRead, SchemaWrite)] pub struct RepairRequestHeader { signature: Signature, sender: Pubkey, @@ -419,13 +445,17 @@ type PingCache = ping_pong::PingCache; /// The message can then be removed once the feature gate is active and there are no responders. #[cfg_attr( feature = "frozen-abi", - derive(AbiEnumVisitor, AbiExample, StableAbi), + derive( + AbiEnumVisitor, AbiExample, StableAbi, Deserialize, Serialize, PartialEq, + ), frozen_abi( api_digest = "2j14Ywc3jWmohnXsEuMUQRPLf7JmxAVKvXKeKpYuzg7S", - abi_digest = "D5RRQygn3D6ux1TYxeyXdksWD2KGA8PYi315hXP3JJ7c" + abi_digest = "D5RRQygn3D6ux1TYxeyXdksWD2KGA8PYi315hXP3JJ7c", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", ) )] -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, SchemaRead, SchemaWrite)] pub enum RepairProtocol { LegacyWindowIndex, LegacyHighestWindowIndex, @@ -542,7 +572,19 @@ fn is_well_formed_repair_request(packet: &PacketRef, stats: &mut ServeRepairStat well_formed } -#[derive(Debug, Deserialize, Serialize)] +#[cfg_attr( + feature = "frozen-abi", + derive( + AbiEnumVisitor, AbiExample, StableAbi, StableAbiSample, Deserialize, Serialize, PartialEq, + ), + frozen_abi( + api_digest = "2atc1j4n5MjGtmAYoL157stGow5ajeDtqAyMhwcniy5b", + abi_digest = "5qmbs9MjvFrMQ2DYmre88SLLjLLDx3pdEW37cKUEQKMK", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Debug, SchemaRead, SchemaWrite)] pub(crate) enum RepairResponse { Ping(Ping), } @@ -1339,8 +1381,6 @@ impl ServeRepair { assert!(REPAIR_PING_CACHE_RATE_LIMIT_DELAY > Duration::from_millis(REPAIR_MS)); let mut ping_cache = PingCache::new( - &mut rand::rng(), - Instant::now(), REPAIR_PING_CACHE_TTL, REPAIR_PING_CACHE_RATE_LIMIT_DELAY, REPAIR_PING_CACHE_CAPACITY, @@ -1465,15 +1505,15 @@ impl ServeRepair { | RepairProtocol::Orphan { .. } | RepairProtocol::WindowIndexForBlockId { .. } => { let ping = RepairResponse::Ping(ping); - Packet::from_data(Some(from_addr), ping).ok() + packet_from_data(Some(from_addr), ping).ok() } RepairProtocol::ParentAndFecSetCount { .. } | RepairProtocol::FecSetRoot { .. } => { let ping = BlockIdRepairResponse::Ping { ping }; - Packet::from_data(Some(from_addr), ping).ok() + packet_from_data(Some(from_addr), ping).ok() } RepairProtocol::AncestorHashes { .. } => { let ping = AncestorHashesResponse::Ping(ping); - Packet::from_data(Some(from_addr), ping).ok() + packet_from_data(Some(from_addr), ping).ok() } RepairProtocol::Pong(_) => None, RepairProtocol::LegacyWindowIndex @@ -1853,7 +1893,9 @@ impl ServeRepair { if packet.meta().size != REPAIR_RESPONSE_SERIALIZED_PING_BYTES { continue; } - if let Ok(RepairResponse::Ping(ping)) = packet.deserialize_slice(..) { + if let Some(data) = packet.data(..) + && let Ok(RepairResponse::Ping(ping)) = wincode::deserialize(data) + { if !ping.verify() { // Do _not_ set `discard` to allow shred processing to attempt to // handle the packet. @@ -1867,7 +1909,7 @@ impl ServeRepair { packet.meta_mut().set_discard(true); stats.ping_count += 1; let pong = RepairProtocol::Pong(Pong::new(&ping, keypair)); - if let Ok(pong) = bincode::serialize(&pong) { + if let Ok(pong) = wincode::serialize(&pong) { let from_addr = packet.meta().socket_addr(); pending_pongs.push((pong, from_addr)); } @@ -1922,15 +1964,11 @@ impl ServeRepair { pub(crate) fn deserialize_request( request: &BytesPacket, -) -> std::result::Result +) -> std::result::Result where - T: serde::de::DeserializeOwned, + T: for<'de> SchemaRead<'de, PacketConfig, Dst = T>, { - bincode::options() - .with_limit(request.buffer().len() as u64) - .with_fixint_encoding() - .reject_trailing_bytes() - .deserialize(request.buffer()) + wincode::config::deserialize_exact(request.buffer(), PacketConfig::new()) } #[cfg(test)] @@ -1939,6 +1977,7 @@ mod tests { super::*, crate::repair::repair_response, agave_feature_set::FeatureSet, + crossbeam_channel::bounded, solana_gossip::{contact_info::ContactInfo, socketaddr, socketaddr_any}, solana_hash::Hash, solana_keypair::Keypair, @@ -1952,11 +1991,16 @@ mod tests { }, }, solana_net_utils::SocketAddrSpace, - solana_perf::packet::{Packet, PacketFlags, PacketRef, deserialize_from_with_limit}, + solana_perf::packet::{ + Packet, PacketFlags, PacketRef, RecycledPacketBatch, deserialize_slice_from_packet, + }, solana_pubkey::Pubkey, solana_runtime::bank::Bank, solana_time_utils::timestamp, - std::{io::Cursor, net::Ipv4Addr}, + std::{ + io::Cursor, + net::{Ipv4Addr, SocketAddrV4}, + }, }; fn discard_malformed_repair_requests( @@ -1973,7 +2017,7 @@ mod tests { let keypair = Keypair::new(); let ping = Ping::new(rng.random(), &keypair); let ping = RepairResponse::Ping(ping); - let pkt = Packet::from_data(None, ping).unwrap(); + let pkt = packet_from_data(None, ping).unwrap(); assert_eq!(pkt.meta().size, REPAIR_RESPONSE_SERIALIZED_PING_BYTES); } @@ -1984,7 +2028,7 @@ mod tests { let mut pkt = Packet::default(); shred.copy_to_packet(&mut pkt); pkt.meta_mut().size = REPAIR_RESPONSE_SERIALIZED_PING_BYTES; - let res = pkt.deserialize_slice::(..); + let res = deserialize_slice_from_packet::(&pkt, ..); if let Ok(RepairResponse::Ping(ping)) = res { assert!(!ping.verify()); } else { @@ -1998,8 +2042,6 @@ mod tests { let remote_keypair = Keypair::new(); let from_addr = socketaddr!(Ipv4Addr::LOCALHOST, 1234); let mut ping_cache = PingCache::new( - &mut rand::rng(), - Instant::now(), REPAIR_PING_CACHE_TTL, REPAIR_PING_CACHE_RATE_LIMIT_DELAY, REPAIR_PING_CACHE_CAPACITY, @@ -2023,7 +2065,8 @@ mod tests { let (check, ping_pkt) = ServeRepair::check_ping_cache(&mut ping_cache, &request, &from_addr, &identity_keypair); assert!(!check); - let response: BlockIdRepairResponse = ping_pkt.unwrap().deserialize_slice(..).unwrap(); + let response: BlockIdRepairResponse = + deserialize_slice_from_packet(&ping_pkt.unwrap(), ..).unwrap(); match response { BlockIdRepairResponse::Ping { ping } => assert!(ping.verify()), response => panic!("Expected Ping challenge, got {response:?}"), @@ -2062,7 +2105,7 @@ mod tests { let ping = Ping::new(rng.random(), &keypair); let pong = Pong::new(&ping, &keypair); let request = RepairProtocol::Pong(pong); - let mut pkt = Packet::from_data(None, request).unwrap(); + let mut pkt = packet_from_data(None, request).unwrap(); let mut batch = vec![make_remote_request(&pkt)]; let mut stats = ServeRepairStats::default(); let num_well_formed = discard_malformed_repair_requests(&mut batch, &mut stats); @@ -2079,7 +2122,7 @@ mod tests { slot: 123, shred_index: 456, }; - let mut pkt = Packet::from_data(None, request).unwrap(); + let mut pkt = packet_from_data(None, request).unwrap(); let mut batch = vec![make_remote_request(&pkt)]; let mut stats = ServeRepairStats::default(); let num_well_formed = discard_malformed_repair_requests(&mut batch, &mut stats); @@ -2095,7 +2138,7 @@ mod tests { header: repair_request_header_for_tests(), slot: 123, }; - let mut pkt = Packet::from_data(None, request).unwrap(); + let mut pkt = packet_from_data(None, request).unwrap(); let mut batch = vec![make_remote_request(&pkt)]; let mut stats = ServeRepairStats::default(); let num_well_formed = discard_malformed_repair_requests(&mut batch, &mut stats); @@ -2111,7 +2154,7 @@ mod tests { header: repair_request_header_for_tests(), slot: 262_547_696, }; - let mut pkt = Packet::from_data(None, request).unwrap(); + let mut pkt = packet_from_data(None, request).unwrap(); let mut batch = vec![make_remote_request(&pkt)]; let mut stats = ServeRepairStats::default(); let num_well_formed = discard_malformed_repair_requests(&mut batch, &mut stats); @@ -2150,8 +2193,7 @@ mod tests { .unwrap(); let mut cursor = Cursor::new(&rsp[..]); - let deserialized_request: RepairProtocol = - deserialize_from_with_limit(&mut cursor).unwrap(); + let deserialized_request: RepairProtocol = wincode::deserialize_from(&mut cursor).unwrap(); assert_eq!(cursor.position(), rsp.len() as u64); if let RepairProtocol::Orphan { header, slot } = deserialized_request { assert_eq!(slot, 123); @@ -2165,7 +2207,7 @@ mod tests { .verify(keypair.pubkey().as_ref(), &signed_data) ); } else { - panic!("unexpected request type {:?}", &deserialized_request); + panic!("unexpected request type {deserialized_request:?}"); } } @@ -2191,8 +2233,7 @@ mod tests { .ancestor_repair_request_bytes(&keypair, &repair_peer_id, slot, nonce) .unwrap(); let mut cursor = Cursor::new(&request_bytes[..]); - let deserialized_request: RepairProtocol = - deserialize_from_with_limit(&mut cursor).unwrap(); + let deserialized_request: RepairProtocol = wincode::deserialize_from(&mut cursor).unwrap(); assert_eq!(cursor.position(), request_bytes.len() as u64); if let RepairProtocol::AncestorHashes { header, @@ -2210,7 +2251,7 @@ mod tests { .verify(keypair.pubkey().as_ref(), &signed_data) ); } else { - panic!("unexpected request type {:?}", &deserialized_request); + panic!("unexpected request type {deserialized_request:?}"); } } @@ -2244,8 +2285,7 @@ mod tests { .unwrap(); let mut cursor = Cursor::new(&request_bytes[..]); - let deserialized_request: RepairProtocol = - deserialize_from_with_limit(&mut cursor).unwrap(); + let deserialized_request: RepairProtocol = wincode::deserialize_from(&mut cursor).unwrap(); assert_eq!(cursor.position(), request_bytes.len() as u64); if let RepairProtocol::WindowIndex { header, @@ -2265,7 +2305,7 @@ mod tests { .verify(keypair.pubkey().as_ref(), &signed_data) ); } else { - panic!("unexpected request type {:?}", &deserialized_request); + panic!("unexpected request type {deserialized_request:?}"); } let request = ShredRepairType::HighestShred(slot, shred_index); @@ -2280,8 +2320,7 @@ mod tests { .unwrap(); let mut cursor = Cursor::new(&request_bytes[..]); - let deserialized_request: RepairProtocol = - deserialize_from_with_limit(&mut cursor).unwrap(); + let deserialized_request: RepairProtocol = wincode::deserialize_from(&mut cursor).unwrap(); assert_eq!(cursor.position(), request_bytes.len() as u64); if let RepairProtocol::HighestWindowIndex { header, @@ -2301,7 +2340,7 @@ mod tests { .verify(keypair.pubkey().as_ref(), &signed_data) ); } else { - panic!("unexpected request type {:?}", &deserialized_request); + panic!("unexpected request type {deserialized_request:?}"); } } @@ -2330,11 +2369,11 @@ mod tests { ); let slot = 239847; let request = RepairProtocol::Orphan { header, slot }; - let mut packet = Packet::from_data(None, request).unwrap(); + let mut packet = packet_from_data(None, request).unwrap(); sign_packet(&mut packet, &my_keypair); packet }; - let request: RepairProtocol = packet.deserialize_slice(..).unwrap(); + let request: RepairProtocol = deserialize_slice_from_packet(&packet, ..).unwrap(); assert_matches!( ServeRepair::verify_signed_packet( &other_keypair.pubkey(), @@ -2354,11 +2393,11 @@ mod tests { ); let slot = 239847; let request = RepairProtocol::Orphan { header, slot }; - let mut packet = Packet::from_data(None, request).unwrap(); + let mut packet = packet_from_data(None, request).unwrap(); sign_packet(&mut packet, &my_keypair); packet }; - let request: RepairProtocol = packet.deserialize_slice(..).unwrap(); + let request: RepairProtocol = deserialize_slice_from_packet(&packet, ..).unwrap(); assert_matches!( ServeRepair::verify_signed_packet( &my_keypair.pubkey(), @@ -2380,11 +2419,11 @@ mod tests { ); let slot = 239847; let request = RepairProtocol::Orphan { header, slot }; - let mut packet = Packet::from_data(None, request).unwrap(); + let mut packet = packet_from_data(None, request).unwrap(); sign_packet(&mut packet, &my_keypair); packet }; - let request: RepairProtocol = packet.deserialize_slice(..).unwrap(); + let request: RepairProtocol = deserialize_slice_from_packet(&packet, ..).unwrap(); assert_matches!( ServeRepair::verify_signed_packet( &other_keypair.pubkey(), @@ -2404,11 +2443,11 @@ mod tests { ); let slot = 239847; let request = RepairProtocol::Orphan { header, slot }; - let mut packet = Packet::from_data(None, request).unwrap(); + let mut packet = packet_from_data(None, request).unwrap(); sign_packet(&mut packet, &other_keypair); packet }; - let request: RepairProtocol = packet.deserialize_slice(..).unwrap(); + let request: RepairProtocol = deserialize_slice_from_packet(&packet, ..).unwrap(); assert_matches!( ServeRepair::verify_signed_packet( &other_keypair.pubkey(), @@ -2503,7 +2542,7 @@ mod tests { shreds.truncate(1); blockstore - .insert_shreds(shreds, None, false) + .insert_shreds(shreds, false) .expect("Expect successful ledger write"); let mut rv = handler @@ -2543,8 +2582,7 @@ mod tests { .root_bank() .epoch_schedule() .clone(); - let (ancestor_duplicate_slots_sender, _ancestor_duplicate_slots_receiver) = - crossbeam_channel::unbounded(); + let (ancestor_duplicate_slots_sender, _ancestor_duplicate_slots_receiver) = bounded(1024); RepairInfo { bank_forks, cluster_info, @@ -2667,7 +2705,7 @@ mod tests { let (shreds, _) = make_many_slot_entries(slot, num_slots, 5); blockstore - .insert_shreds(shreds, None, false) + .insert_shreds(shreds, false) .expect("Expect successful ledger write"); // We don't have slot `slot + num_slots`, so we don't know how to service this request @@ -2723,7 +2761,7 @@ mod tests { // covers packet size check in repair_response_packet_from_bytes. shreds.retain(|shred| shred.slot() != 1); blockstore - .insert_shreds(shreds, None, false) + .insert_shreds(shreds, false) .expect("Expect successful ledger write"); let nonce = 42; // Make sure repair response is corrupted @@ -2758,9 +2796,12 @@ mod tests { #[test] fn test_run_ancestor_hashes() { fn deserialize_ancestor_hashes_response(packet: PacketRef) -> AncestorHashesResponse { - packet - .deserialize_slice(..packet.meta().size - SIZE_OF_NONCE) - .unwrap() + wincode::deserialize( + packet + .data(..(packet.meta().size - SIZE_OF_NONCE)) + .unwrap_or_default(), + ) + .unwrap() } agave_logger::setup(); @@ -2777,7 +2818,7 @@ mod tests { let (shreds, _) = make_many_slot_entries(slot, num_slots, 5); blockstore - .insert_shreds(shreds, None, false) + .insert_shreds(shreds, false) .expect("Expect successful ledger write"); // We don't have slot `slot + num_slots`, so we return empty @@ -2793,7 +2834,7 @@ mod tests { assert!(hashes.is_empty()); } _ => { - panic!("unexpected response: {:?}", &ancestor_hashes_response); + panic!("unexpected response: {ancestor_hashes_response:?}"); } } @@ -2810,7 +2851,7 @@ mod tests { assert!(hashes.is_empty()); } _ => { - panic!("unexpected response: {:?}", &ancestor_hashes_response); + panic!("unexpected response: {ancestor_hashes_response:?}"); } } @@ -2834,7 +2875,7 @@ mod tests { assert_eq!(hashes, expected_ancestors); } _ => { - panic!("unexpected response: {:?}", &ancestor_hashes_response); + panic!("unexpected response: {ancestor_hashes_response:?}"); } } } @@ -3173,4 +3214,89 @@ mod tests { response.push((request_slot, Hash::new_unique())); assert!(!repair.verify_response(&AncestorHashesResponse::Hashes(response))); } + + // A second check() within REPAIR_PING_CACHE_RATE_LIMIT_DELAY must not generate + // a new ping. If it did, it would overwrite the stored token and invalidate the Pong, + // making Ping fail for no reason. + #[test] + fn test_repair_no_ping_overwrite_within_rate_limit_delay() { + let mut rng = rand::rng(); + let this_node = Keypair::new(); + let remote_socket = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8001)); + let remote_keypair = Keypair::new(); + let remote_node = (remote_keypair.pubkey(), remote_socket); + let mut cache = PingCache::new( + REPAIR_PING_CACHE_TTL, + REPAIR_PING_CACHE_RATE_LIMIT_DELAY, + REPAIR_PING_CACHE_CAPACITY, + ); + let now = Instant::now(); + + let (_, ping1) = cache.check(&mut rng, &this_node, now, remote_node); + let ping1 = ping1.expect("should generate ping for unknown node"); + + // Second check within REPAIR_PING_CACHE_RATE_LIMIT_DELAY must not generate + // a new ping — that would overwrite the stored hash and invalidate the in-flight pong. + let within_delay = now + REPAIR_PING_CACHE_RATE_LIMIT_DELAY - Duration::from_millis(1); + let (_, ping2) = cache.check(&mut rng, &this_node, within_delay, remote_node); + assert!( + ping2.is_none(), + "must not generate a second ping within REPAIR_PING_CACHE_RATE_LIMIT_DELAY" + ); + + // Pong for ping1 must still be valid — token was not overwritten. + let pong1 = solana_gossip::ping_pong::Pong::new(&ping1, &remote_keypair); + assert!( + cache.add(&pong1, remote_socket, within_delay), + "pong for original ping must still be valid — token was not overwritten" + ); + } + + #[test] + fn test_verify_fec_set_count_non_malleable() { + let parent_slot = 99u64; + let parent_block_id = Hash::new_unique(); + let fec_set_count: u32 = 2; // even => total leaves = 3, last leaf duplicated + let fec_set_roots: Vec = (0..fec_set_count).map(|_| Hash::new_unique()).collect(); + let real_parent_leaf = hashv(&[ + &parent_slot.to_le_bytes(), + parent_block_id.as_ref(), + &fec_set_count.to_le_bytes(), + ]); + let mut leaves: Vec = fec_set_roots; + leaves.push(real_parent_leaf); + let tree = + merkle_tree::MerkleTree::try_new_with_len(leaves.iter().copied().map(Ok), leaves.len()) + .unwrap(); + let block_id = *tree.root(); + let real_parent_proof: Vec = tree + .make_merkle_proof(fec_set_count as usize, leaves.len()) + .flat_map(|entry| entry.unwrap().iter().copied()) + .collect(); + + let request = BlockIdRepairType::ParentAndFecSetCount { + slot: 100, + block_id, + }; + + // honest response verifies + assert!( + request.verify_response(&BlockIdRepairResponse::ParentFecSetCount { + fec_set_count, + parent_info: (parent_slot, parent_block_id), + parent_proof: real_parent_proof.clone(), + }) + ); + + // Attack: claim N+1 and reuse the honest proof. The padded tree puts + // `real_parent_leaf` at both positions N and N+1, so without binding + // `fec_set_count` into the leaf this proof would verify. + assert!( + !request.verify_response(&BlockIdRepairResponse::ParentFecSetCount { + fec_set_count: fec_set_count + 1, + parent_info: (parent_slot, parent_block_id), + parent_proof: real_parent_proof.clone(), + }) + ); + } } diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 56edaea51c4..0680ae3928b 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -34,8 +34,7 @@ use { }, agave_votor::{ event::{ - CompletedBlock, LeaderWindowInfo, SwitchBankEvent, SwitchBankEventReceiver, VotorEvent, - VotorEventSender, + CompletedBlock, LatestSwitchRequest, LeaderWindowInfo, VotorEvent, VotorEventSender, }, root_utils, vote_history_storage::SavedVoteHistory, @@ -43,7 +42,7 @@ use { voting_utils::{self, GenerateVoteTxResult}, }, agave_votor_messages::{ - consensus_message::ConsensusMessage, + consensus_message::{Block, ConsensusMessage}, migration::{GENESIS_VOTE_REFRESH, MigrationStatus}, vote::Vote, }, @@ -51,12 +50,12 @@ use { itertools::Itertools, rayon::{ThreadPool, prelude::*}, solana_accounts_db::contains::Contains, - solana_clock::{BankId, NUM_CONSECUTIVE_LEADER_SLOTS, Slot}, + solana_clock::{BankId, Slot}, solana_geyser_plugin_manager::block_metadata_notifier_interface::BlockMetadataNotifierArc, solana_gossip::cluster_info::ClusterInfo, solana_hash::Hash, solana_keypair::Keypair, - solana_leader_schedule::SlotLeader, + solana_leader_schedule::{NUM_CONSECUTIVE_LEADER_SLOTS, SlotLeader}, solana_ledger::{ blockstore::{Blockstore, BlockstoreError, UpdateParentReceiver}, blockstore_meta::BlockLocation, @@ -82,9 +81,9 @@ use { slot_status_notifier::SlotStatusNotifier, }, solana_runtime::{ - bank::{Bank, NewBankOptions, bank_hash_details}, + bank::{Bank, MAX_ALPENGLOW_VOTE_ACCOUNTS, NewBankOptions, bank_hash_details}, bank_forks::BankForks, - bank_forks_controller::{BankForksCommand, BankForksCommandReceiver}, + bank_forks_controller::{BankForksCommand, BankForksCommandReceiver, SetRootCommand}, block_component_processor::BlockComponentProcessorError, commitment::BlockCommitmentCache, installed_scheduler_pool::BankWithScheduler, @@ -94,7 +93,6 @@ use { vote_sender_types::{ReplayVoteMessage, ReplayVoteSender}, }, solana_signer::Signer, - solana_svm_timings::ExecuteTimings, solana_time_utils::timestamp, solana_transaction::Transaction, solana_vote::vote_transaction::VoteTransaction, @@ -139,6 +137,8 @@ const MAX_REPAIR_RETRY_LOOP_ATTEMPTS: usize = 10; // Give at least 4 leaders the chance to pack our vote const REFRESH_VOTE_BLOCKHEIGHT: usize = 16; +const VAT_STATUS_CHECK_INTERVAL_SECS: u64 = 30; + #[derive(PartialEq, Eq, Debug)] pub enum HeaviestForkFailures { LockedOut(u64), @@ -446,7 +446,7 @@ pub struct ReplaySenders { pub block_metadata_notifier: Option, pub dumped_slots_sender: Sender>, pub votor_event_sender: VotorEventSender, - pub own_vote_sender: Sender>, + pub own_message_sender: Sender, pub optimistic_parent_sender: Sender, pub lockouts_sender: Sender, } @@ -461,7 +461,7 @@ pub struct ReplayReceivers { pub gossip_verified_vote_hash_receiver: Receiver<(Pubkey, u64, Hash)>, pub popular_pruned_forks_receiver: Receiver>, pub bank_forks_controller_receiver: BankForksCommandReceiver, - pub switch_bank_receiver: SwitchBankEventReceiver, + pub latest_switch_request: LatestSwitchRequest, } /// Timing information for the ReplayStage main processing loop @@ -489,6 +489,7 @@ struct ReplayLoopTiming { process_duplicate_slots_elapsed_us: u64, process_unfrozen_gossip_verified_vote_hashes_elapsed_us: u64, process_popular_pruned_forks_elapsed_us: u64, + process_switch_bank_events_elapsed_us: u64, repair_correct_slots_elapsed_us: u64, retransmit_not_propagated_elapsed_us: u64, generate_new_bank_forks_read_lock_us: Saturating, @@ -650,6 +651,11 @@ impl ReplayLoopTiming { self.process_popular_pruned_forks_elapsed_us as i64, i64 ), + ( + "process_switch_bank_events_elapsed_us", + self.process_switch_bank_events_elapsed_us as i64, + i64 + ), ( "wait_receive_elapsed_us", self.wait_receive_elapsed_us as i64, @@ -761,7 +767,7 @@ impl ReplayStage { block_metadata_notifier, dumped_slots_sender, votor_event_sender, - own_vote_sender, + own_message_sender, optimistic_parent_sender, lockouts_sender, } = senders; @@ -776,7 +782,7 @@ impl ReplayStage { gossip_verified_vote_hash_receiver, popular_pruned_forks_receiver, bank_forks_controller_receiver, - switch_bank_receiver, + latest_switch_request, } = receivers; trace!("replay stage"); @@ -851,6 +857,7 @@ impl ReplayStage { last_print_time: Instant::now(), }; let mut last_genesis_vote_refresh_time = Instant::now(); + let mut last_vat_status_check = Instant::now(); let mut tbft_structs = TowerBFTStructures { heaviest_subtree_fork_choice, duplicate_slots_tracker, @@ -939,6 +946,7 @@ impl ReplayStage { Self::process_bank_forks_commands( &bank_forks_controller_receiver, &process_bank_forks_context, + &my_pubkey, &mut progress, &mut async_verification_freelist, ), @@ -986,6 +994,7 @@ impl ReplayStage { (r_bank_forks.ancestors(), r_bank_forks.descendants()) }; let new_frozen_slots = Self::process_active_banks( + cluster_info.my_shred_version(), &process_active_banks_context, &mut progress, &mut async_verification_freelist, @@ -996,11 +1005,19 @@ impl ReplayStage { &my_pubkey, &vote_account, &mut replay_timing, - &own_vote_sender, + &own_message_sender, ); let did_complete_bank = !new_frozen_slots.is_empty(); replay_active_banks_time.stop(); + // VAT health check + Self::maybe_report_vat_health( + &mut last_vat_status_check, + &authorized_voter_keypairs, + &bank_forks, + &vote_account, + ); + // Check if we've completed the migration conditions if migration_status.is_ready_to_enable() { Self::enable_alpenglow( @@ -1019,6 +1036,15 @@ impl ReplayStage { } if migration_status.is_alpenglow_enabled() { + if my_pubkey != cluster_info.id() { + identity_keypair = cluster_info.keypair(); + let my_old_pubkey = my_pubkey; + my_pubkey = identity_keypair.pubkey(); + migration_status.set_pubkey(my_pubkey); + + warn!("Identity changed from {my_old_pubkey} to {my_pubkey}"); + } + process_soft_dead_slots( &my_pubkey, &blockstore, @@ -1040,12 +1066,12 @@ impl ReplayStage { &optimistic_parent_receiver, &replay_highest_frozen, &mut highest_frozen_slot, - &mut progress, - did_complete_bank, ); + let mut process_switch_bank_events_time = + Measure::start("process_switch_bank_events_time"); Self::process_switch_bank_events( &my_pubkey, - &switch_bank_receiver, + &latest_switch_request, &mut pending_switch, &blockstore, &bank_forks, @@ -1053,6 +1079,10 @@ impl ReplayStage { &mut async_verification_freelist, ) .expect("Blockstore operations must succeed"); + process_switch_bank_events_time.stop(); + replay_timing.process_switch_bank_events_elapsed_us += + process_switch_bank_events_time.as_us(); + // Banks might have been switched above, these maps are no longer accurate drop(ancestors); drop(descendants); @@ -1183,12 +1213,13 @@ impl ReplayStage { if last_genesis_vote_refresh_time.elapsed() > GENESIS_VOTE_REFRESH && migration_status.is_in_migration() && Self::maybe_send_genesis_vote( + cluster_info.my_shred_version(), migration_status.as_ref(), bank_forks.as_ref(), vote_account, &identity_keypair, &authorized_voter_keypairs, - &own_vote_sender, + &own_message_sender, &bls_sender, ) { @@ -1461,8 +1492,9 @@ impl ReplayStage { // may add a bank that will not included in either of these maps. drop(ancestors); drop(descendants); - if !tpu_has_bank && !poh_controller.has_pending_message() { - if let Some(poh_slot) = Self::maybe_start_leader( + if !tpu_has_bank + && !poh_controller.has_pending_message() + && let Some(poh_slot) = Self::maybe_start_leader( &my_pubkey, &bank_forks, &poh_recorder, @@ -1476,14 +1508,14 @@ impl ReplayStage { &banking_tracer, has_new_vote_been_rooted, migration_status.as_ref(), - ) { - Self::log_leader_change( - &my_pubkey, - poh_slot, - &mut current_leader, - &my_pubkey, - ); - } + ) + { + Self::log_leader_change( + &my_pubkey, + poh_slot, + &mut current_leader, + &my_pubkey, + ); } start_leader_time.stop(); @@ -1515,12 +1547,15 @@ impl ReplayStage { // only wait for the signal if we did not just process a bank; maybe there are more slots available let timer = Duration::from_millis(100); + let bank_forks_command_receiver = bank_forks_controller_receiver.receiver(); + let set_root_signal_receiver = + bank_forks_controller_receiver.set_root_signal_receiver(); select! { recv(ledger_signal_receiver) -> result => match result { Err(_) => break, Ok(_) => trace!("blockstore signal"), }, - recv(bank_forks_controller_receiver.receiver()) -> result => match result { + recv(bank_forks_command_receiver) -> result => match result { Err(_) => break, Ok(command) => { Self::process_bank_forks_command( @@ -1531,6 +1566,10 @@ impl ReplayStage { ); } }, + recv(set_root_signal_receiver) -> result => match result { + Err(_) => break, + Ok(()) => trace!("bank forks set-root signal"), + }, default(timer) => (), } } @@ -1551,7 +1590,6 @@ impl ReplayStage { Ok(Self { t_replay }) } - #[allow(clippy::too_many_arguments)] fn alpenglow_handle_newly_frozen_banks( new_frozen_slots: &[Slot], migration_status: &MigrationStatus, @@ -1562,15 +1600,16 @@ impl ReplayStage { optimistic_parent_receiver: &Receiver, replay_highest_frozen: &ReplayHighestFrozen, highest_frozen_slot: &mut Slot, - progress: &mut ProgressMap, - did_complete_bank: bool, ) { let flh_candidate_banks = { let bank_forks_r = bank_forks.read().unwrap(); new_frozen_slots .iter() - .filter(|slot| migration_status.should_allow_fast_leader_handover(**slot)) .filter_map(|slot| bank_forks_r.get(*slot)) + .filter(|bank| { + bank.feature_set.snapshot().alpenglow_fast_leader_handover + && migration_status.should_allow_block_markers(bank.slot()) + }) .collect_vec() }; for bank in flh_candidate_banks { @@ -1583,19 +1622,14 @@ impl ReplayStage { ); } - if let Some(highest) = new_frozen_slots.iter().max() { - if *highest > *highest_frozen_slot { - *highest_frozen_slot = *highest; - let mut l_highest_frozen = - replay_highest_frozen.highest_frozen_slot.lock().unwrap(); - // Let the block creation loop know about this new frozen slot - *l_highest_frozen = *highest; - replay_highest_frozen.freeze_notification.notify_one(); - } - } - if did_complete_bank { - let bank_forks_r = bank_forks.read().unwrap(); - progress.handle_new_root(&bank_forks_r); + if let Some(highest) = new_frozen_slots.iter().max() + && *highest > *highest_frozen_slot + { + *highest_frozen_slot = *highest; + let mut l_highest_frozen = replay_highest_frozen.highest_frozen_slot.lock().unwrap(); + // Let the block creation loop know about this new frozen slot + *l_highest_frozen = *highest; + replay_highest_frozen.freeze_notification.notify_one(); } } @@ -1622,7 +1656,7 @@ impl ReplayStage { ) { let root_bank = bank_forks.read().unwrap().root_bank(); - let genesis_block @ (genesis_slot, block_id) = migration_status + let genesis_block = migration_status .genesis_block() .expect("Must be ready to enable"); info!( @@ -1630,16 +1664,16 @@ impl ReplayStage { alpenglow. Genesis block {genesis_block:?}" ); - let genesis_bank = bank_forks.read().unwrap().get(genesis_slot).expect( + let genesis_bank = bank_forks.read().unwrap().get(genesis_block.slot).expect( "{my_pubkey}: Attempting to enable alpenglow before receiving the genesis block", ); assert!(genesis_bank.is_frozen()); - if genesis_bank.block_id() != Some(block_id) { + if genesis_bank.block_id() != Some(genesis_block.block_id) { panic!( "{my_pubkey}: Attempting to enable alpenglow but we have the wrong version of the \ - genesis block our version: ({genesis_slot}, {:?}), certified version \ - ({genesis_slot}, {block_id})", + genesis block our version: ({}, {:?}), certified version ({genesis_block:?})", + genesis_block.slot, genesis_bank.block_id() ); } @@ -1665,7 +1699,7 @@ impl ReplayStage { .unwrap() .banks() .keys() - .filter_map(|slot| (*slot > genesis_slot).then_some(*slot)) + .filter_map(|slot| (*slot > genesis_block.slot).then_some(*slot)) .collect(); for slot in slots_to_purge.into_iter() { info!("{my_pubkey} Alpenglow migration: Purging poh block in slot {slot}"); @@ -1681,7 +1715,7 @@ impl ReplayStage { } // Purge any partial slots greater than the genesis slot - let start_slot = genesis_slot + 1; + let start_slot = genesis_block.slot + 1; let end_slot = blockstore .highest_slot() .unwrap() @@ -1699,47 +1733,48 @@ impl ReplayStage { assert!(migration_status.is_alpenglow_enabled()); datapoint_info!( "migration-complete", - ("genesis_slot", genesis_slot as i64, i64), + ("genesis_slot", genesis_block.slot as i64, i64), ); } /// If we have an eligible genesis block, send out a genesis vote /// Returns false if no eligible block was found fn maybe_send_genesis_vote( + my_shred_version: u16, migration_status: &MigrationStatus, bank_forks: &RwLock, vote_account: Pubkey, identity_keypair: &Arc, authorized_voter_keypairs: &Arc>>>, - own_vote_sender: &Sender>, + own_message_sender: &Sender, bls_sender: &Sender, ) -> bool { - let Some((slot, block_id)) = migration_status.eligible_genesis_block() else { + let Some(block) = migration_status.eligible_genesis_block() else { // We have not yet discovered the genesis block return false; }; - let vote = Vote::new_genesis_vote(slot, block_id); + let vote = Vote::new_genesis_vote(block); match voting_utils::generate_vote_tx( vote, bank_forks.read().unwrap().root_bank().as_ref(), vote_account, + my_shred_version, identity_keypair, authorized_voter_keypairs, None, &mut HashMap::new(), ) { - GenerateVoteTxResult::ConsensusMessage(message) => { + GenerateVoteTxResult::Vote(vote_msg) => { // Send vote to ConsensusPool and rest of cluster warn!( - "{} Alpenglow migration: Casting genesis vote for ({slot}, {block_id})", + "{} Alpenglow migration: Casting genesis vote for ({block:?})", identity_keypair.pubkey() ); // If sending fails that means the channel is disconnected and we are shutting down - let _ = own_vote_sender.send(vec![message.clone()]); + let _ = own_message_sender.send(ConsensusMessage::Vote(vote_msg.clone())); let _ = bls_sender.send(BLSOp::PushVote { - message: Arc::new(message), - slot, + vote: Arc::new(vote_msg), saved_vote_history: agave_votor::vote_history_storage::SavedVoteHistoryVersions::Current( SavedVoteHistory::default(), @@ -1748,8 +1783,7 @@ impl ReplayStage { } e => { warn!( - "{} Alpenglow migration: Unable to send genesis vote for {slot} {block_id}: \ - {e:?}", + "{} Alpenglow migration: Unable to send genesis vote for {block:?}: {e:?}", identity_keypair.pubkey() ); } @@ -1764,11 +1798,13 @@ impl ReplayStage { tower_storage: &dyn TowerStorage, node_pubkey: &Pubkey, vote_account: &Pubkey, - bank_forks: &Arc>, + bank_forks: &RwLock, ) -> Result { let tower = Tower::restore(tower_storage, node_pubkey).and_then(|restored_tower| { let root_bank = bank_forks.read().unwrap().root_bank(); - let slot_history = root_bank.get_slot_history(); + let slot_history = root_bank + .get_slot_history() + .expect("slot history must exist"); restored_tower.adjust_lockouts_after_replay(root_bank.slot(), &slot_history) }); match tower { @@ -1809,30 +1845,26 @@ impl ReplayStage { for slot in first_leader_group_slot..=latest_leader_slot { let is_propagated = progress.is_propagated(slot); - if let Some(retransmit_info) = progress.get_retransmit_info_mut(slot) { - if !is_propagated.expect( + if let Some(retransmit_info) = progress.get_retransmit_info_mut(slot) + && !is_propagated.expect( "presence of retransmit_info ensures that propagation status is present", - ) { - if retransmit_info.reached_retransmit_threshold() { - info!( - "Retrying retransmit: latest_leader_slot={} slot={} \ - retransmit_info={:?}", - latest_leader_slot, slot, &retransmit_info, - ); - datapoint_info!( - metric_name, - ("latest_leader_slot", latest_leader_slot, i64), - ("slot", slot, i64), - ("retry_iteration", retransmit_info.retry_iteration, i64), - ); - let _ = retransmit_slots_sender.send(slot); - retransmit_info.increment_retry_iteration(); - } else { - debug!( - "Bypass retransmit of slot={} retransmit_info={:?}", - slot, &retransmit_info - ); - } + ) + { + if retransmit_info.reached_retransmit_threshold() { + info!( + "Retrying retransmit: latest_leader_slot={latest_leader_slot} slot={slot} \ + retransmit_info={retransmit_info:?}", + ); + datapoint_info!( + metric_name, + ("latest_leader_slot", latest_leader_slot, i64), + ("slot", slot, i64), + ("retry_iteration", retransmit_info.retry_iteration, i64), + ); + let _ = retransmit_slots_sender.send(slot); + retransmit_info.increment_retry_iteration(); + } else { + debug!("Bypass retransmit of slot={slot} retransmit_info={retransmit_info:?}"); } } } @@ -2359,8 +2391,8 @@ impl ReplayStage { /// this in `pending_switch` fn process_switch_bank_events( my_pubkey: &Pubkey, - switch_bank_receiver: &SwitchBankEventReceiver, - pending_switch: &mut Option<(Slot, Hash)>, + latest_switch_request: &LatestSwitchRequest, + pending_switch: &mut Option, blockstore: &Blockstore, bank_forks: &RwLock, progress: &mut ProgressMap, @@ -2368,31 +2400,33 @@ impl ReplayStage { ) -> Result<(), BlockstoreError> { let root = bank_forks.read().unwrap().root(); - if let Some(switch_bank_event) = switch_bank_receiver - .try_iter() - .max() - .filter(|SwitchBankEvent::Switch { slot, .. }| *slot > root) + if let Some(block) = latest_switch_request + .take() + .map(|ev| ev.block()) + .filter(|block| block.slot > root) { - let (slot, block_id) = switch_bank_event.block(); - - // Overwrite the pending switch, later switches take precedence - if Some(slot) >= pending_switch.map(|(slot, _)| slot) { - if let Some(prev_switch_request) = pending_switch.replace((slot, block_id)) { - trace!( - "{my_pubkey}: Overwriting previous switch request {prev_switch_request:?} \ - with ({slot}, {block_id})" - ); - } else { - trace!("{my_pubkey}: Received switch request in {slot} to {block_id}"); + match pending_switch { + None => { + trace!("{my_pubkey}: Setting empty pending_switch to ({block:?})"); + *pending_switch = Some(block); + } + Some(pending_switch_block) => { + if block.slot >= pending_switch_block.slot { + trace!( + "{my_pubkey}: Overwriting previous switch request \ + {pending_switch_block:?} with ({block:?})" + ); + *pending_switch_block = block; + } } } }; - let Some((slot, block_id)) = *pending_switch else { + let Some(block) = *pending_switch else { return Ok(()); }; - if bank_forks.read().unwrap().block_id(slot) == Some(block_id) { + if bank_forks.read().unwrap().block_id(block.slot) == Some(block.block_id) { // Nothing to switch *pending_switch = None; return Ok(()); @@ -2400,8 +2434,8 @@ impl ReplayStage { // Check if we have received the block and all of its ancestors and collect the ones we // need to switch out - let mut ancestor_slot = slot; - let mut ancestor_block_id = block_id; + let mut ancestor_slot = block.slot; + let mut ancestor_block_id = block.block_id; let mut blocks_to_switch = vec![]; loop { if ancestor_slot <= root { @@ -2471,6 +2505,7 @@ impl ReplayStage { // Switch the blockstore data atomically, handles slot meta chaining so generate new bank forks can proceed blockstore.switch_block_from_alternate(slot, location)?; + progress.increment_num_bank_switches(slot); info!("{my_pubkey}: Switched {slot} from {location:?}"); } @@ -2529,7 +2564,7 @@ impl ReplayStage { let root_bank = w_bank_forks.root_bank(); let (slots_to_purge, removed_banks) = - w_bank_forks.dump_slots(slots_to_clear.iter(), true); + w_bank_forks.dump_slots(slots_to_clear.iter(), false); (root_bank, slots_to_purge, removed_banks) }; @@ -2554,10 +2589,10 @@ impl ReplayStage { async_verification_freelist: &mut Vec, async_verification: Option, ) { - if let Some(async_verification) = async_verification { - if async_verification_freelist.len() < ASYNC_VERIFICATION_FREELIST_CAPACITY { - async_verification_freelist.push(async_verification); - } + if let Some(async_verification) = async_verification + && async_verification_freelist.len() < ASYNC_VERIFICATION_FREELIST_CAPACITY + { + async_verification_freelist.push(async_verification); } } @@ -2621,6 +2656,8 @@ impl ReplayStage { } else if let Some(prev_hash) = duplicate_confirmed_slots.insert(confirmed_slot, duplicate_confirmed_hash) { + // This assertion is intentional - it is not possible to split the cluster to get 52% on two versions + // without a massive turbine failure assert_eq!( prev_hash, duplicate_confirmed_hash, "Additional duplicate confirmed notification for slot {confirmed_slot} \ @@ -2725,17 +2762,17 @@ impl ReplayStage { current_leader: &mut Option, new_leader: &Pubkey, ) { - if let Some(current_leader) = current_leader.as_ref() { - if current_leader != new_leader { - let msg = if Self::leader_is_me(current_leader, my_pubkey) { - ". I am no longer the leader" - } else if Self::leader_is_me(new_leader, my_pubkey) { - ". I am now the leader" - } else { - "" - }; - info!("LEADER CHANGE at slot: {bank_slot} leader: {new_leader}{msg}"); - } + if let Some(current_leader) = current_leader.as_ref() + && current_leader != new_leader + { + let msg = if Self::leader_is_me(current_leader, my_pubkey) { + ". I am no longer the leader" + } else if Self::leader_is_me(new_leader, my_pubkey) { + ". I am now the leader" + } else { + "" + }; + info!("LEADER CHANGE at slot: {bank_slot} leader: {new_leader}{msg}"); } current_leader.replace(new_leader.to_owned()); } @@ -2768,7 +2805,7 @@ impl ReplayStage { progress_map.get_latest_leader_slot_must_exist(parent_slot) { let skip_propagated_check = - poh_slot - latest_leader_slot < NUM_CONSECUTIVE_LEADER_SLOTS; + poh_slot - latest_leader_slot < NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot; if skip_propagated_check { return true; } @@ -2785,7 +2822,7 @@ impl ReplayStage { fn should_retransmit(poh_slot: Slot, last_retransmit_slot: &mut Slot) -> bool { if poh_slot < *last_retransmit_slot - || poh_slot >= *last_retransmit_slot + NUM_CONSECUTIVE_LEADER_SLOTS + || poh_slot >= *last_retransmit_slot + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot { *last_retransmit_slot = poh_slot; true @@ -2806,7 +2843,7 @@ impl ReplayStage { #[allow(clippy::too_many_arguments)] fn maybe_start_leader( my_pubkey: &Pubkey, - bank_forks: &Arc>, + bank_forks: &RwLock, poh_recorder: &Arc>, poh_controller: &mut PohController, leader_schedule_cache: &Arc, @@ -2943,11 +2980,12 @@ impl ReplayStage { } fn replay_blockstore_into_bank( + my_shred_version: u16, process_active_banks_context: &ProcessActiveBanksContext, bank: &BankWithScheduler, replay_stats: &RwLock, replay_progress: &RwLock, - finalization_cert_sender: &Sender>, + finalization_cert_sender: &Sender, ) -> result::Result { let mut w_replay_stats = replay_stats.write().unwrap(); let mut w_replay_progress = replay_progress.write().unwrap(); @@ -2958,6 +2996,7 @@ impl ReplayStage { blockstore_processor::confirm_slot( &process_active_banks_context.blockstore, bank, + my_shred_version, &process_active_banks_context.replay_tx_thread_pool, &mut w_replay_stats, &mut w_replay_progress, @@ -2986,7 +3025,7 @@ impl ReplayStage { fn handle_votable_bank( bank: &Arc, switch_fork_decision: &SwitchForkDecision, - bank_forks: &Arc>, + bank_forks: &RwLock, tower: &mut Tower, progress: &mut ProgressMap, vote_account_pubkey: &Pubkey, @@ -3044,20 +3083,19 @@ impl ReplayStage { ); // Check if we've rooted a bank that will tell us the migration slot - if migration_status.is_pre_feature_activation() { - if let Some(slot) = bank_forks + if migration_status.is_pre_feature_activation() + && let Some(slot) = bank_forks .read() .unwrap() .root_bank() .feature_set .activated_slot(&agave_feature_set::alpenglow::id()) - { - let migration_slot = migration_status.record_feature_activation(slot); - datapoint_info!( - "migration-started", - ("migration_slot", migration_slot as i64, i64), - ); - } + { + let migration_slot = migration_status.record_feature_activation(slot); + datapoint_info!( + "migration-started", + ("migration_slot", migration_slot as i64, i64), + ); } } @@ -3104,6 +3142,46 @@ impl ReplayStage { ); } + fn maybe_report_vat_health( + last_vat_status_check: &mut Instant, + authorized_voter_keypairs: &Arc>>>, + bank_forks: &Arc>, + vote_account: &Pubkey, + ) { + if last_vat_status_check.elapsed().as_secs() < VAT_STATUS_CHECK_INTERVAL_SECS { + return; + } + *last_vat_status_check = Instant::now(); + + let bank = bank_forks.read().unwrap().root_bank(); + if !bank.feature_set.snapshot().validator_admission_ticket { + return; + } + + let is_voting_validator = !authorized_voter_keypairs.read().unwrap().is_empty(); + if !is_voting_validator { + return; + } + + let epoch = bank.epoch(); + if let Err(vat_failure_reason) = bank.get_vat_health_for_next_epoch(vote_account) { + warn!( + "VAT Health Check: Currently you will fail the VAT check at the start of epoch {} \ + meaning that you will be unable to vote or produce blocks in epoch {}. Reason: {}", + epoch.saturating_add(1), + epoch.saturating_add(2), + vat_failure_reason, + ); + } else { + info!( + "VAT Health Check: Passing local VAT checks. Assuming you are in the top {} of \ + stake, you will be included in voting and block building in epoch {}", + MAX_ALPENGLOW_VOTE_ACCOUNTS, + epoch.saturating_add(2), + ); + } + } + fn generate_vote_tx( node_keypair: &Keypair, bank: &Bank, @@ -3118,10 +3196,10 @@ impl ReplayStage { if authorized_voter_keypairs.is_empty() { return GenerateVoteTxResult::NonVoting; } - if let Some(slot) = wait_to_vote_slot { - if bank.slot() < slot { - return GenerateVoteTxResult::WaitToVoteSlot(slot); - } + if let Some(slot) = wait_to_vote_slot + && bank.slot() < slot + { + return GenerateVoteTxResult::WaitToVoteSlot(slot); } let Some(vote_account) = bank.get_vote_account(vote_account_pubkey) else { warn!("Vote account {vote_account_pubkey} does not exist. Unable to vote",); @@ -3583,10 +3661,11 @@ impl ReplayStage { } fn replay_active_bank( + my_shred_version: u16, process_active_banks_context: &ProcessActiveBanksContext, bank_replay_result_tracker: BankReplayResultTracker, my_pubkey: &Pubkey, - finalization_cert_sender: &Sender>, + finalization_cert_sender: &Sender, ) -> (ReplaySlotFromBlockstore, Option) { let BankReplayResultTracker { mut replay_result, @@ -3635,6 +3714,7 @@ impl ReplayStage { let mut replay_blockstore_time = Measure::start("replay_blockstore_into_bank"); let blockstore_result = Self::replay_blockstore_into_bank( + my_shred_version, process_active_banks_context, &bank, &replay_stats, @@ -3656,11 +3736,12 @@ impl ReplayStage { } fn replay_active_banks( + my_shred_version: u16, process_active_banks_context: &ProcessActiveBanksContext, bank_replay_result_trackers: Vec, replay_timing: &mut ReplayLoopTiming, my_pubkey: &Pubkey, - finalization_cert_sender: &Sender>, + finalization_cert_sender: &Sender, ) -> Vec { match &process_active_banks_context.replay_mode { // Skip the overhead of the threadpool if there is only one bank to play @@ -3679,6 +3760,7 @@ impl ReplayStage { ); let (replay_result, replay_blockstore_us) = Self::replay_active_bank( + my_shred_version, process_active_banks_context, bank_replay_result_tracker, my_pubkey, @@ -3707,6 +3789,7 @@ impl ReplayStage { bank_replay_result_tracker.replay_result.bank_slot ); let (replay_result, replay_blockstore_us) = Self::replay_active_bank( + my_shred_version, process_active_banks_context, bank_replay_result_tracker, my_pubkey, @@ -3735,8 +3818,6 @@ impl ReplayStage { let bank_forks = &process_active_banks_context.bank_forks; // TODO: See if processing of blockstore replay results and bank completion can be made thread safe. - let mut tx_count = 0; - let mut execute_timings = ExecuteTimings::default(); let mut new_frozen_slots = vec![]; for replay_result in replay_result_vec { if replay_result.is_slot_dead { @@ -3750,7 +3831,7 @@ impl ReplayStage { }; if let Some(replay_result) = &replay_result.replay_result { match replay_result { - Ok(replay_tx_count) => tx_count += replay_tx_count, + Ok(_) => {} Err(BlockstoreProcessorError::BlockComponentProcessor( BlockComponentProcessorError::AbandonedBank(update_parent), )) => { @@ -3893,12 +3974,12 @@ impl ReplayStage { ); if let Err((expected_hash, computed_hash)) = verify_result { - error!( - "Bank hash mismatch for slot {bank_slot} expected: {expected_hash} \ - computed: {computed_hash}", + warn!( + "For slot {bank_slot} the leader said the bank hash should be: \ + {expected_hash} however we computed: {computed_hash}", ); - datapoint_error!( + datapoint_warn!( "bank_hash_mismatch", ("slot", bank_slot, i64), ("expected", expected_hash.to_string(), String), @@ -4120,7 +4201,6 @@ impl ReplayStage { bank_complete_time.as_us(), is_unified_scheduler_enabled, ); - execute_timings.accumulate(&r_replay_stats.batch_execute.totals); } else { trace!( "bank {} not completed tick_height: {}, max_tick_height: {}", @@ -4136,6 +4216,7 @@ impl ReplayStage { #[allow(clippy::too_many_arguments)] fn process_active_banks( + my_shred_version: u16, process_active_banks_context: &ProcessActiveBanksContext, progress: &mut ProgressMap, async_verification_freelist: &mut Vec, @@ -4146,7 +4227,7 @@ impl ReplayStage { my_pubkey: &Pubkey, vote_account: &Pubkey, replay_timing: &mut ReplayLoopTiming, - finalization_cert_sender: &Sender>, + finalization_cert_sender: &Sender, ) -> Vec /* completed slots */ { let bank_replay_result_trackers = Self::prepare_active_banks_for_replay( process_active_banks_context, @@ -4161,6 +4242,7 @@ impl ReplayStage { // Perform replay execution. let replay_result_vec = Self::replay_active_banks( + my_shred_version, process_active_banks_context, bank_replay_result_trackers, replay_timing, @@ -4216,11 +4298,14 @@ impl ReplayStage { return; }; - let end_slot = next_slot.saturating_add(NUM_CONSECUTIVE_LEADER_SLOTS - 1); + let end_slot = next_slot.saturating_add(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot - 1); let leader_window_info = LeaderWindowInfo { start_slot: next_slot, end_slot, - parent_block: (bank.slot(), block_id), + parent_block: Block { + slot: bank.slot(), + block_id, + }, block_timer: Instant::now(), }; @@ -4394,13 +4479,13 @@ impl ReplayStage { .get(genesis_slot) .expect("Genesis bank must exist, no rooting past migration slot"); - let genesis_block = ( - genesis_slot, - genesis_bank.block_id().expect( + let genesis_block = Block { + slot: genesis_slot, + block_id: genesis_bank.block_id().expect( "It is impossible for block id to not be known at this point, as \ a descendant of this block has reached super oc status", ), - ); + }; migration_status.set_genesis_block(genesis_block); } @@ -4479,36 +4564,35 @@ impl ReplayStage { tower.vote_state.root_slot ); - if let Some(local_root) = tower.vote_state.root_slot { - if bank_vote_state + if let Some(local_root) = tower.vote_state.root_slot + && bank_vote_state .root_slot .map(|bank_root| local_root > bank_root) .unwrap_or(true) - { - // If the local root is larger than this on chain vote state - // root (possible due to supermajority roots being set on - // startup), then we need to adjust the tower - bank_vote_state.root_slot = Some(local_root); - bank_vote_state - .votes - .retain(|lockout| lockout.slot() > local_root); - info!( - "Local root is larger than on chain root, overwrote bank root {:?} and \ - updated votes {:?}", - bank_vote_state.root_slot, bank_vote_state.votes - ); + { + // If the local root is larger than this on chain vote state + // root (possible due to supermajority roots being set on + // startup), then we need to adjust the tower + bank_vote_state.root_slot = Some(local_root); + bank_vote_state + .votes + .retain(|lockout| lockout.slot() > local_root); + info!( + "Local root is larger than on chain root, overwrote bank root {:?} and updated \ + votes {:?}", + bank_vote_state.root_slot, bank_vote_state.votes + ); - if let Some(first_vote) = bank_vote_state.votes.front() { - assert!( - ancestors - .get(&first_vote.slot()) - .expect( - "Ancestors map must contain an entry for all slots on this fork \ - greater than `local_root` and less than `bank_slot`" - ) - .contains(&local_root) - ); - } + if let Some(first_vote) = bank_vote_state.votes.front() { + assert!( + ancestors + .get(&first_vote.slot()) + .expect( + "Ancestors map must contain an entry for all slots on this fork \ + greater than `local_root` and less than `bank_slot`" + ) + .contains(&local_root) + ); } } @@ -5004,9 +5088,14 @@ impl ReplayStage { fn process_bank_forks_commands( bank_forks_controller_receiver: &BankForksCommandReceiver, context: &ProcessBankForksContext, + my_pubkey: &Pubkey, progress: &mut ProgressMap, async_verification_freelist: &mut Vec, ) -> Result<(), TryRecvError> { + if let Some(command) = bank_forks_controller_receiver.take_set_root_command() { + Self::process_set_root_command(command, context, my_pubkey, progress); + } + loop { let command = bank_forks_controller_receiver.receiver().try_recv()?; Self::process_bank_forks_command( @@ -5018,6 +5107,33 @@ impl ReplayStage { } } + fn process_set_root_command( + command: SetRootCommand, + context: &ProcessBankForksContext, + my_pubkey: &Pubkey, + progress: &mut ProgressMap, + ) { + let SetRootCommand { + parent_slot, + new_root, + highest_super_majority_root, + } = command; + root_utils::check_and_handle_new_root( + parent_slot, + new_root, + context.snapshot_controller.as_deref(), + highest_super_majority_root, + &context.bank_notification_sender, + &context.drop_bank_sender, + &context.blockstore, + &context.leader_schedule_cache, + &context.bank_forks, + context.rpc_subscriptions.as_deref(), + my_pubkey, + |bank_forks| progress.handle_new_root(bank_forks), + ); + } + /// Process a bank forks command fn process_bank_forks_command( command: BankForksCommand, @@ -5054,31 +5170,6 @@ impl ReplayStage { warn!("bank forks controller insert-bank response receiver dropped") }); } - BankForksCommand::SetRoot { - my_pubkey, - parent_slot, - new_root, - highest_super_majority_root, - response_sender, - } => { - root_utils::check_and_handle_new_root( - parent_slot, - new_root, - context.snapshot_controller.as_deref(), - highest_super_majority_root, - &context.bank_notification_sender, - &context.drop_bank_sender, - &context.blockstore, - &context.leader_schedule_cache, - &context.bank_forks, - context.rpc_subscriptions.as_deref(), - &my_pubkey, - |_| {}, - ); - response_sender.send(()).unwrap_or_else(|_| { - warn!("bank forks controller set-root response receiver dropped") - }); - } BankForksCommand::ClearBank { slot, response_sender, @@ -5286,7 +5377,7 @@ impl ReplayStage { fn log_heaviest_fork_failures( heaviest_fork_failures: &Vec, - bank_forks: &Arc>, + bank_forks: &RwLock, tower: &Tower, progress: &ProgressMap, ancestors: &HashMap>, diff --git a/core/src/replay_stage/dead_slots.rs b/core/src/replay_stage/dead_slots.rs index f79f7ed39e8..f9c2a79d181 100644 --- a/core/src/replay_stage/dead_slots.rs +++ b/core/src/replay_stage/dead_slots.rs @@ -15,7 +15,8 @@ use { agave_votor_messages::migration::MigrationStatus, solana_clock::Slot, solana_ledger::{ - block_error::BlockError, blockstore::Blockstore, + block_error::BlockError, + blockstore::{Blockstore, BlockstoreError}, blockstore_processor::BlockstoreProcessorError, }, solana_rpc::{rpc_subscriptions::RpcSubscriptions, slot_status_notifier::SlotStatusNotifier}, @@ -44,6 +45,9 @@ impl DeadSlotLogLevel { pub(super) fn for_replay_error(err: &BlockstoreProcessorError) -> Self { match err { BlockstoreProcessorError::InvalidBlock(BlockError::TooFewTicks) => Self::Info, + BlockstoreProcessorError::FailedToLoadEntries(BlockstoreError::BlockAborted(_)) => { + Self::Info + } _ => Self::Error, } } @@ -142,7 +146,8 @@ fn should_mark_soft_dead( migration_status: &MigrationStatus, ) -> bool { let slot = bank.slot(); - if !migration_status.should_allow_fast_leader_handover(slot) + if !bank.feature_set.snapshot().alpenglow_fast_leader_handover + || !migration_status.should_allow_block_markers(slot) || blockstore.is_dead(slot) || !is_update_parent_recoverable_replay_error(err) { diff --git a/core/src/replay_stage/tests.rs b/core/src/replay_stage/tests.rs index 171ea94262d..5ebae786a13 100644 --- a/core/src/replay_stage/tests.rs +++ b/core/src/replay_stage/tests.rs @@ -11,18 +11,20 @@ use { replay_stage::ReplayStage, vote_simulator::{self, VoteSimulator}, }, - agave_votor_messages::consensus_message::{Certificate, CertificateType}, + agave_votor_messages::{ + certificate::{CertSignature, GenesisCert}, + consensus_message::Block, + }, blockstore_processor::{ ConfirmationProgress, ProcessOptions, confirm_full_slot, fill_blockstore_slot_with_ticks, process_bank_0, }, - crossbeam_channel::{bounded, unbounded}, + crossbeam_channel::bounded, itertools::Itertools, solana_account::{ReadableAccount, state_traits::StateMut}, solana_accounts_db::accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsDbConfig}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_client::connection_cache::ConnectionCache, - solana_clock::NUM_CONSECUTIVE_LEADER_SLOTS, solana_entry::{ block_component::{ BlockComponent, BlockFooterV1, BlockHeaderV1, UpdateParentV1, VersionedBlockMarker, @@ -35,6 +37,7 @@ use { solana_hash::Hash, solana_instruction::error::InstructionError, solana_keypair::Keypair, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS as NUM_CONSECUTIVE_LEADER_SLOTS_NZ, solana_ledger::{ block_error::BlockError, blockstore::{ @@ -60,7 +63,9 @@ use { genesis_utils::{GenesisConfigInfo, ValidatorVoteKeypairs}, }, solana_sha256_hasher::hash, + solana_shred_version::compute_shred_version, solana_signature::Signature, + solana_svm_timings::ExecuteTimings, solana_system_transaction as system_transaction, solana_tpu_client::tpu_client::{DEFAULT_TPU_CONNECTION_POOL_SIZE, DEFAULT_VOTE_USE_QUIC}, solana_transaction_error::TransactionError, @@ -80,6 +85,8 @@ use { trees::{Tree, tr}, }; +const NUM_CONSECUTIVE_LEADER_SLOTS: Slot = NUM_CONSECUTIVE_LEADER_SLOTS_NZ.get() as Slot; + static_assertions::const_assert!(REFRESH_VOTE_BLOCKHEIGHT < solana_clock::MAX_PROCESSING_AGE); impl ProcessActiveBanksContext { @@ -88,11 +95,10 @@ impl ProcessActiveBanksContext { blockstore: Arc, replay_vote_sender: ReplayVoteSender, ) -> Self { - use crossbeam_channel::unbounded; - let (cluster_slots_update_sender, _) = unbounded(); - let (cost_update_sender, _) = unbounded(); - let (ancestor_hashes_replay_update_sender, _) = unbounded(); - let (votor_event_sender, _) = unbounded(); + let (cluster_slots_update_sender, _) = bounded(1024); + let (cost_update_sender, _) = bounded(1024); + let (ancestor_hashes_replay_update_sender, _) = bounded(1024); + let (votor_event_sender, _) = bounded(1024); let migration_status = Arc::new(MigrationStatus::default()); let replay_tx_thread_pool = rayon::ThreadPoolBuilder::new() .num_threads(1) @@ -125,11 +131,16 @@ impl ProcessActiveBanksContext { fn post_migration_status_for_tests() -> MigrationStatus { let migration_status = MigrationStatus::default(); migration_status.record_feature_activation(0); - let genesis_block = (0, Hash::default()); - let genesis_certificate = Arc::new(Certificate { - cert_type: CertificateType::Genesis(genesis_block.0, genesis_block.1), - signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), - bitmap: vec![], + let genesis_block = Block { + slot: 0, + block_id: Hash::default(), + }; + let genesis_certificate = Arc::new(GenesisCert { + block: genesis_block, + signature: CertSignature { + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }, }); migration_status.set_genesis_block(genesis_block); migration_status.set_genesis_certificate(genesis_certificate); @@ -137,6 +148,16 @@ fn post_migration_status_for_tests() -> MigrationStatus { migration_status } +fn cluster_info_for_tests() -> ClusterInfo { + let keypair = Arc::new(Keypair::new()); + let my_pubkey = keypair.pubkey(); + ClusterInfo::new( + Node::new_localhost_with_pubkey(&my_pubkey).info, + keypair, + SocketAddrSpace::Unspecified, + ) +} + fn block_marker_shreds( slot: Slot, shred_parent_slot: Slot, @@ -183,11 +204,11 @@ fn insert_update_parent_slot( update_parent_block_id: Hash, replay_fec_set_index: u32, ) { - let header = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let header = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: block_header_parent_slot, parent_block_id: Hash::new_unique(), }); - let update_parent = VersionedBlockMarker::new_update_parent(UpdateParentV1 { + let update_parent = VersionedBlockMarker::from_update_parent(UpdateParentV1 { new_parent_slot: update_parent_slot, new_parent_block_id: update_parent_block_id, }); @@ -198,7 +219,7 @@ fn insert_update_parent_slot( update_parent, replay_fec_set_index, )); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); } /// Build the minimal dead-slot context needed by focused replay tests. @@ -394,7 +415,7 @@ fn test_child_slots_of_same_parent() { 1, // parent_slot 8, // num_entries ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert!( bank_forks .read() @@ -427,7 +448,7 @@ fn test_child_slots_of_same_parent() { // Insert shreds for slot 2 * NUM_CONSECUTIVE_LEADER_SLOTS, // chaining to slot 1 let (shreds, _) = make_slot_entries(2 * NUM_CONSECUTIVE_LEADER_SLOTS, 1, 8); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert!( bank_forks .read() @@ -527,7 +548,7 @@ fn test_handle_new_root() { .into_iter() .map(|slot| (slot, Hash::default())) .collect(); - let (drop_bank_sender, _drop_bank_receiver) = unbounded(); + let (drop_bank_sender, _drop_bank_receiver) = bounded(1024); let mut tbft_structs = TowerBFTStructures { heaviest_subtree_fork_choice, duplicate_slots_tracker, @@ -618,7 +639,7 @@ fn test_handle_new_root_ahead_of_highest_super_majority_root() { ForkProgress::new(Hash::default(), None, None, 0, 0, None), ); } - let (drop_bank_sender, _drop_bank_receiver) = unbounded(); + let (drop_bank_sender, _drop_bank_receiver) = bounded(1024); let mut tbft_structs = TowerBFTStructures { heaviest_subtree_fork_choice: HeaviestSubtreeForkChoice::new((root, root_hash)), duplicate_slots_tracker: DuplicateSlotsTracker::default(), @@ -950,8 +971,8 @@ fn do_test_dead_slot_on_complete_bank(failure: CompleteBankFailure) { let bank = bank_forks.write().unwrap().insert(child_bank); let slot = bank.slot(); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); - let (finalization_cert_sender, _finalization_cert_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); + let (finalization_cert_sender, _finalization_cert_receiver) = bounded(1024); let process_active_banks_context = ProcessActiveBanksContext::new_for_tests( bank_forks.clone(), blockstore.clone(), @@ -1000,12 +1021,14 @@ fn do_test_dead_slot_on_complete_bank(failure: CompleteBankFailure) { }; let entries = make_complete_slot_entries(&bank, vec![tx]); let shreds = entries_to_test_shreds(&entries, slot, bank.parent_slot(), true, 0); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); + let cluster_info = cluster_info_for_tests(); ReplaySlotFromBlockstore { is_slot_dead: false, bank_slot: slot, replay_result: Some(ReplayStage::replay_blockstore_into_bank( + cluster_info.my_shred_version(), &process_active_banks_context, &bank, &bank_progress.replay_stats, @@ -1076,7 +1099,7 @@ fn test_cmr_mismatch_hard_dead() { ForkProgress::new(bank.last_blockhash(), Some(0), None, 0, 0, None), ); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); let process_active_banks_context = ProcessActiveBanksContext::new_for_tests( bank_forks.clone(), blockstore.clone(), @@ -1138,7 +1161,7 @@ fn test_abandon_invalidates() { ForkProgress::new(bank.last_blockhash(), Some(0), None, 0, 0, None), ); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); let process_active_banks_context = ProcessActiveBanksContext::new_for_tests( bank_forks.clone(), blockstore, @@ -1213,17 +1236,19 @@ where &validator_keypairs.values().next().unwrap().node_keypair, bank1.clone(), ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let block_commitment_cache = Arc::new(RwLock::new(BlockCommitmentCache::default())); let exit = Arc::new(AtomicBool::new(false)); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); - let (finalization_cert_sender, _finalization_cert_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); + let (finalization_cert_sender, _finalization_cert_receiver) = bounded(1024); let process_active_banks_context = ProcessActiveBanksContext::new_for_tests( bank_forks.clone(), blockstore.clone(), replay_vote_sender, ); + let cluster_info = cluster_info_for_tests(); let res = ReplayStage::replay_blockstore_into_bank( + cluster_info.my_shred_version(), &process_active_banks_context, &bank1, &bank1_progress.replay_stats, @@ -1254,7 +1279,7 @@ where block_commitment_cache, OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks), )); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); let dead_slots = Arc::new(Mutex::new(HashSet::default())); let slot_status_notifier: Option = Some(Arc::new(RwLock::new( @@ -2679,13 +2704,13 @@ fn test_purge_unconfirmed_duplicate_slots_and_reattach() { 1, // parent_slot 8, // num_entries ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let (shreds, _) = make_slot_entries( 5, // slot 3, // parent_slot 8, // num_entries ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let rpc_subscriptions = Some(rpc_subscriptions); @@ -2826,8 +2851,8 @@ fn test_update_parent_restart() { let bank0 = bank_forks.read().unwrap().get(0).unwrap(); // Slot 4: 5 shreds, replay_fec_set_index=32; 5 < 32 so cleared. - // Slot 5: 40 shreds, replay_fec_set_index=32; 40 >= 32 so skipped. - for (slot, shreds) in [(4, 5), (5, 40)] { + // Slot 8: 40 shreds, replay_fec_set_index=32; 40 >= 32 so skipped. + for (slot, shreds) in [(4, 5), (8, 40)] { let bank = Bank::new_from_parent(bank0.clone(), SlotLeader::default(), slot); bank_forks.write().unwrap().insert(bank); let p = ForkProgress::new(Hash::default(), Some(0), None, 0, 0, None); @@ -2835,8 +2860,8 @@ fn test_update_parent_restart() { progress.insert(slot, p); } - let (tx, rx) = crossbeam_channel::unbounded(); - for slot in [4, 5] { + let (tx, rx) = bounded(1024); + for slot in [4, 8] { let parent_block_id = Hash::new_unique(); insert_update_parent_slot( &blockstore, @@ -2850,7 +2875,7 @@ fn test_update_parent_restart() { } let cleared_bank_id = bank_forks.read().unwrap().get(4).unwrap().bank_id(); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); let mut async_verification_freelist = Vec::new(); handle_update_parent_interrupts( &Pubkey::new_unique(), @@ -2864,7 +2889,7 @@ fn test_update_parent_restart() { ); assert!(progress.get(&4).is_none()); // cleared: 5 < 32 - assert!(progress.get(&5).is_some()); // skipped: 40 >= 32 + assert!(progress.get(&8).is_some()); // skipped: 40 >= 32 assert_eq!( replay_vote_receiver.try_recv(), Ok(ReplayVoteMessage::InvalidBank { @@ -2889,20 +2914,20 @@ fn test_headerless_update_parent() { .. } = vote_simulator; let my_pubkey = Pubkey::new_unique(); - let slot = 1; + let slot = 4; let migration_status = post_migration_status_for_tests(); let footer_marker = || { - VersionedBlockMarker::new_block_footer(BlockFooterV1 { + VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: 0, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }) }; - let update_parent = VersionedBlockMarker::new_update_parent(UpdateParentV1 { + let update_parent = VersionedBlockMarker::from_update_parent(UpdateParentV1 { new_parent_slot: 0, new_parent_block_id: Hash::default(), }); @@ -2916,7 +2941,7 @@ fn test_headerless_update_parent() { 64, true, )); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); let slot_meta = blockstore.meta(slot).unwrap().unwrap(); assert!(blockstore.is_full(slot), "{slot_meta:?}"); assert!(slot_meta.has_update_parent()); @@ -2978,10 +3003,10 @@ fn test_update_parent_tower_gated() { p.replay_progress.write().unwrap().num_shreds = 5; progress.insert(slot, p); - let (tx, rx) = crossbeam_channel::unbounded(); + let (tx, rx) = bounded(1024); tx.send(UpdateParentSignal { slot }).unwrap(); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let mut async_verification_freelist = Vec::new(); handle_update_parent_interrupts( &Pubkey::new_unique(), @@ -2998,6 +3023,50 @@ fn test_update_parent_tower_gated() { assert!(bank_forks.read().unwrap().get(slot).is_some()); } +#[test] +fn test_update_parent_interrupt_ignores_non_first_leader_window_slot() { + let ReplayBlockstoreComponents { + blockstore, + vote_simulator, + .. + } = replay_blockstore_components(Some(tr(0) / tr(1)), 1, None::); + let VoteSimulator { + bank_forks, + mut progress, + .. + } = vote_simulator; + + let slot = 1; + let mut meta = blockstore.meta(slot).unwrap().unwrap(); + meta.parent_slot = Some(0); + meta.parent_block_id = Hash::new_unique(); + meta.replay_fec_set_index = 10; + blockstore.put_meta(slot, &meta).unwrap(); + + let p = ForkProgress::new(Hash::default(), Some(0), None, 0, 0, None); + p.replay_progress.write().unwrap().num_shreds = 5; + progress.insert(slot, p); + + let (tx, rx) = bounded(1024); + tx.send(UpdateParentSignal { slot }).unwrap(); + + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); + let mut async_verification_freelist = Vec::new(); + handle_update_parent_interrupts( + &Pubkey::new_unique(), + &blockstore, + &bank_forks, + &mut progress, + &mut async_verification_freelist, + &rx, + &replay_vote_sender, + &post_migration_status_for_tests(), + ); + + assert!(progress.get(&slot).is_some()); + assert!(bank_forks.read().unwrap().get(slot).is_some()); +} + #[test] fn test_update_parent_keeps_hard() { let ReplayBlockstoreComponents { @@ -3025,10 +3094,10 @@ fn test_update_parent_keeps_hard() { p.mark_dead(DeadSlotReason::Hard); progress.insert(slot, p); - let (tx, rx) = crossbeam_channel::unbounded(); + let (tx, rx) = bounded(1024); tx.send(UpdateParentSignal { slot }).unwrap(); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let mut async_verification_freelist = Vec::new(); handle_update_parent_interrupts( &Pubkey::new_unique(), @@ -3070,8 +3139,8 @@ fn test_pre_update_soft_dead() { p.replay_progress.write().unwrap().num_shreds = 5; progress.insert(slot, p); - let (replay_vote_sender, replay_vote_receiver) = unbounded(); - let (ancestor_hashes_replay_update_sender, _) = unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); + let (ancestor_hashes_replay_update_sender, _) = bounded(1024); let mut duplicate_slots_to_repair = DuplicateSlotsToRepair::default(); let mut purge_repair_slot_counter = PurgeRepairSlotCounter::default(); let migration_status = post_migration_status_for_tests(); @@ -3129,8 +3198,8 @@ fn test_after_update_hard_dead() { p.replay_progress.write().unwrap().num_shreds = 5; progress.insert(slot, p); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); - let (ancestor_hashes_replay_update_sender, _) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); + let (ancestor_hashes_replay_update_sender, _) = bounded(1024); let mut duplicate_slots_to_repair = DuplicateSlotsToRepair::default(); let mut purge_repair_slot_counter = PurgeRepairSlotCounter::default(); let migration_status = post_migration_status_for_tests(); @@ -3170,30 +3239,30 @@ fn test_before_update_soft_dead() { .. } = vote_simulator; - let slot = 1; + let slot = 4; let bank0 = bank_forks.read().unwrap().get(0).unwrap(); let bank = Bank::new_from_parent(bank0, SlotLeader::default(), slot); let bank = bank_forks.write().unwrap().insert(bank); - let header = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let header = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), }); - let update_parent = VersionedBlockMarker::new_update_parent(UpdateParentV1 { + let update_parent = VersionedBlockMarker::from_update_parent(UpdateParentV1 { new_parent_slot: 0, new_parent_block_id: Hash::default(), }); let mut shreds = block_marker_shreds(slot, 0, header, 0); shreds.retain(|shred| !shred.is_data() || shred.index() != 0); shreds.extend(block_marker_shreds(slot, 0, update_parent, 32)); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); assert!(blockstore.meta(slot).unwrap().unwrap().has_update_parent()); let p = ForkProgress::new(bank.last_blockhash(), Some(0), None, 0, 0, None); p.replay_progress.write().unwrap().num_shreds = 5; progress.insert(slot, p); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); - let (ancestor_hashes_replay_update_sender, _) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); + let (ancestor_hashes_replay_update_sender, _) = bounded(1024); let mut duplicate_slots_to_repair = DuplicateSlotsToRepair::default(); let mut purge_repair_slot_counter = PurgeRepairSlotCounter::default(); let migration_status = post_migration_status_for_tests(); @@ -3247,7 +3316,7 @@ fn test_soft_dead_restarts() { p.mark_dead(DeadSlotReason::ReplayFailureBeforeUpdateParent); progress.insert(slot, p); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let mut async_verification_freelist = Vec::new(); process_soft_dead_slots( &Pubkey::new_unique(), @@ -3290,7 +3359,7 @@ fn test_full_soft_dead_hardens() { p.mark_dead(DeadSlotReason::ReplayFailureBeforeUpdateParent); progress.insert(slot, p); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let mut async_verification_freelist = Vec::new(); process_soft_dead_slots( &Pubkey::new_unique(), @@ -3318,7 +3387,10 @@ fn test_latest_parent_coalesces() { .try_send(LeaderWindowInfo { start_slot: 8, end_slot: 11, - parent_block: (7, Hash::new_unique()), + parent_block: Block { + slot: 7, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), }) .unwrap(); @@ -3329,7 +3401,10 @@ fn test_latest_parent_coalesces() { LeaderWindowInfo { start_slot: 12, end_slot: 15, - parent_block: (11, Hash::new_unique()), + parent_block: Block { + slot: 11, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), }, ); @@ -3343,7 +3418,10 @@ fn test_latest_parent_coalesces() { .try_send(LeaderWindowInfo { start_slot: 20, end_slot: 22, - parent_block: (19, Hash::new_unique()), + parent_block: Block { + slot: 19, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), }) .unwrap(); @@ -3354,7 +3432,10 @@ fn test_latest_parent_coalesces() { LeaderWindowInfo { start_slot: 20, end_slot: 23, - parent_block: (19, Hash::new_unique()), + parent_block: Block { + slot: 19, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), }, ); @@ -3368,7 +3449,10 @@ fn test_latest_parent_coalesces() { .try_send(LeaderWindowInfo { start_slot: 20, end_slot: 23, - parent_block: (19, Hash::new_unique()), + parent_block: Block { + slot: 19, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), }) .unwrap(); @@ -3379,7 +3463,10 @@ fn test_latest_parent_coalesces() { LeaderWindowInfo { start_slot: 16, end_slot: 19, - parent_block: (15, Hash::new_unique()), + parent_block: Block { + slot: 15, + block_id: Hash::new_unique(), + }, block_timer: Instant::now(), }, ); @@ -3401,7 +3488,7 @@ fn test_skip_own_update_full() { let my_pubkey = *validator_keypairs.keys().next().unwrap(); let root_bank = bank_forks.read().unwrap().root_bank(); let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&root_bank)); - let slot = 2; + let slot = 4; let replay_fec_set_index = 32; insert_update_parent_slot( @@ -3657,7 +3744,7 @@ fn test_unconfirmed_duplicate_slots_and_lockouts_for_non_heaviest_fork() { || Some(bank5_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 5, bank_forks.read().unwrap().root(), @@ -3868,7 +3955,7 @@ fn test_unconfirmed_duplicate_slots_and_lockouts() { || Some(bank4_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 4, bank_forks.read().unwrap().root(), @@ -4003,7 +4090,7 @@ fn test_dump_then_repair_correct_slots() { duplicate_slots_to_repair.insert(1, Hash::new_unique()); duplicate_slots_to_repair.insert(2, Hash::new_unique()); let mut purge_repair_slot_counter = PurgeRepairSlotCounter::default(); - let (dumped_slots_sender, dumped_slots_receiver) = unbounded(); + let (dumped_slots_sender, dumped_slots_receiver) = bounded(1024); let should_be_dumped = duplicate_slots_to_repair .iter() .map(|(&s, &h)| (s, h)) @@ -4106,7 +4193,7 @@ fn setup_vote_then_rollback( || Some(our_bank2_hash), ); let (ancestor_hashes_replay_update_sender, _ancestor_hashes_replay_update_receiver) = - unbounded(); + bounded(1024); check_slot_agrees_with_cluster( 2, bank_forks.read().unwrap().root(), @@ -4126,7 +4213,7 @@ fn setup_vote_then_rollback( let mut ancestors = bank_forks.read().unwrap().ancestors(); let mut descendants = bank_forks.read().unwrap().descendants(); let old_descendants_of_2 = descendants.get(&2).unwrap().clone(); - let (dumped_slots_sender, _dumped_slots_receiver) = unbounded(); + let (dumped_slots_sender, _dumped_slots_receiver) = bounded(1024); ReplayStage::dump_then_repair_correct_slots( &mut duplicate_slots_to_repair, @@ -4417,7 +4504,7 @@ fn test_gossip_vote_doesnt_affect_fork_choice() { let vote_pubkey = vote_pubkeys[0]; let mut unfrozen_gossip_verified_vote_hashes = UnfrozenGossipVerifiedVoteHashes::default(); - let (gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = unbounded(); + let (gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = bounded(1024); // Best slot is 4 assert_eq!( @@ -4505,7 +4592,7 @@ fn test_replay_stage_refresh_last_vote() { ), ); - let (voting_sender, voting_receiver) = unbounded(); + let (voting_sender, voting_receiver) = bounded(1024); // Simulate landing a vote for slot 0 landing in slot 1 let bank1 = Bank::new_from_parent_with_bank_forks( @@ -5034,7 +5121,7 @@ fn test_replay_stage_last_vote_outside_slot_hashes() { ) }); - let (voting_sender, voting_receiver) = unbounded(); + let (voting_sender, voting_receiver) = bounded(1024); let mut cursor = Cursor::default(); let mut new_bank = send_vote_in_new_bank( @@ -5202,7 +5289,7 @@ fn test_retransmit_latest_unpropagated_leader_slot() { } = vote_simulator; let poh_recorder = Arc::new(poh_recorder); - let (retransmit_slots_sender, retransmit_slots_receiver) = unbounded(); + let (retransmit_slots_sender, retransmit_slots_receiver) = bounded(1024); let bank1 = Bank::new_from_parent( bank_forks.read().unwrap().get(0).unwrap(), @@ -5376,7 +5463,7 @@ fn test_maybe_retransmit_unpropagated_slots() { .. } = vote_simulator; - let (retransmit_slots_sender, retransmit_slots_receiver) = unbounded(); + let (retransmit_slots_sender, retransmit_slots_receiver) = bounded(1024); let retry_time = Instant::now() .checked_sub(Duration::from_millis(RETRANSMIT_BASE_DELAY_MS + 1)) .unwrap(); @@ -5464,7 +5551,7 @@ fn test_dumped_slot_not_causing_panic() { } = vote_simulator; let poh_recorder = Arc::new(poh_recorder); - let (retransmit_slots_sender, _) = unbounded(); + let (retransmit_slots_sender, _) = bounded(1024); // Use a bank slot when I was not leader to avoid panic for dumping my own slot let slot_to_dump = (1..100) @@ -5520,7 +5607,7 @@ fn test_dumped_slot_not_causing_panic() { let bank_to_dump_bad_hash = Hash::new_unique(); duplicate_slots_to_repair.insert(slot_to_dump, bank_to_dump_bad_hash); let mut purge_repair_slot_counter = PurgeRepairSlotCounter::default(); - let (dumped_slots_sender, dumped_slots_receiver) = unbounded(); + let (dumped_slots_sender, dumped_slots_receiver) = bounded(1024); ReplayStage::dump_then_repair_correct_slots( &mut duplicate_slots_to_repair, @@ -5605,7 +5692,7 @@ fn test_dump_own_slots_fails() { duplicate_slots_to_repair.insert(1, Hash::new_unique()); duplicate_slots_to_repair.insert(2, Hash::new_unique()); let mut purge_repair_slot_counter = PurgeRepairSlotCounter::default(); - let (dumped_slots_sender, _) = unbounded(); + let (dumped_slots_sender, _) = bounded(1024); ReplayStage::dump_then_repair_correct_slots( &mut duplicate_slots_to_repair, @@ -6078,6 +6165,7 @@ fn test_initialize_progress_and_fork_choice_with_duplicates() { // Set up bank0 let bank_forks = BankForks::new_rw_arc(Bank::new_for_tests(&genesis_config)); let bank0 = bank_forks.read().unwrap().get_with_scheduler(0).unwrap(); + let shred_version = compute_shred_version(&genesis_config.hash(), None); let replay_tx_thread_pool = rayon::ThreadPoolBuilder::new() .num_threads(1) .thread_name(|i| format!("solReplayTx{i:02}")) @@ -6086,6 +6174,7 @@ fn test_initialize_progress_and_fork_choice_with_duplicates() { process_bank_0( &bank0, + shred_version, &blockstore, &replay_tx_thread_pool, &ProcessOptions::default(), @@ -6107,6 +6196,7 @@ fn test_initialize_progress_and_fork_choice_with_duplicates() { confirm_full_slot( &blockstore, &bank1, + shred_version, &replay_tx_thread_pool, &ProcessOptions::default(), &mut ConfirmationProgress::new(bank0.last_blockhash()), @@ -6126,6 +6216,7 @@ fn test_initialize_progress_and_fork_choice_with_duplicates() { blockstore_processor::process_blockstore_from_root( &blockstore, &bank_forks, + shred_version, &leader_schedule_cache, &ProcessOptions::default(), None, @@ -6196,7 +6287,7 @@ fn test_skip_leader_slot_for_existing_slot() { let initial_slot = working_bank.slot(); let num_entries = 10; let (shreds, _) = make_slot_entries(dummy_slot, initial_slot, num_entries); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // Reset PoH recorder to the completed bank to ensure consistent state ReplayStage::reset_poh_recorder( @@ -6221,7 +6312,7 @@ fn test_skip_leader_slot_for_existing_slot() { .unwrap(); let poh_recorder = Arc::new(poh_recorder); - let (retransmit_slots_sender, _) = unbounded(); + let (retransmit_slots_sender, _) = bounded(1024); let (banking_tracer, _) = BankingTracer::new(None).unwrap(); // A vote has not technically been rooted, but it doesn't matter for // this test to use true to avoid skipping the leader slot @@ -6314,7 +6405,7 @@ fn test_mark_slots_duplicate_confirmed() { .. } = vote_simulator; - let (ancestor_hashes_replay_update_sender, _) = unbounded(); + let (ancestor_hashes_replay_update_sender, _) = bounded(1024); let mut duplicate_confirmed_slots = DuplicateConfirmedSlots::default(); let bank_hash_0 = bank_forks.read().unwrap().bank_hash(0).unwrap(); bank_forks.write().unwrap().set_root(1, None, None); @@ -6433,8 +6524,8 @@ fn test_process_duplicate_confirmed_slots(same_batch: bool) { .. } = vote_simulator; - let (ancestor_hashes_replay_update_sender, _) = unbounded(); - let (sender, receiver) = unbounded(); + let (ancestor_hashes_replay_update_sender, _) = bounded(1024); + let (sender, receiver) = bounded(1024); let mut duplicate_confirmed_slots = DuplicateConfirmedSlots::default(); let bank_hash_0 = bank_forks.read().unwrap().bank_hash(0).unwrap(); bank_forks.write().unwrap().set_root(1, None, None); diff --git a/core/src/replay_stage/update_parent.rs b/core/src/replay_stage/update_parent.rs index b8158a5f1e2..fcdde3e4800 100644 --- a/core/src/replay_stage/update_parent.rs +++ b/core/src/replay_stage/update_parent.rs @@ -21,7 +21,10 @@ use { blockstore_processor::AsyncVerificationProgress, }, solana_pubkey::Pubkey, - solana_runtime::{bank::Bank, bank_forks::BankForks, vote_sender_types::ReplayVoteSender}, + solana_runtime::{ + bank::Bank, bank_forks::BankForks, leader_schedule_utils::leader_slot_index, + vote_sender_types::ReplayVoteSender, + }, std::{ collections::BTreeSet, sync::{Arc, RwLock}, @@ -42,7 +45,7 @@ pub(super) enum ChildBankReplayStart { /// Return the UpdateParent replay offset recorded in SlotMeta, if it is /// usable from the currently rooted fork. fn update_parent_replay_offset(slot: Slot, slot_meta: &SlotMeta, root: Slot) -> Option { - if !slot_meta.has_update_parent() { + if !slot_meta.has_update_parent() || leader_slot_index(slot) != 0 { return None; } let parent_slot = slot_meta.parent_slot?; @@ -110,7 +113,7 @@ fn try_restart_slot_from_update_parent( migration_status: &MigrationStatus, source: &str, ) -> bool { - if !migration_status.should_allow_fast_leader_handover(slot) { + if !migration_status.should_allow_block_markers(slot) { return false; } if blockstore.is_dead(slot) { @@ -132,10 +135,10 @@ fn try_restart_slot_from_update_parent( if bank.is_none() && !progress.contains_key(&slot) { return false; } - if bank - .as_ref() - .is_some_and(|bank| ReplayStage::leader_is_me(bank.leader_id(), my_pubkey)) - { + if bank.as_ref().is_some_and(|bank| { + ReplayStage::leader_is_me(bank.leader_id(), my_pubkey) + || !bank.feature_set.snapshot().alpenglow_fast_leader_handover + }) { return false; } if progress.get(&slot).is_some_and(|progress| { @@ -297,7 +300,7 @@ pub(super) fn handle_abandoned_bank( bank: &Arc, bank_slot: Slot, update_parent: &VersionedUpdateParent, - bank_forks: &Arc>, + bank_forks: &RwLock, progress: &mut ProgressMap, async_verification_freelist: &mut Vec, duplicate_slots_to_repair: &mut DuplicateSlotsToRepair, @@ -364,7 +367,7 @@ pub(super) fn handle_abandoned_bank( // parent by generate_new_bank_forks on the next iteration. ReplayStage::clear_banks( &BTreeSet::from([bank_slot]), - bank_forks.as_ref(), + bank_forks, progress, async_verification_freelist, ); diff --git a/core/src/result.rs b/core/src/result.rs index 65a5959ff46..f794785d481 100644 --- a/core/src/result.rs +++ b/core/src/result.rs @@ -44,12 +44,12 @@ impl std::convert::From> for Error { mod tests { use { crate::result::{Error, Result}, - crossbeam_channel::{RecvError, RecvTimeoutError, unbounded}, + crossbeam_channel::{RecvError, RecvTimeoutError, bounded}, std::{io, io::Write, panic}, }; fn send_error() -> Result<()> { - let (s, r) = unbounded(); + let (s, r) = bounded(1024); drop(r); s.send(())?; Ok(()) diff --git a/core/src/sample_performance_service.rs b/core/src/sample_performance_service.rs index 27237f6e224..fee61278526 100644 --- a/core/src/sample_performance_service.rs +++ b/core/src/sample_performance_service.rs @@ -20,12 +20,10 @@ pub struct SamplePerformanceService { impl SamplePerformanceService { pub fn new( - bank_forks: &Arc>, + bank_forks: Arc>, blockstore: Arc, exit: Arc, ) -> Self { - let bank_forks = bank_forks.clone(); - let thread_hdl = Builder::new() .name("solSamplePerf".to_string()) .spawn(move || { diff --git a/core/src/shred_fetch_stage.rs b/core/src/shred_fetch_stage.rs index 9b71217bfdf..02cb0029ef6 100644 --- a/core/src/shred_fetch_stage.rs +++ b/core/src/shred_fetch_stage.rs @@ -3,7 +3,6 @@ use { crate::repair::{repair_service::OutstandingShredRepairs, serve_repair::ServeRepair}, solana_gossip::cluster_info::ClusterInfo, - solana_keypair::Keypair, solana_ledger::shred::{ self, filter::{ShredFilterContext, TurbineMode}, @@ -63,9 +62,8 @@ impl ShredFetchStage { let mut shred_filter_ctx = ShredFilterContext::new_with_turbine_mode( sharable_banks.root(), shred_version, - turbine_mode, + Some(turbine_mode), ); - let repair_keypair = repair_context.map(RepairContext::keypair); for mut packet_batch in recvr { shred_filter_ctx.maybe_update(sharable_banks.root()); @@ -73,15 +71,13 @@ impl ShredFetchStage { if let Some(repair_context) = repair_context { debug_assert_eq!(flags, PacketFlags::REPAIR); - debug_assert!(repair_keypair.is_some()); - if let Some(ref keypair) = repair_keypair { - ServeRepair::handle_repair_response_pings( - &repair_context.repair_socket, - keypair, - &mut packet_batch, - &mut shred_filter_ctx.stats, - ); - } + let keypair = repair_context.cluster_info.keypair(); + ServeRepair::handle_repair_response_pings( + &repair_context.repair_socket, + &keypair, + &mut packet_batch, + &mut shred_filter_ctx.stats, + ); // Discard packets if repair nonce does not verify. let now = solana_time_utils::timestamp(); let mut outstanding_repair_requests = @@ -112,10 +108,10 @@ impl ShredFetchStage { packet.meta_mut().flags.insert(flags); } } - if shred_filter_ctx.maybe_submit_stats(name, STATS_SUBMIT_CADENCE) { - if let Some(stats) = recvr_stats.as_ref() { - stats.report(); - } + if shred_filter_ctx.maybe_submit_stats(name, STATS_SUBMIT_CADENCE) + && let Some(stats) = recvr_stats.as_ref() + { + stats.report(); } if let Err(send_err) = sendr.try_send(packet_batch) { match send_err { @@ -251,12 +247,6 @@ impl ShredFetchStage { } } -impl RepairContext { - fn keypair(&self) -> Arc { - self.cluster_info.keypair() - } -} - // Returns false if repair nonce is invalid and packet should be discarded. #[must_use] fn verify_repair_nonce( diff --git a/core/src/sigverify.rs b/core/src/sigverify.rs index 816c5534431..6b3f5ace8aa 100644 --- a/core/src/sigverify.rs +++ b/core/src/sigverify.rs @@ -3,15 +3,19 @@ //! cores. use { - crate::{banking_trace::BankingPacketSender, sigverify_stage::SigVerifyServiceError}, - agave_banking_stage_ingress_types::BankingPacketBatch, + crate::{ + banking_trace::BankingPacketSender, sigverify_stage::SigVerifyServiceError, + transaction_priority::calculate_priority_from_bytes, + }, + agave_banking_stage_ingress_types::{BankingPacketBatch, SchedulerPriorityFloor}, crossbeam_channel::{Receiver, Sender, TrySendError, bounded}, solana_measure::measure_us, solana_perf::{ + deduper::{self, Deduper}, packet::PacketBatch, sigverify::{self}, }, - solana_runtime::bank_forks::SharableBanks, + solana_runtime::{bank::Bank, bank_forks::SharableBanks}, solana_transaction::Transaction, std::{ num::NonZeroUsize, @@ -24,14 +28,6 @@ use { }, }; -pub(crate) struct TransactionSigVerifier { - worker_sender: Sender, -} - -struct TransactionVerifyTask { - batch: PacketBatch, -} - pub(crate) struct GossipVerifyTask { batch: PacketBatch, transaction: Transaction, @@ -44,33 +40,46 @@ pub(crate) struct GossipVerifiedVoteBatch { #[derive(Clone)] pub(crate) struct SigVerifyWorkerStats { + pub(crate) total_batches: Arc, + pub(crate) total_packets: Arc, + pub(crate) total_dedup: Arc, + pub(crate) total_dedup_time_us: Arc, pub(crate) total_valid_packets: Arc, pub(crate) total_verify_time_us: Arc, + /// Max occupancy of the banking_stage channel sampled immediately before each send. + pub(crate) max_pre_send_len: Arc, + /// Count of sends where the EvictingSender had to drop a batch to make room. + pub(crate) eviction_drops: Arc, + pub(crate) total_dropped_below_priority_floor: Arc, + pub(crate) total_priority_floor_time_us: Arc, } -impl TransactionSigVerifier { - fn new(worker_sender: Sender) -> Self { - Self { worker_sender } - } +#[derive(Clone)] +pub(crate) struct SigVerifyWorkerState { + banking_stage_sender: BankingPacketSender, + deduper: Arc>, + stats: SigVerifyWorkerStats, + /// Scheduler-published priority floor: when saturated, the scheduler publishes + /// the queue-min transaction's priority and workers drop at-or-below-floor + /// arrivals here, ahead of signature verification. `None` disables the + /// check (e.g. for the vote worker, which is governed by a separate + /// priority policy in banking stage). + priority_floor: Option>, +} - pub(crate) fn send_packets_to_worker_pool( - &mut self, - batches: Vec, - ) -> Result { - let mut dropped_packets = 0; - for batch in batches { - match self.worker_sender.try_send(TransactionVerifyTask { batch }) { - Ok(()) => {} - Err(TrySendError::Full(task)) => { - dropped_packets += task.batch.len(); - } - Err(TrySendError::Disconnected(_)) => { - return Err(SigVerifyServiceError::WorkerQueueClosed); - } - } +impl SigVerifyWorkerState { + pub(crate) fn new( + banking_stage_sender: BankingPacketSender, + deduper: Arc>, + stats: SigVerifyWorkerStats, + priority_floor: Option>, + ) -> Self { + Self { + banking_stage_sender, + deduper, + stats, + priority_floor, } - - Ok(dropped_packets) } } @@ -118,30 +127,28 @@ impl GossipSigVerifier { } } -// Work queues are kept separate so that a spam on TPU -// will not lead to us dropping votes. -const SIGVERIFY_NON_VOTE_WORK_CHANNEL_SIZE: usize = 50_000; -const SIGVERIFY_TPU_VOTE_WORK_CHANNEL_SIZE: usize = 5_000; // channel is batches not individual packets +/// Gossip votes use a bounded queue into the worker pool. const SIGVERIFY_GOSSIP_VOTE_WORK_CHANNEL_SIZE: usize = 50_000; +pub(crate) struct SigVerifyWorkerSenders { + pub(crate) gossip_verified_vote_sender: Sender, + pub(crate) forward_stage_sender: Sender<(BankingPacketBatch, bool)>, +} + #[derive(Clone)] struct WorkerPoolChannels { - non_vote_receiver: Receiver, - tpu_vote_receiver: Receiver, + non_vote_receiver: Receiver, + tpu_vote_receiver: Receiver, gossip_receiver: Receiver, - non_vote_banking_sender: BankingPacketSender, - tpu_vote_banking_sender: BankingPacketSender, gossip_verified_vote_sender: Sender, forward_stage_sender: Sender<(BankingPacketBatch, bool)>, sharable_banks: SharableBanks, - non_vote_stats: SigVerifyWorkerStats, - tpu_vote_stats: SigVerifyWorkerStats, + non_vote_state: SigVerifyWorkerState, + tpu_vote_state: SigVerifyWorkerState, } pub(crate) struct SigVerifyWorkerPool { exit: Arc, - non_vote_sender: Sender, - tpu_vote_sender: Sender, gossip_sender: Sender, worker_hdls: Vec>, } @@ -160,29 +167,24 @@ impl Drop for SigVerifyWorkerPool { impl SigVerifyWorkerPool { pub(crate) fn new( num_workers: NonZeroUsize, - non_vote_banking_sender: BankingPacketSender, - tpu_vote_banking_sender: BankingPacketSender, - gossip_verified_vote_sender: Sender, - forward_stage_sender: Sender<(BankingPacketBatch, bool)>, + non_vote_receiver: Receiver, + tpu_vote_receiver: Receiver, + senders: SigVerifyWorkerSenders, forward_non_votes: bool, sharable_banks: SharableBanks, - non_vote_stats: SigVerifyWorkerStats, - tpu_vote_stats: SigVerifyWorkerStats, + non_vote_state: SigVerifyWorkerState, + tpu_vote_state: SigVerifyWorkerState, ) -> Self { - let (non_vote_sender, non_vote_receiver) = bounded(SIGVERIFY_NON_VOTE_WORK_CHANNEL_SIZE); - let (tpu_vote_sender, tpu_vote_receiver) = bounded(SIGVERIFY_TPU_VOTE_WORK_CHANNEL_SIZE); let (gossip_sender, gossip_receiver) = bounded(SIGVERIFY_GOSSIP_VOTE_WORK_CHANNEL_SIZE); let channels = WorkerPoolChannels { non_vote_receiver, tpu_vote_receiver, gossip_receiver, - non_vote_banking_sender, - tpu_vote_banking_sender, - gossip_verified_vote_sender, - forward_stage_sender, + gossip_verified_vote_sender: senders.gossip_verified_vote_sender, + forward_stage_sender: senders.forward_stage_sender, sharable_banks, - non_vote_stats, - tpu_vote_stats, + non_vote_state, + tpu_vote_state, }; let exit = Arc::new(AtomicBool::new(false)); let worker_hdls = (0..num_workers.get()) @@ -198,21 +200,11 @@ impl SigVerifyWorkerPool { .collect(); Self { exit, - non_vote_sender, - tpu_vote_sender, gossip_sender, worker_hdls, } } - pub(crate) fn non_vote_verifier(&self) -> TransactionSigVerifier { - TransactionSigVerifier::new(self.non_vote_sender.clone()) - } - - pub(crate) fn tpu_vote_verifier(&self) -> TransactionSigVerifier { - TransactionSigVerifier::new(self.tpu_vote_sender.clone()) - } - pub(crate) fn gossip_verifier(&self) -> GossipSigVerifier { GossipSigVerifier { worker_sender: self.gossip_sender.clone(), @@ -232,30 +224,28 @@ impl SigVerifyWorkerPool { crossbeam_channel::select! { recv(&channels.non_vote_receiver) -> maybe_work => { match maybe_work { - Ok(work) => Self::run_transaction_task( - work, + Ok(batch) => Self::run_transaction_task( + batch, false, - &channels.non_vote_banking_sender, &channels.forward_stage_sender, forward_non_votes, false, &channels.sharable_banks, - &channels.non_vote_stats, + &channels.non_vote_state, ), Err(_) => false, } } recv(&channels.tpu_vote_receiver) -> maybe_work => { match maybe_work { - Ok(work) => Self::run_transaction_task( - work, + Ok(batch) => Self::run_transaction_task( + batch, true, - &channels.tpu_vote_banking_sender, &channels.forward_stage_sender, true, true, &channels.sharable_banks, - &channels.tpu_vote_stats, + &channels.tpu_vote_state, ), Err(_) => false, } @@ -274,33 +264,99 @@ impl SigVerifyWorkerPool { } fn run_transaction_task( - mut work: TransactionVerifyTask, + mut batch: PacketBatch, reject_non_vote: bool, - banking_stage_sender: &BankingPacketSender, forward_stage_sender: &Sender<(BankingPacketBatch, bool)>, should_forward: bool, is_tpu_vote: bool, sharable_banks: &SharableBanks, - stats: &SigVerifyWorkerStats, + state: &SigVerifyWorkerState, ) -> bool { - let enable_tx_v1 = sharable_banks.working().feature_set.snapshot().enable_tx_v1; + state.stats.total_batches.fetch_add(1, Ordering::Relaxed); + state + .stats + .total_packets + .fetch_add(batch.len(), Ordering::Relaxed); + + let (discard_or_dedup_fail, dedup_time_us) = + measure_us!(deduper::dedup_packets_and_count_discards( + &state.deduper, + std::slice::from_mut(&mut batch) + )); + state + .stats + .total_dedup + .fetch_add(discard_or_dedup_fail as usize, Ordering::Relaxed); + state + .stats + .total_dedup_time_us + .fetch_add(dedup_time_us as usize, Ordering::Relaxed); + + let working_bank = sharable_banks.working(); + + if let Some(floor) = state.priority_floor.as_ref() { + let floor = floor.get(); + if floor > 0 { + let ((dropped, all_below), priority_floor_time_us) = measure_us!( + apply_priority_floor_to_batch(&mut batch, floor, &working_bank) + ); + state + .stats + .total_priority_floor_time_us + .fetch_add(priority_floor_time_us as usize, Ordering::Relaxed); + if dropped > 0 { + state + .stats + .total_dropped_below_priority_floor + .fetch_add(dropped, Ordering::Relaxed); + } + if all_below { + // Entire batch went below-floor: nothing left to verify or + // forward. + return true; + } + } + } + + let enable_tx_v1 = working_bank.feature_set.snapshot().enable_tx_v1; let (_, verify_time_us) = measure_us!(sigverify::ed25519_verify_serial( - &mut work.batch, + &mut batch, reject_non_vote, enable_tx_v1, )); - let num_valid_packets = sigverify::count_valid_packets(std::iter::once(&work.batch)); - stats + let num_valid_packets = sigverify::count_valid_packets(std::iter::once(&batch)); + state + .stats .total_valid_packets .fetch_add(num_valid_packets, Ordering::Relaxed); - stats + state + .stats .total_verify_time_us .fetch_add(verify_time_us as usize, Ordering::Relaxed); - let banking_packet_batch = BankingPacketBatch::new(vec![work.batch]); - if let Err(err) = banking_stage_sender.send(banking_packet_batch.clone()) { - error!("sigverify send failed: {err:?}"); - return false; + let banking_packet_batch = BankingPacketBatch::new(vec![batch]); + // Sample backlog before the push: measures consumer health without + // including this batch's own contribution. + state + .stats + .max_pre_send_len + .fetch_max(state.banking_stage_sender.len(), Ordering::Relaxed); + match state + .banking_stage_sender + .send(banking_packet_batch.clone()) + { + Ok(0) => {} // avoid poking atomics if nothing was evicted (typical case) + Ok(evicted) => { + // record evicted amount into metrics + state + .stats + .eviction_drops + .fetch_add(evicted, Ordering::Relaxed); + } + Err(err) => { + error!("sigverify send to banking failed: {err:?}"); + return false; + } } if should_forward { Self::try_forward(forward_stage_sender, banking_packet_batch, is_tpu_vote); @@ -338,3 +394,38 @@ impl SigVerifyWorkerPool { } } } + +/// Apply the scheduler-published priority floor to a single batch in place. +/// +/// Below-floor packets are marked `discard`. Returns `(dropped, all_below)`, +/// where `dropped` is the number of packets newly marked and `all_below` is +/// true iff no useful packets remain in the batch (so the caller can skip +/// downstream work for this batch entirely). +fn apply_priority_floor_to_batch( + batch: &mut PacketBatch, + floor: u64, + bank: &Bank, +) -> (usize, bool) { + let mut dropped: usize = 0; + let mut any_kept = false; + for mut packet in batch.iter_mut() { + if packet.meta().discard() { + continue; + } + let Some(data) = packet.data(..) else { + // Zero-length or otherwise unreadable: leave to downstream + // stages to reject. + any_kept = true; + continue; + }; + // Unparseable packets are kept and left for downstream rejection. + match calculate_priority_from_bytes(bank, data) { + Some(priority) if priority <= floor => { + packet.meta_mut().set_discard(true); + dropped = dropped.saturating_add(1); + } + _ => any_kept = true, + } + } + (dropped, !any_kept) +} diff --git a/core/src/sigverify_stage.rs b/core/src/sigverify_stage.rs index a69eff82013..bdbcd11a949 100644 --- a/core/src/sigverify_stage.rs +++ b/core/src/sigverify_stage.rs @@ -8,26 +8,21 @@ use { crate::{ banking_trace::BankingPacketSender, sigverify::{ - GossipSigVerifier, GossipVerifiedVoteBatch, SigVerifyWorkerPool, SigVerifyWorkerStats, - TransactionSigVerifier, + GossipSigVerifier, GossipVerifiedVoteBatch, SigVerifyWorkerPool, + SigVerifyWorkerSenders, SigVerifyWorkerState, SigVerifyWorkerStats, }, }, - agave_banking_stage_ingress_types::BankingPacketBatch, + agave_banking_stage_ingress_types::{BankingPacketBatch, SchedulerPriorityFloor}, core::time::Duration, - crossbeam_channel::{Receiver, RecvTimeoutError, Sender, unbounded}, - solana_measure::measure::Measure, - solana_perf::{ - deduper::{self, Deduper}, - packet::PacketBatch, - }, + crossbeam_channel::{Receiver, Sender, unbounded}, + solana_perf::{deduper::Deduper, packet::PacketBatch}, solana_runtime::bank_forks::SharableBanks, - solana_streamer::streamer::{self, StreamerError}, solana_transaction::Transaction, std::{ num::NonZeroUsize, sync::{ Arc, - atomic::{AtomicUsize, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, }, thread::{self, Builder, JoinHandle}, time::Instant, @@ -37,17 +32,17 @@ use { #[derive(Error, Debug)] pub enum SigVerifyServiceError { - #[error("streamer error")] - Streamer(#[from] StreamerError), #[error("sigverify worker queue closed")] WorkerQueueClosed, } -type Result = std::result::Result; +const DEDUPER_NUM_BITS: u64 = 63_999_979; +const MAX_DEDUPER_AGE: Duration = Duration::from_secs(2); +const DEDUPER_FALSE_POSITIVE_RATE: f64 = 0.001; pub struct SigVerifyStage { - non_vote_thread_hdl: JoinHandle<()>, - tpu_vote_thread_hdl: JoinHandle<()>, + exit: Arc, + servicer_thread_hdl: Option>, // pool held so workers stay alive. If dropped, workers in pool shut down. _worker_pool: SigVerifyWorkerPool, } @@ -59,18 +54,25 @@ pub struct GossipSigVerifyHandle { #[derive(Default)] struct SigVerifierStats { - recv_batches_us_hist: histogram::Histogram, // time to call recv_batch - dedup_packets_pp_us_hist: histogram::Histogram, // per-packet time to call verify_batch - batches_hist: histogram::Histogram, // number of packet batches per verify call - packets_hist: histogram::Histogram, // number of packets per verify call num_deduper_saturations: usize, - total_batches: usize, - total_packets: usize, - total_dedup: usize, + total_batches: Arc, + total_packets: Arc, + total_dedup: Arc, total_valid_packets: Arc, - total_dedup_time_us: usize, + total_dedup_time_us: Arc, total_verify_time_us: Arc, - total_dropped_on_capacity: usize, + /// Max occupancy of the banking_stage output channel since last report. + max_pre_send_len: Arc, + /// Count of sends in which the EvictingSender had to drop a batch. + eviction_drops: Arc, + total_dropped_below_priority_floor: Arc, + total_priority_floor_time_us: Arc, +} + +struct ServicerState { + deduper: Arc>, + metrics_name: &'static str, + stats: SigVerifierStats, } impl SigVerifierStats { @@ -78,110 +80,69 @@ impl SigVerifierStats { fn maybe_report_and_reset(&mut self, name: &'static str) { // No need to report a datapoint if no batches/packets received - if self.total_batches == 0 { + if self.total_batches.load(Ordering::Relaxed) == 0 { return; } datapoint_info!( name, ( - "recv_batches_us_90pct", - self.recv_batches_us_hist.percentile(90.0).unwrap_or(0), - i64 - ), - ( - "recv_batches_us_min", - self.recv_batches_us_hist.minimum().unwrap_or(0), - i64 - ), - ( - "recv_batches_us_max", - self.recv_batches_us_hist.maximum().unwrap_or(0), - i64 - ), - ( - "recv_batches_us_mean", - self.recv_batches_us_hist.mean().unwrap_or(0), - i64 - ), - ( - "dedup_packets_pp_us_90pct", - self.dedup_packets_pp_us_hist.percentile(90.0).unwrap_or(0), - i64 - ), - ( - "dedup_packets_pp_us_min", - self.dedup_packets_pp_us_hist.minimum().unwrap_or(0), - i64 - ), - ( - "dedup_packets_pp_us_max", - self.dedup_packets_pp_us_hist.maximum().unwrap_or(0), + "num_deduper_saturations", + core::mem::take(&mut self.num_deduper_saturations), i64 ), ( - "dedup_packets_pp_us_mean", - self.dedup_packets_pp_us_hist.mean().unwrap_or(0), + "total_batches", + self.total_batches.swap(0, Ordering::Relaxed), i64 ), ( - "batches_90pct", - self.batches_hist.percentile(90.0).unwrap_or(0), + "total_packets", + self.total_packets.swap(0, Ordering::Relaxed), i64 ), - ("batches_min", self.batches_hist.minimum().unwrap_or(0), i64), - ("batches_max", self.batches_hist.maximum().unwrap_or(0), i64), - ("batches_mean", self.batches_hist.mean().unwrap_or(0), i64), ( - "packets_90pct", - self.packets_hist.percentile(90.0).unwrap_or(0), + "total_dedup", + self.total_dedup.swap(0, Ordering::Relaxed), i64 ), - ("packets_min", self.packets_hist.minimum().unwrap_or(0), i64), - ("packets_max", self.packets_hist.maximum().unwrap_or(0), i64), - ("packets_mean", self.packets_hist.mean().unwrap_or(0), i64), ( - "num_deduper_saturations", - core::mem::take(&mut self.num_deduper_saturations), + "total_dedup_time_us", + self.total_dedup_time_us.swap(0, Ordering::Relaxed), i64 ), ( - "total_batches", - core::mem::take(&mut self.total_batches), + "total_dropped_below_priority_floor", + self.total_dropped_below_priority_floor + .swap(0, Ordering::Relaxed), i64 ), ( - "total_packets", - core::mem::take(&mut self.total_packets), + "total_priority_floor_time_us", + self.total_priority_floor_time_us.swap(0, Ordering::Relaxed), i64 ), - ("total_dedup", core::mem::take(&mut self.total_dedup), i64), ( - "total_dedup_time_us", - core::mem::take(&mut self.total_dedup_time_us), + "total_valid_packets", + self.total_valid_packets.swap(0, Ordering::Relaxed), i64 ), ( - "total_dropped_on_capacity", - core::mem::take(&mut self.total_dropped_on_capacity), + "total_verify_time_us", + self.total_verify_time_us.swap(0, Ordering::Relaxed), i64 ), ( - "total_valid_packets", - self.total_valid_packets.swap(0, Ordering::Relaxed) as i64, + "max_pre_send_len", + self.max_pre_send_len.swap(0, Ordering::Relaxed) as i64, i64 ), ( - "total_verify_time_us", - self.total_verify_time_us.swap(0, Ordering::Relaxed) as i64, + "eviction_drops", + self.eviction_drops.swap(0, Ordering::Relaxed) as i64, i64 ), ); - - self.recv_batches_us_hist = histogram::Histogram::new(); - self.dedup_packets_pp_us_hist = histogram::Histogram::new(); - self.batches_hist = histogram::Histogram::new(); - self.packets_hist = histogram::Histogram::new(); } } @@ -195,41 +156,81 @@ impl SigVerifyStage { num_workers: NonZeroUsize, forward_non_votes: bool, sharable_banks: SharableBanks, + scheduler_priority_floor: Option>, ) -> (Self, GossipSigVerifyHandle) { let (gossip_verified_vote_sender, verified_vote_receiver) = unbounded(); let non_vote_stats = SigVerifierStats::default(); let tpu_vote_stats = SigVerifierStats::default(); + let exit = Arc::new(AtomicBool::new(false)); + let mut rng = rand::rng(); + let non_vote_deduper = Arc::new(Deduper::<2, [u8]>::new(&mut rng, DEDUPER_NUM_BITS)); + let tpu_vote_deduper = Arc::new(Deduper::<2, [u8]>::new(&mut rng, DEDUPER_NUM_BITS)); let worker_pool = SigVerifyWorkerPool::new( num_workers, - non_vote_sender, - tpu_vote_sender, - gossip_verified_vote_sender, - forward_stage_sender, + packet_receiver, + vote_packet_receiver, + SigVerifyWorkerSenders { + gossip_verified_vote_sender, + forward_stage_sender, + }, forward_non_votes, sharable_banks, - SigVerifyWorkerStats { - total_valid_packets: non_vote_stats.total_valid_packets.clone(), - total_verify_time_us: non_vote_stats.total_verify_time_us.clone(), + SigVerifyWorkerState::new( + non_vote_sender, + non_vote_deduper.clone(), + SigVerifyWorkerStats { + total_batches: non_vote_stats.total_batches.clone(), + total_packets: non_vote_stats.total_packets.clone(), + total_dedup: non_vote_stats.total_dedup.clone(), + total_dedup_time_us: non_vote_stats.total_dedup_time_us.clone(), + total_valid_packets: non_vote_stats.total_valid_packets.clone(), + total_verify_time_us: non_vote_stats.total_verify_time_us.clone(), + max_pre_send_len: non_vote_stats.max_pre_send_len.clone(), + eviction_drops: non_vote_stats.eviction_drops.clone(), + total_dropped_below_priority_floor: non_vote_stats + .total_dropped_below_priority_floor + .clone(), + total_priority_floor_time_us: non_vote_stats + .total_priority_floor_time_us + .clone(), + }, + scheduler_priority_floor, + ), + SigVerifyWorkerState::new( + tpu_vote_sender, + tpu_vote_deduper.clone(), + SigVerifyWorkerStats { + total_batches: tpu_vote_stats.total_batches.clone(), + total_packets: tpu_vote_stats.total_packets.clone(), + total_dedup: tpu_vote_stats.total_dedup.clone(), + total_dedup_time_us: tpu_vote_stats.total_dedup_time_us.clone(), + total_valid_packets: tpu_vote_stats.total_valid_packets.clone(), + total_verify_time_us: tpu_vote_stats.total_verify_time_us.clone(), + max_pre_send_len: tpu_vote_stats.max_pre_send_len.clone(), + eviction_drops: tpu_vote_stats.eviction_drops.clone(), + total_dropped_below_priority_floor: tpu_vote_stats + .total_dropped_below_priority_floor + .clone(), + total_priority_floor_time_us: tpu_vote_stats + .total_priority_floor_time_us + .clone(), + }, + None, // votes are not dropped for priority-floor + ), + ); + let servicer_thread_hdl = Self::servicer( + exit.clone(), + ServicerState { + deduper: non_vote_deduper, + metrics_name: "tpu-verifier", + stats: non_vote_stats, }, - SigVerifyWorkerStats { - total_valid_packets: tpu_vote_stats.total_valid_packets.clone(), - total_verify_time_us: tpu_vote_stats.total_verify_time_us.clone(), + ServicerState { + deduper: tpu_vote_deduper, + metrics_name: "tpu-vote-verifier", + stats: tpu_vote_stats, }, ); - let non_vote_thread_hdl = Self::verifier_service( - packet_receiver, - worker_pool.non_vote_verifier(), - "solSigVerTpu", - "tpu-verifier", - non_vote_stats, - ); - let tpu_vote_thread_hdl = Self::verifier_service( - vote_packet_receiver, - worker_pool.tpu_vote_verifier(), - "solSigVerTpuVot", - "tpu-vote-verifier", - tpu_vote_stats, - ); let gossip_sigverify_handle = GossipSigVerifyHandle { verifier: worker_pool.gossip_verifier(), verified_vote_receiver, @@ -237,100 +238,71 @@ impl SigVerifyStage { ( Self { - non_vote_thread_hdl, - tpu_vote_thread_hdl, + exit, + servicer_thread_hdl: Some(servicer_thread_hdl), _worker_pool: worker_pool, }, gossip_sigverify_handle, ) } - pub fn join(self) -> thread::Result<()> { - let non_vote_result = self.non_vote_thread_hdl.join(); - let tpu_vote_result = self.tpu_vote_thread_hdl.join(); - non_vote_result?; - tpu_vote_result?; - Ok(()) + pub fn join(mut self) -> thread::Result<()> { + self.join_servicer_thread() } - fn verifier( - deduper: &Deduper, - recvr: &Receiver, - verifier: &mut TransactionSigVerifier, - stats: &mut SigVerifierStats, - ) -> Result<()> { - const SOFT_RECEIVE_CAP: usize = 5_000; - let (mut batches, num_packets, recv_duration) = - streamer::recv_packet_batches(recvr, SOFT_RECEIVE_CAP)?; - - let batches_len = batches.len(); - stats - .recv_batches_us_hist - .increment(recv_duration.as_micros() as u64) - .unwrap(); - stats.batches_hist.increment(batches_len as u64).unwrap(); - stats.packets_hist.increment(num_packets as u64).unwrap(); - stats.total_batches += batches_len; - stats.total_packets += num_packets; - - let mut dedup_time = Measure::start("sigverify_dedup_time"); - let discard_or_dedup_fail = - deduper::dedup_packets_and_count_discards(deduper, &mut batches) as usize; - dedup_time.stop(); - stats.total_dropped_on_capacity += verifier.send_packets_to_worker_pool(batches)?; - stats - .dedup_packets_pp_us_hist - .increment(dedup_time.as_us() / (num_packets as u64)) - .unwrap(); - stats.total_dedup += discard_or_dedup_fail; - stats.total_dedup_time_us += dedup_time.as_us() as usize; - - Ok(()) + fn join_servicer_thread(&mut self) -> thread::Result<()> { + self.exit.store(true, Ordering::Relaxed); + self.servicer_thread_hdl + .take() + .map(JoinHandle::join) + .unwrap_or(Ok(())) } - fn verifier_service( - packet_receiver: Receiver, - mut verifier: TransactionSigVerifier, - thread_name: &'static str, - metrics_name: &'static str, - mut stats: SigVerifierStats, + /// Drives deduper reset and metrics reporting for sigverify packet streams. + fn servicer( + exit: Arc, + mut non_vote_state: ServicerState, + mut tpu_vote_state: ServicerState, ) -> JoinHandle<()> { let mut last_print = Instant::now(); - const MAX_DEDUPER_AGE: Duration = Duration::from_secs(2); - const DEDUPER_FALSE_POSITIVE_RATE: f64 = 0.001; - const DEDUPER_NUM_BITS: u64 = 63_999_979; Builder::new() - .name(thread_name.to_string()) + .name("solSigVerSvc".to_string()) .spawn(move || { let mut rng = rand::rng(); - let deduper = Deduper::<2, [u8]>::new(&mut rng, DEDUPER_NUM_BITS); - loop { - if deduper.maybe_reset(&mut rng, DEDUPER_FALSE_POSITIVE_RATE, MAX_DEDUPER_AGE) { - stats.num_deduper_saturations += 1; - } - if let Err(e) = - Self::verifier(&deduper, &packet_receiver, &mut verifier, &mut stats) - { - match e { - SigVerifyServiceError::Streamer(StreamerError::RecvTimeout( - RecvTimeoutError::Disconnected, - )) => break, - SigVerifyServiceError::Streamer(StreamerError::RecvTimeout( - RecvTimeoutError::Timeout, - )) => (), - _ => error!("{e:?}"), + while !exit.load(Ordering::Relaxed) { + for state in [&mut non_vote_state, &mut tpu_vote_state] { + if state.deduper.maybe_reset( + &mut rng, + DEDUPER_FALSE_POSITIVE_RATE, + MAX_DEDUPER_AGE, + ) { + state.stats.num_deduper_saturations += 1; } } if last_print.elapsed() > SigVerifierStats::REPORT_INTERVAL { - stats.maybe_report_and_reset(metrics_name); + non_vote_state + .stats + .maybe_report_and_reset(non_vote_state.metrics_name); + tpu_vote_state + .stats + .maybe_report_and_reset(tpu_vote_state.metrics_name); last_print = Instant::now(); } + thread::sleep(Duration::from_millis(10)); } }) .unwrap() } } +impl Drop for SigVerifyStage { + fn drop(&mut self) { + if let Err(err) = self.join_servicer_thread() { + error!("sigverify servicer encountered unexpected error: {err:?}"); + } + } +} + impl GossipSigVerifyHandle { #[cfg(test)] pub(crate) fn new_for_tests( @@ -382,7 +354,7 @@ mod tests { use { super::*, crate::banking_trace::BankingTracer, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_hash::Hash, solana_keypair::Keypair, solana_message::{VersionedMessage, v1}, @@ -438,11 +410,11 @@ mod tests { let (_bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&create_genesis_config(1).genesis_config); let sharable_banks = bank_forks.read().unwrap().sharable_banks(); - let (packet_s, packet_r) = unbounded(); - let (vote_packet_s, vote_packet_r) = unbounded(); + let (packet_s, packet_r) = bounded(1024); + let (vote_packet_s, vote_packet_r) = bounded(1024); let (verified_s, verified_r) = BankingTracer::channel_for_test(); let (tpu_vote_s, _tpu_vote_r) = BankingTracer::channel_for_test(); - let (forward_stage_s, _forward_stage_r) = unbounded(); + let (forward_stage_s, _forward_stage_r) = bounded(1024); let (stage, gossip_sigverify_handle) = SigVerifyStage::new( packet_r, vote_packet_r, @@ -452,6 +424,7 @@ mod tests { NonZeroUsize::new(4).unwrap(), false, sharable_banks, + None, ); let now = Instant::now(); @@ -512,11 +485,11 @@ mod tests { } let (_bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); let sharable_banks = bank_forks.read().unwrap().sharable_banks(); - let (packet_s, packet_r) = unbounded(); - let (vote_packet_s, vote_packet_r) = unbounded(); + let (packet_s, packet_r) = bounded(1024); + let (vote_packet_s, vote_packet_r) = bounded(1024); let (verified_s, verified_r) = BankingTracer::channel_for_test(); let (tpu_vote_s, _tpu_vote_r) = BankingTracer::channel_for_test(); - let (forward_stage_s, _forward_stage_r) = unbounded(); + let (forward_stage_s, _forward_stage_r) = bounded(1024); let (stage, gossip_sigverify_handle) = SigVerifyStage::new( packet_r, vote_packet_r, @@ -526,6 +499,7 @@ mod tests { NonZeroUsize::new(1).unwrap(), false, sharable_banks, + None, ); let mut bytes_batch = BytesPacketBatch::with_capacity(1); @@ -534,8 +508,6 @@ mod tests { wincode::serialize(&test_tx_v1()).unwrap(), )); packet_s.send(PacketBatch::from(bytes_batch)).unwrap(); - drop(packet_s); - drop(vote_packet_s); let verified_batch = verified_r.recv_timeout(Duration::from_secs(30)).unwrap(); assert_eq!(verified_batch.len(), 1); @@ -545,6 +517,8 @@ mod tests { expected_valid ); + drop(packet_s); + drop(vote_packet_s); drop(gossip_sigverify_handle); stage.join().unwrap(); } diff --git a/core/src/snapshot_packager_service.rs b/core/src/snapshot_packager_service.rs index cff47b2f30b..063bbf34522 100644 --- a/core/src/snapshot_packager_service.rs +++ b/core/src/snapshot_packager_service.rs @@ -286,6 +286,11 @@ impl SnapshotPackagerService { // the "storages flushed" file, so return early. return; } + // Pin the storage file so it outlives the validator-exit Drop chain (which would + // otherwise remove it via AppendVec::drop) and is available for fastboot on restart. + // The next startup opens a fresh AppendVec over the file; that one defaults back to + // removing-on-drop, so normal runtime cleanup (shrink, clean) still applies later. + storage.disable_remove_on_drop(); } info!("Flushing account storages... Done in {:?}", start.elapsed()); @@ -294,21 +299,21 @@ impl SnapshotPackagerService { snapshot_slot, ); - info!("Hard linking account storages..."); + info!("Writing account storages list..."); let start = Instant::now(); - let result = snapshot_utils::hard_link_storages_to_snapshot( + let result = snapshot_utils::write_storages_list_to_snapshot( &bank_snapshot_dir, - snapshot_slot, &snapshot_storages, + &io_setup, ); if let Err(err) = result { - warn!("Failed to hard link account storages: {err}"); - // If hard linking the storages failed, we do *NOT* want to mark the bank snapshot as + warn!("Failed to write account storages list: {err}"); + // If writing the storages list failed, we do *NOT* want to mark the bank snapshot as // loadable so return early. return; } info!( - "Hard linking account storages... Done in {:?}", + "Writing account storages list... Done in {:?}", start.elapsed(), ); diff --git a/core/src/system_monitor_service.rs b/core/src/system_monitor_service.rs index c1a3c6dede2..0eced0de088 100644 --- a/core/src/system_monitor_service.rs +++ b/core/src/system_monitor_service.rs @@ -5,7 +5,13 @@ use core::arch::x86_64::{__cpuid, __cpuid_count, __get_cpuid_max, CpuidResult}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] use num_enum::{IntoPrimitive, TryFromPrimitive}; #[cfg(target_os = "linux")] -use std::{fs::File, io::BufReader}; +use { + agave_xdp::device::NetworkDevice, + std::{ + fs::{self, File}, + io::{self, BufReader, ErrorKind}, + }, +}; use { solana_time_utils::AtomicInterval, std::{ @@ -26,6 +32,7 @@ const MS_PER_M: u64 = MS_PER_S * 60; const MS_PER_H: u64 = MS_PER_M * 60; const SAMPLE_INTERVAL_UDP_MS: u64 = 2 * MS_PER_S; const SAMPLE_INTERVAL_OS_NETWORK_LIMITS_MS: u64 = MS_PER_H; +const SAMPLE_INTERVAL_XDP_NETWORK_CONFIG_MS: u64 = MS_PER_H; const SAMPLE_INTERVAL_MEM_MS: u64 = 5 * MS_PER_S; const SAMPLE_INTERVAL_CPU_MS: u64 = 10 * MS_PER_S; const SAMPLE_INTERVAL_CPU_ID_MS: u64 = MS_PER_H; @@ -38,6 +45,22 @@ const PROC_NET_SNMP_PATH: &str = "/proc/net/snmp"; const PROC_NET_DEV_PATH: &str = "/proc/net/dev"; #[cfg(target_os = "linux")] const SYS_BLOCK_PATH: &str = "/sys/block"; +#[cfg(target_os = "linux")] +const PCI_IDS_PATHS: &[&str] = &["/usr/share/hwdata/pci.ids", "/usr/share/misc/pci.ids"]; + +#[derive(Clone, Debug)] +pub struct XdpNetworkConfigReport { + pub zero_copy: bool, + pub interface: String, +} + +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +struct XdpNetworkConfigMetrics { + kernel_version: String, + driver: String, + vendor: String, + model: String, +} pub struct SystemMonitorService { thread_hdl: JoinHandle<()>, @@ -297,6 +320,157 @@ fn parse_net_dev_stats(reader_dev: &mut impl BufRead) -> Result Result<(String, String), String> { + let class = match read_network_device_sysfs_value(interface, "class") { + Ok(class) => class, + Err(err) => { + return Err(err.to_string()); + } + }; + if !normalize_pci_id(&class).is_some_and(|class| class.starts_with("02")) { + return Err(format!( + "xdp network config device for interface {interface} has non-network PCI class {class}" + )); + } + + let (Some(vendor_id), Some(model_id)) = + (normalize_pci_id(vendor_id), normalize_pci_id(model_id)) + else { + return Err(format!( + "failed to normalize xdp network config PCI ids for interface {interface}: \ + vendor_id={vendor_id} model_id={model_id}" + )); + }; + + match read_pci_database_device_names(&vendor_id, &model_id) { + Ok(Some((vendor, model))) => Ok((vendor, model)), + Ok(None) => Err(format!( + "failed to find xdp network config PCI names for vendor_id={vendor_id} \ + model_id={model_id}" + )), + Err(err) => Err(format!( + "failed to read PCI database for xdp network config: {err}" + )), + } +} + +#[cfg(target_os = "linux")] +fn read_network_device_sysfs_value(interface: &str, file_name: &str) -> io::Result { + let path = format!("/sys/class/net/{interface}/device/{file_name}"); + let value = fs::read_to_string(&path) + .map_err(|e| { + io::Error::new( + e.kind(), + format!("Failed to read {file_name} for interface {interface}: {e}"), + ) + })? + .trim() + .to_string(); + + if value.is_empty() { + return Err(io::Error::new( + ErrorKind::InvalidData, + format!("Empty {file_name} for interface {interface}"), + )); + } + + Ok(value) +} + +#[cfg(target_os = "linux")] +fn normalize_pci_id(value: &str) -> Option { + let value = value + .strip_prefix("0x") + .or_else(|| value.strip_prefix("0X")) + .unwrap_or(value); + + (!value.is_empty() && value.chars().all(|char| char.is_ascii_hexdigit())) + .then(|| value.to_ascii_lowercase()) +} + +#[cfg(target_os = "linux")] +fn read_pci_database_device_names( + vendor_id: &str, + model_id: &str, +) -> io::Result> { + for path in PCI_IDS_PATHS { + let contents = match fs::read_to_string(path) { + Ok(contents) => contents, + Err(err) if err.kind() == ErrorKind::NotFound => continue, + Err(err) => { + return Err(io::Error::new( + err.kind(), + format!("Failed to read {path}: {err}"), + )); + } + }; + return Ok(parse_pci_database_device_names( + &contents, vendor_id, model_id, + )); + } + + Err(io::Error::new( + ErrorKind::NotFound, + format!("PCI database not found in {}", PCI_IDS_PATHS.join(", ")), + )) +} + +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +fn parse_pci_database_device_names( + contents: &str, + vendor_id: &str, + model_id: &str, +) -> Option<(String, String)> { + let mut matching_vendor_name: Option = None; + + for line in contents.lines() { + if line.is_empty() || line.starts_with('#') || line.starts_with("\t\t") { + continue; + } + // PCI class-code section starts, done with the vendor/device section. + if line.starts_with("C ") { + break; + } + + if let Some(device_line) = line.strip_prefix('\t') { + let Some(vendor_name) = matching_vendor_name.as_ref() else { + continue; + }; + + let Some((device_line_id, device_name)) = parse_pci_ids_line(device_line) else { + continue; + }; + if device_line_id.eq_ignore_ascii_case(model_id) { + return Some((vendor_name.clone(), device_name.to_string())); + } + continue; + } + + let Some((vendor_line_id, vendor_name)) = parse_pci_ids_line(line) else { + continue; + }; + matching_vendor_name = vendor_line_id + .eq_ignore_ascii_case(vendor_id) + .then(|| vendor_name.to_string()); + } + + None +} + +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +fn parse_pci_ids_line(line: &str) -> Option<(&str, &str)> { + let line = line.trim(); + let id_end = line.find(char::is_whitespace)?; + let id = &line[..id_end]; + let name = line[id_end..].trim(); + (!id.is_empty() && !name.is_empty()).then_some((id, name)) +} + #[cfg(target_os = "linux")] pub fn verify_net_stats_access() -> Result<(), String> { read_net_stats()?; @@ -386,6 +560,7 @@ fn parse_disk_stats(reader_diskstats: &mut impl BufRead) -> Result, pub report_os_cpu_stats: bool, pub report_os_disk_stats: bool, } @@ -989,11 +1164,13 @@ impl SystemMonitorService { let mut udp_stats = None; let mut disk_stats = None; let network_limits_timer = AtomicInterval::default(); + let xdp_network_config_timer = AtomicInterval::default(); let udp_timer = AtomicInterval::default(); let mem_timer = AtomicInterval::default(); let cpu_timer = AtomicInterval::default(); let cpuid_timer = AtomicInterval::default(); let disk_timer = AtomicInterval::default(); + let mut xdp_network_config_metrics = None; loop { if exit.load(Ordering::Relaxed) { @@ -1007,6 +1184,15 @@ impl SystemMonitorService { Self::process_net_stats(&mut udp_stats); } } + if let Some(xdp_network_config_report) = &config.xdp_network_config_report + && xdp_network_config_timer + .should_update_ext(SAMPLE_INTERVAL_XDP_NETWORK_CONFIG_MS, false) + { + let metrics = xdp_network_config_metrics.get_or_insert_with(|| { + Self::load_xdp_network_config_metrics(xdp_network_config_report) + }); + Self::report_xdp_network_config(xdp_network_config_report, metrics); + } if config.report_os_memory_stats && mem_timer.should_update(SAMPLE_INTERVAL_MEM_MS) { Self::report_mem_stats(); #[cfg(not(any(target_env = "msvc", target_os = "freebsd")))] @@ -1028,6 +1214,114 @@ impl SystemMonitorService { } } + #[cfg(not(target_os = "linux"))] + fn report_xdp_network_config( + _config: &XdpNetworkConfigReport, + _metrics: &XdpNetworkConfigMetrics, + ) { + } + + #[cfg(target_os = "linux")] + fn load_xdp_network_config_metrics(config: &XdpNetworkConfigReport) -> XdpNetworkConfigMetrics { + let Ok(device) = NetworkDevice::new(&config.interface) else { + warn!( + "failed to get xdp network config device for interface {}", + config.interface + ); + return XdpNetworkConfigMetrics { + kernel_version: Self::kernel_version(), + driver: "unknown".to_string(), + vendor: "unknown".to_string(), + model: "unknown".to_string(), + }; + }; + let driver = device.driver().unwrap_or_else(|err| { + warn!( + "failed to get xdp network config driver for interface {}: {err}", + config.interface + ); + "unknown".to_string() + }); + let vendor_id = read_network_device_sysfs_value(&config.interface, "vendor") + .unwrap_or_else(|err| { + warn!( + "failed to get xdp network config vendor id for interface {}: {err}", + config.interface + ); + "unknown".to_string() + }); + let model_id = + read_network_device_sysfs_value(&config.interface, "device").unwrap_or_else(|err| { + warn!( + "failed to get xdp network config model id for interface {}: {err}", + config.interface + ); + "unknown".to_string() + }); + let (vendor, model) = + match try_resolve_network_device_pci_names(&config.interface, &vendor_id, &model_id) { + Ok((vendor, model)) => (vendor, model), + Err(err) => { + warn!( + "failed to resolve xdp network config PCI names for interface {}: {}", + config.interface, err + ); + // Fall back to reporting raw PCI IDs if name resolution fails + let fallback_vendor = + normalize_pci_id(&vendor_id).unwrap_or_else(|| vendor_id.clone()); + let fallback_model = + normalize_pci_id(&model_id).unwrap_or_else(|| model_id.clone()); + (fallback_vendor, fallback_model) + } + }; + + XdpNetworkConfigMetrics { + kernel_version: Self::kernel_version(), + driver, + vendor, + model, + } + } + + #[cfg(not(target_os = "linux"))] + fn load_xdp_network_config_metrics( + _config: &XdpNetworkConfigReport, + ) -> XdpNetworkConfigMetrics { + XdpNetworkConfigMetrics { + kernel_version: "unknown".to_string(), + driver: "unknown".to_string(), + vendor: "unknown".to_string(), + model: "unknown".to_string(), + } + } + + #[cfg(target_os = "linux")] + fn report_xdp_network_config( + config: &XdpNetworkConfigReport, + metrics: &XdpNetworkConfigMetrics, + ) { + solana_metrics::datapoint_info!( + "xdp-network-config", + "driver" => metrics.driver.clone(), + "zero_copy" => config.zero_copy.to_string(), + ("kernel_version", metrics.kernel_version.clone(), String), + ("vendor", metrics.vendor.clone(), String), + ("model", metrics.model.clone(), String), + ); + } + + #[cfg(target_os = "linux")] + fn kernel_version() -> String { + let mut utsname = unsafe { std::mem::zeroed::() }; + if unsafe { libc::uname(&mut utsname) } != 0 { + return format!("unknown: {}", std::io::Error::last_os_error()); + } + + unsafe { std::ffi::CStr::from_ptr(utsname.release.as_ptr()) } + .to_string_lossy() + .into_owned() + } + pub fn join(self) -> thread::Result<()> { self.thread_hdl.join() } @@ -1037,6 +1331,43 @@ impl SystemMonitorService { mod tests { use super::*; + #[test] + fn test_parse_pci_database_device_names() { + let pci_ids = "\ +8086 Intel Corporation +\t1593 Ethernet Controller E810-C for QSFP +\t\t8086 0001 Ethernet Network Adapter E810-C-Q1 +10ec Realtek Semiconductor Co., Ltd. +\t8136 RTL810xE PCI Express Fast Ethernet controller +C 02 Network controller +\t00 Ethernet controller +"; + + assert_eq!( + parse_pci_database_device_names(pci_ids, "8086", "1593"), + Some(( + "Intel Corporation".to_string(), + "Ethernet Controller E810-C for QSFP".to_string() + )) + ); + assert_eq!( + parse_pci_database_device_names(pci_ids, "10EC", "8136"), + Some(( + "Realtek Semiconductor Co., Ltd.".to_string(), + "RTL810xE PCI Express Fast Ethernet controller".to_string() + )) + ); + assert_eq!( + parse_pci_database_device_names(pci_ids, "8086", "0001"), + None + ); + assert_eq!( + parse_pci_database_device_names(pci_ids, "0000", "1593"), + None + ); + assert_eq!(parse_pci_database_device_names(pci_ids, "C", "02"), None); + } + #[test] fn test_parse_udp_stats() { const MOCK_SNMP: &[u8] = diff --git a/core/src/tpu.rs b/core/src/tpu.rs index 143e8dadfa6..546d0146fdb 100644 --- a/core/src/tpu.rs +++ b/core/src/tpu.rs @@ -11,7 +11,7 @@ use { banking_trace::{Channels, TracerThread}, cluster_info_vote_listener::{ ClusterInfoVoteListener, DuplicateConfirmedSlotsSender, GossipVerifiedVoteHashSender, - VerifiedVoterSlotsSender, VoteTracker, + VoteTracker, }, fetch_stage::FetchStage, forwarding_stage::{ @@ -23,7 +23,9 @@ use { tpu_entry_notifier::TpuEntryNotifier, validator::{BlockProductionMethod, GeneratorConfig}, }, + agave_banking_stage_ingress_types::SchedulerPriorityFloor, agave_votor::event::VotorEventSender, + agave_votor_messages::VerifiedVoterSlotsSender, agave_xdp::transmitter::XdpSender, crossbeam_channel::{Receiver, bounded, unbounded}, solana_clock::Slot, @@ -48,6 +50,7 @@ use { vote_sender_types::{ReplayVoteReceiver, ReplayVoteSender}, }, solana_streamer::{ + evicting_sender::EvictingSender, quic::{ SimpleQosQuicStreamerConfig, SpawnServerResult, SwQosQuicStreamerConfig, spawn_simple_qos_server, spawn_stake_weighted_qos_server, @@ -88,6 +91,15 @@ pub const MAX_VOTES_PER_SECOND: u64 = 20; /// be conservative max of obsersed on mnb during high-load events. const TPU_CHANNEL_SIZE: usize = 50_000; +/// Size of the channel between the vote streamer and the TPU sigverify stage. +/// Chosen based on nominal voting load for a cluster with ~2000 validators + some margin. +pub(crate) const TPU_VOTE_CHANNEL_SIZE: usize = 4_000; + +/// Size of the channel between the TPU forwards streamer and the fetch stage. +/// Mirrors `TPU_CHANNEL_SIZE`; the streamer uses `try_send`, so an over-full +/// channel drops packets (tracked via streamer metrics) rather than blocking. +const TPU_FORWARD_CHANNEL_SIZE: usize = 50_000; + pub struct Tpu { fetch_stage: FetchStage, cluster_info_vote_listener: ClusterInfoVoteListener, @@ -164,13 +176,16 @@ impl Tpu { } = sockets; let (packet_sender, packet_receiver) = bounded(TPU_CHANNEL_SIZE); - let (vote_packet_sender, vote_packet_receiver) = unbounded(); - let (forwarded_packet_sender, forwarded_packet_receiver) = unbounded(); + let (vote_packet_sender, vote_packet_receiver) = bounded(TPU_VOTE_CHANNEL_SIZE); + let evicting_vote_sender = + EvictingSender::new(vote_packet_sender.clone(), vote_packet_receiver.clone()); + let (forwarded_packet_sender, forwarded_packet_receiver) = + bounded(TPU_FORWARD_CHANNEL_SIZE); let fetch_stage = FetchStage::new_with_sender( tpu_vote_sockets, exit.clone(), &packet_sender, - &vote_packet_sender, + &evicting_vote_sender, forwarded_packet_receiver, poh_recorder, None, // coalesce @@ -261,6 +276,11 @@ impl Tpu { let (forward_stage_sender, forward_stage_receiver) = bounded(50_000); + // Shared between sigverify and scheduler. The scheduler publishes + // a priority floor under saturation; sigverify reads it and drops + // below-floor packets ahead of signature verification. + let scheduler_priority_floor = Arc::new(SchedulerPriorityFloor::new()); + let (sigverify_stage, gossip_sigverify_handle) = SigVerifyStage::new( packet_receiver, vote_packet_receiver, @@ -270,6 +290,7 @@ impl Tpu { tpu_sigverify_threads, enable_block_production_forwarding, bank_forks.read().unwrap().sharable_banks(), + Some(scheduler_priority_floor.clone()), ); let cluster_info_vote_listener = ClusterInfoVoteListener::new( @@ -304,6 +325,7 @@ impl Tpu { bank_forks.clone(), prioritization_fee_cache, filter_keys, + scheduler_priority_floor, ); #[cfg(unix)] @@ -394,13 +416,13 @@ impl Tpu { tpu_entry_notifier.join()?; } let _ = broadcast_result?; - if let Some(tracer_thread_hdl) = self.tracer_thread_hdl { - if let Err(tracer_result) = tracer_thread_hdl.join()? { - error!( - "banking tracer thread returned error after successful thread join: \ - {tracer_result:?}" - ); - } + if let Some(tracer_thread_hdl) = self.tracer_thread_hdl + && let Err(tracer_result) = tracer_thread_hdl.join()? + { + error!( + "banking tracer thread returned error after successful thread join: \ + {tracer_result:?}" + ); } Ok(()) } diff --git a/core/src/tpu_entry_notifier.rs b/core/src/tpu_entry_notifier.rs index 6e37c6e716f..d95f799d025 100644 --- a/core/src/tpu_entry_notifier.rs +++ b/core/src/tpu_entry_notifier.rs @@ -64,15 +64,12 @@ impl TpuEntryNotifier { let (bank, (entry_or_marker, tick_height)) = entry_receiver.recv_timeout(Duration::from_secs(1))?; let slot = bank.slot(); - let index = if slot != *current_slot { + if slot != *current_slot { *current_index = 0; *current_transaction_index = 0; *current_slot = slot; - 0 - } else { - *current_index += 1; - *current_index }; + let index = *current_index; if let EntryOrMarker::Entry(ref entry) = entry_or_marker { let entry_summary = EntrySummary { @@ -91,11 +88,11 @@ impl TpuEntryNotifier { EntryNotifierService, error {err:?}", ); } + *current_index += 1; *current_transaction_index += entry.transactions.len(); }; if let Err(err) = broadcast_entry_sender.send((bank, (entry_or_marker, tick_height))) { - let index = *current_index; warn!( "Failed to send slot {slot:?} entry/marker {index:?} from Tpu to BroadcastStage, \ error {err:?}", diff --git a/core/src/transaction_priority.rs b/core/src/transaction_priority.rs index e3056c184d0..0dd0d14465d 100644 --- a/core/src/transaction_priority.rs +++ b/core/src/transaction_priority.rs @@ -1,9 +1,15 @@ use { + agave_transaction_view::transaction_view::SanitizedTransactionView, solana_cost_model::cost_model::CostModel, - solana_runtime::bank::Bank, + solana_fee::FeeFeatures, + solana_runtime::bank::{Bank, CollectorFeeDetails}, solana_runtime_transaction::{ - transaction_meta::TransactionConfiguration, transaction_with_meta::TransactionWithMeta, + runtime_transaction::RuntimeTransaction, + sanitize_config::sanitize_config, + transaction_meta::{TransactionConfiguration, TransactionMeta}, }, + solana_svm_transaction::svm_message::SVMStaticMessage, + solana_transaction::sanitized::MessageHash, }; /// Calculate priority and cost for a transaction: @@ -24,13 +30,27 @@ use { /// from user input. They should never be zero. /// Any difference in the prioritization is negligible for /// the current transaction costs. -pub(crate) fn calculate_priority_and_cost( +pub(crate) fn calculate_priority_and_cost( bank: &Bank, - transaction: &impl TransactionWithMeta, + transaction: &Tx, transaction_configuration: &TransactionConfiguration, ) -> (u64, u64) { - let cost = CostModel::calculate_cost(transaction, &bank.feature_set).sum(); - let reward = bank.calculate_reward_for_transaction(transaction, transaction_configuration); + let cost = CostModel::calculate_cost_for_executed_transaction( + transaction, + u64::from(transaction_configuration.compute_unit_limit), + transaction_configuration.loaded_accounts_data_size_limit, + &bank.feature_set, + ) + .sum(); + let fee_details = solana_fee::calculate_fee_details( + transaction, + bank.fee_structure().lamports_per_signature, + transaction_configuration.priority_fee_lamports, + FeeFeatures::from(bank.feature_set.as_ref()), + ); + let reward = bank + .calculate_reward_and_burn_fee_details(&CollectorFeeDetails::from(fee_details)) + .get_deposit(); // We need a multiplier here to avoid rounding down too aggressively. // For many transactions, the cost will be greater than the fees in terms of raw lamports. @@ -45,3 +65,134 @@ pub(crate) fn calculate_priority_and_cost( cost, ) } + +/// Evaluate raw packet bytes against the pf-floor, returning the computed +/// priority. +/// +/// Returns `None` if the bytes don't parse as a valid transaction, in which +/// case the caller should leave the packet to downstream stages to reject. +pub(crate) fn calculate_priority_from_bytes(bank: &Bank, data: &[u8]) -> Option { + let config = sanitize_config(bank.feature_set.snapshot().limit_instruction_accounts); + let view = SanitizedTransactionView::try_new_sanitized(data, &config).ok()?; + let runtime_tx = RuntimeTransaction::>::try_new( + view, + MessageHash::Compute, + None, + ) + .ok()?; + let transaction_configuration = runtime_tx + .transaction_configuration(&bank.feature_set) + .ok()?; + let (priority, _cost) = + calculate_priority_and_cost(bank, &runtime_tx, &transaction_configuration); + + Some(priority) +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_compute_budget_interface::ComputeBudgetInstruction, + solana_hash::Hash, + solana_keypair::Keypair, + solana_ledger::genesis_utils::{GenesisConfigInfo, create_genesis_config}, + solana_message::Message, + solana_pubkey::Pubkey, + solana_signer::Signer, + solana_system_interface::instruction as system_instruction, + solana_transaction::{Transaction, versioned::VersionedTransaction}, + std::sync::Arc, + }; + + fn test_bank_with_lamports_per_signature(lamports_per_signature: u64) -> (Arc, Keypair) { + let GenesisConfigInfo { + mut genesis_config, + mint_keypair, + .. + } = create_genesis_config(u64::MAX); + if lamports_per_signature > 0 { + genesis_config.fee_rate_governor = + solana_fee_calculator::FeeRateGovernor::new(lamports_per_signature, 0); + } + let (bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + (bank, mint_keypair) + } + + fn test_bank() -> (Arc, Keypair) { + test_bank_with_lamports_per_signature(0) + } + + fn make_tx_bytes(mint: &Keypair, recent_blockhash: Hash, compute_unit_price: u64) -> Vec { + let to = Pubkey::new_unique(); + let transfer = system_instruction::transfer(&mint.pubkey(), &to, 1); + let prioritization = ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price); + let message = Message::new(&[transfer, prioritization], Some(&mint.pubkey())); + let tx = Transaction::new(&[mint], message, recent_blockhash); + bincode::serialize(&VersionedTransaction::from(tx)).unwrap() + } + + fn priority_from(bank: &Bank, bytes: &[u8]) -> u64 { + calculate_priority_from_bytes(bank, bytes).unwrap() + } + + #[test] + fn priority_from_bytes_returns_none_for_garbage() { + let (bank, _) = test_bank(); + assert!(calculate_priority_from_bytes(&bank, &[]).is_none()); + assert!(calculate_priority_from_bytes(&bank, &[0u8; 32]).is_none()); + } + + #[test] + fn priority_is_zero_when_base_and_priority_fees_are_zero() { + // Test bank has lamports_per_signature = 0, so base fee is 0. + // With compute_unit_price = 0, priority fee is also 0 → reward 0 → priority 0. + let (bank, mint) = test_bank(); + assert_eq!(bank.fee_structure().lamports_per_signature, 0); + let bytes = make_tx_bytes(&mint, bank.last_blockhash(), 0); + assert_eq!(priority_from(&bank, &bytes), 0); + } + + #[test] + fn higher_compute_unit_price_yields_higher_priority() { + // Need non-zero base fee, otherwise the reward short-circuits to 0 + // and all priorities collapse regardless of compute_unit_price. + let (bank, mint) = test_bank_with_lamports_per_signature(5_000); + let low = priority_from(&bank, &make_tx_bytes(&mint, bank.last_blockhash(), 1)); + let high = priority_from( + &bank, + &make_tx_bytes(&mint, bank.last_blockhash(), 1_000_000), + ); + assert!(high > low, "expected high {high} > low {low}"); + } + + #[test] + fn floor_priority_from_bytes_matches_typed_path() { + // The bytes-path and the typed-path must agree on the same packet, + // since the scheduler-side queue priority is computed via the typed + // path and the sigverify-side floor check via the bytes path. + let (bank, mint) = test_bank(); + let bytes = make_tx_bytes(&mint, bank.last_blockhash(), 100); + + let from_bytes = priority_from(&bank, &bytes); + + let view = SanitizedTransactionView::try_new_sanitized( + &bytes[..], + &sanitize_config(bank.feature_set.snapshot().limit_instruction_accounts), + ) + .unwrap(); + let runtime_tx = RuntimeTransaction::>::try_new( + view, + MessageHash::Compute, + None, + ) + .unwrap(); + let transaction_configuration = runtime_tx + .transaction_configuration(&bank.feature_set) + .unwrap(); + let (from_typed, _cost) = + calculate_priority_and_cost(&bank, &runtime_tx, &transaction_configuration); + + assert_eq!(from_bytes, from_typed); + } +} diff --git a/core/src/tvu.rs b/core/src/tvu.rs index 1dcc8fb67de..d506785e94b 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -6,10 +6,8 @@ use { admin_rpc_post_init::{KeyUpdaterType, KeyUpdaters}, banking_trace::BankingTracer, block_creation_loop::ReplayHighestFrozen, - bls_sigverify::bls_sigverifier::{self, SigVerifierChannels, SigVerifierContext}, cluster_info_vote_listener::{ - DuplicateConfirmedSlotsReceiver, GossipVerifiedVoteHashReceiver, - VerifiedVoterSlotsReceiver, VerifiedVoterSlotsSender, VoteTracker, + DuplicateConfirmedSlotsReceiver, GossipVerifiedVoteHashReceiver, VoteTracker, }, cluster_slots_service::{ClusterSlotsService, cluster_slots::ClusterSlots}, commitment_service::AggregateCommitmentService, @@ -28,16 +26,23 @@ use { warm_quic_cache_service::WarmQuicCacheService, window_service::{WindowService, WindowServiceChannels}, }, - agave_votor::{ - consensus_metrics::MAX_IN_FLIGHT_CONSENSUS_EVENTS, - event::{LeaderWindowInfo, VotorEventReceiver, VotorEventSender}, + agave_bls_sigverify::{ + bls_sigverifier::{self, SigVerifierChannels, SigVerifierContext}, generated_cert_types::GeneratedCertTypes, + }, + agave_votor::{ + event::{LatestSwitchRequest, LeaderWindowInfo, VotorEventReceiver, VotorEventSender}, vote_history::VoteHistory, vote_history_storage::VoteHistoryStorage, - voting_service::{VotingService as BLSVotingService, VotingServiceOverride}, + voting_service::{ + VOTOR_RATE_LIMIT_PPS, VotingService as BLSVotingService, VotingServiceOverride, + }, votor::{Votor, VotorConfig}, }, - agave_votor_messages::reward_certificate::{BuildRewardCertsRequest, BuildRewardCertsResponse}, + agave_votor_messages::{ + VerifiedVoterSlotsReceiver, VerifiedVoterSlotsSender, consensus_message::Block, + metric_types::MAX_IN_FLIGHT_CONSENSUS_EVENTS, reward_certificate::AddVoteMessage, + }, crossbeam_channel::{Receiver, Sender, bounded, unbounded}, solana_client::connection_cache::ConnectionCache, solana_clock::Slot, @@ -46,7 +51,6 @@ use { cluster_info::ClusterInfo, duplicate_shred_handler::DuplicateShredHandler, duplicate_shred_listener::DuplicateShredListener, }, - solana_hash::Hash, solana_keypair::Keypair, solana_ledger::{ blockstore::{Blockstore, MAX_COMPLETED_SLOTS_IN_CHANNEL, UpdateParentReceiver}, @@ -56,6 +60,7 @@ use { leader_schedule_cache::LeaderScheduleCache, shred::filter::TurbineMode, }, + solana_net_utils::PinnedXdpSender, solana_poh::{poh_controller::PohController, poh_recorder::PohRecorder}, solana_pubkey::Pubkey, solana_rpc::{ @@ -78,7 +83,7 @@ use { quic::{QuicStreamerConfig, SpawnServerResult, spawn_simple_qos_server}, streamer::StakedNodes, }, - solana_turbine::{XdpSender, retransmit_stage::RetransmitStage}, + solana_turbine::{XdpSender as TurbineXdpSender, retransmit_stage::RetransmitStage}, std::{ collections::HashSet, net::UdpSocket, @@ -98,7 +103,7 @@ use { const CHANNEL_SIZE_RETRANSMIT_INGRESS: usize = 16 * 1024; /// The maximum number of alpenglow packets that can be processed in a single batch -const MAX_ALPENGLOW_PACKET_NUM: usize = 10_000; +pub(crate) const MAX_ALPENGLOW_PACKET_NUM: usize = 10_000; /// The maximum number of distinct bls messages that can be sent in a single batch. /// This is overprovisioned to account for standstill scenarios, where a large amount /// of votes / certificate need to be refreshed. @@ -118,7 +123,7 @@ pub struct Tvu { warm_quic_cache_service: Option, drop_bank_service: DropBankService, duplicate_shred_listener: DuplicateShredListener, - bls_sigverify_threads: Option<(JoinHandle<()>, JoinHandle<()>)>, + bls_sigverify_threads: (JoinHandle<()>, JoinHandle<()>), votor: Votor, commitment_service: AggregateCommitmentService, } @@ -128,7 +133,7 @@ pub struct TvuSockets { pub repair: UdpSocket, pub retransmit: Vec, pub ancestor_hashes_requests: UdpSocket, - pub alpenglow: Option, + pub alpenglow: UdpSocket, pub block_id_repair: UdpSocket, } @@ -144,7 +149,8 @@ pub struct TvuConfig { pub replay_transactions_threads: NonZeroUsize, pub shred_sigverify_threads: NonZeroUsize, pub bls_sigverify_threads: NonZeroUsize, - pub turbine_xdp_sender: Option, + pub turbine_xdp_sender: Option, + pub repair_xdp_sender: Option, } impl Default for TvuConfig { @@ -160,6 +166,7 @@ impl Default for TvuConfig { shred_sigverify_threads: NonZeroUsize::new(1).expect("1 is non-zero"), bls_sigverify_threads: NonZeroUsize::new(1).expect("1 is non-zero"), turbine_xdp_sender: None, + repair_xdp_sender: None, } } } @@ -171,7 +178,7 @@ pub struct AlpenglowInitializationState { pub optimistic_parent_sender: Sender, pub optimistic_parent_receiver: Receiver, pub replay_highest_frozen: Arc, - pub highest_parent_ready: Arc>, + pub highest_parent_ready: Arc>, pub highest_finalized: Arc>>, pub bank_forks_controller: Arc, pub bank_forks_controller_receiver: BankForksCommandReceiver, @@ -188,10 +195,6 @@ pub struct AlpenglowInitializationState { // For BLS voting service pub bls_connection_cache: Arc, pub voting_service_test_override: Option, - - // For rewards - pub reward_certs_sender: Sender, - pub build_reward_certs_receiver: Receiver, } impl Tvu { @@ -205,7 +208,7 @@ impl Tvu { pub fn new( vote_account: &Pubkey, authorized_voter_keypairs: Arc>>>, - bank_forks: &Arc>, + bank_forks: Arc>, cluster_info: &Arc, sockets: TvuSockets, blockstore: Arc, @@ -246,6 +249,7 @@ impl Tvu { slot_status_notifier: Option, vote_connection_cache: Arc, votor_init: AlpenglowInitializationState, + reward_votes_sender: Sender, ) -> Result { let migration_status = bank_forks.read().unwrap().migration_status(); @@ -274,21 +278,16 @@ impl Tvu { bls_connection_cache, voting_service_test_override, highest_finalized, - build_reward_certs_receiver, - reward_certs_sender, } = votor_init; // streamer and sigverify for A2A BLS messages let (consensus_message_sender, consensus_message_receiver) = bounded(MAX_ALPENGLOW_PACKET_NUM); - let (reward_votes_sender, reward_votes_receiver) = bounded(MAX_ALPENGLOW_PACKET_NUM); let (consensus_metrics_sender, consensus_metrics_receiver) = bounded(MAX_IN_FLIGHT_CONSENSUS_EVENTS); let generated_cert_types = Arc::new(GeneratedCertTypes::default()); - // The BLS socket is currently only available on Testnet and Development clusters. - // Closer to release we will enable this for all clusters. - let bls_sigverify_threads = if let Some(bls_socket) = bls_socket { + let bls_sigverify_threads = { let (bls_packet_sender, bls_packet_receiver) = bounded(MAX_ALPENGLOW_PACKET_NUM); let ( @@ -304,7 +303,7 @@ impl Tvu { ..Default::default() }; let qos_config = SimpleQosConfig { - max_streams_per_second: 30, + max_streams_per_second: VOTOR_RATE_LIMIT_PPS, // Cap by # of active validators (some overhead for epoch boundaries) max_staked_connections: MAX_ALPENGLOW_VOTE_ACCOUNTS * 2, // Two staked connection per validator to account for hotspares @@ -341,17 +340,14 @@ impl Tvu { packet_receiver: bls_packet_receiver, channel_to_repair: verified_voter_slots_sender, channel_to_reward: reward_votes_sender, - channel_to_pool: consensus_message_sender.clone(), + channel_to_pool: consensus_message_sender, channel_to_metrics: consensus_metrics_sender.clone(), }, ); let mut key_notifiers = key_notifiers.write().unwrap(); key_notifiers.add(KeyUpdaterType::Bls, bls_key_updater); - - Some((bls_streamer_t, bls_sigverifier_t)) - } else { - None + (bls_streamer_t, bls_sigverifier_t) }; let (fetch_sender, fetch_receiver) = EvictingSender::new_bounded(SHRED_FETCH_CHANNEL_SIZE); @@ -410,7 +406,9 @@ impl Tvu { ); let (ancestor_duplicate_slots_sender, ancestor_duplicate_slots_receiver) = unbounded(); - let (duplicate_slots_sender, duplicate_slots_receiver) = unbounded(); + // This channel is used by both gossip and window service. Gossip will not fill this + // beyond 50%, while window_service will perform a blocking send. + let (duplicate_slots_sender, duplicate_slots_receiver) = bounded(2048); let (ancestor_hashes_replay_update_sender, ancestor_hashes_replay_update_receiver) = unbounded(); let (dumped_slots_sender, dumped_slots_receiver) = unbounded(); @@ -428,12 +426,8 @@ impl Tvu { completed_slots_receiver, }; - // Create switch block event channel for ReplayStage - // We emit a switch bank event when we observe a ParentReady. - // The event is immediately consumed and the latest is stored in ReplayStage. - // We overprovision at 100 leader windows - we would require almost 3 minutes of stuck - // replay to hit the limit. - let (switch_bank_sender, switch_bank_receiver) = bounded(100); + // Shared latest switch-bank request from Votor to ReplayStage. + let latest_switch_request = LatestSwitchRequest::default(); let window_service = { let epoch_schedule = bank_forks @@ -473,9 +467,9 @@ impl Tvu { exit.clone(), repair_info, window_service_channels, - leader_schedule_cache.clone(), tvu_config.shred_version, outstanding_repair_requests, + tvu_config.repair_xdp_sender, ) }; @@ -500,6 +494,7 @@ impl Tvu { block_commitment_cache.clone(), rpc_subscriptions.clone(), ); + let (own_message_sender, own_message_receiver) = bounded(MAX_ALPENGLOW_PACKET_NUM); let votor_config = VotorConfig { exit: exit.clone(), @@ -514,7 +509,7 @@ impl Tvu { cluster_info: cluster_info.clone(), leader_schedule_cache: leader_schedule_cache.clone(), consensus_metrics_sender, - highest_finalized, + highest_finalized: highest_finalized.clone(), bank_forks_controller, bls_sender: bls_sender.clone(), commitment_sender: votor_commitment_sender, @@ -522,15 +517,13 @@ impl Tvu { leader_window_info_sender, highest_parent_ready, event_sender: votor_event_sender.clone(), - switch_bank_sender, - own_vote_sender: consensus_message_sender.clone(), - reward_certs_sender, + latest_switch_request: latest_switch_request.clone(), + own_vote_sender: own_message_sender.clone(), repair_event_sender, event_receiver: votor_event_receiver, consensus_message_receiver, + own_message_receiver, consensus_metrics_receiver, - reward_votes_receiver, - build_reward_certs_receiver, }; let votor = Votor::new(votor_config); @@ -551,7 +544,7 @@ impl Tvu { block_metadata_notifier, dumped_slots_sender, votor_event_sender, - own_vote_sender: consensus_message_sender, + own_message_sender, optimistic_parent_sender, lockouts_sender, }; @@ -566,7 +559,7 @@ impl Tvu { gossip_verified_vote_hash_receiver, popular_pruned_forks_receiver, bank_forks_controller_receiver, - switch_bank_receiver, + latest_switch_request, }; let replay_stage_config = ReplayStageConfig { @@ -609,6 +602,7 @@ impl Tvu { vote_history_storage, bls_connection_cache, bank_forks.clone(), + highest_finalized, voting_service_test_override, ); @@ -633,7 +627,7 @@ impl Tvu { ); let epoch_specs: Box = - Box::new(EpochSpecs::from(bank_forks.clone())); + Box::new(EpochSpecs::from(bank_forks)); let duplicate_shred_listener = DuplicateShredListener::new( exit, @@ -683,10 +677,9 @@ impl Tvu { } self.drop_bank_service.join()?; self.duplicate_shred_listener.join()?; - if let Some((streamer, sigverifier)) = self.bls_sigverify_threads { - streamer.join()?; - sigverifier.join()?; - } + let (streamer, sigverifier) = self.bls_sigverify_threads; + streamer.join()?; + sigverifier.join()?; self.votor.join()?; self.commitment_service.join()?; Ok(()) @@ -729,6 +722,7 @@ pub mod tests { }, serial_test::serial, solana_gossip::{cluster_info::ClusterInfo, node::Node}, + solana_hash::Hash, solana_keypair::Keypair, solana_ledger::{ blockstore::BlockstoreSignals, @@ -791,11 +785,11 @@ pub mod tests { let vote_keypair = Keypair::new(); let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank)); let block_commitment_cache = Arc::new(RwLock::new(BlockCommitmentCache::default())); - let (retransmit_slots_sender, _retransmit_slots_receiver) = unbounded(); - let (_gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = unbounded(); - let (verified_voter_slots_sender, verified_voter_slots_receiver) = unbounded(); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); - let (_, gossip_confirmed_slots_receiver) = unbounded(); + let (retransmit_slots_sender, _retransmit_slots_receiver) = bounded(1024); + let (_gossip_verified_vote_hash_sender, gossip_verified_vote_hash_receiver) = bounded(1024); + let (verified_voter_slots_sender, verified_voter_slots_receiver) = bounded(1024); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); + let (_, gossip_confirmed_slots_receiver) = bounded(1024); let max_complete_transaction_status_slot = Arc::new(AtomicU64::default()); let outstanding_repair_requests = Arc::>::default(); let cluster_slots = Arc::new(ClusterSlots::default_for_tests()); @@ -815,11 +809,17 @@ pub mod tests { DEFAULT_TPU_CONNECTION_POOL_SIZE, ); let replay_highest_frozen = Arc::new(ReplayHighestFrozen::default()); - let (leader_window_info_sender, _leader_window_info_receiver) = unbounded(); - let (optimistic_parent_sender, optimistic_parent_receiver) = unbounded(); - let highest_parent_ready = Arc::new(RwLock::new((0, (0, Hash::default())))); + let (leader_window_info_sender, _leader_window_info_receiver) = bounded(1024); + let (optimistic_parent_sender, optimistic_parent_receiver) = bounded(1024); + let highest_parent_ready = Arc::new(RwLock::new(( + 0, + Block { + slot: 0, + block_id: Hash::default(), + }, + ))); let (votor_event_sender, votor_event_receiver): (VotorEventSender, VotorEventReceiver) = - unbounded(); + bounded(1024); let staked_nodes = Arc::new(RwLock::new(StakedNodes::default())); let key_notifiers = Arc::new(RwLock::new(KeyUpdaters::default())); let cancel = CancellationToken::new(); @@ -834,16 +834,15 @@ pub mod tests { thread::sleep(Duration::from_secs(1)); } }); - let (reward_certs_sender, _reward_certs_receiver) = bounded(1); - let (_build_reward_certs_sender, build_reward_certs_receiver) = bounded(1); let (bank_forks_controller, bank_forks_controller_receiver) = BankForksControllerHandle::new(); let bank_forks_controller = Arc::new(bank_forks_controller); + let (reward_votes_sender, _reward_votes_receiver) = bounded(1024); let tvu = Tvu::new( &vote_keypair.pubkey(), Arc::new(RwLock::new(vec![Arc::new(vote_keypair)])), - &bank_forks, + bank_forks.clone(), &cref1, TvuSockets { repair: target1.sockets.repair, @@ -912,9 +911,8 @@ pub mod tests { highest_finalized: Arc::new(RwLock::new(None)), bank_forks_controller, bank_forks_controller_receiver, - build_reward_certs_receiver, - reward_certs_sender, }, + reward_votes_sender, ) .expect("assume success"); exit.store(true, Ordering::Relaxed); diff --git a/core/src/validator.rs b/core/src/validator.rs index 9fcd10167bb..52edbd0ff92 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -24,7 +24,8 @@ use { snapshot_packager_service::SnapshotPackagerService, stats_reporter_service::StatsReporterService, system_monitor_service::{ - SystemMonitorService, SystemMonitorStatsReportConfig, verify_net_stats_access, + SystemMonitorService, SystemMonitorStatsReportConfig, XdpNetworkConfigReport, + verify_net_stats_access, }, tpu::{Tpu, TpuSockets}, tvu::{AlpenglowInitializationState, Tvu, TvuConfig, TvuSockets}, @@ -34,23 +35,22 @@ use { snapshot_config::SnapshotConfig, snapshot_hash::StartingSnapshotHashes, }, agave_votor::{ - vote_history::VoteHistory, + vote_history::{VoteHistory, VoteHistoryError}, vote_history_storage::{NullVoteHistoryStorage, VoteHistoryStorage}, voting_service::VotingServiceOverride, }, agave_xdp::transmitter::{Transmitter, TransmitterBuilder}, - anyhow::{Context, Result, anyhow}, + anyhow::{Result, anyhow}, crossbeam_channel::{Receiver, bounded, unbounded}, serde::{Deserialize, Serialize}, solana_account::ReadableAccount, solana_accounts_db::{ accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsDbConfig}, accounts_update_notifier_interface::AccountsUpdateNotifier, - utils::{move_and_async_delete_path_contents, validate_account_paths_for_direct_io}, + utils::validate_account_paths_for_direct_io, }, solana_client::connection_cache::{ConnectionCache, Protocol}, solana_clock::Slot, - solana_cluster_type::ClusterType, solana_entry::poh::compute_hash_time, solana_epoch_schedule::MAX_LEADER_SCHEDULE_EPOCH_OFFSET, solana_genesis_config::GenesisConfig, @@ -95,7 +95,7 @@ use { }, solana_measure::measure::Measure, solana_metrics::{datapoint_info, metrics::metrics_config_sanity_check}, - solana_net_utils::SocketAddrSpace, + solana_net_utils::{PinnedXdpSender, SocketAddrSpace}, solana_poh::{ poh_controller::PohController, poh_recorder::PohRecorder, @@ -123,7 +123,7 @@ use { AbsRequestHandlers, AccountsBackgroundService, DroppedSlotsReceiver, PendingSnapshotPackages, PrunedBanksRequestHandler, SnapshotRequestHandler, }, - bank::Bank, + bank::{Bank, MAX_ALPENGLOW_VOTE_ACCOUNTS}, bank_forks::BankForks, bank_forks_controller::BankForksControllerHandle, commitment::BlockCommitmentCache, @@ -132,7 +132,7 @@ use { runtime_config::RuntimeConfig, snapshot_bank_utils, snapshot_controller::SnapshotController, - snapshot_utils::{self, clean_orphaned_account_snapshot_dirs}, + snapshot_utils, }, solana_send_transaction_service::send_transaction_service::Config as SendTransactionServiceConfig, solana_shred_version::compute_shred_version, @@ -144,15 +144,15 @@ use { }, solana_time_utils::timestamp, solana_tpu_client::tpu_client::{DEFAULT_TPU_CONNECTION_POOL_SIZE, DEFAULT_VOTE_USE_QUIC}, - solana_turbine::{self, XdpSender, broadcast_stage::BroadcastStageType}, + solana_turbine::{self, broadcast_stage::BroadcastStageType}, solana_unified_scheduler_pool::DefaultSchedulerPool, solana_validator_exit::Exit, - solana_vote_program::vote_state::VoteStateV4, + solana_vote_program::vote_state::{VoteStateV4, handler::VoteStateHandler}, std::{ borrow::Cow, cmp, collections::{HashMap, HashSet}, - net::{SocketAddr, SocketAddrV4}, + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, num::{NonZeroU64, NonZeroUsize}, path::{Path, PathBuf}, str::FromStr, @@ -349,6 +349,7 @@ pub struct ValidatorConfig { /// processing. pub run_verification: bool, pub require_tower: bool, + pub require_vote_history: bool, pub tower_storage: Arc, pub vote_history_storage: Arc, pub debug_keys: Option>>, @@ -359,10 +360,11 @@ pub struct ValidatorConfig { pub no_poh_speed_test: bool, pub no_os_memory_stats_reporting: bool, pub no_os_network_stats_reporting: bool, + pub xdp_network_config_report: Option, pub no_os_cpu_stats_reporting: bool, pub no_os_disk_stats_reporting: bool, pub enforce_ulimit_nofile: bool, - pub poh_pinned_cpu_core: usize, + pub poh_pinned_cpu_core: Option, pub poh_hashes_per_batch: u64, pub process_ledger_before_services: bool, pub accounts_db_config: AccountsDbConfig, @@ -429,6 +431,7 @@ impl ValidatorConfig { max_genesis_archive_unpacked_size: MAX_GENESIS_ARCHIVE_UNPACKED_SIZE, run_verification: true, require_tower: false, + require_vote_history: false, tower_storage: Arc::new(NullTowerStorage::default()), vote_history_storage: Arc::new(NullVoteHistoryStorage::default()), debug_keys: None, @@ -439,6 +442,7 @@ impl ValidatorConfig { no_poh_speed_test: true, no_os_memory_stats_reporting: true, no_os_network_stats_reporting: true, + xdp_network_config_report: None, no_os_cpu_stats_reporting: true, no_os_disk_stats_reporting: true, // No need to enforce nofile limit in tests @@ -475,7 +479,7 @@ impl ValidatorConfig { replay_transactions_threads: NonZeroUsize::new(2).expect("2 is non-zero"), tvu_shred_sigverify_threads: NonZeroUsize::new(2).expect("2 is non-zero"), tvu_bls_sigverify_threads: NonZeroUsize::new(2).expect("2 is non-zero"), - delay_leader_block_for_pending_fork: false, + delay_leader_block_for_pending_fork: true, voting_service_test_override: None, repair_handler_type: RepairHandlerType::default(), snapshot_packager_niceness_adj: 0, @@ -530,6 +534,11 @@ pub enum ValidatorStartProgress { Running, } +pub struct XdpTransmitSetup { + pub transmitter_builder: TransmitterBuilder, + pub src_ip: Ipv4Addr, +} + struct BlockstoreRootScan { thread: Option>>, } @@ -553,10 +562,10 @@ impl BlockstoreRootScan { } fn join(self) { - if let Some(blockstore_root_scan) = self.thread { - if let Err(err) = blockstore_root_scan.join() { - warn!("blockstore_root_scan failed to join {err:?}"); - } + if let Some(blockstore_root_scan) = self.thread + && let Err(err) = blockstore_root_scan.join() + { + warn!("blockstore_root_scan failed to join {err:?}"); } } } @@ -591,6 +600,8 @@ impl ValidatorTpuConfig { let tpu_quic_server_config = SwQosQuicStreamerConfig { quic_streamer_config: QuicStreamerConfig { max_connections_per_ipaddr_per_min: 32, + stream_receive_window_size: solana_message::v1::MAX_TRANSACTION_SIZE as u32, + max_stream_data_bytes: solana_message::v1::MAX_TRANSACTION_SIZE as u32, ..Default::default() }, qos_config: SwQosConfig::default(), @@ -687,7 +698,7 @@ impl Validator { socket_addr_space: SocketAddrSpace, tpu_config: ValidatorTpuConfig, admin_rpc_service_post_init: Arc>>, - xdp_builder_with_src_addr: Option<(TransmitterBuilder, SocketAddrV4)>, + xdp_transmit_setup: Option, ) -> Result { let exit = Arc::new(AtomicBool::new(false)); Self::new_with_exit( @@ -703,7 +714,7 @@ impl Validator { socket_addr_space, tpu_config, admin_rpc_service_post_init, - xdp_builder_with_src_addr, + xdp_transmit_setup, exit, ) } @@ -722,7 +733,7 @@ impl Validator { socket_addr_space: SocketAddrSpace, tpu_config: ValidatorTpuConfig, admin_rpc_service_post_init: Arc>>, - xdp_builder_with_src_addr: Option<(TransmitterBuilder, SocketAddrV4)>, + xdp_transmit_setup: Option, exit: Arc, ) -> Result { #[cfg(debug_assertions)] @@ -821,29 +832,18 @@ impl Validator { let genesis_config = load_genesis(config, ledger_path)?; metrics_config_sanity_check(genesis_config.cluster_type)?; - info!("Validating and cleaning accounts paths..."); + info!("Validating accounts paths..."); *start_progress.write().unwrap() = ValidatorStartProgress::CleaningAccounts; - let mut timer = Measure::start("validate_and_clean_accounts_paths"); + let mut timer = Measure::start("validate_account_paths"); validate_account_paths(config)?; - cleanup_accounts_paths(config); timer.stop(); - info!("Validating and cleaning accounts paths done. {timer}"); + info!("Validating accounts paths done. {timer}"); snapshot_utils::purge_incomplete_bank_snapshots(&config.snapshot_config.bank_snapshots_dir); snapshot_utils::purge_old_bank_snapshots_at_startup( &config.snapshot_config.bank_snapshots_dir, ); - info!("Cleaning orphaned account snapshot directories..."); - let mut timer = Measure::start("clean_orphaned_account_snapshot_dirs"); - clean_orphaned_account_snapshot_dirs( - &config.snapshot_config.bank_snapshots_dir, - &config.account_snapshot_paths, - ) - .context("failed to clean orphaned account snapshot directories")?; - timer.stop(); - info!("Cleaning orphaned account snapshot directories done. {timer}"); - // token used to cancel tpu-client-next, streamer and BLS streamer. let cancel = CancellationToken::new(); { @@ -895,6 +895,7 @@ impl Validator { SystemMonitorStatsReportConfig { report_os_memory_stats: !config.no_os_memory_stats_reporting, report_os_network_stats: !config.no_os_network_stats_reporting, + xdp_network_config_report: config.xdp_network_config_report.clone(), report_os_cpu_stats: !config.no_os_cpu_stats_reporting, report_os_disk_stats: !config.no_os_disk_stats_reporting, }, @@ -937,7 +938,7 @@ impl Validator { let migration_status = bank_forks.read().unwrap().migration_status(); - if !config.no_poh_speed_test { + if !config.no_poh_speed_test && !migration_status.is_alpenglow_enabled() { check_poh_speed(&bank_forks.read().unwrap().root_bank(), None)?; } @@ -948,14 +949,14 @@ impl Validator { let shred_version = compute_shred_version(&genesis_config.hash(), Some(&hard_forks)); info!("shred version: {shred_version}, hard forks: {hard_forks:?}"); - if let Some(expected_shred_version) = config.expected_shred_version { - if expected_shred_version != shred_version { - return Err(ValidatorError::ShredVersionMismatch { - actual: shred_version, - expected: expected_shred_version, - } - .into()); + if let Some(expected_shred_version) = config.expected_shred_version + && expected_shred_version != shred_version + { + return Err(ValidatorError::ShredVersionMismatch { + actual: shred_version, + expected: expected_shred_version, } + .into()); } if let Some(start_slot) = should_cleanup_blockstore_incorrect_shred_versions( @@ -1132,6 +1133,7 @@ impl Validator { blockstore_root_scan, &snapshot_controller, config, + cluster_info.my_shred_version(), ); maybe_warp_slot( @@ -1194,11 +1196,13 @@ impl Validator { )) }; - let bls_connection_cache = Arc::new(ConnectionCache::new_with_client_options( + let bls_connection_cache = Arc::new(ConnectionCache::new_with_max_connections( "connection_cache_bls_quic", // BLS consensus messaging is extremely low throughput (5 PPS). Even during standstill operations // we wouldn't expect more than a 100 PPS. 1 connection is enough. 1, /* connection_pool_size */ + // Overprovision to account for epoch boundary validator set rotations + MAX_ALPENGLOW_VOTE_ACCOUNTS * 2, /* max_connections */ Some(node.sockets.quic_alpenglow_client), Some(( &identity_keypair, @@ -1240,8 +1244,6 @@ impl Validator { json_rpc_service, rpc_subscriptions, pubsub_service, - completed_data_sets_sender, - completed_data_sets_service, rpc_completed_slots_service, sample_performance_service, optimistically_confirmed_bank_tracker, @@ -1324,27 +1326,6 @@ impl Validator { Some(pubsub_service) }; - let (completed_data_sets_sender, completed_data_sets_service) = - if !config.rpc_config.full_api { - (None, None) - } else { - let (completed_data_sets_sender, completed_data_sets_receiver) = - bounded(MAX_COMPLETED_DATA_SETS_IN_CHANNEL); - let completed_data_sets_service = CompletedDataSetsService::new( - completed_data_sets_receiver, - blockstore.clone(), - rpc_subscriptions.clone(), - deshred_transaction_notifier.clone(), - exit.clone(), - max_slots.clone(), - bank_forks.clone(), - ); - ( - Some(completed_data_sets_sender), - Some(completed_data_sets_service), - ) - }; - let rpc_completed_slots_service = if config.rpc_config.full_api || geyser_plugin_service.is_some() { let (completed_slots_sender, completed_slots_receiver) = @@ -1363,7 +1344,7 @@ impl Validator { let sample_performance_service = if config.rpc_config.enable_rpc_transaction_history { Some(SamplePerformanceService::new( - &bank_forks, + bank_forks.clone(), blockstore.clone(), exit.clone(), )) @@ -1394,17 +1375,43 @@ impl Validator { Some(json_rpc_service), Some(rpc_subscriptions), pubsub_service, - completed_data_sets_sender, - completed_data_sets_service, rpc_completed_slots_service, sample_performance_service, optimistically_confirmed_bank_tracker, bank_notification_sender_config, ) } else { - (None, None, None, None, None, None, None, None, None) + (None, None, None, None, None, None, None) }; + // CompletedDataSetsService feeds two independent sinks: RPC signatureSubscribe + // notifications (which need rpc_subscriptions) and the geyser deshred-transaction notifier + // (which does not). Spawn it whenever either sink wants it, kept out of the rpc_addrs block + // above so a geyser node started without --rpc-port still gets deshred notifications. + // Gating on the notifier itself rather than on a plugin being loaded keeps the per-data-set + // blockstore reads off nodes whose plugins don't subscribe; --geyser-plugin-always-enabled + // is the exception, where the notifier is present with no subscribers. + let (completed_data_sets_sender, completed_data_sets_service) = + if config.rpc_config.full_api || deshred_transaction_notifier.is_some() { + let (completed_data_sets_sender, completed_data_sets_receiver) = + bounded(MAX_COMPLETED_DATA_SETS_IN_CHANNEL); + let completed_data_sets_service = CompletedDataSetsService::new( + completed_data_sets_receiver, + blockstore.clone(), + rpc_subscriptions.clone(), + deshred_transaction_notifier.clone(), + exit.clone(), + max_slots.clone(), + bank_forks.clone(), + ); + ( + Some(completed_data_sets_sender), + Some(completed_data_sets_service), + ) + } else { + (None, None) + }; + let ip_echo_server = match node.sockets.ip_echo { None => None, Some(tcp_listener) => Some(solana_net_utils::ip_echo_server( @@ -1422,10 +1429,60 @@ impl Validator { let epoch_specs: Box = Box::new(crate::epoch_specs::EpochSpecs::from(bank_forks.clone())); + let ( + xdp_transmitter, + turbine_xdp_sender, + quic_xdp_sender, + repair_xdp_sender, + gossip_xdp_sender, + ) = if let Some(XdpTransmitSetup { + transmitter_builder, + src_ip, + }) = xdp_transmit_setup + { + let turbine_src_port = node.sockets.retransmit_sockets[0] + .local_addr() + .expect("retransmit socket should have local address") + .port(); + + let repair_src_port = node + .sockets + .repair + .local_addr() + .expect("repair socket should have local address") + .port(); + + let gossip_src_port = node.sockets.gossip[0] + .local_addr() + .expect("gossip socket should have local address") + .port(); + + let (transmitter, sender) = transmitter_builder.build(); + ( + Some(transmitter), + Some(PinnedXdpSender::new( + sender.clone(), + SocketAddrV4::new(src_ip, turbine_src_port), + )), + Some((sender.clone(), src_ip)), + Some(PinnedXdpSender::new( + sender.clone(), + SocketAddrV4::new(src_ip, repair_src_port), + )), + Some(PinnedXdpSender::new( + sender, + SocketAddrV4::new(src_ip, gossip_src_port), + )), + ) + } else { + (None, None, None, None, None) + }; + let gossip_service = GossipService::new( &cluster_info, Some(epoch_specs), node.sockets.gossip.clone(), + gossip_xdp_sender, config.gossip_validators.clone(), config.should_check_duplicate_instance, Some(stats_reporter_sender.clone()), @@ -1489,10 +1546,6 @@ impl Validator { // small (but highly overprovisioned) number for performance and easier // debug if things go off the rails. let (optimistic_parent_sender, optimistic_parent_receiver) = bounded(100); - // There will only ever be a single msg in flight so bound channel for [`BuildRewardCertsRequest`] to 1 message. - let (build_reward_certs_sender, build_reward_certs_receiver) = bounded(1); - // There will only ever be a single msg in flight so bound channel for [`BuildRewardCertsResponse`] to 1 message. - let (reward_certs_sender, reward_certs_receiver) = bounded(1); let banking_stage_sender_for_bcl = banking_tracer_channels.non_vote_sender.clone(); @@ -1513,11 +1566,11 @@ impl Validator { record_receiver_receiver, optimistic_parent_receiver: optimistic_parent_receiver.clone(), highest_finalized: highest_finalized.clone(), - build_reward_certs_sender, - reward_certs_receiver, banking_stage_sender: banking_stage_sender_for_bcl, + sharable_banks: bank_forks.read().unwrap().sharable_banks(), }; - let block_creation_loop = BlockCreationLoop::new(block_creation_loop_config); + let (block_creation_loop, reward_votes_sender) = + BlockCreationLoop::new(block_creation_loop_config); assert_eq!( blockstore.get_new_shred_signals_len(), @@ -1544,20 +1597,14 @@ impl Validator { exit.clone(), ); - let tower = match process_blockstore.process_to_create_tower() { - Ok(tower) => { - info!("Tower state: {tower:?}"); - tower - } - Err(e) => { - warn!("Unable to retrieve tower: {e:?} creating default tower...."); - Tower::default() - } - }; - // Future upstream PR will handle reconciliation of VoteHistory against hard forks - let vote_history = - VoteHistory::restore(config.vote_history_storage.as_ref(), &cluster_info.id()) - .unwrap_or(VoteHistory::new(cluster_info.id(), 0)); + let (tower, vote_history) = process_blockstore.process().map_err(|e| { + ValidatorError::Other(format!( + "Unable to restore Tower or VoteHistory and either --require-tower was specified \ + or --do-not-require-vote-history was not specified. Aborting {e}" + )) + })?; + info!("Tower state: {tower:?}, Vote History state: {vote_history:?}"); + migration_status.log_phase(); let outstanding_repair_requests = @@ -1572,38 +1619,17 @@ impl Validator { // This channel backing up indicates a serious problem in votor let (votor_event_sender, votor_event_receiver) = bounded(1000); - let (xdp_transmitter, turbine_xdp_sender, quic_xdp_sender) = - if let Some((xdp_transmit_builder, src_addr)) = xdp_builder_with_src_addr { - let (transmitter, sender) = xdp_transmit_builder.build(); - ( - Some(transmitter), - Some(XdpSender::new(sender.clone(), src_addr)), - Some((sender, *src_addr.ip())), - ) - } else { - (None, None, None) - }; - - // disable all2all tests if not allowed for a given cluster type - let alpenglow_socket = if genesis_config.cluster_type == ClusterType::Testnet - || genesis_config.cluster_type == ClusterType::Development - { - node.sockets.alpenglow - } else { - None - }; - let tvu = Tvu::new( vote_account, authorized_voter_keypairs, - &bank_forks, + bank_forks.clone(), &cluster_info, TvuSockets { repair: node.sockets.repair.try_clone().unwrap(), retransmit: node.sockets.retransmit_sockets, fetch: node.sockets.tvu, ancestor_hashes_requests: node.sockets.ancestor_hashes_requests, - alpenglow: alpenglow_socket, + alpenglow: node.sockets.alpenglow, block_id_repair: node.sockets.block_id_repair, }, blockstore.clone(), @@ -1642,6 +1668,7 @@ impl Validator { shred_sigverify_threads: config.tvu_shred_sigverify_threads, bls_sigverify_threads: config.tvu_bls_sigverify_threads, turbine_xdp_sender: turbine_xdp_sender.clone(), + repair_xdp_sender, }, &max_slots, block_metadata_notifier, @@ -1670,9 +1697,8 @@ impl Validator { bls_connection_cache, voting_service_test_override: config.voting_service_test_override.clone(), highest_finalized, - build_reward_certs_receiver, - reward_certs_sender, }, + reward_votes_sender, ) .map_err(ValidatorError::Other)?; @@ -1846,15 +1872,14 @@ impl Validator { logfile, logrotate_flag, }) = self.log_config.as_ref() + && logrotate_flag.load(Ordering::Relaxed) { - if logrotate_flag.load(Ordering::Relaxed) { - info!("Received SIGUSR1, reopening {}", logfile.display()); - agave_logger::redirect_stderr(logfile); - // Reset the flag to `false` to allow detection of the - // signal again and to avoid hitting this case every - // iteration - logrotate_flag.store(false, Ordering::Relaxed); - } + info!("Received SIGUSR1, reopening {}", logfile.display()); + agave_logger::redirect_stderr(logfile); + // Reset the flag to `false` to allow detection of the + // signal again and to avoid hitting this case every + // iteration + logrotate_flag.store(false, Ordering::Relaxed); } // One second is a reasonable response time for these signals to @@ -1902,6 +1927,10 @@ impl Validator { "local retransmit address: {}", node.sockets.retransmit_sockets[0].local_addr().unwrap() ); + info!( + "local alpenglow address: {}", + node.sockets.alpenglow.local_addr().unwrap() + ); } pub fn join(self) { @@ -1998,14 +2027,88 @@ impl Validator { } fn active_vote_account_exists_in_bank(bank: &Bank, vote_account: &Pubkey) -> bool { - if let Some(account) = &bank.get_account(vote_account) { - if let Ok(vote_state) = VoteStateV4::deserialize(account.data(), vote_account) { - return !vote_state.votes.is_empty(); - } + if let Some(account) = &bank.get_account(vote_account) + && let Ok(vote_state) = VoteStateV4::deserialize(account.data(), vote_account) + { + return !vote_state.votes.is_empty(); } false } +/// Should we require that a vote history file is present +pub fn should_require_vote_history_file( + bank: &Bank, + vote_account: &Pubkey, + identity: &Pubkey, +) -> bool { + let Some(genesis_certificate) = bank.get_alpenglow_genesis_certificate() else { + // Vote history is only used when Alpenglow is active + return false; + }; + + let Some(Ok(vote_state)) = bank + .get_account(vote_account) + .map(|acct| acct.deserialize_data()) + else { + // Must have a vote account + return false; + }; + + let Ok(vote_state_handler) = VoteStateHandler::try_new_from_vote_state_versions(vote_state) + else { + return false; + }; + + if vote_state_handler.node_pubkey() != identity { + // We are starting up or set-identity with a dummy keypair + // We don't need to require the vote history file + return false; + } + + let Some(last_voted_slot) = vote_state_handler.last_voted_slot() else { + // New vote account + return false; + }; + let genesis_slot = genesis_certificate.block.slot; + + // We've voted past the alpenglow genesis + last_voted_slot > genesis_slot +} + +fn restore_vote_history( + config: &ValidatorConfig, + bank_forks: &RwLock, + identity: &Pubkey, + vote_account: &Pubkey, +) -> Result { + match VoteHistory::restore(config.vote_history_storage.as_ref(), identity) { + Ok(vote_history) => Ok(vote_history), + Err(err) => { + let should_require_vote_history = { + let bank_forks = bank_forks.read().unwrap(); + should_require_vote_history_file(&bank_forks.working_bank(), vote_account, identity) + }; + if config.require_vote_history && should_require_vote_history { + return Err(format!( + "Unable to retrieve vote history for identity {identity}. The vote account \ + {vote_account} has prior Alpenglow votes. If this is intentional, use \ + --do-not-require-vote-history: {err:?}" + ) + .to_string()); + } + if err.is_file_missing() && !should_require_vote_history { + info!( + "Ignoring expected failed vote history restore because this vote account has \ + not voted before" + ); + } else { + warn!("Unable to retrieve vote history: {err:?} creating default vote history..."); + } + Ok(VoteHistory::new(*identity, 0)) + } + } +} + fn check_poh_speed(bank: &Bank, maybe_hash_samples: Option) -> Result<(), ValidatorError> { let Some(hashes_per_tick) = bank.hashes_per_tick() else { warn!("Unable to read hashes per tick from Bank, skipping PoH speed check"); @@ -2039,10 +2142,10 @@ fn check_poh_speed(bank: &Bank, maybe_hash_samples: Option) -> Result<(), V fn maybe_cluster_restart_with_hard_fork(config: &ValidatorConfig, root_slot: Slot) -> Option { // detect cluster restart (hard fork) indirectly via wait_for_supermajority... - if let Some(wait_slot_for_supermajority) = config.wait_for_supermajority { - if wait_slot_for_supermajority == root_slot { - return Some(wait_slot_for_supermajority); - } + if let Some(wait_slot_for_supermajority) = config.wait_for_supermajority + && wait_slot_for_supermajority == root_slot + { + return Some(wait_slot_for_supermajority); } None @@ -2059,7 +2162,9 @@ fn post_process_restored_tower( let restored_tower = restored_tower.and_then(|tower| { let root_bank = bank_forks.root_bank(); - let slot_history = root_bank.get_slot_history(); + let slot_history = root_bank + .get_slot_history() + .expect("slot history must exist"); // make sure tower isn't corrupted first before the following hard fork check let tower = tower.adjust_lockouts_after_replay(root_bank.slot(), &slot_history); @@ -2130,6 +2235,75 @@ fn post_process_restored_tower( Ok(restored_tower) } +fn post_process_restored_vote_history( + mut vote_history: VoteHistory, + validator_identity: &Pubkey, + config: &ValidatorConfig, + bank_forks: &BankForks, +) -> Result { + let mut should_require_vote_history = config.require_vote_history; + + let restored_vote_history = { + let root_bank = bank_forks.root_bank(); + + if vote_history.root() < root_bank.slot() { + // Vote history is old, update + vote_history.set_root(root_bank.slot()); + } + + if let Some(hard_fork_restart_slot) = + maybe_cluster_restart_with_hard_fork(config, root_bank.slot()) + { + // intentionally fail to restore vote_history; we're supposedly in a new hard fork; past + // out-of-chain votor state doesn't make sense at all + // what if --wait-for-supermajority again if the validator restarted? + let message = format!( + "Hard fork is detected; discarding vote_history restoration result: \ + {vote_history:?}" + ); + datapoint_error!("vote_history_error", ("error", message, String),); + error!("{message}"); + + // unconditionally relax vote_history requirement + should_require_vote_history = false; + Err(VoteHistoryError::HardFork(hard_fork_restart_slot)) + } else if let Some(warp_slot) = config.warp_slot { + // unconditionally relax vote_history requirement + should_require_vote_history = false; + Err(VoteHistoryError::HardFork(warp_slot)) + } else { + Ok(vote_history) + } + }; + + let restored_vote_history = match restored_vote_history { + Ok(vote_history) => vote_history, + Err(err) => { + if !err.is_file_missing() { + datapoint_error!( + "vote_history_error", + ( + "error", + format!("Unable to restore vote_history: {err}"), + String + ), + ); + } + if should_require_vote_history { + return Err(format!( + "Requested mandatory vote_history restore failed: {err}. Ensure that the vote \ + history storage file has been copied to the correct directory. Aborting" + )); + } + error!("Rebuilding an empty vote_history from root slot due to failed restore: {err}"); + + VoteHistory::new(*validator_identity, bank_forks.root()) + } + }; + + Ok(restored_vote_history) +} + fn load_genesis( config: &ValidatorConfig, ledger_path: &Path, @@ -2147,13 +2321,13 @@ fn load_genesis( let genesis_hash = genesis_config.hash(); info!("genesis hash: {genesis_hash}"); - if let Some(expected_genesis_hash) = config.expected_genesis_hash { - if genesis_hash != expected_genesis_hash { - return Err(ValidatorError::GenesisHashMismatch( - genesis_hash, - expected_genesis_hash, - )); - } + if let Some(expected_genesis_hash) = config.expected_genesis_hash + && genesis_hash != expected_genesis_hash + { + return Err(ValidatorError::GenesisHashMismatch( + genesis_hash, + expected_genesis_hash, + )); } Ok(genesis_config) @@ -2252,6 +2426,11 @@ fn load_blockstore( let (bank_forks, starting_snapshot_hashes) = bank_from_snapshot_opt .unwrap_or_else(|| { + // Clean run from genesis — must not use any existing state from previous runs. + bank_forks_utils::discard_previous_run_state( + &config.snapshot_config.bank_snapshots_dir, + &config.account_paths, + ); bank_forks_utils::load_bank_forks_from_genesis( genesis_config, &blockstore, @@ -2318,6 +2497,8 @@ pub struct ProcessBlockStore<'a> { snapshot_controller: &'a SnapshotController, config: &'a ValidatorConfig, tower: Option, + vote_history: Option, + my_shred_version: u16, } impl<'a> ProcessBlockStore<'a> { @@ -2336,6 +2517,7 @@ impl<'a> ProcessBlockStore<'a> { blockstore_root_scan: BlockstoreRootScan, snapshot_controller: &'a SnapshotController, config: &'a ValidatorConfig, + my_shred_version: u16, ) -> Self { Self { id, @@ -2352,94 +2534,121 @@ impl<'a> ProcessBlockStore<'a> { snapshot_controller, config, tower: None, + vote_history: None, + my_shred_version, } } - pub(crate) fn process(&mut self) -> Result<(), String> { - if self.tower.is_none() { - let previous_start_process = *self.start_progress.read().unwrap(); - *self.start_progress.write().unwrap() = ValidatorStartProgress::LoadingLedger; - - let exit = Arc::new(AtomicBool::new(false)); - if let Ok(Some(max_slot)) = self.blockstore.highest_slot() { - let bank_forks = self.bank_forks.clone(); - let exit = exit.clone(); - let start_progress = self.start_progress.clone(); - - let _ = Builder::new() - .name("solRptLdgrStat".to_string()) - .spawn(move || { - while !exit.load(Ordering::Relaxed) { - let slot = bank_forks.read().unwrap().working_bank().slot(); - *start_progress.write().unwrap() = - ValidatorStartProgress::ProcessingLedger { slot, max_slot }; - thread::sleep(Duration::from_secs(2)); - } - }) - .unwrap(); - } - blockstore_processor::process_blockstore_from_root( - self.blockstore, - self.bank_forks, - self.leader_schedule_cache, - self.process_options, - self.transaction_status_sender, - self.entry_notification_sender, - Some(self.snapshot_controller), - ) - .map_err(|err| { - exit.store(true, Ordering::Relaxed); - format!("Failed to load ledger: {err:?}") - })?; - exit.store(true, Ordering::Relaxed); + pub(crate) fn process(&mut self) -> Result<(Tower, VoteHistory), String> { + if let (Some(tower), Some(vote_history)) = (self.tower.as_ref(), self.vote_history.as_ref()) + { + return Ok((tower.clone(), vote_history.clone())); + } - if let Some(blockstore_root_scan) = self.blockstore_root_scan.take() { - blockstore_root_scan.join(); - } + // This means we have not fully processed blockstore yet. Attempt to load and process + let previous_start_process = *self.start_progress.read().unwrap(); + *self.start_progress.write().unwrap() = ValidatorStartProgress::LoadingLedger; - self.tower = Some({ - let restored_tower = Tower::restore(self.config.tower_storage.as_ref(), self.id); - if let Ok(tower) = &restored_tower { - // reconciliation attempt 1 of 2 with tower - reconcile_blockstore_roots_with_external_source( - ExternalRootSource::Tower(tower.root()), - self.blockstore, - &mut self.original_blockstore_root, - ) - .map_err(|err| format!("Failed to reconcile blockstore with tower: {err:?}"))?; - } + let exit = Arc::new(AtomicBool::new(false)); + if let Ok(Some(max_slot)) = self.blockstore.highest_slot() { + let bank_forks = self.bank_forks.clone(); + let exit = exit.clone(); + let start_progress = self.start_progress.clone(); + + let _ = Builder::new() + .name("solRptLdgrStat".to_string()) + .spawn(move || { + while !exit.load(Ordering::Relaxed) { + let slot = bank_forks.read().unwrap().working_bank().slot(); + *start_progress.write().unwrap() = + ValidatorStartProgress::ProcessingLedger { slot, max_slot }; + thread::sleep(Duration::from_secs(2)); + } + }) + .unwrap(); + } - post_process_restored_tower( - restored_tower, - self.id, - self.vote_account, - self.config, - &self.bank_forks.read().unwrap(), - )? - }); + blockstore_processor::process_blockstore_from_root( + self.blockstore, + self.bank_forks, + self.my_shred_version, + self.leader_schedule_cache, + self.process_options, + self.transaction_status_sender, + self.entry_notification_sender, + Some(self.snapshot_controller), + ) + .map_err(|err| { + exit.store(true, Ordering::Relaxed); + format!("Failed to load ledger: {err:?}") + })?; + exit.store(true, Ordering::Relaxed); - if let Some(hard_fork_restart_slot) = maybe_cluster_restart_with_hard_fork( - self.config, - self.bank_forks.read().unwrap().root(), - ) { - // reconciliation attempt 2 of 2 with hard fork - // this should be #2 because hard fork root > tower root in almost all cases + if let Some(blockstore_root_scan) = self.blockstore_root_scan.take() { + blockstore_root_scan.join(); + } + + // Load and post process tower + let tower = { + let restored_tower = Tower::restore(self.config.tower_storage.as_ref(), self.id); + if let Ok(tower) = &restored_tower { + // reconciliation attempt 1 of 2 with tower reconcile_blockstore_roots_with_external_source( - ExternalRootSource::HardFork(hard_fork_restart_slot), + ExternalRootSource::Tower(tower.root()), self.blockstore, &mut self.original_blockstore_root, ) - .map_err(|err| format!("Failed to reconcile blockstore with hard fork: {err:?}"))?; + .map_err(|err| format!("Failed to reconcile blockstore with tower: {err:?}"))?; } - *self.start_progress.write().unwrap() = previous_start_process; + post_process_restored_tower( + restored_tower, + self.id, + self.vote_account, + self.config, + &self.bank_forks.read().unwrap(), + )? + }; + + // Load and post process vote history + let vote_history = { + let vote_history = + restore_vote_history(self.config, self.bank_forks, self.id, self.vote_account)?; + // reconciliation attempt 1 of 2 with vote history + reconcile_blockstore_roots_with_external_source( + ExternalRootSource::VoteHistory(vote_history.root()), + self.blockstore, + &mut self.original_blockstore_root, + ) + .map_err(|err| format!("Failed to reconcile blockstore with vote history: {err:?}"))?; + + post_process_restored_vote_history( + vote_history, + self.id, + self.config, + &self.bank_forks.read().unwrap(), + )? + }; + + if let Some(hard_fork_restart_slot) = maybe_cluster_restart_with_hard_fork( + self.config, + self.bank_forks.read().unwrap().root(), + ) { + // reconciliation attempt 2 of 2 with hard fork + // it is intentional that we do this second, as having the hard fork root < tower/vote_history root + // is invalid! This means we've hard forked and missed a finalized slot + reconcile_blockstore_roots_with_external_source( + ExternalRootSource::HardFork(hard_fork_restart_slot), + self.blockstore, + &mut self.original_blockstore_root, + ) + .map_err(|err| format!("Failed to reconcile blockstore with hard fork: {err:?}"))?; } - Ok(()) - } - pub(crate) fn process_to_create_tower(mut self) -> Result { - self.process()?; - Ok(self.tower.unwrap()) + *self.start_progress.write().unwrap() = previous_start_process; + self.tower = Some(tower.clone()); + self.vote_history = Some(vote_history.clone()); + Ok((tower, vote_history)) } } @@ -2587,16 +2796,23 @@ fn scan_blockstore_for_incorrect_shred_version( // Search for shreds with incompatible version in blockstore let slot_meta_iterator = blockstore.slot_meta_iterator(start_slot)?; - info!("Searching blockstore for shred with incorrect version from slot {start_slot}"); + info!( + "Blockstore search for shreds with incorrect version starting from slot {start_slot}; \ + searching for 60s" + ); for (slot, _meta) in slot_meta_iterator { let shreds = blockstore.get_data_shreds_for_slot(slot, 0)?; for shred in &shreds { if shred.version() != expected_shred_version { + info!( + "Blockstore search found shred with incorrect version {} in slot {slot}", + shred.version() + ); return Ok(Some(shred.version())); } } if timer.elapsed() > TIMEOUT { - info!("Didn't find incorrect shreds after 60 seconds, aborting"); + info!("Blockstore search did not find any shreds with incorrect version"); break; } } @@ -2644,7 +2860,7 @@ fn cleanup_blockstore_incorrect_shred_versions( for (slot, _meta) in slot_meta_iterator { let shreds = blockstore.get_data_shreds_for_slot(slot, 0)?; let shreds = shreds.into_iter().map(Cow::Owned); - let _ = backup_blockstore.insert_cow_shreds(shreds, None, true); + let _ = backup_blockstore.insert_cow_shreds(shreds, true); num_slots_copied += 1; if print_timer.elapsed() > PRINT_INTERVAL { @@ -2776,13 +2992,13 @@ fn wait_for_supermajority( _ => {} } - if let Some(expected_bank_hash) = config.expected_bank_hash { - if bank.hash() != expected_bank_hash { - return Err(ValidatorError::BankHashMismatch( - bank.hash(), - expected_bank_hash, - )); - } + if let Some(expected_bank_hash) = config.expected_bank_hash + && bank.hash() != expected_bank_hash + { + return Err(ValidatorError::BankHashMismatch( + bank.hash(), + expected_bank_hash, + )); } for i in 1.. { @@ -2897,17 +3113,6 @@ fn get_stake_percent_in_gossip(bank: &Bank, cluster_info: &ClusterInfo, log: boo online_stake_percentage as u64 } -fn cleanup_accounts_paths(config: &ValidatorConfig) { - for account_path in &config.account_paths { - move_and_async_delete_path_contents(account_path); - } - if let Some(shrink_paths) = &config.accounts_db_config.shrink_paths { - for shrink_path in shrink_paths { - move_and_async_delete_path_contents(shrink_path); - } - } -} - fn validate_account_paths(config: &ValidatorConfig) -> std::io::Result<()> { validate_account_paths_for_direct_io( config.snapshot_config.use_direct_io, @@ -2948,6 +3153,7 @@ pub fn is_snapshot_config_valid(snapshot_config: &SnapshotConfig) -> bool { mod tests { use { super::*, + agave_votor_messages::certificate::{CertSignature, GenesisCert}, crossbeam_channel::{RecvTimeoutError, bounded}, solana_entry::entry, solana_genesis_config::create_genesis_config, @@ -2959,9 +3165,111 @@ mod tests { }, solana_poh_config::PohConfig, solana_sha256_hasher::hash, + solana_vote_program::vote_state::{LandedVote, Lockout, VoteStateVersions}, std::{fs::remove_dir_all, num::NonZeroU64, thread, time::Duration}, }; + #[test] + fn test_should_require_vote_history_file() { + use { + agave_votor_messages::consensus_message::Block, + solana_account::{AccountSharedData, state_traits::StateMut}, + solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, + }; + + let genesis_config = create_genesis_config(1_000_000).0; + let bank = Bank::new_for_tests(&genesis_config); + let vote_account_pubkey = Pubkey::new_unique(); + let identity = Pubkey::new_unique(); + + assert!(!active_vote_account_exists_in_bank( + &bank, + &vote_account_pubkey + )); + assert!(!should_require_vote_history_file( + &bank, + &vote_account_pubkey, + &identity, + )); + + let mut vote_state = VoteStateV4 { + node_pubkey: identity, + ..VoteStateV4::default() + }; + let mut vote_account = + AccountSharedData::new(1, VoteStateV4::size_of(), &solana_vote_program::id()); + vote_account + .set_state(&VoteStateVersions::new_v4(vote_state.clone())) + .unwrap(); + bank.store_account(&vote_account_pubkey, &vote_account); + assert!(!active_vote_account_exists_in_bank( + &bank, + &vote_account_pubkey + )); + assert!(!should_require_vote_history_file( + &bank, + &vote_account_pubkey, + &identity, + )); + + vote_state.votes.push_back(LandedVote { + latency: 0, + lockout: Lockout::new(7), + }); + vote_account + .set_state(&VoteStateVersions::new_v4(vote_state.clone())) + .unwrap(); + bank.store_account(&vote_account_pubkey, &vote_account); + assert!(active_vote_account_exists_in_bank( + &bank, + &vote_account_pubkey + )); + assert!(!should_require_vote_history_file( + &bank, + &vote_account_pubkey, + &identity, + )); + + let cert = GenesisCert { + block: Block { + slot: 40, + block_id: Hash::new_unique(), + }, + signature: CertSignature { + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }, + }; + bank.set_alpenglow_genesis_certificate(&cert); + assert!(!should_require_vote_history_file( + &bank, + &vote_account_pubkey, + &identity, + )); + + vote_state.votes.push_back(LandedVote { + latency: 0, + lockout: Lockout::new(43), + }); + vote_state.root_slot = Some(42); + vote_account + .set_state(&VoteStateVersions::new_v4(vote_state)) + .unwrap(); + bank.store_account(&vote_account_pubkey, &vote_account); + assert!(should_require_vote_history_file( + &bank, + &vote_account_pubkey, + &identity, + )); + + // Use an unstaked identity + assert!(!should_require_vote_history_file( + &bank, + &vote_account_pubkey, + &Pubkey::new_unique(), + )); + } + #[test] fn validator_exit() { agave_logger::setup(); @@ -3071,7 +3379,7 @@ mod tests { true, // is_full_slot 1, // version ); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); } // No check as all blockstore data is newer than latest hard fork @@ -3162,7 +3470,7 @@ mod tests { true, // is_full_slot 1, // version ); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); } // this purges and compacts all slots greater than or equal to 5 diff --git a/core/src/vote_simulator.rs b/core/src/vote_simulator.rs index b020e44f91a..b6377f83229 100644 --- a/core/src/vote_simulator.rs +++ b/core/src/vote_simulator.rs @@ -17,7 +17,7 @@ use { replay_stage::{HeaviestForkFailures, ReplayStage, TowerBFTStructures}, unfrozen_gossip_verified_vote_hashes::UnfrozenGossipVerifiedVoteHashes, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_clock::Slot, solana_epoch_schedule::EpochSchedule, solana_hash::Hash, @@ -247,7 +247,7 @@ impl VoteSimulator { } pub fn set_root(&mut self, new_root: Slot) { - let (drop_bank_sender, _drop_bank_receiver) = unbounded(); + let (drop_bank_sender, _drop_bank_receiver) = bounded(1024); ReplayStage::handle_new_root( new_root, &self.bank_forks, diff --git a/core/src/warm_quic_cache_service.rs b/core/src/warm_quic_cache_service.rs index 397db1dcd9d..88e96fb4a81 100644 --- a/core/src/warm_quic_cache_service.rs +++ b/core/src/warm_quic_cache_service.rs @@ -35,17 +35,16 @@ impl WarmQuicCacheService { contact_info_selector: impl ContactInfoQuery>, log_context: &str, ) { - if let Some(connection_cache) = cache { - if let Some(Some(addr)) = + if let Some(connection_cache) = cache + && let Some(Some(addr)) = cluster_info.lookup_contact_info(leader_pubkey, contact_info_selector) - { - let conn = connection_cache.get_connection(&addr); - if let Err(err) = conn.send_data(&[]) { - warn!( - "Failed to warmup QUIC connection to the leader {leader_pubkey:?} at \ - {addr:?}, Context: {log_context}, Error: {err:?}" - ); - } + { + let conn = connection_cache.get_connection(&addr); + if let Err(err) = conn.send_data(&[]) { + warn!( + "Failed to warmup QUIC connection to the leader {leader_pubkey:?} at \ + {addr:?}, Context: {log_context}, Error: {err:?}" + ); } } } @@ -75,26 +74,26 @@ impl WarmQuicCacheService { .read() .unwrap() .leader_after_n_slots((CACHE_OFFSET_SLOT + slot_jitter) as u64); - if let Some(leader_pubkey) = leader_pubkey { - if maybe_last_leader != Some(leader_pubkey) { - maybe_last_leader = Some(leader_pubkey); - // Warm cache for regular transactions - Self::warmup_connection( - tpu_connection_cache.as_deref(), - &cluster_info, - &leader_pubkey, - |node| node.tpu(Protocol::QUIC), - "tpu", - ); - // Warm cache for vote - Self::warmup_connection( - vote_connection_cache.as_deref(), - &cluster_info, - &leader_pubkey, - |node| node.tpu_vote(Protocol::QUIC), - "vote", - ); - } + if let Some(leader_pubkey) = leader_pubkey + && maybe_last_leader != Some(leader_pubkey) + { + maybe_last_leader = Some(leader_pubkey); + // Warm cache for regular transactions + Self::warmup_connection( + tpu_connection_cache.as_deref(), + &cluster_info, + &leader_pubkey, + |node| node.tpu(Protocol::QUIC), + "tpu", + ); + // Warm cache for vote + Self::warmup_connection( + vote_connection_cache.as_deref(), + &cluster_info, + &leader_pubkey, + |node| node.tpu_vote(Protocol::QUIC), + "vote", + ); } sleep(Duration::from_millis(200)); } diff --git a/core/src/window_service.rs b/core/src/window_service.rs index 63bfba32004..4ecd5100f78 100644 --- a/core/src/window_service.rs +++ b/core/src/window_service.rs @@ -21,11 +21,10 @@ use { solana_ledger::{ blockstore::{Blockstore, BlockstoreInsertionMetrics, PossibleDuplicateShred}, blockstore_meta::BlockLocation, - leader_schedule_cache::LeaderScheduleCache, shred::{self, ReedSolomonCache, Shred, filter::ShredRecoveryContext}, }, solana_measure::measure::Measure, - solana_metrics::inc_new_counter_error, + solana_net_utils::PinnedXdpSender, solana_rayon_threadlimit::get_thread_count, solana_runtime::bank_forks::{BankForks, SharableBanks}, solana_streamer::evicting_sender::EvictingSender, @@ -114,7 +113,10 @@ fn run_check_duplicate( duplicate_slots_sender: &DuplicateSlotSender, bank_forks: &RwLock, ) -> Result<()> { - let mut root_bank = bank_forks.read().unwrap().root_bank(); + let (mut root_bank, migration_status) = { + let bank_forks_r = bank_forks.read().unwrap(); + (bank_forks_r.root_bank(), bank_forks_r.migration_status()) + }; let mut last_updated = Instant::now(); let check_duplicate = |shred: PossibleDuplicateShred| -> Result<()> { if last_updated.elapsed().as_nanos() > root_bank.ns_per_slot { @@ -128,18 +130,45 @@ fn run_check_duplicate( shred_slot, &root_bank, ); + let validate_chained_block_id_2 = shred::filter::check_feature_activation_from_bank( + &feature_set::validate_chained_block_id_2::id(), + shred_slot, + &root_bank, + ); + let no_verify_chained_merkle_root = shred::filter::check_feature_activation_from_bank( + &feature_set::alpenglow::id(), + shred_slot, + &root_bank, + ); + let (shred1, shred2) = match shred { PossibleDuplicateShred::LastIndexConflict(shred, conflict) | PossibleDuplicateShred::ErasureConflict(shred, conflict) | PossibleDuplicateShred::MerkleRootConflict(shred, conflict) => (shred, conflict), - PossibleDuplicateShred::ChainedMerkleRootConflict(_shred, _conflict) => { - if validate_chained_block_id { + PossibleDuplicateShred::ChainedMerkleRootConflict(_slot) => { + if no_verify_chained_merkle_root { + // If we're in the full alpenglow epoch, we stop validating the chained merkle root. + // In Alpenglow we only use the double merkle root + return Ok(()); + } + if validate_chained_block_id || validate_chained_block_id_2 { // Although chained merkle roots are not necessary for agave duplicate resolution protocols, // We still need to mark the block as dead for other client teams. blockstore.set_dead_slot(shred_slot)?; } return Ok(()); } + PossibleDuplicateShred::FixedFECChainedMerkleRootConflict(_slot) => { + if no_verify_chained_merkle_root { + // If we're in the full alpenglow epoch, we stop validating the chained merkle root. + // In Alpenglow we only use the double merkle root + return Ok(()); + } + if validate_chained_block_id_2 { + blockstore.set_dead_slot(shred_slot)?; + } + return Ok(()); + } PossibleDuplicateShred::Exists(shred) => { // Unlike the other cases we have to wait until here to decide to handle the duplicate and store // in blockstore. This is because the duplicate could have been part of the same insert batch, @@ -159,10 +188,19 @@ fn run_check_duplicate( } }; - // Propagate duplicate proof through gossip - cluster_info.push_duplicate_shred(&shred1, &shred2)?; - // Notify duplicate consensus state machine - duplicate_slots_sender.send(shred_slot)?; + if migration_status.should_respond_to_ancestor_hashes_requests(shred_slot) { + // In alpenglow we store the duplicate block proofs in blockstore for the purposes of slashing, + // however we do not need to propagate the duplicate proof through gossip. + // We still propagate during the mixed migration epoch, to account for other nodes that are stuck + // and require a duplicate proof to proceed + cluster_info.push_duplicate_shred(&shred1, &shred2)?; + } + + if !migration_status.is_alpenglow_enabled() { + // The state machine can be exited as soon as alpenglow is enabled. + // Notify duplicate consensus state machine. If channel is full we wait. + duplicate_slots_sender.send(shred_slot)?; + } Ok(()) }; @@ -178,7 +216,6 @@ fn run_insert( verified_receiver: &Receiver>, blockstore: &Blockstore, shred_recovery_context: &mut ShredRecoveryContext, - leader_schedule_cache: &LeaderScheduleCache, handle_duplicate: F, metrics: &mut BlockstoreInsertionMetrics, ws_metrics: &mut WindowServiceMetrics, @@ -213,7 +250,6 @@ where ws_metrics.num_shreds_received += shreds.len(); let completed_data_sets = blockstore.insert_shreds_at_location_handle_duplicate( shreds, - Some(leader_schedule_cache), false, // is_trusted shred_recovery_context, &handle_duplicate, @@ -273,9 +309,9 @@ impl WindowService { exit: Arc, repair_info: RepairInfo, window_service_channels: WindowServiceChannels, - leader_schedule_cache: Arc, shred_version: u16, outstanding_repair_requests: Arc>, + repair_xdp_sender: Option, ) -> WindowService { let cluster_info = repair_info.cluster_info.clone(); let bank_forks = repair_info.bank_forks.clone(); @@ -297,6 +333,7 @@ impl WindowService { repair_info.clone(), outstanding_repair_requests.clone(), repair_service_channels, + repair_xdp_sender, ); let block_id_repair_service = BlockIdRepairService::new( @@ -325,7 +362,6 @@ impl WindowService { exit, blockstore, sharable_banks, - leader_schedule_cache, shred_version, verified_receiver, duplicate_sender, @@ -349,9 +385,6 @@ impl WindowService { duplicate_slots_sender: DuplicateSlotSender, bank_forks: Arc>, ) -> JoinHandle<()> { - let handle_error = || { - inc_new_counter_error!("solana-check-duplicate-error", 1, 1); - }; Builder::new() .name("solWinCheckDup".to_string()) .spawn(move || { @@ -362,10 +395,9 @@ impl WindowService { &duplicate_receiver, &duplicate_slots_sender, &bank_forks, - ) { - if Self::should_exit_on_error(e, &handle_error) { - break; - } + ) && Self::should_exit_on_error(e) + { + break; } } }) @@ -376,16 +408,12 @@ impl WindowService { exit: Arc, blockstore: Arc, sharable_banks: SharableBanks, - leader_schedule_cache: Arc, shred_version: u16, verified_receiver: Receiver>, check_duplicate_sender: Sender, completed_data_sets_sender: Option, retransmit_sender: EvictingSender>, ) -> JoinHandle<()> { - let handle_error = || { - inc_new_counter_error!("solana-window-insert-error", 1, 1); - }; let reed_solomon_cache = ReedSolomonCache::default(); Builder::new() .name("solWinInsert".to_string()) @@ -422,14 +450,13 @@ impl WindowService { &verified_receiver, &blockstore, &mut shred_recovery_context, - &leader_schedule_cache, handle_duplicate, &mut metrics, &mut ws_metrics, completed_data_sets_sender.as_ref(), ) { ws_metrics.record_error(&e); - if Self::should_exit_on_error(e, &handle_error) { + if Self::should_exit_on_error(e) { break; } } @@ -447,16 +474,19 @@ impl WindowService { .unwrap() } - fn should_exit_on_error(e: Error, handle_error: &H) -> bool - where - H: Fn(), - { + fn should_exit_on_error(e: Error) -> bool { match e { Error::RecvTimeout(RecvTimeoutError::Disconnected) => true, Error::RecvTimeout(RecvTimeoutError::Timeout) => false, Error::Send => true, _ => { - handle_error(); + let version = solana_version::version!(); + datapoint_error!( + "error", + ("thread", thread::current().name().unwrap_or("?"), String), + ("message", format!("{e}"), String), + ("version", version, String) + ); error!("thread {:?} error {:?}", thread::current().name(), e); false } @@ -475,6 +505,7 @@ impl WindowService { mod test { use { super::*, + crossbeam_channel::bounded, rand::Rng, solana_entry::entry::{Entry, create_ticks}, solana_gossip::contact_info::ContactInfo, @@ -522,7 +553,7 @@ mod test { let mut shreds = local_entries_to_shred(&original_entries, 0, 0, &Keypair::new()); shreds.reverse(); blockstore - .insert_shreds(shreds, None, false) + .insert_shreds(shreds, false) .expect("Expect successful processing of shred"); assert_eq!(blockstore.get_slot_entries(0, 0).unwrap(), original_entries); @@ -534,12 +565,10 @@ mod test { let genesis_config = create_genesis_config(10_000).genesis_config; let bank_forks = BankForks::new_rw_arc(Bank::new_for_tests(&genesis_config)); let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); - let (sender, receiver) = unbounded(); - let (duplicate_slot_sender, duplicate_slot_receiver) = unbounded(); + let (sender, receiver) = bounded(1024); + let (duplicate_slot_sender, duplicate_slot_receiver) = bounded(1024); let (shreds, _) = make_many_slot_entries(5, 5, 10); - blockstore - .insert_shreds(shreds.clone(), None, false) - .unwrap(); + blockstore.insert_shreds(shreds.clone(), false).unwrap(); let duplicate_index = 0; let original_shred = shreds[duplicate_index].clone(); let duplicate_shred = { @@ -584,8 +613,8 @@ mod test { fn test_store_duplicate_shreds_same_batch() { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); - let (duplicate_shred_sender, duplicate_shred_receiver) = unbounded(); - let (duplicate_slot_sender, duplicate_slot_receiver) = unbounded(); + let (duplicate_shred_sender, duplicate_shred_receiver) = bounded(1024); + let (duplicate_slot_sender, duplicate_slot_receiver) = bounded(1024); let exit = Arc::new(AtomicBool::new(false)); let keypair = Keypair::new(); let contact_info = ContactInfo::new_localhost(&keypair.pubkey(), timestamp()); @@ -628,7 +657,6 @@ mod test { blockstore .insert_shreds_handle_duplicate( shreds, - None, false, // is_trusted &mut ShredRecoveryContext::new( ReedSolomonCache::default(), diff --git a/core/tests/snapshots.rs b/core/tests/snapshots.rs index 762b788e8b0..d30d51f0cb9 100644 --- a/core/tests/snapshots.rs +++ b/core/tests/snapshots.rs @@ -6,7 +6,7 @@ use { SnapshotInterval, SnapshotKind, paths as snapshot_paths, snapshot_archive_info::FullSnapshotArchiveInfo, snapshot_config::SnapshotConfig, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, itertools::Itertools, log::{info, trace}, solana_accounts_db::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING, @@ -172,7 +172,7 @@ where let mint_keypair = &snapshot_test_config.genesis_config_info.mint_keypair; let pending_snapshot_packages = Arc::new(Mutex::new(PendingSnapshotPackages::default())); - let (snapshot_request_sender, snapshot_request_receiver) = unbounded(); + let (snapshot_request_sender, snapshot_request_receiver) = bounded(1024); let snapshot_controller = Arc::new(SnapshotController::new( snapshot_request_sender, snapshot_test_config.snapshot_config.clone(), @@ -276,7 +276,7 @@ fn test_slots_to_snapshot() { let num_set_roots = status_cache_max_entries * 2; for add_root_interval in &[1, 3, 9] { - let (snapshot_sender, _snapshot_receiver) = unbounded(); + let (snapshot_sender, _snapshot_receiver) = bounded(1024); // Make sure this test never clears bank.slots_since_snapshot let snapshot_test_config = SnapshotTestConfig::new( SnapshotInterval::Slots( @@ -405,7 +405,7 @@ fn test_bank_forks_incremental_snapshot() { let mint_keypair = &snapshot_test_config.genesis_config_info.mint_keypair; let pending_snapshot_packages = Arc::new(Mutex::new(PendingSnapshotPackages::default())); - let (snapshot_request_sender, snapshot_request_receiver) = unbounded(); + let (snapshot_request_sender, snapshot_request_receiver) = bounded(1024); let snapshot_controller = Arc::new(SnapshotController::new( snapshot_request_sender, snapshot_test_config.snapshot_config.clone(), @@ -594,8 +594,8 @@ fn test_snapshots_with_background_services() { SocketAddrSpace::Unspecified, )); - let (pruned_banks_sender, pruned_banks_receiver) = unbounded(); - let (snapshot_request_sender, snapshot_request_receiver) = unbounded(); + let (pruned_banks_sender, pruned_banks_receiver) = bounded(1024); + let (snapshot_request_sender, snapshot_request_receiver) = bounded(1024); let pending_snapshot_packages = Arc::new(Mutex::new(PendingSnapshotPackages::default())); let bank_forks = snapshot_test_config.bank_forks.clone(); @@ -777,7 +777,7 @@ fn test_fastboot_snapshots_teardown(exit_backpressure: bool) { SocketAddrSpace::Unspecified, )); - let (snapshot_request_sender, _snapshot_request_receiver) = unbounded(); + let (snapshot_request_sender, _snapshot_request_receiver) = bounded(1024); let pending_snapshot_packages = Arc::new(Mutex::new(PendingSnapshotPackages::default())); let bank_forks = snapshot_test_config.bank_forks.clone(); diff --git a/core/tests/unified_scheduler.rs b/core/tests/unified_scheduler.rs index 39b1f1cbd57..bc34ff94417 100644 --- a/core/tests/unified_scheduler.rs +++ b/core/tests/unified_scheduler.rs @@ -1,5 +1,5 @@ use { - crossbeam_channel::unbounded, + crossbeam_channel::bounded, itertools::Itertools, log::*, solana_core::{ @@ -109,8 +109,8 @@ fn test_scheduler_waited_by_drop_bank_service() { drop(lock_to_stall); // Create 2 channels to check actual pruned banks - let (drop_bank_sender1, drop_bank_receiver1) = unbounded(); - let (drop_bank_sender2, drop_bank_receiver2) = unbounded(); + let (drop_bank_sender1, drop_bank_receiver1) = bounded(1024); + let (drop_bank_sender2, drop_bank_receiver2) = bounded(1024); let drop_bank_service = DropBankService::new(drop_bank_receiver2); info!("calling handle_new_root()..."); diff --git a/cost-model/Cargo.toml b/cost-model/Cargo.toml index d64e653e93c..7e9ceaf14fa 100644 --- a/cost-model/Cargo.toml +++ b/cost-model/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -83,8 +85,5 @@ solana-vote = { path = "../vote", features = ["agave-unstable-api"] } static_assertions = { workspace = true } test-case = { workspace = true } -[[bench]] -name = "cost_tracker" - [lints] workspace = true diff --git a/cost-model/benches/cost_model.rs b/cost-model/benches/cost_model.rs deleted file mode 100644 index 0a336815cee..00000000000 --- a/cost-model/benches/cost_model.rs +++ /dev/null @@ -1,58 +0,0 @@ -#![feature(test)] -extern crate test; -use { - agave_feature_set::FeatureSet, - solana_cost_model::cost_model::CostModel, - solana_hash::Hash, - solana_keypair::Keypair, - solana_message::Message, - solana_pubkey::Pubkey, - solana_runtime_transaction::runtime_transaction::RuntimeTransaction, - solana_signer::Signer, - solana_system_interface::instruction as system_instruction, - solana_transaction::{Transaction, sanitized::SanitizedTransaction}, - test::Bencher, -}; - -struct BenchSetup { - transactions: Vec>, - feature_set: FeatureSet, -} - -const NUM_TRANSACTIONS_PER_ITER: usize = 1024; - -fn setup(num_transactions: usize) -> BenchSetup { - let transactions = (0..num_transactions) - .map(|_| { - // As many transfer instructions as is possible in a regular packet. - let from_keypair = Keypair::new(); - let to_lamports = - Vec::from_iter(std::iter::repeat_with(|| (Pubkey::new_unique(), 1)).take(24)); - let ixs = system_instruction::transfer_many(&from_keypair.pubkey(), &to_lamports); - let message = Message::new(&ixs, Some(&from_keypair.pubkey())); - let transaction = Transaction::new(&[from_keypair], message, Hash::default()); - RuntimeTransaction::from_transaction_for_tests(transaction) - }) - .collect(); - - let feature_set = FeatureSet::default(); - - BenchSetup { - transactions, - feature_set, - } -} - -#[bench] -fn bench_cost_model(bencher: &mut Bencher) { - let BenchSetup { - transactions, - feature_set, - } = setup(NUM_TRANSACTIONS_PER_ITER); - - bencher.iter(|| { - for transaction in &transactions { - let _ = CostModel::calculate_cost(test::black_box(transaction), &feature_set); - } - }); -} diff --git a/cost-model/benches/cost_tracker.rs b/cost-model/benches/cost_tracker.rs deleted file mode 100644 index 9e3893c338e..00000000000 --- a/cost-model/benches/cost_tracker.rs +++ /dev/null @@ -1,99 +0,0 @@ -#![feature(test)] -extern crate test; -use { - itertools::Itertools, - solana_cost_model::{ - cost_tracker::CostTracker, - transaction_cost::{TransactionCost, UsageCostDetails, WritableKeysTransaction}, - }, - solana_pubkey::Pubkey, - test::Bencher, -}; - -struct BenchSetup { - cost_tracker: CostTracker, - transactions: Vec, -} - -fn setup(num_transactions: usize, contentious_transactions: bool) -> BenchSetup { - let mut cost_tracker = CostTracker::default(); - // set cost_tracker with max limits to stretch testing - cost_tracker.set_limits_max(); - - let max_accounts_per_tx = 128; - let pubkey = Pubkey::new_unique(); - let transactions = (0..num_transactions) - .map(|_| { - let mut writable_accounts = Vec::with_capacity(max_accounts_per_tx); - (0..max_accounts_per_tx).for_each(|_| { - let writable_account_key = if contentious_transactions { - pubkey - } else { - Pubkey::new_unique() - }; - writable_accounts.push(writable_account_key) - }); - WritableKeysTransaction::new(writable_accounts) - }) - .collect_vec(); - - BenchSetup { - cost_tracker, - transactions, - } -} - -fn get_costs( - transactions: &[WritableKeysTransaction], -) -> Vec> { - transactions - .iter() - .map(|transaction| { - TransactionCost::Transaction(UsageCostDetails { - transaction, - signature_cost: 0, - write_lock_cost: 0, - data_bytes_cost: 0, - programs_execution_cost: 9999, - loaded_accounts_data_size_cost: 0, - allocated_accounts_data_size: 0, - }) - }) - .collect_vec() -} - -#[bench] -fn bench_cost_tracker_non_contentious_transaction(bencher: &mut Bencher) { - let BenchSetup { - mut cost_tracker, - transactions, - } = setup(1024, false); - let tx_costs = get_costs(&transactions); - - bencher.iter(|| { - for tx_cost in tx_costs.iter() { - if cost_tracker.try_add(tx_cost).is_err() { - break; - } // stop when hit limits - cost_tracker.update_execution_cost(tx_cost, 0, 0); // update execution cost down to zero - } - }); -} - -#[bench] -fn bench_cost_tracker_contentious_transaction(bencher: &mut Bencher) { - let BenchSetup { - mut cost_tracker, - transactions, - } = setup(1024, true); - let tx_costs = get_costs(&transactions); - - bencher.iter(|| { - for tx_cost in tx_costs.iter() { - if cost_tracker.try_add(tx_cost).is_err() { - break; - } // stop when hit limits - cost_tracker.update_execution_cost(tx_cost, 0, 0); // update execution cost down to zero - } - }); -} diff --git a/cost-model/src/block_cost_limits.rs b/cost-model/src/block_cost_limits.rs index fd8ffd26784..caa18b6594f 100644 --- a/cost-model/src/block_cost_limits.rs +++ b/cost-model/src/block_cost_limits.rs @@ -32,10 +32,6 @@ pub const MAX_BLOCK_UNITS_SIMD_0286: u64 = 100_000_000; /// reduce block's parallelism. pub const MAX_WRITABLE_ACCOUNT_UNITS: u64 = 24_000_000; -/// Number of compute units that a block can have for vote transactions, -/// set to less than MAX_BLOCK_UNITS to leave room for non-vote transactions -pub const MAX_VOTE_UNITS: u64 = 36_000_000; - /// The maximum allowed size, in bytes, that accounts data can grow, per block. /// This can also be thought of as the maximum size of new allocations per block. pub const MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA: u64 = 100_000_000; diff --git a/cost-model/src/cost_model.rs b/cost-model/src/cost_model.rs index 51de53ebc5e..28e72ad4c56 100644 --- a/cost-model/src/cost_model.rs +++ b/cost-model/src/cost_model.rs @@ -37,24 +37,18 @@ impl CostModel { transaction: &'a Tx, feature_set: &FeatureSet, ) -> TransactionCost<'a, Tx> { - let remove_simple_vote_from_cost_model = - feature_set.snapshot().remove_simple_vote_from_cost_model; - if transaction.is_simple_vote_transaction() && !remove_simple_vote_from_cost_model { - TransactionCost::SimpleVote { transaction } - } else { - let (programs_execution_cost, loaded_accounts_data_size_cost) = - Self::get_estimated_execution_cost(transaction, feature_set); - let data_bytes_cost = Self::get_instructions_data_cost(transaction); - Self::calculate_non_vote_transaction_cost( - transaction, - transaction.program_instructions_iter(), - transaction.num_write_locks(), - programs_execution_cost, - loaded_accounts_data_size_cost, - data_bytes_cost, - feature_set, - ) - } + let (programs_execution_cost, loaded_accounts_data_size_cost) = + Self::get_estimated_execution_cost(transaction, feature_set); + let data_bytes_cost = Self::get_instructions_data_cost(transaction); + Self::calculate_transaction_cost( + transaction, + transaction.program_instructions_iter(), + transaction.num_write_locks(), + programs_execution_cost, + loaded_accounts_data_size_cost, + data_bytes_cost, + feature_set, + ) } // Calculate executed transaction CU cost, with actual execution and loaded accounts size @@ -65,27 +59,21 @@ impl CostModel { actual_loaded_accounts_data_size_bytes: u32, feature_set: &FeatureSet, ) -> TransactionCost<'a, Tx> { - let remove_simple_vote_from_cost_model = - feature_set.snapshot().remove_simple_vote_from_cost_model; - if transaction.is_simple_vote_transaction() && !remove_simple_vote_from_cost_model { - TransactionCost::SimpleVote { transaction } - } else { - let loaded_accounts_data_size_cost = Self::calculate_loaded_accounts_data_size_cost( - actual_loaded_accounts_data_size_bytes, - feature_set, - ); - let instructions_data_cost = Self::get_instructions_data_cost(transaction); - - Self::calculate_non_vote_transaction_cost( - transaction, - transaction.program_instructions_iter(), - transaction.num_write_locks(), - actual_programs_execution_cost, - loaded_accounts_data_size_cost, - instructions_data_cost, - feature_set, - ) - } + let loaded_accounts_data_size_cost = Self::calculate_loaded_accounts_data_size_cost( + actual_loaded_accounts_data_size_bytes, + feature_set, + ); + let instructions_data_cost = Self::get_instructions_data_cost(transaction); + + Self::calculate_transaction_cost( + transaction, + transaction.program_instructions_iter(), + transaction.num_write_locks(), + actual_programs_execution_cost, + loaded_accounts_data_size_cost, + instructions_data_cost, + feature_set, + ) } /// Return an estimated total cost for a transaction given its': @@ -98,15 +86,10 @@ impl CostModel { num_write_locks: u64, feature_set: &FeatureSet, ) -> TransactionCost<'a, Tx> { - let remove_simple_vote_from_cost_model = - feature_set.snapshot().remove_simple_vote_from_cost_model; - if transaction.is_simple_vote_transaction() && !remove_simple_vote_from_cost_model { - return TransactionCost::SimpleVote { transaction }; - } let (programs_execution_cost, loaded_accounts_data_size_cost) = Self::get_estimated_execution_cost(transaction, feature_set); let data_bytes_cost = Self::get_instructions_data_cost(transaction); - Self::calculate_non_vote_transaction_cost( + Self::calculate_transaction_cost( transaction, instructions, num_write_locks, @@ -117,7 +100,7 @@ impl CostModel { ) } - fn calculate_non_vote_transaction_cost<'a, Tx: TransactionMeta>( + fn calculate_transaction_cost<'a, Tx: TransactionMeta>( transaction: &'a Tx, instructions: impl Iterator)>, num_write_locks: u64, @@ -142,7 +125,7 @@ impl CostModel { allocated_accounts_data_size, }; - TransactionCost::Transaction(usage_cost_details) + TransactionCost::new(usage_cost_details) } /// Returns signature details and the total signature cost diff --git a/cost-model/src/cost_tracker.rs b/cost-model/src/cost_tracker.rs index 23bd69816a6..9e612281234 100644 --- a/cost-model/src/cost_tracker.rs +++ b/cost-model/src/cost_tracker.rs @@ -29,9 +29,6 @@ pub enum CostTrackerError { /// would exceed block max limit WouldExceedBlockMaxLimit, - /// would exceed vote max limit - WouldExceedVoteMaxLimit, - /// would exceed account max limit WouldExceedAccountMaxLimit, @@ -46,7 +43,6 @@ impl From for TransactionError { fn from(err: CostTrackerError) -> Self { match err { CostTrackerError::WouldExceedBlockMaxLimit => Self::WouldExceedMaxBlockCostLimit, - CostTrackerError::WouldExceedVoteMaxLimit => Self::WouldExceedMaxVoteCostLimit, CostTrackerError::WouldExceedAccountMaxLimit => Self::WouldExceedMaxAccountCostLimit, CostTrackerError::WouldExceedAccountDataBlockLimit => { Self::WouldExceedAccountDataBlockLimit @@ -67,17 +63,44 @@ pub struct UpdatedCosts { pub updated_costliest_account_cost: u64, } +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct CostTrackerLimits { + pub account_cost: u64, + pub block_cost: u64, + // Maximum new account allocation data per block in bytes. + pub allocated_data_size: u64, +} + +impl CostTrackerLimits { + const MAX: Self = Self::new(u64::MAX, u64::MAX, u64::MAX); + + pub const fn new(account_cost: u64, block_cost: u64, allocated_data_size: u64) -> Self { + Self { + account_cost, + block_cost, + allocated_data_size, + } + } +} + +impl Default for CostTrackerLimits { + fn default() -> Self { + const _: () = assert!(MAX_WRITABLE_ACCOUNT_UNITS <= MAX_BLOCK_UNITS); + Self { + account_cost: MAX_WRITABLE_ACCOUNT_UNITS, + block_cost: MAX_BLOCK_UNITS, + allocated_data_size: MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA, + } + } +} + #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] #[derive(Debug)] pub struct CostTracker { - account_cost_limit: u64, - block_cost_limit: u64, - vote_cost_limit: u64, - // Maximum new account allocation data per block in bytes. - allocated_data_size_limit: u64, + limits: CostTrackerLimits, cost_by_writable_accounts: HashMap, block_cost: SharedBlockCost, - vote_cost: u64, transaction_count: Saturating, allocated_accounts_data_size: Saturating, transaction_signature_count: Saturating, @@ -92,20 +115,13 @@ pub struct CostTracker { impl Default for CostTracker { fn default() -> Self { - const _: () = assert!(MAX_WRITABLE_ACCOUNT_UNITS <= MAX_BLOCK_UNITS); - const _: () = assert!(MAX_VOTE_UNITS <= MAX_BLOCK_UNITS); - Self { - account_cost_limit: MAX_WRITABLE_ACCOUNT_UNITS, - block_cost_limit: MAX_BLOCK_UNITS, - vote_cost_limit: MAX_VOTE_UNITS, - allocated_data_size_limit: MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA, + limits: CostTrackerLimits::default(), cost_by_writable_accounts: HashMap::with_capacity_and_hasher( WRITABLE_ACCOUNTS_PER_BLOCK, ahash::RandomState::new(), ), block_cost: SharedBlockCost::new(0), - vote_cost: 0, transaction_count: Saturating(0), allocated_accounts_data_size: Saturating(0), transaction_signature_count: Saturating(0), @@ -120,51 +136,37 @@ impl Default for CostTracker { impl CostTracker { pub fn new_from_parent_limits(&self) -> Self { let mut new = Self::default(); - new.set_limits( - self.account_cost_limit, - self.block_cost_limit, - self.vote_cost_limit, - self.allocated_data_size_limit, - ); + new.set_limits(self.limits); new } + /// Get the cost tracker limits. + pub fn get_limits(&self) -> CostTrackerLimits { + self.limits + } + /// Get the overall account limit. pub fn get_account_limit(&self) -> u64 { - self.account_cost_limit + self.limits.account_cost } /// Get the overall block limit. pub fn get_block_limit(&self) -> u64 { - self.block_cost_limit - } - - /// Get the overall vote limit. - pub fn get_vote_limit(&self) -> u64 { - self.vote_cost_limit + self.limits.block_cost } /// Get the overall allocated account data size limit. pub fn get_allocated_data_size_limit(&self) -> u64 { - self.allocated_data_size_limit + self.limits.allocated_data_size } /// allows to adjust limits initiated during construction - pub fn set_limits( - &mut self, - account_cost_limit: u64, - block_cost_limit: u64, - vote_cost_limit: u64, - allocated_data_size_limit: u64, - ) { - self.account_cost_limit = account_cost_limit; - self.block_cost_limit = block_cost_limit; - self.vote_cost_limit = vote_cost_limit; - self.allocated_data_size_limit = allocated_data_size_limit; + pub fn set_limits(&mut self, limits: CostTrackerLimits) { + self.limits = limits; } pub fn set_limits_max(&mut self) { - self.set_limits(u64::MAX, u64::MAX, u64::MAX, u64::MAX); + self.set_limits(CostTrackerLimits::MAX); } pub fn in_flight_transaction_count(&self) -> usize { @@ -231,10 +233,6 @@ impl CostTracker { self.block_cost.clone() } - pub fn vote_cost(&self) -> u64 { - self.vote_cost - } - pub fn transaction_count(&self) -> u64 { self.transaction_count.0 } @@ -259,7 +257,6 @@ impl CostTracker { "is_leader" => is_leader.to_string(), ("bank_slot", bank_slot, i64), ("block_cost", self.block_cost(), i64), - ("vote_cost", self.vote_cost, i64), ("transaction_count", self.transaction_count.0, i64), ("number_of_accounts", self.number_of_accounts(), i64), ("costliest_account", costliest_account.to_string(), String), @@ -311,7 +308,8 @@ impl CostTracker { fn find_number_of_contended_accounts(&self) -> usize { // accounts has more than 95% of account_cu_limit is considered as highly contended let contended_cost_mark: u64 = self - .account_cost_limit + .limits + .account_cost .saturating_mul(95) .saturating_div(100); @@ -327,27 +325,20 @@ impl CostTracker { ) -> Result<(), CostTrackerError> { let cost: u64 = tx_cost.sum(); - if tx_cost.should_track_as_simple_vote() { - // if vote transaction, check if it exceeds vote_transaction_limit - if self.vote_cost.saturating_add(cost) > self.vote_cost_limit { - return Err(CostTrackerError::WouldExceedVoteMaxLimit); - } - } - - if self.block_cost().saturating_add(cost) > self.block_cost_limit { + if self.block_cost().saturating_add(cost) > self.limits.block_cost { // check against the total package cost return Err(CostTrackerError::WouldExceedBlockMaxLimit); } // check if the transaction itself is more costly than the account_cost_limit - if cost > self.account_cost_limit { + if cost > self.limits.account_cost { return Err(CostTrackerError::WouldExceedAccountMaxLimit); } let allocated_accounts_data_size = self.allocated_accounts_data_size + Saturating(tx_cost.allocated_accounts_data_size()); - if allocated_accounts_data_size.0 > self.allocated_data_size_limit { + if allocated_accounts_data_size.0 > self.limits.allocated_data_size { return Err(CostTrackerError::WouldExceedAccountDataBlockLimit); } @@ -355,7 +346,7 @@ impl CostTracker { for account_key in tx_cost.writable_accounts() { match self.cost_by_writable_accounts.get(account_key) { Some(chained_cost) => { - if chained_cost.saturating_add(cost) > self.account_cost_limit { + if chained_cost.saturating_add(cost) > self.limits.account_cost { return Err(CostTrackerError::WouldExceedAccountMaxLimit); } else { continue; @@ -411,9 +402,6 @@ impl CostTracker { costliest_account_cost = costliest_account_cost.max(*account_cost); } self.block_cost.fetch_add(adjustment); - if tx_cost.should_track_as_simple_vote() { - self.vote_cost = self.vote_cost.saturating_add(adjustment); - } costliest_account_cost } @@ -432,9 +420,6 @@ impl CostTracker { *account_cost = account_cost.saturating_sub(adjustment); } self.block_cost.fetch_sub(adjustment); - if tx_cost.should_track_as_simple_vote() { - self.vote_cost = self.vote_cost.saturating_sub(adjustment); - } } /// count number of none-zero CU accounts @@ -486,19 +471,18 @@ mod tests { solana_keypair::Keypair, solana_signer::Signer, std::cmp, - test_case::test_case, }; impl CostTracker { - fn new(account_cost_limit: u64, block_cost_limit: u64, vote_cost_limit: u64) -> Self { + fn new(account_cost_limit: u64, block_cost_limit: u64) -> Self { assert!(account_cost_limit <= block_cost_limit); - assert!(vote_cost_limit <= block_cost_limit); - Self { - account_cost_limit, - block_cost_limit, - vote_cost_limit, - ..Self::default() - } + let mut cost_tracker = Self::default(); + cost_tracker.set_limits(CostTrackerLimits { + account_cost: account_cost_limit, + block_cost: block_cost_limit, + ..CostTrackerLimits::default() + }); + cost_tracker } } @@ -537,7 +521,7 @@ mod tests { transaction: &WritableKeysTransaction, programs_execution_cost: u64, ) -> TransactionCost<'_, WritableKeysTransaction> { - TransactionCost::Transaction(simple_usage_cost_details( + TransactionCost::new(simple_usage_cost_details( transaction, programs_execution_cost, )) @@ -545,29 +529,23 @@ mod tests { fn simple_vote_transaction_cost( transaction: &WritableKeysTransaction, - remove_simple_vote_from_cost_model: bool, ) -> TransactionCost<'_, WritableKeysTransaction> { - if !remove_simple_vote_from_cost_model { - TransactionCost::SimpleVote { transaction } - } else { - TransactionCost::Transaction(UsageCostDetails { - transaction, - signature_cost: 1, - write_lock_cost: 2, - data_bytes_cost: 0, - programs_execution_cost: solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS, - loaded_accounts_data_size_cost: 8, - allocated_accounts_data_size: 0, - }) - } + TransactionCost::new(UsageCostDetails { + transaction, + signature_cost: 1, + write_lock_cost: 2, + data_bytes_cost: 0, + programs_execution_cost: solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS, + loaded_accounts_data_size_cost: 8, + allocated_accounts_data_size: 0, + }) } #[test] fn test_cost_tracker_initialization() { - let testee = CostTracker::new(10, 11, 8); - assert_eq!(10, testee.account_cost_limit); - assert_eq!(11, testee.block_cost_limit); - assert_eq!(8, testee.vote_cost_limit); + let testee = CostTracker::new(10, 11); + assert_eq!(10, testee.limits.account_cost); + assert_eq!(11, testee.limits.block_cost); assert_eq!(0, testee.cost_by_writable_accounts.len()); assert_eq!(0, testee.block_cost()); } @@ -580,34 +558,26 @@ mod tests { let cost = tx_cost.sum(); // build testee to have capacity for one simple transaction - let mut testee = CostTracker::new(cost, cost, cost); + let mut testee = CostTracker::new(cost, cost); assert!(testee.would_fit(&tx_cost).is_ok()); testee.add_transaction_cost(&tx_cost); assert_eq!(cost, testee.block_cost()); - assert_eq!(0, testee.vote_cost); let (_costliest_account, costliest_account_cost) = testee.find_costliest_account(); assert_eq!(cost, costliest_account_cost); } - #[test_case(false; "remove_simple_vote_from_cost_model not activated")] - #[test_case(true; "remove_simple_vote_from_cost_model activated")] - fn test_cost_tracker_ok_add_one_vote(remove_simple_vote_from_cost_model: bool) { + #[test] + fn test_cost_tracker_ok_add_one_vote() { let mint_keypair = test_setup(); let tx = build_simple_vote_transaction(&mint_keypair); - let tx_cost = simple_vote_transaction_cost(&tx, remove_simple_vote_from_cost_model); + let tx_cost = simple_vote_transaction_cost(&tx); let cost = tx_cost.sum(); // build testee to have capacity for one simple transaction - let mut testee = CostTracker::new(cost, cost, cost); + let mut testee = CostTracker::new(cost, cost); assert!(testee.would_fit(&tx_cost).is_ok()); testee.add_transaction_cost(&tx_cost); assert_eq!(cost, testee.block_cost()); - if !remove_simple_vote_from_cost_model { - assert_eq!(cost, testee.vote_cost); - } else { - // not trackijng vote cu after feature activation - assert_eq!(0, testee.vote_cost); - } let (_costliest_account, costliest_account_cost) = testee.find_costliest_account(); assert_eq!(cost, costliest_account_cost); } @@ -617,15 +587,13 @@ mod tests { let mint_keypair = test_setup(); let tx = build_simple_transaction(&mint_keypair); let mut tx_cost = simple_transaction_cost(&tx, 5); - if let TransactionCost::Transaction(ref mut usage_cost) = tx_cost { - usage_cost.allocated_accounts_data_size = 1; - } else { - unreachable!(); - } + tx_cost + .usage_cost_details_mut() + .allocated_accounts_data_size = 1; let cost = tx_cost.sum(); // build testee to have capacity for one simple transaction - let mut testee = CostTracker::new(cost, cost, cost); + let mut testee = CostTracker::new(cost, cost); assert!(testee.would_fit(&tx_cost).is_ok()); let old = testee.allocated_accounts_data_size; testee.add_transaction_cost(&tx_cost); @@ -644,7 +612,7 @@ mod tests { let cost2 = tx_cost2.sum(); // build testee to have capacity for two simple transactions, with same accounts - let mut testee = CostTracker::new(cost1 + cost2, cost1 + cost2, cost1 + cost2); + let mut testee = CostTracker::new(cost1 + cost2, cost1 + cost2); { assert!(testee.would_fit(&tx_cost1).is_ok()); testee.add_transaction_cost(&tx_cost1); @@ -673,7 +641,7 @@ mod tests { let cost2 = tx_cost2.sum(); // build testee to have capacity for two simple transactions, with same accounts - let mut testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2, cost1 + cost2); + let mut testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2); { assert!(testee.would_fit(&tx_cost1).is_ok()); testee.add_transaction_cost(&tx_cost1); @@ -700,7 +668,7 @@ mod tests { let cost2 = tx_cost2.sum(); // build testee to have capacity for two simple transactions, but not for same accounts - let mut testee = CostTracker::new(cmp::min(cost1, cost2), cost1 + cost2, cost1 + cost2); + let mut testee = CostTracker::new(cmp::min(cost1, cost2), cost1 + cost2); // should have room for first transaction { assert!(testee.would_fit(&tx_cost1).is_ok()); @@ -725,8 +693,7 @@ mod tests { let cost2 = tx_cost2.sum(); // build testee to have capacity for each chain, but not enough room for both transactions - let mut testee = - CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2 - 1, cost1 + cost2 - 1); + let mut testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2 - 1); // should have room for first transaction { assert!(testee.would_fit(&tx_cost1).is_ok()); @@ -738,40 +705,26 @@ mod tests { } } - #[test_case(false; "remove_simple_vote_from_cost_model not activated")] - #[test_case(true; "remove_simple_vote_from_cost_model activated")] - fn test_cost_tracker_reach_vote_limit(remove_simple_vote_from_cost_model: bool) { + #[test] + fn test_cost_tracker_vote_transactions_use_regular_limits() { let mint_keypair = test_setup(); // build two mocking vote transactions with diff accounts let second_account = Keypair::new(); let tx1 = build_simple_vote_transaction(&mint_keypair); - let tx_cost1 = simple_vote_transaction_cost(&tx1, remove_simple_vote_from_cost_model); + let tx_cost1 = simple_vote_transaction_cost(&tx1); let cost1 = tx_cost1.sum(); let tx2 = build_simple_vote_transaction(&second_account); - let tx_cost2 = simple_vote_transaction_cost(&tx2, remove_simple_vote_from_cost_model); + let tx_cost2 = simple_vote_transaction_cost(&tx2); let cost2 = tx_cost2.sum(); - // build testee to have capacity for each chain, but not enough room for both votes - let mut testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2, cost1 + cost2 - 1); + // build testee to have capacity for both vote transactions + let mut testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2); // should have room for first vote { assert!(testee.would_fit(&tx_cost1).is_ok()); testee.add_transaction_cost(&tx_cost1); } - if !remove_simple_vote_from_cost_model { - // but no more room for package as whole - assert!(testee.would_fit(&tx_cost2).is_err()); - } else { - // no more vote cu limit if feature activated - assert!(testee.would_fit(&tx_cost2).is_ok()); - } - // however there is room for none-vote tx3 - { - let third_account = Keypair::new(); - let tx3 = build_simple_transaction(&third_account); - let tx_cost3 = simple_transaction_cost(&tx3, 5); - assert!(testee.would_fit(&tx_cost3).is_ok()); - } + assert!(testee.would_fit(&tx_cost2).is_ok()); } #[test] @@ -783,21 +736,17 @@ mod tests { let mut tx_cost1 = simple_transaction_cost(&tx1, 5); let tx2 = build_simple_transaction(&second_account); let mut tx_cost2 = simple_transaction_cost(&tx2, 5); - if let TransactionCost::Transaction(ref mut usage_cost) = tx_cost1 { - usage_cost.allocated_accounts_data_size = MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA; - } else { - unreachable!(); - } - if let TransactionCost::Transaction(ref mut usage_cost) = tx_cost2 { - usage_cost.allocated_accounts_data_size = MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA + 1; - } else { - unreachable!(); - } + tx_cost1 + .usage_cost_details_mut() + .allocated_accounts_data_size = MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA; + tx_cost2 + .usage_cost_details_mut() + .allocated_accounts_data_size = MAX_BLOCK_ACCOUNTS_DATA_SIZE_DELTA + 1; let cost1 = tx_cost1.sum(); let cost2 = tx_cost2.sum(); // build testee that passes - let testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2 - 1, cost1 + cost2 - 1); + let testee = CostTracker::new(cmp::max(cost1, cost2), cost1 + cost2 - 1); assert!(testee.would_fit(&tx_cost1).is_ok()); // data is too big assert_eq!( @@ -812,18 +761,19 @@ mod tests { let mint_keypair = test_setup(); let tx = build_simple_transaction(&mint_keypair); let mut tx_cost = simple_transaction_cost(&tx, 5); - if let TransactionCost::Transaction(ref mut usage_cost) = tx_cost { - usage_cost.allocated_accounts_data_size = 2; - } else { - unreachable!(); - } + tx_cost + .usage_cost_details_mut() + .allocated_accounts_data_size = 2; // Transaction fits with default limit. - let mut testee = CostTracker::new(u64::MAX, u64::MAX, u64::MAX); + let mut testee = CostTracker::new(u64::MAX, u64::MAX); assert_eq!(testee.would_fit(&tx_cost), Ok(()),); // Transaction does not fit with 1B limit. - testee.allocated_data_size_limit = 1; + testee.set_limits(CostTrackerLimits { + allocated_data_size: 1, + ..testee.get_limits() + }); assert_eq!( testee.would_fit(&tx_cost), Err(CostTrackerError::WouldExceedAccountDataBlockLimit), @@ -843,7 +793,7 @@ mod tests { let cost2 = tx_cost2.sum(); // build testee - let mut testee = CostTracker::new(cost1 + cost2, cost1 + cost2, cost1 + cost2); + let mut testee = CostTracker::new(cost1 + cost2, cost1 + cost2); assert!(testee.try_add(&tx_cost1).is_ok()); assert!(testee.try_add(&tx_cost2).is_ok()); @@ -870,7 +820,7 @@ mod tests { let account_max = cost * 2; let block_max = account_max * 3; // for three accts - let mut testee = CostTracker::new(account_max, block_max, block_max); + let mut testee = CostTracker::new(account_max, block_max); // case 1: a tx writes to 3 accounts, should success, we will have: // | acct1 | $cost | @@ -930,7 +880,7 @@ mod tests { let account_max = cost * 2; let block_max = account_max * 3; // for three accts - let mut testee = CostTracker::new(account_max, block_max, block_max); + let mut testee = CostTracker::new(account_max, block_max); let transaction = WritableKeysTransaction::new(vec![acct1, acct2, acct3]); let tx_cost = simple_transaction_cost(&transaction, cost); let mut expected_block_cost = tx_cost.sum(); @@ -990,7 +940,7 @@ mod tests { let mut usage_cost = simple_usage_cost_details(&transaction, estimated_programs_execution_cost); usage_cost.loaded_accounts_data_size_cost = estimated_loaded_accounts_data_size_cost; - let tx_cost = TransactionCost::Transaction(usage_cost); + let tx_cost = TransactionCost::new(usage_cost); // confirm tx_cost is only made up by programs_execution_cost and // loaded_accounts_data_size_cost let estimated_tx_cost = tx_cost.sum(); @@ -1021,7 +971,6 @@ mod tests { ); assert_eq!(expected_cost, cost_tracker.block_cost()); - assert_eq!(0, cost_tracker.vote_cost); assert_eq!( number_writeble_accounts, cost_tracker.cost_by_writable_accounts.len() @@ -1055,7 +1004,6 @@ mod tests { assert_eq!(1, cost_tracker.transaction_count.0); assert_eq!(1, cost_tracker.number_of_accounts()); assert_eq!(cost, cost_tracker.block_cost()); - assert_eq!(0, cost_tracker.vote_cost); assert_eq!(0, cost_tracker.allocated_accounts_data_size.0); cost_tracker.remove_transaction_cost(&tx_cost); @@ -1063,7 +1011,6 @@ mod tests { assert_eq!(0, cost_tracker.transaction_count.0); assert_eq!(0, cost_tracker.number_of_accounts()); assert_eq!(0, cost_tracker.block_cost()); - assert_eq!(0, cost_tracker.vote_cost); assert_eq!(0, cost_tracker.allocated_accounts_data_size.0); } diff --git a/cost-model/src/transaction_cost.rs b/cost-model/src/transaction_cost.rs index 2b97dde84d4..2faede7e14f 100644 --- a/cost-model/src/transaction_cost.rs +++ b/cost-model/src/transaction_cost.rs @@ -1,105 +1,58 @@ use { - crate::block_cost_limits, solana_pubkey::Pubkey, - solana_runtime_transaction::transaction_meta::TransactionMeta, + solana_pubkey::Pubkey, solana_runtime_transaction::transaction_meta::TransactionMeta, solana_svm_transaction::svm_message::SVMMessage, }; -/// `TransactionCost`` is used to represent resources required to process a -/// transaction, denominated in Compute Units (CUs). Resources required to -/// process a regular transaction often include an array of variables, such as -/// execution cost, loaded bytes, write lock and read lock etc. -/// -/// SimpleVote has a simpler and pre-determined format. It has: -/// - 1 or 2 signatures -/// - 2 write locks -/// - 1 vote instruction -/// - less than 32k (page size) accounts to load -/// -/// Its cost therefore can be static #33269. -const SIMPLE_VOTE_USAGE_COST: u64 = 3428; - #[derive(Debug)] -pub enum TransactionCost<'a, Tx> { - SimpleVote { transaction: &'a Tx }, - Transaction(UsageCostDetails<'a, Tx>), +pub struct TransactionCost<'a, Tx> { + usage_cost: UsageCostDetails<'a, Tx>, } -impl TransactionCost<'_, Tx> { - pub fn sum(&self) -> u64 { - #![allow(clippy::assertions_on_constants)] - match self { - Self::SimpleVote { .. } => { - const _: () = assert!( - SIMPLE_VOTE_USAGE_COST - == solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS - + block_cost_limits::SIGNATURE_COST - + 2 * block_cost_limits::WRITE_LOCK_UNITS - + 8 - ); +impl<'a, Tx> TransactionCost<'a, Tx> { + pub fn new(usage_cost: UsageCostDetails<'a, Tx>) -> Self { + Self { usage_cost } + } - SIMPLE_VOTE_USAGE_COST - } - Self::Transaction(usage_cost) => usage_cost.sum(), - } + pub fn usage_cost_details(&self) -> &UsageCostDetails<'a, Tx> { + &self.usage_cost } - pub fn programs_execution_cost(&self) -> u64 { - match self { - Self::SimpleVote { .. } => solana_vote_program::vote_processor::DEFAULT_COMPUTE_UNITS, - Self::Transaction(usage_cost) => usage_cost.programs_execution_cost, - } + pub fn usage_cost_details_mut(&mut self) -> &mut UsageCostDetails<'a, Tx> { + &mut self.usage_cost } - pub fn should_track_as_simple_vote(&self) -> bool { - match self { - Self::SimpleVote { .. } => true, - Self::Transaction(_) => false, - } + pub fn sum(&self) -> u64 { + self.usage_cost.sum() + } + + pub fn programs_execution_cost(&self) -> u64 { + self.usage_cost.programs_execution_cost } pub fn data_bytes_cost(&self) -> u16 { - match self { - Self::SimpleVote { .. } => 0, - Self::Transaction(usage_cost) => usage_cost.data_bytes_cost, - } + self.usage_cost.data_bytes_cost } pub fn allocated_accounts_data_size(&self) -> u64 { - match self { - Self::SimpleVote { .. } => 0, - Self::Transaction(usage_cost) => usage_cost.allocated_accounts_data_size, - } + self.usage_cost.allocated_accounts_data_size } pub fn loaded_accounts_data_size_cost(&self) -> u64 { - match self { - Self::SimpleVote { .. } => 8, // simple-vote loads less than 32K account data, - // the cost round up to be one page (32K) cost: 8CU - Self::Transaction(usage_cost) => usage_cost.loaded_accounts_data_size_cost, - } + self.usage_cost.loaded_accounts_data_size_cost } pub fn signature_cost(&self) -> u64 { - match self { - Self::SimpleVote { .. } => block_cost_limits::SIGNATURE_COST, - Self::Transaction(usage_cost) => usage_cost.signature_cost, - } + self.usage_cost.signature_cost } pub fn write_lock_cost(&self) -> u64 { - match self { - Self::SimpleVote { .. } => block_cost_limits::WRITE_LOCK_UNITS.saturating_mul(2), - Self::Transaction(usage_cost) => usage_cost.write_lock_cost, - } + self.usage_cost.write_lock_cost } } impl TransactionCost<'_, Tx> { pub fn writable_accounts(&self) -> impl Iterator { - let transaction = match self { - Self::SimpleVote { transaction } => transaction, - Self::Transaction(usage_cost) => usage_cost.transaction, - }; + let transaction = self.usage_cost.transaction; transaction .account_keys() .iter() @@ -110,43 +63,31 @@ impl TransactionCost<'_, Tx> { impl TransactionCost<'_, Tx> { pub fn num_transaction_signatures(&self) -> u64 { - match self { - Self::SimpleVote { .. } => 1, - Self::Transaction(usage_cost) => usage_cost - .transaction - .signature_details() - .num_transaction_signatures(), - } + self.usage_cost + .transaction + .signature_details() + .num_transaction_signatures() } pub fn num_secp256k1_instruction_signatures(&self) -> u64 { - match self { - Self::SimpleVote { .. } => 0, - Self::Transaction(usage_cost) => usage_cost - .transaction - .signature_details() - .num_secp256k1_instruction_signatures(), - } + self.usage_cost + .transaction + .signature_details() + .num_secp256k1_instruction_signatures() } pub fn num_ed25519_instruction_signatures(&self) -> u64 { - match self { - Self::SimpleVote { .. } => 0, - Self::Transaction(usage_cost) => usage_cost - .transaction - .signature_details() - .num_ed25519_instruction_signatures(), - } + self.usage_cost + .transaction + .signature_details() + .num_ed25519_instruction_signatures() } pub fn num_secp256r1_instruction_signatures(&self) -> u64 { - match self { - Self::SimpleVote { .. } => 0, - Self::Transaction(usage_cost) => usage_cost - .transaction - .signature_details() - .num_secp256r1_instruction_signatures(), - } + self.usage_cost + .transaction + .signature_details() + .num_secp256r1_instruction_signatures() } } @@ -336,7 +277,7 @@ impl solana_runtime_transaction::transaction_with_meta::TransactionWithMeta mod tests { use { super::*, - crate::cost_model::CostModel, + crate::{block_cost_limits, cost_model::CostModel}, agave_feature_set::{FeatureSet, bls_pubkey_management_in_vote_account}, agave_reserved_account_keys::ReservedAccountKeys, solana_hash::Hash, @@ -346,7 +287,6 @@ mod tests { solana_transaction::{sanitized::MessageHash, versioned::VersionedTransaction}, solana_vote::vote_transaction, solana_vote_program::vote_state::TowerSync, - test_case::test_matrix, }; fn get_example_transaction() -> VersionedTransaction { @@ -365,19 +305,8 @@ mod tests { VersionedTransaction::from(transaction) } - #[test_matrix( - [false, true], - [false, true] - )] - fn test_vote_transaction_cost( - remove_simple_vote_from_cost_model: bool, - simd_0387_enabled: bool, - ) { - // SIMD-0387 requires `remove_simple_vote_from_cost_model`. - if simd_0387_enabled && !remove_simple_vote_from_cost_model { - return; - } - + #[test] + fn test_vote_transaction_cost() { agave_logger::setup(); use { @@ -399,24 +328,13 @@ mod tests { ) .unwrap(); - let mut feature_set = FeatureSet::all_enabled(); - if !remove_simple_vote_from_cost_model { - feature_set.deactivate(&agave_feature_set::remove_simple_vote_from_cost_model::id()); - } - if !simd_0387_enabled { - feature_set.deactivate(&bls_pubkey_management_in_vote_account::id()); - } + for simd_0387_enabled in [false, true] { + let mut feature_set = FeatureSet::all_enabled(); + if !simd_0387_enabled { + feature_set.deactivate(&bls_pubkey_management_in_vote_account::id()); + } - // Verify actual cost matches expected. - let expected_cost = if !remove_simple_vote_from_cost_model { - SIMPLE_VOTE_USAGE_COST - } else { - // when feature `stop-use-static-simple-vote-tx-cost` is enabled, vote transaction - // cost is calculated based on its UsageCostDetails too: - // - // sample transaction has 2 signatures let signature_cost = 2 * block_cost_limits::SIGNATURE_COST; - // sample transaction has 2 write lock let write_lock_cost = 2 * block_cost_limits::WRITE_LOCK_UNITS; let data_bytes_cost = vote_transaction.instruction_data_len() / (INSTRUCTION_DATA_BYTES_COST as u16); @@ -443,11 +361,11 @@ mod tests { loaded_accounts_data_size_cost, allocated_accounts_data_size: 0, }; - vote_program_usage_details.sum() - }; + let expected_cost = vote_program_usage_details.sum(); - let vote_cost = CostModel::calculate_cost(&vote_transaction, &feature_set); - assert_eq!(expected_cost, vote_cost.sum()); + let vote_cost = CostModel::calculate_cost(&vote_transaction, &feature_set); + assert_eq!(expected_cost, vote_cost.sum()); + } } #[test] diff --git a/cpu-utils/Cargo.toml b/cpu-utils/Cargo.toml new file mode 100644 index 00000000000..734a299a55b --- /dev/null +++ b/cpu-utils/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "agave-cpu-utils" +description = "Agave CPU Utils" +version = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +publish = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + +[features] +agave-unstable-api = [] + +[dependencies] +libc = { workspace = true } diff --git a/cpu-utils/src/affinity.rs b/cpu-utils/src/affinity.rs new file mode 100644 index 00000000000..bac514ede83 --- /dev/null +++ b/cpu-utils/src/affinity.rs @@ -0,0 +1,145 @@ +//! Core CPU affinity operations. + +use std::{io, mem, ops::Deref}; + +const CPU_SETSIZE: usize = libc::CPU_SETSIZE as usize; + +/// Identifies a logical CPU (hardware thread) by its kernel-assigned ID. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CpuId(usize); + +impl CpuId { + pub fn new(cpu: usize) -> io::Result { + if cpu < CPU_SETSIZE { + Ok(Self(cpu)) + } else { + Err(io::Error::from_raw_os_error(libc::EINVAL)) + } + } +} + +impl Deref for CpuId { + type Target = usize; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// Set CPU affinity for a thread. +/// +/// Restricts the thread to run only on the specified CPUs. +/// +/// # Arguments +/// * `thread_id` - Thread ID to set affinity for. `None` means the calling thread. +/// * `cpus` - CPU IDs to bind the thread to. Can be any iterable collection. +/// +/// # Examples +/// +/// ```no_run +/// # use agave_cpu_utils::*; +/// # fn main() -> std::io::Result<()> { +/// // Pin current thread to CPU 0 +/// set_cpu_affinity(None, [CpuId::new(0)?])?; +/// +/// // Pin current thread to multiple CPUs +/// set_cpu_affinity(None, [CpuId::new(0)?, CpuId::new(1)?, CpuId::new(2)?])?; +/// # Ok(()) +/// # } +/// ``` +/// +/// # Errors +/// +/// Returns [`io::ErrorKind::InvalidInput`] if any CPU ID is too large for `cpu_set_t`. +/// Returns [`io::Error`] if the system call fails, including offline CPUs or CPUs +/// disallowed by the task's hard cpuset/cgroup. +pub fn set_cpu_affinity( + thread_id: Option, + cpus: impl IntoIterator, +) -> io::Result<()> { + // safety: cpu_set_t is a POD type, zero-initialization is standard + let mut cpu_set: libc::cpu_set_t = unsafe { mem::zeroed() }; + + for cpu in cpus { + // safety: CpuId values are constructed only after validating cpu < CPU_SETSIZE. + unsafe { libc::CPU_SET(*cpu, &mut cpu_set) }; + } + + let tid = thread_id.unwrap_or(0); + + // safety: sched_setaffinity is safe with valid parameters + let result = + unsafe { libc::sched_setaffinity(tid, mem::size_of::(), &cpu_set) }; + + if result != 0 { + return Err(io::Error::last_os_error()); + } + + Ok(()) +} + +/// Get the CPU affinity mask for a thread. +/// +/// Returns a sorted vector of CPU IDs that the thread is allowed to run on. +/// +/// # Arguments +/// * `thread_id` - Thread ID to query. `None` means the calling thread. +/// +/// # Examples +/// +/// ```no_run +/// # use agave_cpu_utils::*; +/// # fn main() -> std::io::Result<()> { +/// let cpus = cpu_affinity(None)?; +/// println!("Thread can run on CPUs: {:?}", cpus); +/// # Ok(()) +/// # } +/// ``` +/// +/// # Errors +/// +/// Returns [`io::Error`] if the system call fails. +pub fn cpu_affinity(thread_id: Option) -> io::Result> { + // safety: cpu_set_t is a POD type, zero-initialization is standard + let mut cpu_set: libc::cpu_set_t = unsafe { mem::zeroed() }; + + let tid = thread_id.unwrap_or(0); + + // safety: sched_getaffinity is safe with valid parameters + let result = + unsafe { libc::sched_getaffinity(tid, mem::size_of::(), &mut cpu_set) }; + + if result != 0 { + return Err(io::Error::last_os_error()); + } + + let mut cpus = Vec::new(); + for cpu in 0..CPU_SETSIZE { + // safety: cpu < CPU_SETSIZE by construction + if unsafe { libc::CPU_ISSET(cpu, &cpu_set) } { + cpus.push(CpuId::new(cpu)?); + } + } + + Ok(cpus) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cpu_id_validation() { + let result = CpuId::new(CPU_SETSIZE); + assert_eq!(result.unwrap_err().raw_os_error(), Some(libc::EINVAL)); + } + + #[test] + fn test_cpu_affinity_returns_sorted() { + let cpus = cpu_affinity(None).expect("failed to query current CPU affinity"); + assert!( + cpus.windows(2).all(|window| *window[0] <= *window[1]), + "cpu_affinity should return sorted CPU list" + ); + } +} diff --git a/cpu-utils/src/lib.rs b/cpu-utils/src/lib.rs new file mode 100644 index 00000000000..7806376f1a6 --- /dev/null +++ b/cpu-utils/src/lib.rs @@ -0,0 +1,26 @@ +#![cfg(all(feature = "agave-unstable-api", target_os = "linux"))] + +//! CPU affinity utilities for Linux systems. +//! +//! This crate provides safe Rust bindings for setting CPU affinity and querying +//! the current task affinity mask. Useful for performance-critical applications +//! that need precise control over thread placement. +//! +//! # Examples +//! +//! ```no_run +//! use agave_cpu_utils::*; +//! +//! # fn main() -> std::io::Result<()> { +//! let allowed = cpu_affinity(None)?; +//! if let Some(&cpu) = allowed.first() { +//! set_cpu_affinity(None, [cpu])?; +//! } +//! # Ok(()) +//! # } +//! ``` +//! + +mod affinity; + +pub use affinity::{CpuId, cpu_affinity, set_cpu_affinity}; diff --git a/cpu-utils/tests/integration_test.rs b/cpu-utils/tests/integration_test.rs new file mode 100644 index 00000000000..12c81cd3fcc --- /dev/null +++ b/cpu-utils/tests/integration_test.rs @@ -0,0 +1,77 @@ +#![cfg(all(feature = "agave-unstable-api", target_os = "linux"))] + +//! Integration tests for agave-cpu-utils +//! +//! These tests require a Linux system. +//! Tests that modify CPU affinity spawn dedicated threads to avoid affecting the +//! test harness. + +use { + agave_cpu_utils::{CpuId, cpu_affinity, set_cpu_affinity}, + std::{io, thread}, +}; + +fn current_affinity() -> Vec { + cpu_affinity(None).expect("failed to query current CPU affinity") +} + +fn handle_affinity_result(result: io::Result<()>, test_name: &str) { + match result { + Ok(()) => {} + Err(e) => panic!("{test_name}: unexpected error: {e:?}"), + } +} + +#[test] +fn test_set_and_get_affinity() { + let affinity = current_affinity(); + let cpu = affinity + .first() + .copied() + .expect("current CPU affinity mask should not be empty"); + + let result = thread::spawn(move || { + set_cpu_affinity(None, [cpu])?; + let affinity = cpu_affinity(None)?; + assert_eq!(affinity, vec![cpu]); + Ok::<(), io::Error>(()) + }) + .join() + .expect("Thread panicked"); + + handle_affinity_result(result, "test_set_and_get_affinity"); +} + +#[test] +fn test_affinity_with_multiple_cpus() { + let available = current_affinity(); + if available.len() < 2 { + eprintln!( + "Skipping test_affinity_with_multiple_cpus: current CPU affinity mask has fewer than \ + 2 CPUs" + ); + return; + } + let (cpu0, cpu1) = (available[0], available[1]); + + let result = thread::spawn(move || { + set_cpu_affinity(None, [cpu0, cpu1])?; + let affinity = cpu_affinity(None)?; + assert_eq!(affinity, vec![cpu0, cpu1]); + Ok::<(), io::Error>(()) + }) + .join() + .expect("Thread panicked"); + + handle_affinity_result(result, "test_affinity_with_multiple_cpus"); +} + +#[test] +fn test_invalid_cpu_index_rejected() { + assert_eq!( + CpuId::new(libc::CPU_SETSIZE as usize) + .unwrap_err() + .raw_os_error(), + Some(libc::EINVAL) + ); +} diff --git a/dev-bins/.config/nextest.toml b/dev-bins/.config/nextest.toml index 0e6a1ad1a65..bb5ee619063 100644 --- a/dev-bins/.config/nextest.toml +++ b/dev-bins/.config/nextest.toml @@ -11,7 +11,3 @@ retries = { backoff = "fixed", count = 3, delay = "1s" } [profile.ci.junit] path = "../junit.xml" - -[[profile.ci.overrides]] -filter = "package(solana-bench-tps)" -threads-required = "num-cpus" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 30469159172..2d0e7bac255 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -56,7 +56,7 @@ dependencies = [ [[package]] name = "agave-banking-stage-ingress-types" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "crossbeam-channel", "solana-perf", @@ -64,7 +64,7 @@ dependencies = [ [[package]] name = "agave-bls-cert-verify" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-votor-messages", "bitvec", @@ -76,13 +76,49 @@ dependencies = [ "wincode", ] +[[package]] +name = "agave-bls-sigverify" +version = "4.3.0-alpha.0" +dependencies = [ + "agave-bls-cert-verify", + "agave-math-utils", + "agave-votor-messages", + "bitvec", + "crossbeam-channel", + "log", + "parking_lot 0.12.5", + "qualifier_attr", + "rayon", + "solana-bls-signatures", + "solana-clock", + "solana-gossip", + "solana-ledger", + "solana-measure", + "solana-metrics", + "solana-packet 4.1.0", + "solana-perf", + "solana-pubkey 4.2.0", + "solana-runtime", + "solana-signer-store", + "solana-streamer", + "thiserror 2.0.18", + "wincode", +] + +[[package]] +name = "agave-cpu-utils" +version = "4.3.0-alpha.0" +dependencies = [ + "libc", +] + [[package]] name = "agave-feature-set" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "ahash 0.8.12", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-pubkey 4.2.0", "solana-sha256-hasher", @@ -91,7 +127,7 @@ dependencies = [ [[package]] name = "agave-fs" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-io-uring", "io-uring", @@ -105,11 +141,11 @@ dependencies = [ [[package]] name = "agave-geyser-plugin-interface" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "log", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-signature", "solana-transaction", @@ -119,7 +155,7 @@ dependencies = [ [[package]] name = "agave-io-uring" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "io-uring", "libc", @@ -130,15 +166,16 @@ dependencies = [ [[package]] name = "agave-ledger-tool" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-logger", "agave-reserved-account-keys", "agave-snapshots", + "agave-votor", "assert_cmd", "chrono", - "clap 2.34.0", + "clap", "crossbeam-channel", "csv", "dashmap", @@ -154,7 +191,7 @@ dependencies = [ "serde_bytes", "serde_json", "signal-hook", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-accounts-db", "solana-clap-utils", @@ -168,7 +205,7 @@ dependencies = [ "solana-genesis-config", "solana-genesis-utils", "solana-geyser-plugin-manager", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-instruction", "solana-keypair", @@ -187,6 +224,7 @@ dependencies = [ "solana-sdk-ids", "solana-shred-version", "solana-signature", + "solana-signer-store", "solana-stake-interface", "solana-storage-bigtable", "solana-svm-callback", @@ -209,7 +247,7 @@ dependencies = [ [[package]] name = "agave-logger" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "env_logger", "libc", @@ -219,14 +257,14 @@ dependencies = [ [[package]] name = "agave-math-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "num-traits", ] [[package]] name = "agave-precompiles" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "bincode", @@ -245,14 +283,14 @@ dependencies = [ [[package]] name = "agave-random" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "rand 0.9.4", ] [[package]] name = "agave-reserved-account-keys" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "solana-pubkey 4.2.0", @@ -261,11 +299,11 @@ dependencies = [ [[package]] name = "agave-scheduler-bindings" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "agave-scheduling-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-scheduler-bindings", @@ -278,13 +316,14 @@ dependencies = [ "slotmap", "solana-fee", "solana-pubkey 4.2.0", + "solana-runtime-transaction", "solana-transaction-error", "thiserror 2.0.18", ] [[package]] name = "agave-snapshots" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-fs", "bincode", @@ -298,7 +337,7 @@ dependencies = [ "solana-accounts-db", "solana-clock", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-lattice-hash", "solana-measure", "solana-metrics", @@ -313,12 +352,12 @@ dependencies = [ [[package]] name = "agave-store-tool" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "ahash 0.8.12", - "clap 2.34.0", + "clap", "rayon", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-accounts-db", "solana-pubkey 4.2.0", "solana-system-interface 3.2.0", @@ -327,48 +366,44 @@ dependencies = [ [[package]] name = "agave-transaction-view" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-packet 4.1.0", - "solana-program-runtime", "solana-pubkey 4.2.0", "solana-sdk-ids", "solana-short-vec", "solana-signature", "solana-svm-transaction", "solana-transaction", - "solana-transaction-context", ] [[package]] name = "agave-votor" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-bls-sigverify", "agave-logger", "agave-math-utils", "agave-votor-messages", - "bincode", "bitvec", "crossbeam-channel", "itertools 0.14.0", "lazy-lru", "log", "parking_lot 0.12.5", - "qualifier_attr", "serde", "serde_bytes", - "solana-accounts-db", - "solana-bloom", "solana-bls-signatures", "solana-client", "solana-clock", "solana-connection-cache", "solana-epoch-schedule", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-metrics", @@ -382,24 +417,23 @@ dependencies = [ "solana-time-utils", "solana-transaction", "solana-transaction-error", - "solana-vote", - "solana-vote-program", "thiserror 2.0.18", "wincode", ] [[package]] name = "agave-votor-messages" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", + "crossbeam-channel", "log", "serde", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-bls-signatures", "solana-clock", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-short-vec", "solana-signer-store", @@ -409,15 +443,15 @@ dependencies = [ [[package]] name = "agave-xdp" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-xdp-ebpf", "arc-swap", "arrayvec", "aya", "bytes", "caps", - "core_affinity", "crossbeam-channel", "libc", "log", @@ -426,7 +460,7 @@ dependencies = [ [[package]] name = "agave-xdp-ebpf" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "aya", "aya-ebpf", @@ -630,7 +664,7 @@ dependencies = [ "ark-poly 0.5.0", "ark-serialize 0.5.0", "ark-std 0.5.0", - "educe 0.6.0", + "educe", "fnv", "hashbrown 0.15.5", "itertools 0.13.0", @@ -672,7 +706,7 @@ dependencies = [ "ark-std 0.5.0", "arrayvec", "digest 0.10.7", - "educe 0.6.0", + "educe", "itertools 0.13.0", "num-bigint 0.4.6", "num-traits", @@ -749,7 +783,7 @@ dependencies = [ "ark-ff 0.5.0", "ark-serialize 0.5.0", "ark-std 0.5.0", - "educe 0.6.0", + "educe", "fnv", "hashbrown 0.15.5", ] @@ -829,9 +863,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] name = "ascii" @@ -947,7 +981,7 @@ checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ "base64 0.22.1", "flate2", - "http 1.4.0", + "http 1.4.2", "log", "native-tls", "serde", @@ -980,7 +1014,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "itoa", @@ -1004,7 +1038,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "mime", @@ -1016,19 +1050,20 @@ dependencies = [ [[package]] name = "aya" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18bc4e506fbb85ab7392ed993a7db4d1a452c71b75a246af4a80ab8c9d2dd50" +checksum = "66e644424fada9fff4fdc63848db1732fb69b626e8328202ef55c03df1f4d939" dependencies = [ "assert_matches", "aya-obj", - "bitflags 2.11.1", - "bytes", + "bitflags 2.13.0", + "hashbrown 0.17.0", "libc", "log", "object", "once_cell", - "thiserror 1.0.69", + "scopeguard", + "thiserror 2.0.18", ] [[package]] @@ -1086,16 +1121,14 @@ dependencies = [ [[package]] name = "aya-obj" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51b96c5a8ed8705b40d655273bc4212cbbf38d4e3be2788f36306f154523ec7" +checksum = "8c76b9c75d9cdc155ff8f6a06d61e873f67bf47be8cfa92a3b5aaea43f4b4077" dependencies = [ "bytes", - "core-error", - "hashbrown 0.15.5", "log", "object", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -1137,18 +1170,40 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", "syn 2.0.117", ] +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.6", + "rand_core 0.6.4", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +dependencies = [ + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -1157,9 +1212,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -1172,9 +1227,9 @@ checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" [[package]] name = "bitvec" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" dependencies = [ "funty", "radium", @@ -1255,9 +1310,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" dependencies = [ "borsh-derive", "bytes", @@ -1266,9 +1321,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" dependencies = [ "once_cell", "proc-macro-crate 3.5.0", @@ -1368,9 +1423,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" dependencies = [ "serde", ] @@ -1494,9 +1549,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -1547,46 +1602,6 @@ dependencies = [ "unicode-width 0.1.14", ] -[[package]] -name = "clap" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim 0.11.1", -] - -[[package]] -name = "clap_derive" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - [[package]] name = "cmov" version = "0.5.2" @@ -1701,15 +1716,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "core-error" -version = "0.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efcdb2972eb64230b4c50646d8498ff73f5128d196a90c7236eec4cbe8619b8f" -dependencies = [ - "version_check", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -1726,17 +1732,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core_affinity" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a034b3a7b624016c6e13f5df875747cc25f884156aad2abd12b6c46797971342" -dependencies = [ - "libc", - "num_cpus", - "winapi", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -1927,38 +1922,14 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.117", + "darling_core", + "darling_macro", ] [[package]] @@ -1974,24 +1945,13 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.117", -] - [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.117", ] @@ -2196,29 +2156,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "downcast" version = "0.11.0" @@ -2305,25 +2242,13 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "educe" -version = "0.4.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" -dependencies = [ - "enum-ordinalize 3.1.15", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "educe" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" dependencies = [ - "enum-ordinalize 4.3.2", + "enum-ordinalize", "proc-macro2", "quote", "syn 2.0.117", @@ -2366,7 +2291,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.117", @@ -2392,19 +2317,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "enum-ordinalize" -version = "3.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" -dependencies = [ - "num-bigint 0.4.6", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -2576,12 +2488,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406" -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "fixedbitset" version = "0.5.7" @@ -2785,7 +2691,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.4", + "rustix", "windows-link", ] @@ -2822,24 +2728,11 @@ dependencies = [ "cfg-if 1.0.4", "js-sys", "libc", - "r-efi 5.3.0", + "r-efi", "wasip2", "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if 1.0.4", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - [[package]] name = "glob" version = "0.3.3" @@ -2900,7 +2793,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.4.2", "indexmap", "slab", "tokio", @@ -2963,12 +2856,6 @@ dependencies = [ "foldhash 0.2.0", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -2987,6 +2874,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + [[package]] name = "histogram" version = "0.6.9" @@ -3023,15 +2919,6 @@ dependencies = [ "hmac 0.8.1", ] -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "http" version = "0.2.12" @@ -3045,9 +2932,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -3071,7 +2958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.2", ] [[package]] @@ -3082,7 +2969,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "pin-project-lite", ] @@ -3148,7 +3035,7 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "httparse", "httpdate", @@ -3166,7 +3053,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.4.0", + "http 1.4.2", "hyper 1.8.1", "hyper-util", "rustls", @@ -3216,7 +3103,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "hyper 1.8.1", "ipnet", @@ -3334,12 +3221,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -3438,8 +3319,6 @@ dependencies = [ "equivalent", "hashbrown 0.17.0", "rayon", - "serde", - "serde_core", ] [[package]] @@ -3479,7 +3358,7 @@ version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "libc", ] @@ -3515,15 +3394,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -3764,12 +3634,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libbz2-rs-sys" version = "0.2.2" @@ -3804,7 +3668,7 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "libc", "plain", "redox_syscall 0.7.3", @@ -3919,12 +3783,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -3948,9 +3806,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru" @@ -4030,9 +3888,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -4144,12 +4002,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "multimap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" - [[package]] name = "multimap" version = "0.10.1" @@ -4190,7 +4042,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "cfg_aliases", "libc", @@ -4308,6 +4160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4344,12 +4197,12 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "crc32fast", - "hashbrown 0.15.5", + "hashbrown 0.17.0", "indexmap", "memchr", ] @@ -4383,11 +4236,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.80" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "foreign-types", "libc", @@ -4423,9 +4276,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.116" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", @@ -4435,29 +4288,10 @@ dependencies = [ ] [[package]] -name = "opentelemetry" -version = "0.17.0" +name = "pairing" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8" -dependencies = [ - "async-trait", - "crossbeam-channel", - "futures-channel", - "futures-executor", - "futures-util", - "js-sys", - "lazy_static", - "percent-encoding 2.3.2", - "pin-project", - "rand 0.8.6", - "thiserror 1.0.69", -] - -[[package]] -name = "pairing" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" dependencies = [ "group", ] @@ -4524,9 +4358,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pastey" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "pbkdf2" @@ -4537,16 +4371,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - [[package]] name = "pem" version = "1.1.1" @@ -4577,23 +4401,13 @@ dependencies = [ "num", ] -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset 0.4.2", - "indexmap", -] - [[package]] name = "petgraph" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ - "fixedbitset 0.5.7", + "fixedbitset", "hashbrown 0.15.5", "indexmap", ] @@ -4736,16 +4550,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a65843dfefbafd3c879c683306959a6de478443ffe9c9adf02f5976432402d7" -[[package]] -name = "prettyplease" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" -dependencies = [ - "proc-macro2", - "syn 1.0.109", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -4818,60 +4622,28 @@ dependencies = [ [[package]] name = "prost" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" -dependencies = [ - "bytes", - "prost-derive 0.11.9", -] - -[[package]] -name = "prost" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" -dependencies = [ - "bytes", - "prost-derive 0.14.3", -] - -[[package]] -name = "prost-build" -version = "0.11.9" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", - "heck 0.4.1", - "itertools 0.10.5", - "lazy_static", - "log", - "multimap 0.8.3", - "petgraph 0.6.5", - "prettyplease 0.1.25", - "prost 0.11.9", - "prost-types 0.11.9", - "regex", - "syn 1.0.109", - "tempfile", - "which", + "prost-derive", ] [[package]] name = "prost-build" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ - "heck 0.5.0", + "heck", "itertools 0.14.0", "log", - "multimap 0.10.1", - "petgraph 0.8.3", - "prettyplease 0.2.37", - "prost 0.14.3", - "prost-types 0.14.3", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", @@ -4881,22 +4653,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools 0.14.0", @@ -4907,20 +4666,11 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" -dependencies = [ - "prost 0.11.9", -] - -[[package]] -name = "prost-types" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ - "prost 0.14.3", + "prost", ] [[package]] @@ -4952,23 +4702,13 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "protosol" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e70d64aff3756d57a2b5e84ee2a01923f2a5350a51b93e8ec4631b817d8dcd" -dependencies = [ - "prost 0.11.9", - "prost-build 0.11.9", -] - [[package]] name = "pulldown-cmark" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "memchr", "unicase", ] @@ -5004,16 +4744,16 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "socket2 0.6.3", "thiserror 2.0.18", @@ -5024,9 +4764,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "bytes", "fastbloom", @@ -5034,7 +4774,7 @@ dependencies = [ "lru-slab", "rand 0.9.4", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -5074,12 +4814,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "radium" version = "0.7.0" @@ -5239,7 +4973,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -5248,7 +4982,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -5279,9 +5013,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -5302,9 +5036,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -5317,7 +5051,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -5357,7 +5091,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -5391,7 +5125,7 @@ checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" dependencies = [ "anyhow", "async-trait", - "http 1.4.0", + "http 1.4.2", "reqwest 0.12.28", "serde", "thiserror 1.0.69", @@ -5443,9 +5177,9 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.5.2" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" dependencies = [ "libc", "rtoolbox", @@ -5488,12 +5222,6 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -5518,37 +5246,24 @@ dependencies = [ "nom", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.11.1", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", - "linux-raw-sys 0.12.1", + "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "log", "once_cell", @@ -5649,15 +5364,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" version = "0.1.29" @@ -5673,12 +5379,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "sec1" version = "0.7.3" @@ -5699,7 +5399,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation", "core-foundation-sys", "libc", @@ -5786,9 +5486,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -5811,9 +5511,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "serde_core", "serde_with_macros", @@ -5821,11 +5521,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -5844,32 +5544,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "serial_test" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" -dependencies = [ - "futures-executor", - "futures-util", - "log", - "once_cell", - "parking_lot 0.12.5", - "scc", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "sha1" version = "0.10.6" @@ -5931,15 +5605,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shell-words" version = "1.1.1" @@ -6029,9 +5694,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" dependencies = [ "serde", ] @@ -6102,9 +5767,9 @@ dependencies = [ [[package]] name = "solana-account" -version = "4.3.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c1b95bd432efb92837bc38001365e5acb5c0175b199bc447b08a8c320ecf6c" +checksum = "385cad928d576683db3afef1b88d00a31a7126cc9f6f3f0c9f6b2d181437f53c" dependencies = [ "bincode", "qualifier_attr", @@ -6121,7 +5786,7 @@ dependencies = [ [[package]] name = "solana-account-decoder" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "Inflector", "base64 0.22.1", @@ -6130,7 +5795,7 @@ dependencies = [ "bv", "serde", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder-client-types", "solana-address-lookup-table-interface", "solana-clock", @@ -6150,24 +5815,26 @@ dependencies = [ "solana-stake-interface", "solana-sysvar", "solana-vote-interface", + "solana-zk-sdk-pod", "spl-generic-token", "spl-token-2022-interface", "spl-token-group-interface", "spl-token-interface", "spl-token-metadata-interface", "thiserror 2.0.18", + "wincode", "zstd", ] [[package]] name = "solana-account-decoder-client-types" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "bs58", "serde", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-pubkey 4.2.0", "zstd", ] @@ -6178,14 +5845,14 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9cf16495d9eb53e3d04e72366a33bb1c20c24e78c171d8b8f5978357b63ae95" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-program-error", "solana-program-memory", ] [[package]] name = "solana-accounts-db" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-fs", "ahash 0.8.12", @@ -6202,13 +5869,13 @@ dependencies = [ "seqlock", "serde", "smallvec", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-address-lookup-table-interface", "solana-bucket-map", "solana-clock", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-lattice-hash", "solana-measure", @@ -6241,14 +5908,14 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] name = "solana-address" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1384b52c435a750cc9c538760fc7bb472fd78e65a9900a2d07312c5bb335b72" +checksum = "39c93e262f671bf402e1040e4a7e40b05d81da5956c7681948c975a0997517bb" dependencies = [ "borsh", "bytemuck", @@ -6285,6 +5952,7 @@ dependencies = [ "solana-pubkey 4.2.0", "solana-sdk-ids", "solana-slot-hashes", + "wincode", ] [[package]] @@ -6296,142 +5964,6 @@ dependencies = [ "parking_lot 0.12.5", ] -[[package]] -name = "solana-banks-client" -version = "4.2.0-alpha.0" -dependencies = [ - "borsh", - "futures 0.3.32", - "solana-account 4.3.0", - "solana-banks-interface", - "solana-clock", - "solana-commitment-config", - "solana-hash 4.3.0", - "solana-message", - "solana-program-pack", - "solana-pubkey 4.2.0", - "solana-rent", - "solana-signature", - "solana-sysvar", - "solana-transaction", - "solana-transaction-context", - "solana-transaction-error", - "tarpc", - "thiserror 2.0.18", - "tokio", - "tokio-serde", -] - -[[package]] -name = "solana-banks-interface" -version = "4.2.0-alpha.0" -dependencies = [ - "serde", - "solana-account 4.3.0", - "solana-clock", - "solana-commitment-config", - "solana-hash 4.3.0", - "solana-message", - "solana-pubkey 4.2.0", - "solana-signature", - "solana-transaction", - "solana-transaction-context", - "solana-transaction-error", - "tarpc", -] - -[[package]] -name = "solana-banks-server" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "bincode", - "crossbeam-channel", - "futures 0.3.32", - "solana-account 4.3.0", - "solana-banks-interface", - "solana-clock", - "solana-commitment-config", - "solana-hash 4.3.0", - "solana-message", - "solana-net-utils", - "solana-pubkey 4.2.0", - "solana-runtime", - "solana-runtime-transaction", - "solana-send-transaction-service", - "solana-signature", - "solana-transaction", - "solana-transaction-error", - "tarpc", - "tokio", - "tokio-serde", - "tokio-util 0.7.18", -] - -[[package]] -name = "solana-bench-tps" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "agave-logger", - "chrono", - "clap 2.34.0", - "crossbeam-channel", - "csv", - "log", - "rand 0.9.4", - "rayon", - "serde", - "serde_json", - "serde_yaml", - "serial_test", - "solana-account 4.3.0", - "solana-clap-utils", - "solana-cli-config", - "solana-client", - "solana-clock", - "solana-commitment-config", - "solana-compute-budget-interface", - "solana-connection-cache", - "solana-core", - "solana-faucet", - "solana-fee-calculator", - "solana-genesis", - "solana-genesis-config", - "solana-hash 4.3.0", - "solana-instruction", - "solana-keypair", - "solana-local-cluster", - "solana-measure", - "solana-message", - "solana-metrics", - "solana-native-token", - "solana-net-utils", - "solana-nonce", - "solana-pubkey 4.2.0", - "solana-quic-client", - "solana-rent", - "solana-rpc", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-runtime", - "solana-sdk-ids", - "solana-signature", - "solana-signer", - "solana-streamer", - "solana-system-interface 3.2.0", - "solana-test-validator", - "solana-time-utils", - "solana-tps-client", - "solana-tpu-client", - "solana-transaction", - "solana-transaction-status", - "solana-version", - "spl-instruction-padding-interface", - "tempfile", - "tikv-jemallocator", -] - [[package]] name = "solana-bincode" version = "3.1.0" @@ -6451,12 +5983,12 @@ checksum = "7116e1d942a2432ca3f514625104757ab8a56233787e95144c93950029e31176" dependencies = [ "blake3", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] name = "solana-bloom" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bv", "fnv", @@ -6464,6 +5996,7 @@ dependencies = [ "serde", "solana-sanitize", "solana-time-utils", + "wincode", ] [[package]] @@ -6533,11 +6066,11 @@ dependencies = [ [[package]] name = "solana-bpf-loader-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", "qualifier_attr", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bincode", "solana-clock", "solana-instruction", @@ -6558,13 +6091,13 @@ dependencies = [ [[package]] name = "solana-bucket-map" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "ahash 0.8.12", "bv", "bytemuck", "bytemuck_derive", - "memmap2 0.9.10", + "memmap2 0.9.11", "modular-bitfield", "num_enum", "rand 0.9.4", @@ -6576,12 +6109,12 @@ dependencies = [ [[package]] name = "solana-builtins" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "solana-bpf-loader-program", "solana-compute-budget-program", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-program-runtime", "solana-pubkey 4.2.0", "solana-sdk-ids", @@ -6593,7 +6126,7 @@ dependencies = [ [[package]] name = "solana-builtins-default-costs" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "ahash 0.8.12", @@ -6603,17 +6136,19 @@ dependencies = [ [[package]] name = "solana-clap-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "bip39", + "bs58", "chrono", - "clap 2.34.0", + "clap", "rpassword", "solana-bls-signatures", "solana-clock", "solana-cluster-type", "solana-commitment-config", "solana-derivation-path", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-native-token", @@ -6624,14 +6159,13 @@ dependencies = [ "solana-signature", "solana-signer", "thiserror 2.0.18", - "tiny-bip39", "uriparse", "url 2.5.8", ] [[package]] name = "solana-cli-config" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "dirs-next", "serde", @@ -6643,13 +6177,15 @@ dependencies = [ [[package]] name = "solana-cli-output" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "Inflector", "agave-reserved-account-keys", + "agave-votor-messages", "base64 0.22.1", + "bitvec", "chrono", - "clap 2.34.0", + "clap", "console 0.16.3", "humantime", "indicatif", @@ -6657,15 +6193,18 @@ dependencies = [ "semver", "serde", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-bincode", + "solana-bls-signatures", "solana-clap-utils", "solana-cli-config", "solana-clock", "solana-epoch-info", - "solana-hash 4.3.0", + "solana-epoch-schedule", + "solana-hash 4.4.0", "solana-message", + "solana-native-token", "solana-packet 4.1.0", "solana-pubkey 4.2.0", "solana-rpc-client-api", @@ -6682,7 +6221,7 @@ dependencies = [ [[package]] name = "solana-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-trait", "bincode", @@ -6692,7 +6231,7 @@ dependencies = [ "log", "solana-commitment-config", "solana-connection-cache", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-message", @@ -6721,10 +6260,10 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19635cc703678bcb26eb6faee4a363c71f4a0d622c445d9b70c048c42f4cbcd3" dependencies = [ - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-commitment-config", "solana-epoch-info", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -6738,12 +6277,13 @@ dependencies = [ [[package]] name = "solana-clock" -version = "3.0.1" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95cf11109c3b6115cc510f1e31f06fdd52f504271bc24ef5f1249fbbcae5f9f3" +checksum = "f0acdace90d96e2c9e70d681465b4fe888b6bcf27c354ae9774e9f8a3b72923d" dependencies = [ "serde", "serde_derive", + "solana-get-sysvar", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -6757,7 +6297,7 @@ checksum = "3a494cf8eda7d98d9f0144b288bb409c88308d2e86f15cc1045aa77b83304718" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -6772,7 +6312,7 @@ dependencies = [ [[package]] name = "solana-compute-budget" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-fee-structure", "solana-program-runtime", @@ -6780,7 +6320,7 @@ dependencies = [ [[package]] name = "solana-compute-budget-instruction" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "solana-borsh", @@ -6808,7 +6348,7 @@ dependencies = [ [[package]] name = "solana-compute-budget-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-program-runtime", ] @@ -6832,7 +6372,7 @@ dependencies = [ [[package]] name = "solana-connection-cache" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-trait", "crossbeam-channel", @@ -6849,10 +6389,11 @@ dependencies = [ [[package]] name = "solana-core" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-banking-stage-ingress-types", "agave-bls-cert-verify", + "agave-bls-sigverify", "agave-feature-set", "agave-fs", "agave-logger", @@ -6871,6 +6412,7 @@ dependencies = [ "assert_matches", "async-trait", "bincode", + "bitvec", "bytes", "chrono", "crossbeam-channel", @@ -6896,7 +6438,8 @@ dependencies = [ "shaq", "signal-hook", "slab", - "solana-account 4.3.0", + "smallvec", + "solana-account 4.3.1", "solana-accounts-db", "solana-address-lookup-table-interface", "solana-bincode", @@ -6917,7 +6460,7 @@ dependencies = [ "solana-geyser-plugin-manager", "solana-gossip", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-leader-schedule", @@ -6944,6 +6487,7 @@ dependencies = [ "solana-shred-version", "solana-signature", "solana-signer", + "solana-signer-store", "solana-slot-hashes", "solana-slot-history", "solana-streamer", @@ -6983,7 +6527,7 @@ dependencies = [ [[package]] name = "solana-cost-model" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "ahash 0.8.12", @@ -7020,23 +6564,9 @@ dependencies = [ [[package]] name = "solana-curve25519" -version = "3.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d7e1177e6006823b91e0a930d94992ed74f8a6327d54ee50a9457ff72e625a" -dependencies = [ - "bytemuck", - "bytemuck_derive", - "curve25519-dalek 4.1.3", - "solana-define-syscall 3.0.0", - "subtle", - "thiserror 2.0.18", -] - -[[package]] -name = "solana-curve25519" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ee66a3e25ed8d20ed6fcd1ac71735415ff1edaf33ff1ec2118826eba927bcc" +checksum = "14b4d2a4bf0d0b0a86c22111917e86e8bd39a7b31420fb2c7d73eb83761fc7af" dependencies = [ "bytemuck", "bytemuck_derive", @@ -7046,12 +6576,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "solana-define-syscall" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9697086a4e102d28a156b8d6b521730335d6951bd39a5e766512bbe09007cee" - [[package]] name = "solana-define-syscall" version = "4.0.1" @@ -7077,7 +6601,7 @@ dependencies = [ [[package]] name = "solana-download-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-snapshots", "log", @@ -7101,20 +6625,19 @@ dependencies = [ [[package]] name = "solana-entry" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-votor-messages", "crossbeam-channel", - "dlopen2", "log", "num_cpus", "rayon", "smallvec", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-bls-signatures", "solana-clock", "solana-cost-model", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-measure", "solana-merkle-tree", "solana-message", @@ -7141,13 +6664,14 @@ dependencies = [ [[package]] name = "solana-epoch-rewards" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e7b0ba210593ba8ddd39d6d234d81795d1671cebf3026baa10d5dc23ac42f0" +checksum = "daf7eb4986b0b1d6f562b21f75a836f1a6df6e00c275efcef50aab5c144dc59e" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-get-sysvar", + "solana-hash 4.4.0", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -7160,18 +6684,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ee8beac9bff4db9225e57d532d169b0be5e447f1e6601a2f50f27a01bf5518f" dependencies = [ "siphasher 0.3.11", - "solana-address 2.6.0", - "solana-hash 4.3.0", + "solana-address 2.6.1", + "solana-hash 4.4.0", ] [[package]] name = "solana-epoch-schedule" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce264b7b42322325947c4136a09460bf5c73d9aa8262c9b0a2064be63ba8639" +checksum = "8116e6ffa6002237d5ab5edcbda17f9ba66b6742c45a89c9fb40a94dbacd4c1d" dependencies = [ "serde", "serde_derive", + "solana-get-sysvar", + "solana-program-error", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -7179,12 +6705,12 @@ dependencies = [ [[package]] name = "solana-faucet" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "crossbeam-channel", "log", "solana-cli-output", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -7211,7 +6737,7 @@ dependencies = [ "bincode", "serde", "serde_derive", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-instruction", "solana-program-error", @@ -7223,7 +6749,7 @@ dependencies = [ [[package]] name = "solana-fee" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "solana-fee-structure", @@ -7232,9 +6758,9 @@ dependencies = [ [[package]] name = "solana-fee-calculator" -version = "3.2.0" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e8add96b5741573e9f7529c4bb7719cfcfa999c3847a68cdfaef0cb6adf567" +checksum = "ef67f01cc6a0c72e99a08d0d484683f995de4c80e9568728fa77d1537f9b7e09" dependencies = [ "log", "serde", @@ -7264,53 +6790,6 @@ dependencies = [ "reqwest 0.13.2", ] -[[package]] -name = "solana-genesis" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "agave-logger", - "agave-snapshots", - "base64 0.22.1", - "bincode", - "clap 2.34.0", - "itertools 0.14.0", - "serde", - "serde_json", - "serde_yaml", - "solana-account 4.3.0", - "solana-bls-signatures", - "solana-clap-utils", - "solana-cli-config", - "solana-clock", - "solana-cluster-type", - "solana-commitment-config", - "solana-entry", - "solana-epoch-schedule", - "solana-feature-gate-interface", - "solana-fee-calculator", - "solana-genesis-config", - "solana-genesis-utils", - "solana-inflation", - "solana-keypair", - "solana-ledger", - "solana-loader-v3-interface", - "solana-native-token", - "solana-poh-config", - "solana-pubkey 4.2.0", - "solana-rent", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-runtime", - "solana-sdk-ids", - "solana-signer", - "solana-stake-interface", - "solana-time-utils", - "solana-version", - "solana-vote-program", - "tempfile", -] - [[package]] name = "solana-genesis-config" version = "4.0.0" @@ -7322,12 +6801,12 @@ dependencies = [ "memmap2 0.5.10", "serde", "serde_derive", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-clock", "solana-cluster-type", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-keypair", "solana-poh-config", @@ -7342,20 +6821,31 @@ dependencies = [ [[package]] name = "solana-genesis-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-snapshots", "log", "solana-download-utils", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-rpc-client", "thiserror 2.0.18", ] +[[package]] +name = "solana-get-sysvar" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef3bc859fc036ed490146793557386cbfae614ebba4adc704c37d94350824ed4" +dependencies = [ + "solana-address 2.6.1", + "solana-define-syscall 5.1.0", + "solana-program-error", +] + [[package]] name = "solana-geyser-plugin-manager" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-geyser-plugin-interface", "arc-swap", @@ -7367,16 +6857,15 @@ dependencies = [ "libloading", "log", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-accounts-db", "solana-clock", "solana-entry", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-ledger", "solana-measure", "solana-message", - "solana-metrics", "solana-pubkey 4.2.0", "solana-rpc", "solana-runtime", @@ -7389,16 +6878,16 @@ dependencies = [ [[package]] name = "solana-gossip" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "agave-random", "arc-swap", - "arrayvec", "assert_matches", - "bincode", "bv", + "bytes", "crossbeam-channel", + "ed25519-dalek 2.2.0", "flate2", "indexmap", "itertools 0.14.0", @@ -7411,13 +6900,12 @@ dependencies = [ "serde", "serde-big-array", "serde_bytes", - "siphasher 1.0.2", "solana-bloom", "solana-client", "solana-clock", "solana-entry", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-ledger", "solana-measure", @@ -7440,6 +6928,7 @@ dependencies = [ "solana-version", "solana-vote", "solana-vote-program", + "solana-wincode-varint", "static_assertions", "thiserror 2.0.18", "wincode", @@ -7447,9 +6936,9 @@ dependencies = [ [[package]] name = "solana-hard-forks" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52fd9cc610fd0782f09482527cb7b4f41ec22071303742718b7b57fc43bb236b" +checksum = "45406eccad36220e52988b024d8daa93e691e38d5d71ad5fec55410cc9cf427d" dependencies = [ "serde", "serde_derive", @@ -7461,14 +6950,14 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "337c246447142f660f778cf6cb582beba8e28deb05b3b24bfb9ffd7c562e5f41" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] name = "solana-hash" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b113239362cee7093bfb250467138f079a2a03673181dc15bff6ccd677912d" +checksum = "fe51db00ac3aa9f950d1e6201a126acfa26e6d81bc4a183ba64ec02effcad883" dependencies = [ "bytemuck", "bytemuck_derive", @@ -7488,9 +6977,9 @@ checksum = "7ce934b02bab639341f2fd62c3e7e3c39dcceb47f0196b7630ed1f82ecb704bd" [[package]] name = "solana-inflation" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f762559c5f962727efdcb03c61f5cf6c5364645695978fb145d25c88bbacdada" +checksum = "ccf104167e42e747602b88e02b25cacfc5de699c3b7cbba60d3250437e6a22ed" dependencies = [ "serde", "serde_derive", @@ -7503,7 +6992,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ebb0ffd19263051bc3f683fcc086134b8ff23af894dcb63f7563c7137b42f1" dependencies = [ "bincode", - "borsh", "serde", "serde_derive", "solana-define-syscall 5.1.0", @@ -7514,9 +7002,9 @@ dependencies = [ [[package]] name = "solana-instruction-error" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b188842592fdf6cb96f55263ae1bf11713ab5114401d1d5a881ed7cc41bef6" +checksum = "3b7d34343838343a3755b7dfb1e438d94c6db2263b519cfe3c2257af932b6e93" dependencies = [ "num-traits", "serde", @@ -7527,16 +7015,32 @@ dependencies = [ [[package]] name = "solana-instructions-sysvar" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddf67876c541aa1e21ee1acae35c95c6fbc61119814bfef70579317a5e26955" +checksum = "9e0732294560e88ecdb2bbc656e67383e9f88c78ec09469cef172f0d28cd1bcd" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", + "solana-account-info", + "solana-instruction", + "solana-instruction-error", + "solana-program-error", + "solana-sanitize", + "solana-sdk-ids", + "solana-serialize-utils", + "solana-sysvar-id", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e38363a181313d607f7a118df2b401bb27b477a0104b09283cbf504f5f0f00cc" +dependencies = [ + "bitflags 2.13.0", "solana-account-info", "solana-instruction", "solana-instruction-error", "solana-program-error", - "solana-pubkey 3.0.0", "solana-sanitize", "solana-sdk-ids", "solana-serialize-utils", @@ -7551,7 +7055,7 @@ checksum = "ed1c0d16d6fdeba12291a1f068cdf0d479d9bff1141bf44afd7aa9d485f65ef8" dependencies = [ "sha3", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -7565,7 +7069,7 @@ dependencies = [ "five8", "five8_core", "rand 0.9.4", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-derivation-path", "solana-seed-derivable", "solana-seed-phrase", @@ -7575,12 +7079,13 @@ dependencies = [ [[package]] name = "solana-last-restart-slot" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" +checksum = "c22474b83d3c7c318e1c3a725784fc2d1d03b728e36369e58ce48769a61ed85e" dependencies = [ "serde", "serde_derive", + "solana-get-sysvar", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -7588,7 +7093,7 @@ dependencies = [ [[package]] name = "solana-lattice-hash" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "blake3", @@ -7598,7 +7103,7 @@ dependencies = [ [[package]] name = "solana-leader-schedule" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-random", "itertools 0.14.0", @@ -7610,7 +7115,7 @@ dependencies = [ [[package]] name = "solana-ledger" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-reserved-account-keys", @@ -7618,8 +7123,7 @@ dependencies = [ "agave-votor-messages", "anyhow", "assert_matches", - "bincode", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "bzip2", "chrono", @@ -7636,7 +7140,7 @@ dependencies = [ "mockall", "num_cpus", "num_enum", - "prost 0.14.3", + "prost", "qualifier_attr", "rand 0.9.4", "rand_chacha 0.9.0", @@ -7645,7 +7149,7 @@ dependencies = [ "rocksdb", "scopeguard", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-accounts-db", "solana-address-lookup-table-interface", @@ -7655,7 +7159,7 @@ dependencies = [ "solana-epoch-schedule", "solana-genesis-config", "solana-genesis-utils", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-leader-schedule", @@ -7743,99 +7247,30 @@ dependencies = [ "solana-sdk-ids", ] -[[package]] -name = "solana-local-cluster" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "agave-logger", - "agave-snapshots", - "agave-votor", - "agave-votor-messages", - "crossbeam-channel", - "itertools 0.14.0", - "log", - "rand 0.9.4", - "rayon", - "solana-account 4.3.0", - "solana-accounts-db", - "solana-client", - "solana-client-traits", - "solana-clock", - "solana-cluster-type", - "solana-commitment-config", - "solana-core", - "solana-entry", - "solana-epoch-schedule", - "solana-feature-gate-interface", - "solana-fee-structure", - "solana-genesis-config", - "solana-gossip", - "solana-hard-forks", - "solana-hash 4.3.0", - "solana-keypair", - "solana-leader-schedule", - "solana-ledger", - "solana-message", - "solana-native-token", - "solana-net-utils", - "solana-poh-config", - "solana-program-binaries", - "solana-pubkey 4.2.0", - "solana-pubsub-client", - "solana-quic-client", - "solana-rent", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-runtime", - "solana-sdk-ids", - "solana-shred-version", - "solana-signer", - "solana-slot-hashes", - "solana-stake-interface", - "solana-streamer", - "solana-system-interface 3.2.0", - "solana-system-transaction", - "solana-time-utils", - "solana-tpu-client", - "solana-transaction", - "solana-transaction-error", - "solana-turbine", - "solana-validator-exit", - "solana-vote", - "solana-vote-interface", - "solana-vote-program", - "static_assertions", - "strum", - "tempfile", - "tokio-util 0.7.18", -] - [[package]] name = "solana-measure" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "solana-merkle-tree" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "fast-math", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", ] [[package]] name = "solana-message" -version = "4.1.1" +version = "4.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a634d1db65a393d4e87ec49ef97c8dfb12201e2ba9e5faf87ff34e632f29e509" +checksum = "d2666e0ff38557ad1239d0fc42f4688fb9df0817fb12c041dbae90c2b2e3fa55" dependencies = [ "blake3", - "lazy_static", "serde", "serde_derive", - "solana-address 2.6.0", - "solana-hash 4.3.0", + "solana-address 2.6.1", + "solana-hash 4.4.0", "solana-instruction", "solana-sanitize", "solana-sdk-ids", @@ -7846,7 +7281,7 @@ dependencies = [ [[package]] name = "solana-metrics" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "crossbeam-channel", "gethostname", @@ -7875,11 +7310,13 @@ checksum = "ae8dd4c280dca9d046139eb5b7a5ac9ad10403fbd64964c7d7571214950d758f" [[package]] name = "solana-net-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-xdp", "bincode", "bytes", "cfg-if 1.0.4", + "crossbeam-channel", "dashmap", "itertools 0.14.0", "log", @@ -7909,7 +7346,7 @@ dependencies = [ "serde", "serde_derive", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-sha256-hasher", "wincode", @@ -7921,8 +7358,8 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda945f4ccb317791c26379ca8ec410c5938aa7a5eff211fe5fe0bb428c3a75f" dependencies = [ - "solana-account 4.3.0", - "solana-hash 4.3.0", + "solana-account 4.3.1", + "solana-hash 4.4.0", "solana-nonce", "solana-sdk-ids", "wincode", @@ -7930,10 +7367,11 @@ dependencies = [ [[package]] name = "solana-nullable" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90429a75d69fdcb31952c3dea79f5f3c8157cfe88221e066103c9c237876073" +checksum = "a0f95d3028ef0f682bb174b77379c19d5dae2904a649f4a103fe29be7a139980" dependencies = [ + "borsh", "bytemuck", ] @@ -7958,7 +7396,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edf2f25743c95229ac0fdc32f8f5893ef738dbf332c669e9861d33ddb0f469d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -7968,7 +7406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad62e1045c2347a0c0e219a6ceb0abfe904be622920996bfcac8d116fabe3c7" dependencies = [ "bincode", - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg_eval", "serde", "serde_derive", @@ -7978,7 +7416,7 @@ dependencies = [ [[package]] name = "solana-perf" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-transaction-view", "ahash 0.8.12", @@ -7994,12 +7432,13 @@ dependencies = [ "rayon", "serde", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-metrics", "solana-packet 4.1.0", "solana-pubkey 4.2.0", + "solana-runtime-transaction", "solana-sdk-ids", "solana-short-vec", "solana-signature", @@ -8011,21 +7450,22 @@ dependencies = [ "solana-transaction-context", "solana-vote", "solana-vote-program", + "wincode", ] [[package]] name = "solana-poh" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-votor-messages", "arc-swap", - "core_affinity", "crossbeam-channel", "log", "qualifier_attr", "solana-clock", "solana-entry", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-leader-schedule", "solana-ledger", "solana-measure", @@ -8083,10 +7523,10 @@ dependencies = [ [[package]] name = "solana-program-binaries" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-loader-v3-interface", "solana-pubkey 4.2.0", "solana-rent", @@ -8141,7 +7581,7 @@ dependencies = [ [[package]] name = "solana-program-runtime" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "bincode", @@ -8151,13 +7591,13 @@ dependencies = [ "percentage", "rand 0.9.4", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-clock", "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-structure", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-last-restart-slot", "solana-loader-v3-interface", @@ -8182,67 +7622,7 @@ dependencies = [ "solana-sysvar-id", "solana-transaction-context", "thiserror 2.0.18", -] - -[[package]] -name = "solana-program-test" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "agave-logger", - "assert_matches", - "async-trait", - "base64 0.22.1", - "bincode", - "chrono-humanize", - "log", - "serde", - "solana-account 4.3.0", - "solana-account-info", - "solana-accounts-db", - "solana-address 2.6.0", - "solana-banks-client", - "solana-banks-interface", - "solana-banks-server", - "solana-clock", - "solana-cluster-type", - "solana-commitment-config", - "solana-compute-budget", - "solana-epoch-rewards", - "solana-epoch-schedule", - "solana-fee-calculator", - "solana-genesis-config", - "solana-hash 4.3.0", - "solana-instruction", - "solana-keypair", - "solana-loader-v3-interface", - "solana-message", - "solana-msg", - "solana-native-token", - "solana-poh-config", - "solana-program-binaries", - "solana-program-entrypoint", - "solana-program-error", - "solana-program-runtime", - "solana-pubkey 4.2.0", - "solana-rent", - "solana-runtime", - "solana-sbpf", - "solana-sdk-ids", - "solana-signer", - "solana-stake-interface", - "solana-svm-log-collector", - "solana-svm-timings", - "solana-system-interface 3.2.0", - "solana-sysvar", - "solana-sysvar-id", - "solana-transaction", - "solana-transaction-context", - "solana-transaction-error", - "solana-vote-program", - "spl-generic-token", - "thiserror 2.0.18", - "tokio", + "wincode", ] [[package]] @@ -8261,12 +7641,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7db719574990de7e8b0f55a8593ac92a5ccb42c8ce67b3e4bf05b139d5d9ee71" dependencies = [ "rand 0.9.4", - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] name = "solana-pubsub-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "crossbeam-channel", "futures-util", @@ -8288,7 +7668,7 @@ dependencies = [ [[package]] name = "solana-quic-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-lock", "async-trait", @@ -8312,7 +7692,7 @@ dependencies = [ [[package]] name = "solana-rayon-threadlimit" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "log", "num_cpus", @@ -8320,7 +7700,7 @@ dependencies = [ [[package]] name = "solana-remote-wallet" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "console 0.16.3", "dialoguer", @@ -8341,12 +7721,13 @@ dependencies = [ [[package]] name = "solana-rent" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9809b081e99bc142ce803bcd7ee18306759ce3b30a96a9da3f6f41c45e50ef0" +checksum = "39f0d780bf8e8a1fe8b5b5fce1acad6b209485b86dec246e7523d5e4a8b7c7fc" dependencies = [ "serde", "serde_derive", + "solana-get-sysvar", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -8365,10 +7746,11 @@ dependencies = [ [[package]] name = "solana-rpc" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-snapshots", + "agave-votor-messages", "base64 0.22.1", "bs58", "crossbeam-channel", @@ -8386,7 +7768,7 @@ dependencies = [ "serde", "serde_json", "soketto", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-accounts-db", "solana-cli-output", @@ -8400,8 +7782,9 @@ dependencies = [ "solana-faucet", "solana-genesis-config", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-message", @@ -8445,8 +7828,9 @@ dependencies = [ [[package]] name = "solana-rpc-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-votor-messages", "async-trait", "base64 0.22.1", "bincode", @@ -8459,15 +7843,16 @@ dependencies = [ "semver", "serde", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-account-decoder-client-types", + "solana-bls-signatures", "solana-clock", "solana-commitment-config", "solana-epoch-info", "solana-epoch-schedule", "solana-feature-gate-interface", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-message", "solana-pubkey 4.2.0", @@ -8479,11 +7864,12 @@ dependencies = [ "solana-version", "solana-vote-interface", "tokio", + "wincode", ] [[package]] name = "solana-rpc-client-api" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "anyhow", "jsonrpc-core", @@ -8501,11 +7887,11 @@ dependencies = [ [[package]] name = "solana-rpc-client-nonce-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-nonce", "solana-pubkey 4.2.0", @@ -8516,7 +7902,7 @@ dependencies = [ [[package]] name = "solana-rpc-client-types" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "bs58", @@ -8524,7 +7910,7 @@ dependencies = [ "serde", "serde_json", "solana-account-decoder-client-types", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-clock", "solana-commitment-config", "solana-fee-calculator", @@ -8539,7 +7925,7 @@ dependencies = [ [[package]] name = "solana-runtime" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-bls-cert-verify", "agave-feature-set", @@ -8555,8 +7941,10 @@ dependencies = [ "assert_matches", "base64 0.22.1", "bincode", + "bitvec", "bytemuck", "crossbeam-channel", + "crossbeam-utils", "dashmap", "imbl", "itertools 0.14.0", @@ -8565,7 +7953,6 @@ dependencies = [ "mockall", "num-derive", "num-traits", - "num_cpus", "percentage", "qualifier_attr", "rand 0.9.4", @@ -8576,7 +7963,7 @@ dependencies = [ "serde_json", "serde_with", "smallvec", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-accounts-db", "solana-address-lookup-table-interface", @@ -8603,7 +7990,7 @@ dependencies = [ "solana-fee-structure", "solana-genesis-config", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-instruction", "solana-keypair", @@ -8661,7 +8048,6 @@ dependencies = [ "static_assertions", "strum", "strum_macros", - "symlink", "tempfile", "thiserror 2.0.18", "wincode", @@ -8669,13 +8055,13 @@ dependencies = [ [[package]] name = "solana-runtime-transaction" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-transaction-view", "solana-compute-budget", "solana-compute-budget-instruction", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-program-entrypoint", "solana-pubkey 4.2.0", @@ -8696,9 +8082,9 @@ checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" [[package]] name = "solana-sbpf" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b120fcbbc0d8562997183bef32c76a28400ecae7d8c9fff279d33002783a29" +checksum = "d777d7a89267dd133e985113c7e7f820fb7cfd9123a4a350cf8b39ebae1920bc" dependencies = [ "byteorder", "combine 3.8.1", @@ -8718,7 +8104,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] @@ -8786,19 +8172,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc905b200a95f2ea9146e43f2a7181e3aeb55de6bc12afb36462d00a3c7310de" dependencies = [ "hmac 0.12.1", - "pbkdf2 0.11.0", + "pbkdf2", "sha2 0.10.9", ] [[package]] name = "solana-send-transaction-service" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-trait", "crossbeam-channel", "itertools 0.14.0", "log", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-metrics", @@ -8833,9 +8219,9 @@ dependencies = [ [[package]] name = "solana-serialize-utils" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cc401931d178472358e6b78dc72d031dc08f752d7410f0e8bd259dd6f02fa" +checksum = "761357b0853c9623bf12c1d2314b3d6160a85b087b84c45224fb85766d22616b" dependencies = [ "solana-instruction-error", "solana-pubkey 4.2.0", @@ -8850,7 +8236,7 @@ checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" dependencies = [ "sha2 0.10.9", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -8866,9 +8252,9 @@ dependencies = [ [[package]] name = "solana-short-vec" -version = "3.2.1" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb8cc883fc7b8ce4a7814cb1441b48c06437049ec11847005cf63bcfa85c546" +checksum = "7d8250a4495aad49ad20556a607da53bdcb20de78da10b65afbf918b7f1de647" dependencies = [ "serde_core", "wincode", @@ -8881,15 +8267,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6c79722e299d957958bf33695f7cd1ef6724ff55563c60fd9e3e24487cccde2" dependencies = [ "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", ] [[package]] name = "solana-signature" -version = "3.4.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a73c6e97cc2108be0adf6a6ea326434f8398df9d7eed81da2a4548b69e971c" +checksum = "b0364c7577c3c82a693ce28a1febc8d1b5d1b0a175fdc2114ae6186b69effe1e" dependencies = [ "ed25519-dalek 2.2.0", "five8", @@ -8902,11 +8288,11 @@ dependencies = [ [[package]] name = "solana-signer" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bfea97951fee8bae0d6038f39a5efcb6230ecdfe33425ac75196d1a1e3e3235" +checksum = "520bd6021163ee517f4bdc7ae03ded904f97e11320001ba0b3355f45eb14f558" dependencies = [ - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", "solana-signature", "solana-transaction-error", ] @@ -8924,15 +8310,17 @@ dependencies = [ [[package]] name = "solana-slot-hashes" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2585f70191623887329dfb5078da3a00e15e3980ea67f42c2e10b07028419f43" +checksum = "5c7ce2b4b8911bf2db3de7b6266e67bfc21a6a9f8c566fb096d9782ca2ad16ee" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-get-sysvar", + "solana-hash 4.4.0", "solana-sdk-ids", "solana-sysvar-id", + "wincode", ] [[package]] @@ -8946,6 +8334,7 @@ dependencies = [ "serde_derive", "solana-sdk-ids", "solana-sysvar-id", + "wincode", ] [[package]] @@ -8960,11 +8349,10 @@ dependencies = [ [[package]] name = "solana-stake-interface" -version = "3.1.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49eb5c77484214c3484921e2cdda79d185373118b5458c1b2df0f1a04c3bc30" +checksum = "252b4291eb25a5a356149ed4d4a940d5f655186210fa84b5d0d8d55c3dd42a81" dependencies = [ - "borsh", "num-traits", "serde", "serde_derive", @@ -8976,11 +8364,12 @@ dependencies = [ "solana-system-interface 3.2.0", "solana-sysvar", "solana-sysvar-id", + "wincode", ] [[package]] name = "solana-storage-bigtable" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-reserved-account-keys", "base64 0.22.1", @@ -8989,11 +8378,11 @@ dependencies = [ "flate2", "futures 0.3.32", "goauth", - "http 1.4.0", + "http 1.4.2", "hyper-util", "log", - "prost 0.14.3", - "prost-types 0.14.3", + "prost", + "prost-types", "serde", "smpl_jwt", "solana-clock", @@ -9016,14 +8405,14 @@ dependencies = [ [[package]] name = "solana-storage-proto" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bs58", - "prost 0.14.3", + "prost", "protobuf-src", "serde", "solana-account-decoder", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-message", "solana-pubkey 4.2.0", @@ -9040,7 +8429,7 @@ dependencies = [ [[package]] name = "solana-streamer" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-xdp", "bytes", @@ -9054,12 +8443,12 @@ dependencies = [ "nix", "num_cpus", "pem", - "percentage", "quinn", "rand 0.9.4", "rustls", "smallvec", "solana-keypair", + "solana-measure", "solana-metrics", "solana-net-utils", "solana-packet 4.1.0", @@ -9076,24 +8465,29 @@ dependencies = [ [[package]] name = "solana-svm" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "ahash 0.8.12", "log", "percentage", "qualifier_attr", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", + "solana-bpf-loader-program", "solana-clock", + "solana-compute-budget", + "solana-compute-budget-program", "solana-fee-structure", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", - "solana-instructions-sysvar", + "solana-instruction-error", + "solana-instructions-sysvar 4.0.0", "solana-loader-v3-interface", "solana-loader-v4-interface", "solana-message", "solana-nonce", "solana-nonce-account", + "solana-precompile-error", "solana-program-entrypoint", "solana-program-pack", "solana-program-runtime", @@ -9107,7 +8501,9 @@ dependencies = [ "solana-svm-timings", "solana-svm-transaction", "solana-svm-type-overrides", + "solana-syscalls", "solana-system-interface 3.2.0", + "solana-system-program", "solana-sysvar-id", "solana-transaction-context", "solana-transaction-error", @@ -9117,9 +8513,9 @@ dependencies = [ [[package]] name = "solana-svm-callback" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-clock", "solana-precompile-error", "solana-pubkey 4.2.0", @@ -9127,83 +8523,22 @@ dependencies = [ [[package]] name = "solana-svm-feature-set" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "solana-svm-log-collector" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "log", ] [[package]] name = "solana-svm-measure" -version = "4.2.0-alpha.0" - -[[package]] -name = "solana-svm-test-harness" -version = "4.2.0-alpha.0" -dependencies = [ - "clap 4.6.1", - "prost 0.14.3", - "solana-svm-test-harness-fixture", - "solana-svm-test-harness-instr", -] - -[[package]] -name = "solana-svm-test-harness-fixture" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "bincode", - "prost 0.14.3", - "prost-build 0.14.3", - "protosol", - "solana-account 4.3.0", - "solana-instruction", - "solana-instruction-error", - "solana-pubkey 4.2.0", - "solana-svm-feature-set", - "thiserror 2.0.18", -] - -[[package]] -name = "solana-svm-test-harness-instr" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "agave-precompiles", - "bincode", - "env_logger", - "prost 0.14.3", - "solana-account 4.3.0", - "solana-bpf-loader-program", - "solana-clock", - "solana-compute-budget", - "solana-compute-budget-program", - "solana-instruction", - "solana-instruction-error", - "solana-precompile-error", - "solana-program-runtime", - "solana-pubkey 4.2.0", - "solana-rent", - "solana-sdk-ids", - "solana-svm", - "solana-svm-callback", - "solana-svm-feature-set", - "solana-svm-log-collector", - "solana-svm-test-harness-fixture", - "solana-svm-timings", - "solana-svm-transaction", - "solana-syscalls", - "solana-system-program", - "solana-sysvar-id", - "solana-transaction-context", -] +version = "4.3.0-alpha.0" [[package]] name = "solana-svm-timings" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "eager", "enum-iterator", @@ -9212,9 +8547,11 @@ dependencies = [ [[package]] name = "solana-svm-transaction" -version = "4.2.0-alpha.0" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736d2cec944c6f8f298a7bd8ada5eb318cca9ab859281b1300842fa0e9a25afa" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-pubkey 4.2.0", "solana-sdk-ids", @@ -9224,27 +8561,27 @@ dependencies = [ [[package]] name = "solana-svm-type-overrides" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "rand 0.9.4", ] [[package]] name = "solana-syscalls" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", "libsecp256k1", "num-traits", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-blake3-hasher", "solana-bls12-381-syscall", "solana-bn254", "solana-clock", "solana-cpi", - "solana-curve25519 4.0.0", - "solana-hash 4.3.0", + "solana-curve25519", + "solana-hash 4.4.0", "solana-hash-512", "solana-instruction", "solana-keccak-hasher", @@ -9266,6 +8603,7 @@ dependencies = [ "solana-sysvar-id", "solana-transaction-context", "thiserror 2.0.18", + "wincode", ] [[package]] @@ -9292,7 +8630,7 @@ dependencies = [ "num-traits", "serde", "serde_derive", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-instruction", "solana-msg", "solana-program-error", @@ -9301,11 +8639,11 @@ dependencies = [ [[package]] name = "solana-system-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", "log", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bincode", "solana-fee-calculator", "solana-instruction", @@ -9327,7 +8665,7 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a39522012be4127884bc611d65f58f1cf33a3afb7baee7e6774be90b48edc3c" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-pubkey 4.2.0", @@ -9353,7 +8691,7 @@ dependencies = [ "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-last-restart-slot", "solana-program-entrypoint", @@ -9374,63 +8712,10 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-sdk-ids", ] -[[package]] -name = "solana-test-validator" -version = "4.2.0-alpha.0" -dependencies = [ - "agave-feature-set", - "agave-snapshots", - "arc-swap", - "base64 0.22.1", - "bincode", - "crossbeam-channel", - "log", - "serde_json", - "solana-account 4.3.0", - "solana-accounts-db", - "solana-cli-output", - "solana-clock", - "solana-cluster-type", - "solana-commitment-config", - "solana-compute-budget", - "solana-core", - "solana-epoch-schedule", - "solana-feature-gate-interface", - "solana-fee-calculator", - "solana-genesis-utils", - "solana-geyser-plugin-manager", - "solana-gossip", - "solana-inflation", - "solana-instruction", - "solana-keypair", - "solana-ledger", - "solana-loader-v3-interface", - "solana-message", - "solana-native-token", - "solana-net-utils", - "solana-program-binaries", - "solana-program-runtime", - "solana-program-test", - "solana-pubkey 4.2.0", - "solana-rent", - "solana-rpc", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-runtime", - "solana-sbpf", - "solana-sdk-ids", - "solana-signer", - "solana-streamer", - "solana-syscalls", - "solana-transaction", - "solana-validator-exit", - "tokio", -] - [[package]] name = "solana-time-utils" version = "3.0.0" @@ -9439,8 +8724,9 @@ checksum = "0ced92c60aa76ec4780a9d93f3bd64dfa916e1b998eacc6f1c110f3f444f02c9" [[package]] name = "solana-tls-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "quinn", "rustls", "solana-keypair", "solana-pubkey 4.2.0", @@ -9448,39 +8734,9 @@ dependencies = [ "x509-parser", ] -[[package]] -name = "solana-tps-client" -version = "4.2.0-alpha.0" -dependencies = [ - "log", - "solana-account 4.3.0", - "solana-client", - "solana-client-traits", - "solana-clock", - "solana-commitment-config", - "solana-connection-cache", - "solana-epoch-info", - "solana-hash 4.3.0", - "solana-keypair", - "solana-message", - "solana-net-utils", - "solana-pubkey 4.2.0", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-runtime", - "solana-signature", - "solana-signer", - "solana-streamer", - "solana-tpu-client", - "solana-transaction", - "solana-transaction-error", - "solana-transaction-status", - "thiserror 2.0.18", -] - [[package]] name = "solana-tpu-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "futures-util", "indicatif", @@ -9491,6 +8747,7 @@ dependencies = [ "solana-commitment-config", "solana-connection-cache", "solana-epoch-schedule", + "solana-leader-schedule", "solana-message", "solana-pubkey 4.2.0", "solana-pubsub-client", @@ -9507,7 +8764,7 @@ dependencies = [ [[package]] name = "solana-tpu-client-next" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-trait", "futures 0.3.32", @@ -9519,6 +8776,7 @@ dependencies = [ "solana-clock", "solana-commitment-config", "solana-keypair", + "solana-leader-schedule", "solana-measure", "solana-metrics", "solana-packet 4.1.0", @@ -9535,14 +8793,14 @@ dependencies = [ [[package]] name = "solana-transaction" -version = "4.1.1" +version = "4.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a98253cefd62dea714b67926d3d918c3b9f7d66993f1f2322ac7549ea991dc3e" +checksum = "2509e70bdce879db3e0f56cf97e40edd53742e8f0e6f34d64c46e7900071b53f" dependencies = [ "serde", "serde_derive", - "solana-address 2.6.0", - "solana-hash 4.3.0", + "solana-address 2.6.1", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-message", @@ -9557,13 +8815,13 @@ dependencies = [ [[package]] name = "solana-transaction-context" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-instruction", - "solana-instructions-sysvar", + "solana-instructions-sysvar 4.0.0", "solana-pubkey 4.2.0", "solana-rent", "solana-sbpf", @@ -9573,9 +8831,9 @@ dependencies = [ [[package]] name = "solana-transaction-error" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a2165ad25b694c654d5395fc7a049452a192376e4c96a7fad05580f6ba5ba1c" +checksum = "a949797bc1ac31836d0070e791083028a8c2e0d493fa4cf51c0bed7a04c65c22" dependencies = [ "serde", "serde_derive", @@ -9586,7 +8844,7 @@ dependencies = [ [[package]] name = "solana-transaction-status" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "Inflector", "agave-reserved-account-keys", @@ -9599,7 +8857,7 @@ dependencies = [ "solana-account-decoder", "solana-address-lookup-table-interface", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-loader-v2-interface", "solana-loader-v3-interface", @@ -9615,6 +8873,7 @@ dependencies = [ "solana-transaction-error", "solana-transaction-status-client-types", "solana-vote-interface", + "solana-zk-sdk-pod", "spl-associated-token-account-interface", "spl-memo-interface", "spl-token-2022-interface", @@ -9622,11 +8881,12 @@ dependencies = [ "spl-token-interface", "spl-token-metadata-interface", "thiserror 2.0.18", + "wincode", ] [[package]] name = "solana-transaction-status-client-types" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "bincode", @@ -9648,12 +8908,11 @@ dependencies = [ [[package]] name = "solana-turbine" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-votor", "agave-votor-messages", - "agave-xdp", "bytes", "crossbeam-channel", "itertools 0.14.0", @@ -9668,8 +8927,9 @@ dependencies = [ "solana-cost-model", "solana-entry", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-metrics", @@ -9694,7 +8954,7 @@ dependencies = [ [[package]] name = "solana-udp-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-trait", "solana-connection-cache", @@ -9706,11 +8966,11 @@ dependencies = [ [[package]] name = "solana-unified-scheduler-logic" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "assert_matches", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-runtime-transaction", "solana-transaction", @@ -9720,7 +8980,7 @@ dependencies = [ [[package]] name = "solana-unified-scheduler-pool" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-banking-stage-ingress-types", "aquamarine", @@ -9756,7 +9016,7 @@ checksum = "c5d2face763df5afeaa9509b9019968860e69cc1531ec8b4a2e6c7b702204d5a" [[package]] name = "solana-version" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "rand 0.9.4", @@ -9764,22 +9024,24 @@ dependencies = [ "serde", "solana-sanitize", "solana-serde-varint", + "solana-wincode-varint", + "wincode", ] [[package]] name = "solana-vote" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", "log", "qualifier_attr", "rand 0.9.4", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bincode", "solana-bls-signatures", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-packet 4.1.0", @@ -9796,9 +9058,9 @@ dependencies = [ [[package]] name = "solana-vote-interface" -version = "6.0.0" +version = "6.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d495cb001d3373229ba523625783e5168707f70a9c38ddc90fbc5b0470d600ea" +checksum = "61843d7be827cac5e025c3a16c1101a34fcdd8cf593f6a82eafdd253bd55a26b" dependencies = [ "bincode", "cfg_eval", @@ -9808,7 +9070,7 @@ dependencies = [ "serde_derive", "serde_with", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-pubkey 4.2.0", @@ -9822,17 +9084,17 @@ dependencies = [ [[package]] name = "solana-vote-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "bincode", "log", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bincode", "solana-bls-signatures", "solana-clock", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-packet 4.1.0", "solana-program-runtime", @@ -9846,33 +9108,57 @@ dependencies = [ ] [[package]] -name = "solana-zero-copy" +name = "solana-wincode-varint" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f52dd8f733a13f6a18e55de83cf97c4c3f5fdf27ea3830bcff0b35313efcc2" +checksum = "6679e72a01dcfe7ff180f0e9d3a0d165e4e9664426930014f2702e7571c259c7" +dependencies = [ + "wincode", +] + +[[package]] +name = "solana-zero-copy" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea15126ebdc7e270c50d43884369af9f51d2308156d46a18e351522a164844d" dependencies = [ - "borsh", "bytemuck", "bytemuck_derive", ] +[[package]] +name = "solana-zk-elgamal-proof-interface" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8da7f01db2148a1dc16261ff1dc6f3930a1e255a33cece4f1b56658694f27f7" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "num-derive", + "num-traits", + "solana-address 2.6.1", + "solana-instruction", + "solana-sdk-ids", + "solana-zk-sdk-pod", +] + [[package]] name = "solana-zk-elgamal-proof-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bytemuck", "solana-instruction", "solana-program-runtime", "solana-sdk-ids", "solana-svm-log-collector", - "solana-zk-sdk 5.0.1", + "solana-zk-sdk", ] [[package]] name = "solana-zk-sdk" -version = "4.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9602bcb1f7af15caef92b91132ec2347e1c51a72ecdbefdaefa3eac4b8711475" +checksum = "09670ff59f60e6ddc2209c2e4353992a9b9f01d4e244f3e9d67bd5146e33d388" dependencies = [ "aes-gcm-siv", "base64 0.22.1", @@ -9880,9 +9166,7 @@ dependencies = [ "bytemuck", "bytemuck_derive", "curve25519-dalek 4.1.3", - "getrandom 0.2.17", - "itertools 0.12.1", - "js-sys", + "itertools 0.14.0", "merlin", "num-derive", "num-traits", @@ -9891,9 +9175,9 @@ dependencies = [ "serde_derive", "serde_json", "sha3", + "solana-address 2.6.1", "solana-derivation-path", "solana-instruction", - "solana-pubkey 3.0.0", "solana-sdk-ids", "solana-seed-derivable", "solana-seed-phrase", @@ -9901,47 +9185,25 @@ dependencies = [ "solana-signer", "subtle", "thiserror 2.0.18", - "wasm-bindgen", "zeroize", ] [[package]] -name = "solana-zk-sdk" -version = "5.0.1" +name = "solana-zk-sdk-pod" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09670ff59f60e6ddc2209c2e4353992a9b9f01d4e244f3e9d67bd5146e33d388" +checksum = "a800583b7a4cea3e851686af162cc6e4712eef97fa91dfa9aca2459b95c84777" dependencies = [ - "aes-gcm-siv", "base64 0.22.1", - "bincode", "bytemuck", "bytemuck_derive", - "curve25519-dalek 4.1.3", - "itertools 0.14.0", - "merlin", - "num-derive", - "num-traits", - "rand 0.8.6", - "serde", - "serde_derive", - "serde_json", - "sha3", - "solana-address 2.6.0", - "solana-derivation-path", - "solana-instruction", - "solana-sdk-ids", - "solana-seed-derivable", - "solana-seed-phrase", - "solana-signature", - "solana-signer", - "subtle", + "solana-nullable", "thiserror 2.0.18", - "zeroize", ] [[package]] name = "solana-zk-token-proof-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-program-runtime", ] @@ -9975,9 +9237,9 @@ dependencies = [ [[package]] name = "spl-discriminator" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48cc11459e265d5b501534144266620289720b4c44522a47bc6b63cd295d2f3" +checksum = "e597c5ff9ed7c74a54dbc47bae2f06e4db8c98f4356ad280200dc11878266db1" dependencies = [ "bytemuck", "solana-program-error", @@ -10019,70 +9281,40 @@ dependencies = [ "solana-pubkey 3.0.0", ] -[[package]] -name = "spl-instruction-padding-interface" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3a77c0c9b83b111ee29bc6aa6eaab54b82e1ed5db40ba9786527b80283c3ef" -dependencies = [ - "num_enum", - "solana-instruction", - "solana-program-error", - "solana-pubkey 3.0.0", -] - [[package]] name = "spl-memo-interface" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4e2aedd58f858337fa609af5ad7100d4a243fdaf6a40d6eb4c28c5f19505d3" +checksum = "3745d384b0afee980d43d62b66c27bdcbbd03507732b8d3626d3413cb72084f2" dependencies = [ "solana-instruction", - "solana-pubkey 3.0.0", -] - -[[package]] -name = "spl-pod" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9c6e142cdf1e7e77f480053ec9f0ce989890768ddf91f619b50f39d1b456f5" -dependencies = [ - "borsh", - "bytemuck", - "bytemuck_derive", - "num-derive", - "num-traits", - "num_enum", - "solana-program-error", - "solana-program-option", - "solana-pubkey 3.0.0", - "solana-zero-copy", - "solana-zk-sdk 4.0.0", - "thiserror 2.0.18", + "solana-pubkey 4.2.0", ] [[package]] name = "spl-token-2022-interface" -version = "2.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fcd81188211f4b3c8a5eba7fd534c7142f9dd026123b3472492782cc72f4dc6" +checksum = "821d96d034ea31c4965d182c742153c491ae0abee531331b55771086c5030d86" dependencies = [ "arrayref", "bytemuck", + "getrandom 0.2.17", "num-derive", "num-traits", "num_enum", "solana-account-info", + "solana-address 2.6.1", "solana-instruction", + "solana-nullable", "solana-program-error", "solana-program-option", "solana-program-pack", - "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-zk-sdk 4.0.0", - "spl-pod", + "solana-zero-copy", + "solana-zk-elgamal-proof-interface", + "solana-zk-sdk-pod", "spl-token-confidential-transfer-proof-extraction", - "spl-token-confidential-transfer-proof-generation", "spl-token-group-interface", "spl-token-metadata-interface", "spl-type-length-value", @@ -10091,32 +9323,21 @@ dependencies = [ [[package]] name = "spl-token-confidential-transfer-proof-extraction" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879a9ebad0d77383d3ea71e7de50503554961ff0f4ef6cbca39ad126e6f6da3a" +checksum = "0c1a845ec724e807643a04f1d43439ee3571dd913848112ac76aa90b850eaf7c" dependencies = [ "bytemuck", "solana-account-info", - "solana-curve25519 3.1.10", + "solana-address 2.6.1", + "solana-curve25519", "solana-instruction", - "solana-instructions-sysvar", + "solana-instructions-sysvar 3.0.1", "solana-msg", "solana-program-error", - "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-zk-sdk 4.0.0", - "spl-pod", - "thiserror 2.0.18", -] - -[[package]] -name = "spl-token-confidential-transfer-proof-generation" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0cd59fce3dc00f563c6fa364d67c3f200d278eae681f4dc250240afcfe044b1" -dependencies = [ - "curve25519-dalek 4.1.3", - "solana-zk-sdk 4.0.0", + "solana-zk-elgamal-proof-interface", + "solana-zk-sdk-pod", "thiserror 2.0.18", ] @@ -10130,7 +9351,7 @@ dependencies = [ "num-derive", "num-traits", "num_enum", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-instruction", "solana-nullable", "solana-program-error", @@ -10141,9 +9362,9 @@ dependencies = [ [[package]] name = "spl-token-interface" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c564ac05a7c8d8b12e988a37d82695b5ba4db376d07ea98bc4882c81f96c7f3" +checksum = "7143c4e676984047a400e2fbd7ac29a1659f2b1b52b897cfde19316b562e4589" dependencies = [ "arrayref", "bytemuck", @@ -10161,38 +9382,38 @@ dependencies = [ [[package]] name = "spl-token-metadata-interface" -version = "0.8.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c467c7c3bd056f8fe60119e7ec34ddd6f23052c2fa8f1f51999098063b72676" +checksum = "3d3d96f175e7022ff200464dfa75a3708a4e9b70c83c4ecd04fe52ee479f4fef" dependencies = [ "borsh", "num-derive", "num-traits", + "num_enum", + "solana-address 2.6.1", "solana-borsh", "solana-instruction", + "solana-nullable", "solana-program-error", - "solana-pubkey 3.0.0", "spl-discriminator", - "spl-pod", "spl-type-length-value", "thiserror 2.0.18", ] [[package]] name = "spl-type-length-value" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca20a1a19f4507a98ca4b28ff5ed54cac9b9d34ed27863e2bde50a3238f9a6ac" +checksum = "2504631748c48d2a937414d64a12dcac4588d34bd07d355d648619c189d29435" dependencies = [ "bytemuck", "num-derive", "num-traits", "num_enum", "solana-account-info", - "solana-msg", "solana-program-error", + "solana-zero-copy", "spl-discriminator", - "spl-pod", "thiserror 2.0.18", ] @@ -10246,7 +9467,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.117", @@ -10322,7 +9543,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cca424247104946a59dacd27eaad296223b7feec3d168a6dd04585183091eb0b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "byteorder", "enum-as-inner", "libc", @@ -10347,41 +9568,6 @@ dependencies = [ "xattr", ] -[[package]] -name = "tarpc" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38a012bed6fb9681d3bf71ffaa4f88f3b4b9ed3198cda6e4c8462d24d4bb80" -dependencies = [ - "anyhow", - "fnv", - "futures 0.3.32", - "humantime", - "opentelemetry", - "pin-project", - "rand 0.8.6", - "serde", - "static_assertions", - "tarpc-plugins", - "thiserror 1.0.69", - "tokio", - "tokio-serde", - "tokio-util 0.6.10", - "tracing", - "tracing-opentelemetry", -] - -[[package]] -name = "tarpc-plugins" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee42b4e559f17bce0385ebf511a7beb67d5cc33c12c96b7f4e9789919d9c10f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "tempfile" version = "3.27.0" @@ -10389,9 +9575,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] @@ -10450,15 +9636,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if 1.0.4", -] - [[package]] name = "threadpool" version = "1.8.1" @@ -10530,23 +9707,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-bip39" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30fd743a02bf35236f6faf99adb03089bb77e91c998dac2c2ad76bb424f668c" -dependencies = [ - "once_cell", - "pbkdf2 0.12.2", - "rand 0.8.6", - "rustc-hash 1.1.0", - "sha2 0.10.9", - "thiserror 1.0.69", - "unicode-normalization", - "wasm-bindgen", - "zeroize", -] - [[package]] name = "tinystr" version = "0.8.2" @@ -10620,22 +9780,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-serde" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911a61637386b789af998ee23f50aa30d5fd7edcec8d6d3dedae5e5815205466" -dependencies = [ - "bincode", - "bytes", - "educe 0.4.23", - "futures-core", - "futures-sink", - "pin-project", - "serde", - "serde_json", -] - [[package]] name = "tokio-stream" version = "0.1.18" @@ -10674,7 +9818,6 @@ dependencies = [ "futures-sink", "log", "pin-project-lite", - "slab", "tokio", ] @@ -10743,7 +9886,7 @@ dependencies = [ "base64 0.22.1", "bytes", "h2", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -10768,7 +9911,7 @@ version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" dependencies = [ - "prettyplease 0.2.37", + "prettyplease", "proc-macro2", "quote", "syn 2.0.117", @@ -10781,7 +9924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", - "prost 0.14.3", + "prost", "tonic", ] @@ -10791,10 +9934,10 @@ version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" dependencies = [ - "prettyplease 0.2.37", + "prettyplease", "proc-macro2", - "prost-build 0.14.3", - "prost-types 0.14.3", + "prost-build", + "prost-types", "quote", "syn 2.0.117", "tempfile", @@ -10827,11 +9970,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "iri-string", @@ -10885,31 +10028,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-opentelemetry" -version = "0.17.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbbe89715c1dbbb790059e2565353978564924ee85017b5fff365c872ff6721f" -dependencies = [ - "once_cell", - "opentelemetry", - "tracing", - "tracing-core", - "tracing-subscriber", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "sharded-slab", - "thread_local", - "tracing-core", ] [[package]] @@ -10946,7 +10064,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http 1.4.2", "httparse", "log", "rand 0.9.4", @@ -11115,12 +10233,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - [[package]] name = "vcpkg" version = "0.2.15" @@ -11188,15 +10300,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" version = "0.2.114" @@ -11256,40 +10359,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" version = "0.3.91" @@ -11337,18 +10406,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "wide" version = "0.7.33" @@ -11392,10 +10449,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "wincode" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37095eb18dd6254c66217edc61a29d83d51f8818de8a2ffe88e4584ad73fb5f9" +checksum = "66d967db7705dc29120bb6e8ce5b5a2e27734ed5976d1c904e95bd238d1c3c5a" dependencies = [ + "bv", "bytes", "pastey", "proc-macro2", @@ -11406,11 +10464,11 @@ dependencies = [ [[package]] name = "wincode-derive" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e262d55d1261f31e2cfe49cc6385a421d14d99faa0526bbe3cc1bda0d3005c62" +checksum = "15ab90b719560d0fda79c74550ad1c948d17b118765942838055ebaf34d67071" dependencies = [ - "darling 0.21.3", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -11720,88 +10778,6 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "prettyplease 0.2.37", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease 0.2.37", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] [[package]] name = "writeable" @@ -11842,7 +10818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.4", + "rustix", ] [[package]] diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index 73978714589..5d3e5381c4a 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -1,17 +1,10 @@ [workspace] -members = [ - "../accounts-db/store-tool", - "../bench-tps", - "../ledger-tool", - "../svm-test-harness", - "../svm-test-harness/fixture", - "../svm-test-harness/instr", -] +members = ["../accounts-db/store-tool", "../ledger-tool"] resolver = "2" [workspace.package] -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" authors = ["Anza Maintainers "] description = "Blockchain, Rebuilt for Scale" repository = "https://github.com/anza-xyz/agave" @@ -28,17 +21,23 @@ check-cfg = [ # Clippy lint configuration that can not be applied in clippy.toml [workspace.lints.clippy] +# Denied lints arithmetic_side_effects = "deny" default_trait_access = "deny" manual_let_else = "deny" +uninlined_format_args = "deny" used_underscore_binding = "deny" +# Allowed lints +new_without_default = "allow" + [workspace.dependencies] -agave-feature-set = { path = "../feature-set", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-logger = { path = "../logger", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-precompiles = { path = "../precompiles", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-reserved-account-keys = { path = "../reserved-account-keys", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -agave-snapshots = { path = "../snapshots", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +agave-feature-set = { path = "../feature-set", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-logger = { path = "../logger", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-precompiles = { path = "../precompiles", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-reserved-account-keys = { path = "../reserved-account-keys", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-snapshots = { path = "../snapshots", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +agave-votor = { path = "../votor", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } ahash = "0.8.11" assert_cmd = "2.2.2" bincode = "1.3.3" @@ -57,100 +56,98 @@ jemallocator = { package = "tikv-jemallocator", version = "0.6.0", features = [ log = "0.4.28" num_cpus = "1.17.0" pretty-hex = "0.4.2" -prost = "0.14.3" -prost-build = "0.14.3" +prost = "0.14.4" +prost-build = "0.14.4" protosol = "2.0.0" rand = "0.9.4" rayon = "1.12.0" -regex = "1.12.3" +regex = "1.12.4" serde = { version = "1.0.228", features = ["derive"] } serde_bytes = "0.11.19" -serde_json = "1.0.149" +serde_json = "1.0.150" serde_yaml = "0.9.34" serial_test = "3.2.0" signal-hook = "0.4.4" -solana-account = "4.3.0" -solana-account-decoder = { path = "../account-decoder", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-accounts-db = { path = "../accounts-db", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-bpf-loader-program = { path = "../programs/bpf_loader", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-builtins = { path = "../builtins", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-clap-utils = { path = "../clap-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-cli-config = { path = "../cli-config", version = "=4.2.0-alpha.0" } -solana-cli-output = { path = "../cli-output", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-client = { path = "../client", version = "=4.2.0-alpha.0" } -solana-clock = "3.0.1" +solana-account = "4.3.1" +solana-account-decoder = { path = "../account-decoder", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-accounts-db = { path = "../accounts-db", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-bpf-loader-program = { path = "../programs/bpf_loader", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-builtins = { path = "../builtins", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-clap-utils = { path = "../clap-utils", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-cli-config = { path = "../cli-config", version = "=4.3.0-alpha.0" } +solana-cli-output = { path = "../cli-output", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-client = { path = "../client", version = "=4.3.0-alpha.0" } +solana-clock = "3.1.1" solana-cluster-type = "3.1.0" solana-commitment-config = "3.0.0" -solana-compute-budget = { path = "../compute-budget", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-compute-budget = { path = "../compute-budget", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-compute-budget-interface = "3.0.0" -solana-compute-budget-program = { path = "../programs/compute-budget", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-connection-cache = { path = "../connection-cache", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } -solana-core = { path = "../core", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-cost-model = { path = "../cost-model", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-entry = { path = "../entry", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-faucet = { path = "../faucet", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-compute-budget-program = { path = "../programs/compute-budget", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-connection-cache = { path = "../connection-cache", version = "=4.3.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } +solana-core = { path = "../core", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-cost-model = { path = "../cost-model", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-entry = { path = "../entry", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-faucet = { path = "../faucet", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-feature-gate-interface = "4.0.0" -solana-fee-calculator = "3.2.0" -solana-genesis = { path = "../genesis", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-fee-calculator = "3.2.2" +solana-genesis = { path = "../genesis", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-genesis-config = "4.0.0" -solana-genesis-utils = { path = "../genesis-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-geyser-plugin-manager = { path = "../geyser-plugin-manager", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-hash = "4.3.0" -solana-inflation = "3.1.0" +solana-genesis-utils = { path = "../genesis-utils", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-geyser-plugin-manager = { path = "../geyser-plugin-manager", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-hash = "4.4.0" +solana-inflation = "3.1.1" solana-instruction = "3.4.0" -solana-instruction-error = "2.3.0" +solana-instruction-error = "2.4.0" solana-keypair = "3.1.2" -solana-ledger = { path = "../ledger", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-ledger = { path = "../ledger", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-loader-v3-interface = "7.0.0" solana-loader-v4-interface = "3.1.0" -solana-local-cluster = { path = "../local-cluster", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-measure = { path = "../measure", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-local-cluster = { path = "../local-cluster", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-measure = { path = "../measure", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-message = "4.1.0" -solana-metrics = { path = "../metrics", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-metrics = { path = "../metrics", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-native-token = "3.0.0" -solana-net-utils = { path = "../net-utils", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-net-utils = { path = "../net-utils", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-nonce = "3.2.0" solana-precompile-error = "3.0.0" -solana-program-runtime = { path = "../program-runtime", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-program-runtime = { path = "../program-runtime", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-pubkey = { version = "4.2.0", default-features = false } -solana-quic-client = { path = "../quic-client", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-rent = "4.2.0" -solana-rpc = { path = "../rpc", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-rpc-client = { path = "../rpc-client", version = "=4.2.0-alpha.0", default-features = false } -solana-rpc-client-api = { path = "../rpc-client-api", version = "=4.2.0-alpha.0" } -solana-runtime = { path = "../runtime", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-runtime-transaction = { path = "../runtime-transaction", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-sbpf = { version = "=0.20.0", default-features = false } +solana-quic-client = { path = "../quic-client", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-rent = "4.2.1" +solana-rpc = { path = "../rpc", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-rpc-client = { path = "../rpc-client", version = "=4.3.0-alpha.0", default-features = false } +solana-rpc-client-api = { path = "../rpc-client-api", version = "=4.3.0-alpha.0" } +solana-runtime = { path = "../runtime", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-runtime-transaction = { path = "../runtime-transaction", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-sbpf = { version = "=0.21.1", default-features = false } solana-sdk-ids = "3.1.0" solana-shred-version = "3.0.1" -solana-signature = { version = "3.4.0", default-features = false } -solana-signer = "3.0.0" -solana-stake-interface = "3.0.0" -solana-storage-bigtable = { path = "../storage-bigtable", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-streamer = { path = "../streamer", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm = { path = "../svm", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-callback = { path = "../svm-callback", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-feature-set = { path = "../svm-feature-set", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-log-collector = { path = "../svm-log-collector", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-test-harness-fixture = { path = "../svm-test-harness/fixture", version = "=4.2.0-alpha.0" } -solana-svm-test-harness-instr = { path = "../svm-test-harness/instr", version = "=4.2.0-alpha.0" } -solana-svm-timings = { path = "../svm-timings", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-svm-transaction = { path = "../svm-transaction", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-syscalls = { path = "../syscalls", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-signature = { version = "3.4.1", default-features = false } +solana-signer = "3.0.1" +solana-signer-store = "0.1.0" +solana-stake-interface = "4.3.0" +solana-storage-bigtable = { path = "../storage-bigtable", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-streamer = { path = "../streamer", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm = { path = "../svm", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm-callback = { path = "../svm-callback", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm-feature-set = { path = "../svm-feature-set", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm-log-collector = { path = "../svm-log-collector", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm-timings = { path = "../svm-timings", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-svm-transaction = "4.2.0" +solana-syscalls = { path = "../syscalls", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-system-interface = "3.2" -solana-system-program = { path = "../programs/system", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } +solana-system-program = { path = "../programs/system", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } solana-sysvar-id = "3.1.0" -solana-test-validator = { path = "../test-validator", version = "=4.2.0-alpha.0" } +solana-test-validator = { path = "../test-validator", version = "=4.3.0-alpha.0" } solana-time-utils = "3.0.0" -solana-tps-client = { path = "../tps-client", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-tpu-client = { path = "../tpu-client", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } +solana-tpu-client = { path = "../tpu-client", version = "=4.3.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } solana-transaction = "4.1.0" -solana-transaction-context = { path = "../transaction-context", version = "=4.2.0-alpha.0", features = ["agave-unstable-api", "bincode"] } -solana-transaction-status = { path = "../transaction-status", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-unified-scheduler-pool = { path = "../unified-scheduler-pool", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-version = { path = "../version", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-vote = { path = "../vote", version = "=4.2.0-alpha.0", features = ["agave-unstable-api"] } -solana-vote-program = { path = "../programs/vote", version = "=4.2.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } +solana-transaction-context = { path = "../transaction-context", version = "=4.3.0-alpha.0", features = ["agave-unstable-api", "bincode"] } +solana-transaction-status = { path = "../transaction-status", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-unified-scheduler-pool = { path = "../unified-scheduler-pool", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-version = { path = "../version", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-vote = { path = "../vote", version = "=4.3.0-alpha.0", features = ["agave-unstable-api"] } +solana-vote-program = { path = "../programs/vote", version = "=4.3.0-alpha.0", default-features = false, features = ["agave-unstable-api"] } tempfile = "3.23.0" thiserror = "2.0.17" tokio = "1.48.0" diff --git a/docs/.eslintignore b/docs/.eslintignore deleted file mode 100644 index 869a07b8e36..00000000000 --- a/docs/.eslintignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules -build -static -html - -# FIXME -src/pages/index.js -src/theme/Footer/index.js diff --git a/docs/.eslintrc b/docs/.eslintrc deleted file mode 100644 index d2204b0b8d6..00000000000 --- a/docs/.eslintrc +++ /dev/null @@ -1,21 +0,0 @@ -{ - "env": { - "browser": true, - "node": true - }, - "parser": "babel-eslint", - "rules": { - "strict": 0, - "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], - "no-trailing-spaces": ["error", { "skipBlankLines": true }] - }, - "settings": { - "react": { - "version": "detect", // React version. "detect" automatically picks the version you have installed. - } - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended" - ] - } \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index dc83674a20e..00000000000 --- a/docs/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# Dependencies -/node_modules - -# Production -/build - -# Generated files -.docusaurus -.cache-loader -.vercel -/static/img/*.svg -/static/img/*.png -vercel.json - -# use pnpm and pnpm-lock.yaml -yarn.lock -package-lock.json - -# Misc -.DS_Store -.env -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Documentation build artifacts -/html/ -/src/tests.ok -/src/cli/usage.md -/src/.gitbook/assets/*.svg diff --git a/docs/.npmrc b/docs/.npmrc deleted file mode 100644 index e595aad2e6b..00000000000 --- a/docs/.npmrc +++ /dev/null @@ -1 +0,0 @@ -minimum-release-age=10080 diff --git a/docs/.prettierignore b/docs/.prettierignore deleted file mode 100644 index 12ef0727eb2..00000000000 --- a/docs/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -.docusaurus -build -html -static diff --git a/docs/.prettierrc.json b/docs/.prettierrc.json deleted file mode 100644 index d3e25d5eb89..00000000000 --- a/docs/.prettierrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "trailingComma": "all", - "tabWidth": 2, - "semi": true, - "singleQuote": false, - "proseWrap": "always", - "printWidth": 80 -} diff --git a/docs/README.md b/docs/README.md index ebc9ad78ee7..f9e72e94921 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,124 +1,5 @@ -# Solana Validator Docs Readme +# Documentation -This validator's documentation is built using [Docusaurus v2](https://v2.docusaurus.io/) with `npm`. -Static content delivery is handled using `vercel`. +The documentation has moved to its own repository: -> Note: The documentation within this repo is specifically focused on the -> Agave validator client maintained by Anza. Solana protocol documentation, which applies -> to all Solana validator implementations, is maintained within the -> [`developer-content`](https://github.com/solana-foundation/developer-content/) -> repo. Those protocol docs are managed by the Solana Foundation within their -> GitHub organization and are publicly accessible via -> [solana.com/docs](https://solana.com/docs) - -## Local Development - -To set up the Solana Validator Docs site locally: - -- install dependencies using `npm` -- build locally via `./build.sh` -- run the local development server -- make your changes and updates as needed - -> Note: After cloning this repo to your local machine, all the local development commands are run from within this `docs` directory. - -### Install dependencies - -Install the site's dependencies via `npm`: - -```bash -npm install -``` - -### Build locally - -The build script generates static content into the `build` directory and can be served using any static content hosting service. - -```bash -./build.sh -``` - -Running this build script requires **Docker**, and will auto fetch the [solanalabs/rust](https://hub.docker.com/r/solanalabs/rust) image from Docker hub to compile the desired version of the [Solana CLI](https://docs.anza.xyz/cli) from source. - -This build script will also: - -- generate the `cli/usage.md` document from the output of each of the Solana CLI commands and sub-commands -- convert each of the `art/*.bob` files into SVG images used throughout the docs - -> Note: Running this build script is **required** before being able to run the site locally via the `npm run start` command since it will generate the `cli/usage.md` document. - -If you run into errors or issues with this step, see [Common Issues](#common-issues) below. See also [CI Build Flow](#ci-build-flow) for more details on production deployments of the docs. - -### Local development server - -This command starts the Docusaurus local development server and opens up a browser window. - -```bash -npm run start -``` - -> Note: Most changes are reflected live without having to restart the server or refresh the page. However, some changes may require a manual refresh of the page or a restart of the development server (via the command above). - -## CI Build Flow - -The docs are built and published in GitHub Actions with the `docs.yml` workflow. On each PR, the docs are built but not published. - -In each post-commit build, docs are built and published using `vercel` to their respective domain depending on the build branch. - -- Master branch docs are published to `edge.docs.anza.xyz` -- Beta branch docs are published to `beta.docs.anza.xyz` -- Latest release tag docs are published to `docs.anza.xyz` - -## Common Issues - -### Bad sidebars file (or `cli/usage` not found) - -```bash -Error: Bad sidebars file. -These sidebar document ids do not exist: -- cli/usage, -``` - -If you have NOT successfully run the build script, then the `cli/usage.md` file will not exist within your local repo (since it is in `.gitignore`). Not having this doc file, will result in the error message above. - -If the Rust toolchain (specifically `cargo`) is installed on your system, you can specifically build the `cli/usage.md` document via: - -```bash -./build-cli-usage.sh -``` - -Or using Docker and the normal build script, you can perform a full production build of the docs to generate this file: - -```bash -./build.sh -``` - -### Permission denied for the Docker socket - -```bash -Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post -``` - -Running docs build script (`./build.sh`) required the use of Docker.\*\*\*\* - -Ensuring you have Docker installed on your system and it is running. - -You may also try running either of these build scripts (and by association, Docker) with elevation permissions via `sudo`: - -```bash -sudo ./build.sh -# or -sudo ./build-cli-usage.sh -``` - -### Multiple SVG images not found - -```bash -Error: Image static/img/***.svg used in src/***.md not found. -``` - -During the build process of the docs (specifically within the `./convert-ascii-to-svg.sh` script run by `./build.sh`), each of the `art/*.bob` files are converted to SVG images and saved to the `static/img` directory. - -To correct this issue, use the steps above to [build the docs locally](#build-locally). - -> Note: While not generating and saving these SVG images within your local repo will **NOT** prevent you from running the local development server, it will result in numerous output errors in your terminal. +https://github.com/anza-xyz/docs.anza.xyz diff --git a/docs/art/fork-generation.bob b/docs/art/fork-generation.bob deleted file mode 100644 index e8f60e02523..00000000000 --- a/docs/art/fork-generation.bob +++ /dev/null @@ -1,13 +0,0 @@ - validator action - +----+ ---------------- - | | L1 | E1 - | +----+ / \ vote(E1) - | | L2 | E2 x - | +----+ / \ / \ vote(E2) - time | | L3 | E3 x E3' x - | +----+ / \ / \ / \ / \ slash(E3) - | | L4 | x x E4 x x x x x - | +----+ | | | | | | | | vote(E4) - v | L5 | xx xx xx E5 xx xx xx xx - +----+ hang on to E4 and E5 for more... - diff --git a/docs/art/forks-pruned.bob b/docs/art/forks-pruned.bob deleted file mode 100644 index 78a951c3afe..00000000000 --- a/docs/art/forks-pruned.bob +++ /dev/null @@ -1,9 +0,0 @@ - 1 - | - 2 - /| - / | - | | - | 4 - | - 5 diff --git a/docs/art/forks-pruned2.bob b/docs/art/forks-pruned2.bob deleted file mode 100644 index 64e3d4cb1f7..00000000000 --- a/docs/art/forks-pruned2.bob +++ /dev/null @@ -1,11 +0,0 @@ - 1 - | - 3 - |\ - | \ - | | - | | - | | - 6 | - | - 7 diff --git a/docs/art/forks.bob b/docs/art/forks.bob deleted file mode 100644 index 0c335e95cc5..00000000000 --- a/docs/art/forks.bob +++ /dev/null @@ -1,13 +0,0 @@ - 1 - |\ - 2 \ - /| | - / | 3 - | | |\ - | 4 | \ - | | | - 5 | | - | | - 6 | - | - 7 diff --git a/docs/art/passive-staking-callflow.msc b/docs/art/passive-staking-callflow.msc deleted file mode 100644 index ae754411e48..00000000000 --- a/docs/art/passive-staking-callflow.msc +++ /dev/null @@ -1,30 +0,0 @@ -msc { - hscale="2.2"; - VoteSigner, - Validator, - Cluster, - StakerX, - StakerY; - - |||; - Validator box Validator [label="boot.."]; - - VoteSigner <:> Validator [label="register\n\n(optional)"]; - Validator => Cluster [label="VoteState::Initialize(VoteSigner)"]; - StakerX => Cluster [label="StakeStateV2::Delegate(Validator)"]; - StakerY => Cluster [label="StakeStateV2::Delegate(Validator)"]; - - |||; - Validator box Cluster [label="\nvalidate\n"]; - Validator => VoteSigner [label="sign(vote)"]; - VoteSigner >> Validator [label="signed vote"]; - - Validator => Cluster [label="gossip(vote)"]; - ...; - ... ; - Validator abox Validator [label="\nmax\nlockout\n"]; - |||; - Cluster box Cluster [label="credits redeemed (at epoch)"]; - - -} diff --git a/docs/art/retransmit_stage.bob b/docs/art/retransmit_stage.bob deleted file mode 100644 index 425aed753a4..00000000000 --- a/docs/art/retransmit_stage.bob +++ /dev/null @@ -1,45 +0,0 @@ - +------------+ - | Gossip | - | Service | - | | - +------------+ - |ContactInfo - | - +------------------------------------------------------------------------+ - | | | - | Retransmit Stage | | - | | | - | +--------------------------------------+ | - | |Window Service | | | +---------+ - | | | | +---------------+ | +-----------+ | | - Packets | | +----------------+ | | Shreds | | | | Deshredder| | Replay | - +------------------->+ ShredFilter +----------------------------->+ Blockstore +------| (entries) |------>+ Stage | - | | +-----------+-+--+ | | | | | +-----------+ | | -+--------+ | | ^ | | | | | | +---------+ -| | Leader Schedule| | | | | | | | | -| Bank +----------------------------+ | +------+----+ | +-------+-------+ | -| | | | | | Repair | | | | -+--------+ +----------------------------------->+ Service | | Incomplete | | - | | | | | +<----------------------+ | - | | | | +-----------+ | Slots | - | | | | | | - | | +--------------------------------------+ | - | | |Shreds | - | | v | - v | +-------+---------+ | - +--------+-----+ | | | | - | | | | Retransmitter | | - | Peer | | | | | - | Validators +<---------------+ | | - | | | +--------+--------+ | - | | | ^ | - +--------------+ | | | - +------------------------------------------------------------------------+ - | - ContactInfo| - | - +-------+-+ - | Gossip | - | Service | - | | - +---------+ diff --git a/docs/art/runtime.bob b/docs/art/runtime.bob deleted file mode 100644 index 3f56d8d145b..00000000000 --- a/docs/art/runtime.bob +++ /dev/null @@ -1,10 +0,0 @@ -.------------. .-----------. .---------------. .--------------. .-----------------------. -| PoH verify +---> | sigverify +--->| lock accounts +--->| validate fee +--->| allocate new accounts +---> -| TVU | `-----------` `---------------` `--------------` `-----------------------` -`------------` - - .---------------. .---------. .------------. .-----------------. .-----------------. ---->| load accounts +--->| execute +--->| PoH record +--->| commit accounts +-->| unlock accounts | - `---------------` `---------` | TPU | `-----------------` `-----------------` - `------------` - diff --git a/docs/art/sdk-tools.bob b/docs/art/sdk-tools.bob deleted file mode 100644 index 64c1affee6b..00000000000 --- a/docs/art/sdk-tools.bob +++ /dev/null @@ -1,20 +0,0 @@ - - .-------------------------------------. - | Solana Runtime | - | | - .----------. | .------------. .------------. | - | Program | | | SBF | | Executable | | - | Author +------>| Bytecode +-->| Account | | - | | | | Verifier | | | | - `----------` | `------------` `------------` | - | | | - | .----------------` | - | | LoadAccounts | - | V | - .----------. | .------------. .-------------. | - | | | | SBF | | SBF | | - | Client +------>| Loader +-->| Interpreter | | - | | | | | | | | - `----------` | `------------` `-------------` | - | | - `-------------------------------------` diff --git a/docs/art/spv-bank-hash.bob b/docs/art/spv-bank-hash.bob deleted file mode 100644 index fce7870d631..00000000000 --- a/docs/art/spv-bank-hash.bob +++ /dev/null @@ -1,19 +0,0 @@ - +----------+ - | Bank-Hash| - +----------+ - ^ - | - +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ - : : - : +--------------+ +-------------+ : - : Hash( | Accounts-Hash| + | Block-Merkle| ) : - : +--------------+ +-------------+ : - : ^ : - +~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ - | - +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ - : +---------------+ +---------------+ +---------------+ : - : Hash( | Hash(Account1)| + | Hash(Account2)| + ... + | Hash(AccountN)| ) : - : +---------------+ +---------------+ +---------------+ : - +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ - diff --git a/docs/art/spv-block-merkle.bob b/docs/art/spv-block-merkle.bob deleted file mode 100644 index 8b236e6b5ce..00000000000 --- a/docs/art/spv-block-merkle.bob +++ /dev/null @@ -1,19 +0,0 @@ - +---------------+ - | Block-Merkle | - +---------------+ - ^ ^ - / \ - +-------------+ +-------------+ - | Entry-Merkle| | Entry-Merkle| - +-------------+ +-------------+ - ^ ^ - / \ - +-------+ +-------+ - | Hash | | Hash | - +-------+ +-------+ - ^ ^ ^ ^ - / | | \ - +-----------------+ +-----------------+ +-----------------+ +---+ - | Hash(T1, status)| | Hash(T2, status)| | Hash(T3, status)| | 0 | - +-----------------+ +-----------------+ +-----------------+ +---+ - diff --git a/docs/art/tpu.bob b/docs/art/tpu.bob deleted file mode 100644 index 6c431efb077..00000000000 --- a/docs/art/tpu.bob +++ /dev/null @@ -1,25 +0,0 @@ - - .-------------. - | PoH Service | - `--------+----` - ^ | - .--------------------------------|----|--------------------. - | TPU | v | - | .-------. .-----------. .-+-------. .-----------. | .------------. - .---------. | | Fetch | | SigVerify | | Banking | | Broadcast | | | Downstream | - | Clients |--->| Stage |->| Stage |-+->| Stage |->| Stage |---->| Validators | - `---------` | | | | | | | | | | | | | - | `-------` `-----------` | `----+----` `-----------` | `------------` - | v | | - | .--------+---. | | - | | Forwarding | | | - | | Stage | | | - | | | | | - | `------------` | | - | | | - `-----------------------------------|----------------------` - | - v - .------. - | Bank | - `------` diff --git a/docs/art/transaction.bob b/docs/art/transaction.bob deleted file mode 100644 index 90e439e5f7f..00000000000 --- a/docs/art/transaction.bob +++ /dev/null @@ -1,9 +0,0 @@ - - +------------+-----------------------------------------------------+ - | | Message | - | |----------+-------------+-------------+--------------| - | | | | | | - | Signatures | | Account | Recent | | - | | Header | addresses | blockhash | Instructions | - | | | | | | - +------------+----------+-------------+-------------+--------------+ \ No newline at end of file diff --git a/docs/art/tvu.bob b/docs/art/tvu.bob deleted file mode 100644 index c298653478d..00000000000 --- a/docs/art/tvu.bob +++ /dev/null @@ -1,37 +0,0 @@ - +------------+ - | Gossip | - | Service | - +-------------+ | | - |Child | +------------+ +-+-----+----+ - |Validators | |Neighborhood| | ^ - | | |Validators | | | - +-------+-----+ | | | | - ^ +---------+--+ Peer| |Votes - | ^ List| | - +------------------------+ | | | - shreds(forward = true) | | | | - +---------------------------------------------------------------------------------------------+ - | | | | | | - | TVU | | +--------+ | | - | | | v | | -+------------+ | +-------+ +------------+ +-+--+---+---+ +---+--------------+ +-------------+ | -| | Repair | | | | | | Retransmit | | Replay | | Transaction | | -| Upstream +------------->+ +->+ Shred +--->+ Stage +----->+ Stage +----->+ Status | | -| Validators | Turbine | | Shred | | Verify | | | | +--------------+ | | Service | | -| +------------->+ Fetch | | Leader Sig | +------+-----+ | | PoH Verify | | | (optional) | | -| | | | Stage | | Stage | ^ | | TX Sig Verify| | +-------------+ | -| +------------->+ | | | | | | | | | -| | Turbine | | | +--+---------+ | | +-+------------+ | | -+------------+ Forwards | +-------+ ^ | | | | | - | | | +------------------+ | - | | | | | - | | | | | - | | | | | - +---------------------------------------------------------------------------------------------+ - | | | - | |Validator v - | |Stakes +---+-----+ - | +------------+ | - +----------------------------------+ Bank | - Leader | | - Schedule +---------+ diff --git a/docs/art/validator-proposal.bob b/docs/art/validator-proposal.bob deleted file mode 100644 index cde432bbc66..00000000000 --- a/docs/art/validator-proposal.bob +++ /dev/null @@ -1,60 +0,0 @@ - - .------------. - | Upstream | - | Validators | - `----+-------` - | - | - .-----------------------------------. - | Validator | | - | v | - | .-----------. .------------. | - .--------. | | Fetch | | Repair | | - | Client +---->| Stage | | Stage | | - `--------` | `---+-------` `----+-------` | - | | | | - | v v | - | .-----------. .------------. | - | | TPU |<-->| Blockstore | | - | | | | | | - | `-----------` `----+-------` | - | | | - | v | - | .------------. | - | | Multicast | | - | | Stage | | - | `----+-------` | - | | | - `-----------------------------------` - | - v - .------------. - | Downstream | - | Validators | - `------------` - - - - .------------. - | PoH | - | Service | - `-------+----` - ^ | - | | - .-----------------------------------. - | TPU | | | - | | v | - .-------. | .-----------. .---+--------. | .------------. - | Fetch +---->| SigVerify +--->| Banking |<--->| Blockstore | - | Stage | | | Stage | | Stage | | | | - `-------` | `-----------` `-----+------` | `------------` - | | | - | | | - `-----------------------------------` - | - v - .------------. - | Banktree | - | | - `------------` - diff --git a/docs/art/validator.bob b/docs/art/validator.bob deleted file mode 100644 index 87c7610a9a0..00000000000 --- a/docs/art/validator.bob +++ /dev/null @@ -1,30 +0,0 @@ - .---------------------------------------. - | Validator | - | | - .--------. | .-------------------. | - | |---->| | | - | Client | | | JSON RPC Service | | - | |<----| | | - `----+---` | `-------------------` | - | | ^ | - | | | .----------------. | .------------------. - | | | | Gossip Service |<-----------| Validators | - | | | `----------------` | | | - | | | ^ | | | - | | | | | | .------------. | - | | .---+---. .----+---. .------------. | | | | | - | | | Bank |<-+ Replay | | ShredFetch |<------+ Upstream | | - | | | Forks | | Stage | | Stage | | | | Validators | | - | | `-------` `--------` `--+---------` | | | | | - | | ^ ^ | | | `------------` | - | | | | v | | | - | | | .--+---------. | | | - | | | | Blockstore | | | | - | | | `------------` | | .------------. | - | | | ^ | | | | | - | | | | | | | Downstream | | - | | .--+--. .-------+---. | | | Validators | | - `-------->| TPU +---->| Broadcast +---------------->| | | - | `-----` | Stage | | | `------------` | - | `-----------` | `------------------` - `---------------------------------------` diff --git a/docs/babel.config.js b/docs/babel.config.js deleted file mode 100644 index bfd75dbdfc7..00000000000 --- a/docs/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: [require.resolve("@docusaurus/core/lib/babel/preset")], -}; diff --git a/docs/build-cli-usage.sh b/docs/build-cli-usage.sh deleted file mode 100755 index bc0d850337d..00000000000 --- a/docs/build-cli-usage.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash -set -e - -cd "$(dirname "$0")" - -# shellcheck source=ci/env.sh -source ../ci/env.sh - -# Get current channel -eval "$(../ci/channel-info.sh)" - -# set the output file' location -out=${1:-src/cli/usage.md} - -# load the usage file's header -cat src/cli/.usage.md.header > "$out" - -# Skip generating the detailed usage doc for non deployment commits of the docs -if [[ -n $CI ]]; then - if [[ $CI_BRANCH != $EDGE_CHANNEL* ]] && [[ $CI_BRANCH != $BETA_CHANNEL* ]] && [[ $CI_BRANCH != $STABLE_CHANNEL* ]]; then - echo "**NOTE:** The usage doc is only auto-generated during full production deployments of the docs" - echo "**NOTE:** This usage doc is auto-generated during deployments" >> "$out" - exit - fi -fi - -echo 'Building the solana cli from source...' - -# shellcheck source=ci/rust-version.sh -source ../ci/rust-version.sh stable - -: "${rust_stable:=}" # Pacify shellcheck - -usage=$(cargo -q run -p solana-cli -- -C ~/.foo --help | sed -e 's|'"$HOME"'|~|g' -e 's/[[:space:]]\+$//') - -section() { - declare mark=${2:-"###"} - declare section=$1 - read -r name rest <<<"$section" - - printf '%s %s -' "$mark" "$name" - printf '```text -%s -``` - -' "$section" -} - -section "$usage" >> "$out" - -usage=$(sed -e '/^ \{5,\}/d' <<<"$usage") - -in_subcommands=0 -while read -r subcommand rest; do - [[ $subcommand == "SUBCOMMANDS:" ]] && in_subcommands=1 && continue - if ((in_subcommands)); then - section "$(cargo -q run -p solana-cli -- help "$subcommand" | sed -e 's|'"$HOME"'|~|g' -e 's/[[:space:]]\+$//')" "###" >> "$out" - fi -done <<<"$usage">>"$out" diff --git a/docs/build.sh b/docs/build.sh deleted file mode 100755 index 860e12d35f0..00000000000 --- a/docs/build.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -ex - -cd "$(dirname "$0")" - -# shellcheck source=ci/env.sh -source ../ci/env.sh - -# shellcheck source=ci/rust-version.sh -source ../ci/rust-version.sh -../ci/docker-run-default-image.sh docs/build-cli-usage.sh -../ci/docker-run-default-image.sh docs/convert-ascii-to-svg.sh -./set-solana-release-tag.sh - -# Build from /src into /build -pnpm run build -echo $? diff --git a/docs/components/Card.jsx b/docs/components/Card.jsx deleted file mode 100644 index 67989e67dc1..00000000000 --- a/docs/components/Card.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import Link from "@docusaurus/Link"; -import styles from "../src/pages/styles.module.css"; -import Translate from "@docusaurus/Translate"; - -function Card({ to, header, body, externalIcon = false }) { - /* - Both the `header` and `body` expect an object with the following type - header = { - label: String, // - translateId: String // - } - */ - - return ( -
- -
-

- - {header.label} - - {externalIcon && ( - - )} -

-
- - {typeof body === "object" && ( -
-

- {body.label} -

-
- )} - -
- ); -} - -export default Card; diff --git a/docs/components/HomeCtaLinks.jsx b/docs/components/HomeCtaLinks.jsx deleted file mode 100644 index 71d20014adf..00000000000 --- a/docs/components/HomeCtaLinks.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react"; -import Card from "./Card"; - -export default function HomeCtaLinks() { - return ( -
-
- - - - - -
-
- ); -} diff --git a/docs/convert-ascii-to-svg.sh b/docs/convert-ascii-to-svg.sh deleted file mode 100755 index f5abd7b229f..00000000000 --- a/docs/convert-ascii-to-svg.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash - -# Convert .bob and .msc files in docs/art to .svg files located where the -# site build will find them. - -set -e - -cd "$(dirname "$0")" -output_dir=static/img - -svgbob_cli="$(command -v svgbob_cli || true)" -if [[ -z "$svgbob_cli" ]]; then - svgbob_cli="$(command -v svgbob || true)" - [[ -n "$svgbob_cli" ]] || ( echo "svgbob_cli binary not found" && exit 1 ) -fi - -mkdir -p "$output_dir" - -while read -r bob_file; do - out_file=$(basename "${bob_file%.*}".svg) - "$svgbob_cli" "$bob_file" --output "$output_dir/$out_file" -done < <(find art/*.bob) - -while read -r msc_file; do - out_file=$(basename "${msc_file%.*}".png) - mscgen -T png -o "$output_dir/$out_file" -i "$msc_file" -done < <(find art/*.msc) diff --git a/docs/deploy.sh b/docs/deploy.sh deleted file mode 100755 index b0bd9e28b90..00000000000 --- a/docs/deploy.sh +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env bash - -set -eo pipefail - -here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) - -# shellcheck source=ci/env.sh -source "$here/../ci/env.sh" - -if [[ -z $CI ]]; then - echo "Publishing docs from local" - if [[ -z $PROJECT_NAME ]]; then - echo "❌ PROJECT_NAME is undefined" - exit 1 - fi - PROJECT_NAMES=("$PROJECT_NAME") -else - echo "Publishing docs from CI" - - # skip docs publish from pull requests - if [[ -n $CI_PULL_REQUEST ]]; then - echo "skipping docs publish from pull requests" - exit 0 - fi - - # get the channel info - eval "$("$here/../ci/channel-info.sh")" - if [[ -n $CI_TAG ]]; then - if [[ $CI_TAG != $BETA_CHANNEL* ]]; then - echo "skipping docs publish from non-beta tag" - exit 0 - fi - PROJECT_NAMES=("docs-anza-xyz" "beta-docs-anza-xyz") - else - case $CHANNEL in - edge) - PROJECT_NAMES=("edge-docs-anza-xyz") - ;; - beta) - PROJECT_NAMES=("beta-docs-anza-xyz") - ;; - stable) - echo "skipping docs publish from stable channel" - exit 0 - ;; - *) - echo "❌ unknown channel: '$CHANNEL'" - exit 1 - ;; - esac - fi -fi -echo "PROJECT_NAMES: ${PROJECT_NAMES[*]}" - -if [[ -z $VERCEL_TOKEN ]]; then - echo "❌ VERCEL_TOKEN is undefined" - exit 1 -fi - -for PROJECT_NAME in "${PROJECT_NAMES[@]}"; do - echo "Publishing docs for $PROJECT_NAME" - # create the vercel.json file - CONFIG_FILE=vercel.json - cat >"$CONFIG_FILE" <0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } -} diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml deleted file mode 100644 index b03764aa8bf..00000000000 --- a/docs/pnpm-lock.yaml +++ /dev/null @@ -1,11000 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -overrides: - got@<11.8.5: ^11.8.5 - katex@^0.12: ^0.16.21 - minimatch@^3.0.0: ^3.0.5 - path-to-regexp@^1.7.0: ^1.9.0 - serve-handler@^6.1.0: ^6.1.6 - trim@^0.0.1: ^0.0.3 - wait-on@^6.0.0: ^8.0.3 - -importers: - - .: - dependencies: - '@docusaurus/core': - specifier: ^2.4.3 - version: 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-google-gtag': - specifier: ^2.4.3 - version: 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/preset-classic': - specifier: ^2.4.3 - version: 2.4.3(@algolia/client-search@4.14.2)(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-search-algolia': - specifier: ^2.4.3 - version: 2.4.3(@algolia/client-search@4.14.2)(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@mdx-js/react': - specifier: ^1.6.22 - version: 1.6.22(react@16.14.0) - babel-eslint: - specifier: ^10.1.0 - version: 10.1.0(eslint@7.32.0) - clsx: - specifier: ^1.2.1 - version: 1.2.1 - eslint: - specifier: ^7.32.0 - version: 7.32.0 - eslint-plugin-react: - specifier: ^7.37.4 - version: 7.37.4(eslint@7.32.0) - postcss: - specifier: ^8.5.3 - version: 8.5.3 - postcss-loader: - specifier: ^4.3.0 - version: 4.3.0(postcss@8.5.3)(webpack@5.105.0) - prettier: - specifier: ^2.8.8 - version: 2.8.8 - react: - specifier: ^16.14.0 - version: 16.14.0 - react-dom: - specifier: ^16.14.0 - version: 16.14.0(react@16.14.0) - rehype-katex: - specifier: ^4.0.0 - version: 4.0.0 - remark-math: - specifier: ^3.0.1 - version: 3.0.1 - -packages: - - '@algolia/autocomplete-core@1.7.1': - resolution: {integrity: sha512-eiZw+fxMzNQn01S8dA/hcCpoWCOCwcIIEUtHHdzN5TGB3IpzLbuhqFeTfh2OUhhgkE8Uo17+wH+QJ/wYyQmmzg==} - - '@algolia/autocomplete-preset-algolia@1.7.1': - resolution: {integrity: sha512-pJwmIxeJCymU1M6cGujnaIYcY3QPOVYZOXhFkWVM7IxKzy272BwCvMFMyc5NpG/QmiObBxjo7myd060OeTNJXg==} - peerDependencies: - '@algolia/client-search': ^4.9.1 - algoliasearch: ^4.9.1 - - '@algolia/autocomplete-shared@1.7.1': - resolution: {integrity: sha512-eTmGVqY3GeyBTT8IWiB2K5EuURAqhnumfktAEoHxfDY2o7vg2rSnO16ZtIG0fMgt3py28Vwgq42/bVEuaQV7pg==} - - '@algolia/cache-browser-local-storage@4.14.2': - resolution: {integrity: sha512-FRweBkK/ywO+GKYfAWbrepewQsPTIEirhi1BdykX9mxvBPtGNKccYAxvGdDCumU1jL4r3cayio4psfzKMejBlA==} - - '@algolia/cache-common@4.14.2': - resolution: {integrity: sha512-SbvAlG9VqNanCErr44q6lEKD2qoK4XtFNx9Qn8FK26ePCI8I9yU7pYB+eM/cZdS9SzQCRJBbHUumVr4bsQ4uxg==} - - '@algolia/cache-in-memory@4.14.2': - resolution: {integrity: sha512-HrOukWoop9XB/VFojPv1R5SVXowgI56T9pmezd/djh2JnVN/vXswhXV51RKy4nCpqxyHt/aGFSq2qkDvj6KiuQ==} - - '@algolia/client-account@4.14.2': - resolution: {integrity: sha512-WHtriQqGyibbb/Rx71YY43T0cXqyelEU0lB2QMBRXvD2X0iyeGl4qMxocgEIcbHyK7uqE7hKgjT8aBrHqhgc1w==} - - '@algolia/client-analytics@4.14.2': - resolution: {integrity: sha512-yBvBv2mw+HX5a+aeR0dkvUbFZsiC4FKSnfqk9rrfX+QrlNOKEhCG0tJzjiOggRW4EcNqRmaTULIYvIzQVL2KYQ==} - - '@algolia/client-common@4.14.2': - resolution: {integrity: sha512-43o4fslNLcktgtDMVaT5XwlzsDPzlqvqesRi4MjQz2x4/Sxm7zYg5LRYFol1BIhG6EwxKvSUq8HcC/KxJu3J0Q==} - - '@algolia/client-personalization@4.14.2': - resolution: {integrity: sha512-ACCoLi0cL8CBZ1W/2juehSltrw2iqsQBnfiu/Rbl9W2yE6o2ZUb97+sqN/jBqYNQBS+o0ekTMKNkQjHHAcEXNw==} - - '@algolia/client-search@4.14.2': - resolution: {integrity: sha512-L5zScdOmcZ6NGiVbLKTvP02UbxZ0njd5Vq9nJAmPFtjffUSOGEp11BmD2oMJ5QvARgx2XbX4KzTTNS5ECYIMWw==} - - '@algolia/events@4.0.1': - resolution: {integrity: sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==} - - '@algolia/logger-common@4.14.2': - resolution: {integrity: sha512-/JGlYvdV++IcMHBnVFsqEisTiOeEr6cUJtpjz8zc0A9c31JrtLm318Njc72p14Pnkw3A/5lHHh+QxpJ6WFTmsA==} - - '@algolia/logger-console@4.14.2': - resolution: {integrity: sha512-8S2PlpdshbkwlLCSAB5f8c91xyc84VM9Ar9EdfE9UmX+NrKNYnWR1maXXVDQQoto07G1Ol/tYFnFVhUZq0xV/g==} - - '@algolia/requester-browser-xhr@4.14.2': - resolution: {integrity: sha512-CEh//xYz/WfxHFh7pcMjQNWgpl4wFB85lUMRyVwaDPibNzQRVcV33YS+63fShFWc2+42YEipFGH2iPzlpszmDw==} - - '@algolia/requester-common@4.14.2': - resolution: {integrity: sha512-73YQsBOKa5fvVV3My7iZHu1sUqmjjfs9TteFWwPwDmnad7T0VTCopttcsM3OjLxZFtBnX61Xxl2T2gmG2O4ehg==} - - '@algolia/requester-node-http@4.14.2': - resolution: {integrity: sha512-oDbb02kd1o5GTEld4pETlPZLY0e+gOSWjWMJHWTgDXbv9rm/o2cF7japO6Vj1ENnrqWvLBmW1OzV9g6FUFhFXg==} - - '@algolia/transporter@4.14.2': - resolution: {integrity: sha512-t89dfQb2T9MFQHidjHcfhh6iGMNwvuKUvojAj+JsrHAGbuSy7yE4BylhLX6R0Q1xYRoC4Vvv+O5qIw/LdnQfsQ==} - - '@ampproject/remapping@2.2.0': - resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} - engines: {node: '>=6.0.0'} - - '@babel/code-frame@7.12.11': - resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==} - - '@babel/code-frame@7.26.2': - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.19.4': - resolution: {integrity: sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.12.9': - resolution: {integrity: sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.19.6': - resolution: {integrity: sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.23.0': - resolution: {integrity: sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-annotate-as-pure@7.18.6': - resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-builder-binary-assignment-operator-visitor@7.18.9': - resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.19.3': - resolution: {integrity: sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-create-class-features-plugin@7.19.0': - resolution: {integrity: sha512-NRz8DwF4jT3UfrmUoZjd0Uph9HQnP30t7Ash+weACcyNkiYTywpIjDBgReJMKgr+n86sn2nPVVmJ28Dm053Kqw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-create-regexp-features-plugin@7.19.0': - resolution: {integrity: sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-define-polyfill-provider@0.3.3': - resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} - peerDependencies: - '@babel/core': ^7.4.0-0 - - '@babel/helper-environment-visitor@7.22.20': - resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-explode-assignable-expression@7.18.6': - resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-function-name@7.23.0': - resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-hoist-variables@7.22.5': - resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-member-expression-to-functions@7.18.9': - resolution: {integrity: sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.18.6': - resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.19.6': - resolution: {integrity: sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-optimise-call-expression@7.18.6': - resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-plugin-utils@7.10.4': - resolution: {integrity: sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==} - - '@babel/helper-plugin-utils@7.20.2': - resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-remap-async-to-generator@7.18.9': - resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-replace-supers@7.19.1': - resolution: {integrity: sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-simple-access@7.19.4': - resolution: {integrity: sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-skip-transparent-expression-wrappers@7.18.9': - resolution: {integrity: sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-split-export-declaration@7.22.6': - resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.25.9': - resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.25.9': - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.18.6': - resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-wrap-function@7.19.0': - resolution: {integrity: sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.27.0': - resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} - engines: {node: '>=6.9.0'} - - '@babel/highlight@7.22.20': - resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.27.0': - resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6': - resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.18.9': - resolution: {integrity: sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - - '@babel/plugin-proposal-async-generator-functions@7.19.1': - resolution: {integrity: sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-class-properties@7.18.6': - resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-class-static-block@7.18.6': - resolution: {integrity: sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead. - peerDependencies: - '@babel/core': ^7.12.0 - - '@babel/plugin-proposal-dynamic-import@7.18.6': - resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-export-namespace-from@7.18.9': - resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-json-strings@7.18.6': - resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-logical-assignment-operators@7.18.9': - resolution: {integrity: sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6': - resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-numeric-separator@7.18.6': - resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-object-rest-spread@7.12.1': - resolution: {integrity: sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-object-rest-spread@7.19.4': - resolution: {integrity: sha512-wHmj6LDxVDnL+3WhXteUBaoM1aVILZODAUjg11kHqG4cOlfgMQGxw6aCgvrXrmaJR3Bn14oZhImyCPZzRpC93Q==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-optional-catch-binding@7.18.6': - resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-optional-chaining@7.18.9': - resolution: {integrity: sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-private-methods@7.18.6': - resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-private-property-in-object@7.18.6': - resolution: {integrity: sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-unicode-property-regex@7.18.6': - resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} - engines: {node: '>=4'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-dynamic-import@7.8.3': - resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-export-namespace-from@7.8.3': - resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-assertions@7.18.6': - resolution: {integrity: sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.12.1': - resolution: {integrity: sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.18.6': - resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.18.6': - resolution: {integrity: sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-arrow-functions@7.18.6': - resolution: {integrity: sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-async-to-generator@7.18.6': - resolution: {integrity: sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-block-scoped-functions@7.18.6': - resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-block-scoping@7.19.4': - resolution: {integrity: sha512-934S2VLLlt2hRJwPf4MczaOr4hYF0z+VKPwqTNxyKX7NthTiPfhuKFWQZHXRM0vh/wo/VyXB3s4bZUNA08l+tQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-classes@7.19.0': - resolution: {integrity: sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-computed-properties@7.18.9': - resolution: {integrity: sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-destructuring@7.19.4': - resolution: {integrity: sha512-t0j0Hgidqf0aM86dF8U+vXYReUgJnlv4bZLsyoPnwZNrGY+7/38o8YjaELrvHeVfTZao15kjR0PVv0nju2iduA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-dotall-regex@7.18.6': - resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-duplicate-keys@7.18.9': - resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-exponentiation-operator@7.18.6': - resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-for-of@7.18.8': - resolution: {integrity: sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-function-name@7.18.9': - resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-literals@7.18.9': - resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-member-expression-literals@7.18.6': - resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-amd@7.19.6': - resolution: {integrity: sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-commonjs@7.19.6': - resolution: {integrity: sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-systemjs@7.19.6': - resolution: {integrity: sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-umd@7.18.6': - resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-named-capturing-groups-regex@7.19.1': - resolution: {integrity: sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-transform-new-target@7.18.6': - resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-object-super@7.18.6': - resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-parameters@7.18.8': - resolution: {integrity: sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-property-literals@7.18.6': - resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-constant-elements@7.20.2': - resolution: {integrity: sha512-KS/G8YI8uwMGKErLFOHS/ekhqdHhpEloxs43NecQHVgo2QuQSyJhGIY1fL8UGl9wy5ItVwwoUL4YxVqsplGq2g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-display-name@7.18.6': - resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-development@7.18.6': - resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx@7.19.0': - resolution: {integrity: sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-pure-annotations@7.18.6': - resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-regenerator@7.18.6': - resolution: {integrity: sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-reserved-words@7.18.6': - resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-runtime@7.19.6': - resolution: {integrity: sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-shorthand-properties@7.18.6': - resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-spread@7.19.0': - resolution: {integrity: sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-sticky-regex@7.18.6': - resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-template-literals@7.18.9': - resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typeof-symbol@7.18.9': - resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typescript@7.19.3': - resolution: {integrity: sha512-z6fnuK9ve9u/0X0rRvI9MY0xg+DOUaABDYOe+/SQTxtlptaBB/V9JIUxJn6xp3lMBeb9qe8xSFmHU35oZDXD+w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-escapes@7.18.10': - resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-regex@7.18.6': - resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-env@7.19.4': - resolution: {integrity: sha512-5QVOTXUdqTCjQuh2GGtdd7YEhoRXBMVGROAtsBeLGIbIz3obCBIfRMT1I3ZKkMgNzwkyCkftDXSSkHxnfVf4qg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-modules@0.1.5': - resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-react@7.18.6': - resolution: {integrity: sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-typescript@7.18.6': - resolution: {integrity: sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/runtime-corejs3@7.27.0': - resolution: {integrity: sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==} - engines: {node: '>=6.9.0'} - - '@babel/runtime@7.27.0': - resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.27.0': - resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.23.2': - resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.27.0': - resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} - engines: {node: '>=6.9.0'} - - '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - - '@docsearch/css@3.2.2': - resolution: {integrity: sha512-VB0Evx4ikS1ZlW1YVUw+vI9b3H/UXMCo4W/ZWy+n56Sho4KOqyCHcINVays91TJt7HTV/CKO3FCbm2VJg5Wipw==} - - '@docsearch/react@3.2.2': - resolution: {integrity: sha512-1Hn2SNQUFVPrzqvaj+vxXZfsfn3rnW8CoyGAJ1LqXMY9py8GbxK8VfmJ5Z6z4LwG9849tGru/N6dp0cQO6r9Ag==} - peerDependencies: - '@types/react': '>= 16.8.0 < 19.0.0' - react: '>= 16.8.0 < 19.0.0' - react-dom: '>= 16.8.0 < 19.0.0' - peerDependenciesMeta: - '@types/react': - optional: true - react: - optional: true - react-dom: - optional: true - - '@docusaurus/core@2.4.3': - resolution: {integrity: sha512-dWH5P7cgeNSIg9ufReX6gaCl/TmrGKD38Orbwuz05WPhAQtFXHd5B8Qym1TiXfvUNvwoYKkAJOJuGe8ou0Z7PA==} - engines: {node: '>=16.14'} - hasBin: true - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/cssnano-preset@2.4.3': - resolution: {integrity: sha512-ZvGSRCi7z9wLnZrXNPG6DmVPHdKGd8dIn9pYbEOFiYihfv4uDR3UtxogmKf+rT8ZlKFf5Lqne8E8nt08zNM8CA==} - engines: {node: '>=16.14'} - - '@docusaurus/logger@2.4.3': - resolution: {integrity: sha512-Zxws7r3yLufk9xM1zq9ged0YHs65mlRmtsobnFkdZTxWXdTYlWWLWdKyNKAsVC+D7zg+pv2fGbyabdOnyZOM3w==} - engines: {node: '>=16.14'} - - '@docusaurus/mdx-loader@2.4.3': - resolution: {integrity: sha512-b1+fDnWtl3GiqkL0BRjYtc94FZrcDDBV1j8446+4tptB9BAOlePwG2p/pK6vGvfL53lkOsszXMghr2g67M0vCw==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/module-type-aliases@2.4.3': - resolution: {integrity: sha512-cwkBkt1UCiduuvEAo7XZY01dJfRn7UR/75mBgOdb1hKknhrabJZ8YH+7savd/y9kLExPyrhe0QwdS9GuzsRRIA==} - peerDependencies: - react: '*' - react-dom: '*' - - '@docusaurus/plugin-content-blog@2.4.3': - resolution: {integrity: sha512-PVhypqaA0t98zVDpOeTqWUTvRqCEjJubtfFUQ7zJNYdbYTbS/E/ytq6zbLVsN/dImvemtO/5JQgjLxsh8XLo8Q==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-content-docs@2.4.3': - resolution: {integrity: sha512-N7Po2LSH6UejQhzTCsvuX5NOzlC+HiXOVvofnEPj0WhMu1etpLEXE6a4aTxrtg95lQ5kf0xUIdjX9sh3d3G76A==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-content-pages@2.4.3': - resolution: {integrity: sha512-txtDVz7y3zGk67q0HjG0gRttVPodkHqE0bpJ+7dOaTH40CQFLSh7+aBeGnPOTl+oCPG+hxkim4SndqPqXjQ8Bg==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-debug@2.4.3': - resolution: {integrity: sha512-LkUbuq3zCmINlFb+gAd4ZvYr+bPAzMC0hwND4F7V9bZ852dCX8YoWyovVUBKq4er1XsOwSQaHmNGtObtn8Av8Q==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-google-analytics@2.4.3': - resolution: {integrity: sha512-KzBV3k8lDkWOhg/oYGxlK5o9bOwX7KpPc/FTWoB+SfKhlHfhq7qcQdMi1elAaVEIop8tgK6gD1E58Q+XC6otSQ==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-google-gtag@2.4.3': - resolution: {integrity: sha512-5FMg0rT7sDy4i9AGsvJC71MQrqQZwgLNdDetLEGDHLfSHLvJhQbTCUGbGXknUgWXQJckcV/AILYeJy+HhxeIFA==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-google-tag-manager@2.4.3': - resolution: {integrity: sha512-1jTzp71yDGuQiX9Bi0pVp3alArV0LSnHXempvQTxwCGAEzUWWaBg4d8pocAlTpbP9aULQQqhgzrs8hgTRPOM0A==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/plugin-sitemap@2.4.3': - resolution: {integrity: sha512-LRQYrK1oH1rNfr4YvWBmRzTL0LN9UAPxBbghgeFRBm5yloF6P+zv1tm2pe2hQTX/QP5bSKdnajCvfnScgKXMZQ==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/preset-classic@2.4.3': - resolution: {integrity: sha512-tRyMliepY11Ym6hB1rAFSNGwQDpmszvWYJvlK1E+md4SW8i6ylNHtpZjaYFff9Mdk3i/Pg8ItQq9P0daOJAvQw==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/react-loadable@5.5.2': - resolution: {integrity: sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==} - peerDependencies: - react: '*' - - '@docusaurus/theme-classic@2.4.3': - resolution: {integrity: sha512-QKRAJPSGPfDY2yCiPMIVyr+MqwZCIV2lxNzqbyUW0YkrlmdzzP3WuQJPMGLCjWgQp/5c9kpWMvMxjhpZx1R32Q==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/theme-common@2.4.3': - resolution: {integrity: sha512-7KaDJBXKBVGXw5WOVt84FtN8czGWhM0lbyWEZXGp8AFfL6sZQfRTluFp4QriR97qwzSyOfQb+nzcDZZU4tezUw==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/theme-search-algolia@2.4.3': - resolution: {integrity: sha512-jziq4f6YVUB5hZOB85ELATwnxBz/RmSLD3ksGQOLDPKVzat4pmI8tddNWtriPpxR04BNT+ZfpPUMFkNFetSW1Q==} - engines: {node: '>=16.14'} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/theme-translations@2.4.3': - resolution: {integrity: sha512-H4D+lbZbjbKNS/Zw1Lel64PioUAIT3cLYYJLUf3KkuO/oc9e0QCVhIYVtUI2SfBCF2NNdlyhBDQEEMygsCedIg==} - engines: {node: '>=16.14'} - - '@docusaurus/types@2.4.3': - resolution: {integrity: sha512-W6zNLGQqfrp/EoPD0bhb9n7OobP+RHpmvVzpA+Z/IuU3Q63njJM24hmT0GYboovWcDtFmnIJC9wcyx4RVPQscw==} - peerDependencies: - react: ^16.8.4 || ^17.0.0 - react-dom: ^16.8.4 || ^17.0.0 - - '@docusaurus/utils-common@2.4.3': - resolution: {integrity: sha512-/jascp4GbLQCPVmcGkPzEQjNaAk3ADVfMtudk49Ggb+131B1WDD6HqlSmDf8MxGdy7Dja2gc+StHf01kiWoTDQ==} - engines: {node: '>=16.14'} - peerDependencies: - '@docusaurus/types': '*' - peerDependenciesMeta: - '@docusaurus/types': - optional: true - - '@docusaurus/utils-validation@2.4.3': - resolution: {integrity: sha512-G2+Vt3WR5E/9drAobP+hhZQMaswRwDlp6qOMi7o7ZypB+VO7N//DZWhZEwhcRGepMDJGQEwtPv7UxtYwPL9PBw==} - engines: {node: '>=16.14'} - - '@docusaurus/utils@2.4.3': - resolution: {integrity: sha512-fKcXsjrD86Smxv8Pt0TBFqYieZZCPh4cbf9oszUq/AMhZn3ujwpKaVYZACPX8mmjtYx0JOgNx52CREBfiGQB4A==} - engines: {node: '>=16.14'} - peerDependencies: - '@docusaurus/types': '*' - peerDependenciesMeta: - '@docusaurus/types': - optional: true - - '@eslint/eslintrc@0.4.3': - resolution: {integrity: sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==} - engines: {node: ^10.12.0 || >=12.0.0} - - '@hapi/hoek@9.3.0': - resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} - - '@hapi/topo@5.1.0': - resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - - '@humanwhocodes/config-array@0.5.0': - resolution: {integrity: sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - - '@humanwhocodes/object-schema@1.2.1': - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} - deprecated: Use @eslint/object-schema instead - - '@jest/schemas@29.0.0': - resolution: {integrity: sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/types@29.3.1': - resolution: {integrity: sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jridgewell/gen-mapping@0.1.1': - resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} - engines: {node: '>=6.0.0'} - - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} - - '@jridgewell/resolve-uri@3.1.0': - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} - engines: {node: '>=6.0.0'} - - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} - - '@jridgewell/sourcemap-codec@1.4.14': - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - - '@leichtgewicht/ip-codec@2.0.4': - resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==} - - '@mdx-js/mdx@1.6.22': - resolution: {integrity: sha512-AMxuLxPz2j5/6TpF/XSdKpQP1NlG0z11dFOlq+2IP/lSgl11GY8ji6S/rgsViN/L0BDvHvUMruRb7ub+24LUYA==} - - '@mdx-js/react@1.6.22': - resolution: {integrity: sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==} - peerDependencies: - react: ^16.13.1 || ^17.0.0 - - '@mdx-js/util@1.6.22': - resolution: {integrity: sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@polka/url@1.0.0-next.21': - resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} - - '@sideway/address@4.1.5': - resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} - - '@sideway/formula@3.0.1': - resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} - - '@sideway/pinpoint@2.0.0': - resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} - - '@sinclair/typebox@0.24.51': - resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} - - '@sindresorhus/is@4.6.0': - resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} - engines: {node: '>=10'} - - '@slorber/static-site-generator-webpack-plugin@4.0.7': - resolution: {integrity: sha512-Ug7x6z5lwrz0WqdnNFOMYrDQNTPAprvHLSh6+/fmml3qUiz6l5eq+2MzLKWtn/q5K5NpSiFsZTP/fck/3vjSxA==} - engines: {node: '>=14'} - - '@svgr/babel-plugin-add-jsx-attribute@6.5.1': - resolution: {integrity: sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-remove-jsx-attribute@6.5.0': - resolution: {integrity: sha512-8zYdkym7qNyfXpWvu4yq46k41pyNM9SOstoWhKlm+IfdCE1DdnRKeMUPsWIEO/DEkaWxJ8T9esNdG3QwQ93jBA==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-remove-jsx-empty-expression@6.5.0': - resolution: {integrity: sha512-NFdxMq3xA42Kb1UbzCVxplUc0iqSyM9X8kopImvFnB+uSDdzIHOdbs1op8ofAvVRtbg4oZiyRl3fTYeKcOe9Iw==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-replace-jsx-attribute-value@6.5.1': - resolution: {integrity: sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-svg-dynamic-title@6.5.1': - resolution: {integrity: sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-svg-em-dimensions@6.5.1': - resolution: {integrity: sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-transform-react-native-svg@6.5.1': - resolution: {integrity: sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-plugin-transform-svg-component@6.5.1': - resolution: {integrity: sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==} - engines: {node: '>=12'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/babel-preset@6.5.1': - resolution: {integrity: sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw==} - engines: {node: '>=10'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@svgr/core@6.5.1': - resolution: {integrity: sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==} - engines: {node: '>=10'} - - '@svgr/hast-util-to-babel-ast@6.5.1': - resolution: {integrity: sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==} - engines: {node: '>=10'} - - '@svgr/plugin-jsx@6.5.1': - resolution: {integrity: sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw==} - engines: {node: '>=10'} - peerDependencies: - '@svgr/core': ^6.0.0 - - '@svgr/plugin-svgo@6.5.1': - resolution: {integrity: sha512-omvZKf8ixP9z6GWgwbtmP9qQMPX4ODXi+wzbVZgomNFsUIlHA1sf4fThdwTWSsZGgvGAG6yE+b/F5gWUkcZ/iQ==} - engines: {node: '>=10'} - peerDependencies: - '@svgr/core': '*' - - '@svgr/webpack@6.5.1': - resolution: {integrity: sha512-cQ/AsnBkXPkEK8cLbv4Dm7JGXq2XrumKnL1dRpJD9rIO2fTIlJI9a1uCciYG1F2aUsox/hJQyNGbt3soDxSRkA==} - engines: {node: '>=10'} - - '@szmarczak/http-timer@4.0.6': - resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} - engines: {node: '>=10'} - - '@trysound/sax@0.2.0': - resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} - engines: {node: '>=10.13.0'} - - '@types/body-parser@1.19.2': - resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} - - '@types/bonjour@3.5.10': - resolution: {integrity: sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==} - - '@types/cacheable-request@6.0.3': - resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} - - '@types/connect-history-api-fallback@1.3.5': - resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==} - - '@types/connect@3.4.35': - resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} - - '@types/eslint-scope@3.7.7': - resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} - - '@types/eslint@9.6.1': - resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/express-serve-static-core@4.17.32': - resolution: {integrity: sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA==} - - '@types/express@4.17.15': - resolution: {integrity: sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==} - - '@types/hast@2.3.4': - resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==} - - '@types/history@4.7.11': - resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} - - '@types/html-minifier-terser@6.1.0': - resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} - - '@types/http-cache-semantics@4.0.4': - resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - - '@types/http-proxy@1.17.9': - resolution: {integrity: sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==} - - '@types/istanbul-lib-coverage@2.0.4': - resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} - - '@types/istanbul-lib-report@3.0.0': - resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} - - '@types/istanbul-reports@3.0.1': - resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/katex@0.11.1': - resolution: {integrity: sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg==} - - '@types/keyv@3.1.4': - resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - - '@types/mdast@3.0.10': - resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==} - - '@types/mime@3.0.1': - resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} - - '@types/node@17.0.45': - resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - - '@types/node@18.11.3': - resolution: {integrity: sha512-fNjDQzzOsZeKZu5NATgXUPsaFaTxeRgFXoosrHivTl8RGeV733OLawXsGfEk9a8/tySyZUyiZ6E8LcjPFZ2y1A==} - - '@types/parse-json@4.0.0': - resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} - - '@types/parse5@5.0.3': - resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==} - - '@types/prop-types@15.7.5': - resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} - - '@types/qs@6.9.7': - resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} - - '@types/range-parser@1.2.4': - resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} - - '@types/react-router-config@5.0.11': - resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} - - '@types/react-router-dom@5.3.3': - resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} - - '@types/react-router@5.1.20': - resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} - - '@types/react@18.0.26': - resolution: {integrity: sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==} - - '@types/responselike@1.0.3': - resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - - '@types/retry@0.12.0': - resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - - '@types/sax@1.2.7': - resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} - - '@types/scheduler@0.16.2': - resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} - - '@types/serve-index@1.9.1': - resolution: {integrity: sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==} - - '@types/serve-static@1.15.0': - resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==} - - '@types/sockjs@0.3.33': - resolution: {integrity: sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==} - - '@types/unist@2.0.6': - resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} - - '@types/ws@8.18.0': - resolution: {integrity: sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==} - - '@types/yargs-parser@21.0.0': - resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} - - '@types/yargs@17.0.19': - resolution: {integrity: sha512-cAx3qamwaYX9R0fzOIZAlFpo4A+1uBVCxqpKz9D26uTF4srRXaGTTsikQmaotCtNdbhzyUH7ft6p9ktz9s6UNQ==} - - '@webassemblyjs/ast@1.14.1': - resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - - '@webassemblyjs/floating-point-hex-parser@1.13.2': - resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - - '@webassemblyjs/helper-api-error@1.13.2': - resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - - '@webassemblyjs/helper-buffer@1.14.1': - resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - - '@webassemblyjs/helper-numbers@1.13.2': - resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': - resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - - '@webassemblyjs/helper-wasm-section@1.14.1': - resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - - '@webassemblyjs/ieee754@1.13.2': - resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - - '@webassemblyjs/leb128@1.13.2': - resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - - '@webassemblyjs/utf8@1.13.2': - resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - - '@webassemblyjs/wasm-edit@1.14.1': - resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} - - '@webassemblyjs/wasm-gen@1.14.1': - resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} - - '@webassemblyjs/wasm-opt@1.14.1': - resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} - - '@webassemblyjs/wasm-parser@1.14.1': - resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} - - '@webassemblyjs/wast-printer@1.14.1': - resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - - '@xtuc/ieee754@1.2.0': - resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - - '@xtuc/long@4.2.2': - resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - - acorn-import-phases@1.0.4: - resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} - engines: {node: '>=10.13.0'} - peerDependencies: - acorn: ^8.14.0 - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn-walk@8.2.0: - resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} - engines: {node: '>=0.4.0'} - - acorn@7.4.1: - resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} - engines: {node: '>=0.4.0'} - hasBin: true - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - address@1.2.1: - resolution: {integrity: sha512-B+6bi5D34+fDYENiH5qOlA0cV2rAGKuWZ9LeyUUehbXy8e0VS9e498yO0Jeeh+iM+6KbfudHTFjXw2MmJD4QRA==} - engines: {node: '>= 10.0.0'} - - aggregate-error@3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} - engines: {node: '>=8'} - - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv-keywords@3.5.2: - resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} - peerDependencies: - ajv: ^6.9.1 - - ajv-keywords@5.1.0: - resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} - peerDependencies: - ajv: ^8.8.2 - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - - algoliasearch-helper@3.11.1: - resolution: {integrity: sha512-mvsPN3eK4E0bZG0/WlWJjeqe/bUD2KOEVOl0GyL/TGXn6wcpZU8NOuztGHCUKXkyg5gq6YzUakVTmnmSSO5Yiw==} - peerDependencies: - algoliasearch: '>= 3.1 < 6' - - algoliasearch@4.14.2: - resolution: {integrity: sha512-ngbEQonGEmf8dyEh5f+uOIihv4176dgbuOZspiuhmTTBRBuzWu3KCGHre6uHj5YyuC7pNvQGzB6ZNJyZi0z+Sg==} - - ansi-align@3.0.1: - resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} - - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - - ansi-html-community@0.0.8: - resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} - engines: {'0': node >= 0.8.0} - hasBin: true - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - - anymatch@3.1.2: - resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} - engines: {node: '>= 8'} - - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - - array-flatten@2.1.2: - resolution: {integrity: sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==} - - array-includes@3.1.8: - resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} - engines: {node: '>= 0.4'} - - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - - array.prototype.findlast@1.2.5: - resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - - array.prototype.tosorted@1.1.4: - resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - - asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - - astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - at-least-node@1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} - - autoprefixer@10.4.12: - resolution: {integrity: sha512-WrCGV9/b97Pa+jtwf5UGaRjgQIg7OK3D06GnoYoZNcG1Xb8Gt3EfuKjlhh9i/VtT16g6PYjZ69jdJ2g8FxSC4Q==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - - axios@1.13.5: - resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} - - babel-eslint@10.1.0: - resolution: {integrity: sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==} - engines: {node: '>=6'} - deprecated: babel-eslint is now @babel/eslint-parser. This package will no longer receive updates. - peerDependencies: - eslint: '>= 4.12.1' - - babel-loader@8.2.5: - resolution: {integrity: sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==} - engines: {node: '>= 8.9'} - peerDependencies: - '@babel/core': ^7.0.0 - webpack: '>=2' - - babel-plugin-apply-mdx-type-prop@1.6.22: - resolution: {integrity: sha512-VefL+8o+F/DfK24lPZMtJctrCVOfgbqLAGZSkxwhazQv4VxPg3Za/i40fu22KR2m8eEda+IfSOlPLUSIiLcnCQ==} - peerDependencies: - '@babel/core': ^7.11.6 - - babel-plugin-dynamic-import-node@2.3.3: - resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==} - - babel-plugin-extract-import-names@1.6.22: - resolution: {integrity: sha512-yJ9BsJaISua7d8zNT7oRG1ZLBJCIdZ4PZqmH8qa9N5AK01ifk3fnkc98AXhtzE7UkfCsEumvoQWgoYLhOnJ7jQ==} - - babel-plugin-polyfill-corejs2@0.3.3: - resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - babel-plugin-polyfill-corejs3@0.6.0: - resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - babel-plugin-polyfill-regenerator@0.4.1: - resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - bail@1.0.5: - resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - base16@1.0.0: - resolution: {integrity: sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==} - - baseline-browser-mapping@2.9.19: - resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} - hasBin: true - - batch@0.6.1: - resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} - - big.js@5.2.2: - resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - - binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} - engines: {node: '>=8'} - - body-parser@1.20.3: - resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - bonjour-service@1.0.14: - resolution: {integrity: sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==} - - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - - boxen@5.1.2: - resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} - engines: {node: '>=10'} - - boxen@6.2.1: - resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - bytes@3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} - engines: {node: '>= 0.8'} - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - cacheable-lookup@5.0.4: - resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} - engines: {node: '>=10.6.0'} - - cacheable-request@7.0.4: - resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} - engines: {node: '>=8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camel-case@4.1.2: - resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} - - camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - caniuse-api@3.0.0: - resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - - caniuse-lite@1.0.30001768: - resolution: {integrity: sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==} - - ccount@1.1.0: - resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - character-entities-legacy@1.1.4: - resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} - - character-entities@1.2.4: - resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} - - character-reference-invalid@1.1.4: - resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} - - cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - - cheerio@1.0.0: - resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} - engines: {node: '>=18.17'} - - chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - - chrome-trace-event@1.0.3: - resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} - engines: {node: '>=6.0'} - - ci-info@2.0.0: - resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} - - ci-info@3.7.1: - resolution: {integrity: sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==} - engines: {node: '>=8'} - - clean-css@5.3.1: - resolution: {integrity: sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg==} - engines: {node: '>= 10.0'} - - clean-stack@2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} - - cli-boxes@2.2.1: - resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} - engines: {node: '>=6'} - - cli-boxes@3.0.0: - resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} - engines: {node: '>=10'} - - cli-table3@0.6.3: - resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} - engines: {node: 10.* || >= 12.*} - - clone-deep@4.0.1: - resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} - engines: {node: '>=6'} - - clone-response@1.0.3: - resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} - - clsx@1.2.1: - resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} - engines: {node: '>=6'} - - collapse-white-space@1.0.6: - resolution: {integrity: sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==} - - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - colord@2.9.3: - resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} - - colorette@2.0.19: - resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} - - combine-promises@1.1.0: - resolution: {integrity: sha512-ZI9jvcLDxqwaXEixOhArm3r7ReIivsXkpbyEWyeOhzz1QS0iSgBPnWvEqvIQtYyamGCYA88gFhmUrs9hrrQ0pg==} - engines: {node: '>=10'} - - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - - comma-separated-tokens@1.0.8: - resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} - - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - - commander@5.1.0: - resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} - engines: {node: '>= 6'} - - commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - - commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - - commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - - compressible@2.0.18: - resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} - engines: {node: '>= 0.6'} - - compression@1.7.4: - resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} - engines: {node: '>= 0.8.0'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - configstore@5.0.1: - resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} - engines: {node: '>=8'} - - connect-history-api-fallback@2.0.0: - resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} - engines: {node: '>=0.8'} - - consola@2.15.3: - resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} - - content-disposition@0.5.2: - resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} - engines: {node: '>= 0.6'} - - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - convert-source-map@1.9.0: - resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - - cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - - cookie@0.7.1: - resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} - engines: {node: '>= 0.6'} - - copy-text-to-clipboard@3.2.0: - resolution: {integrity: sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==} - engines: {node: '>=12'} - - copy-webpack-plugin@11.0.0: - resolution: {integrity: sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==} - engines: {node: '>= 14.15.0'} - peerDependencies: - webpack: ^5.1.0 - - core-js-compat@3.25.5: - resolution: {integrity: sha512-ovcyhs2DEBUIE0MGEKHP4olCUW/XYte3Vroyxuh38rD1wAO4dHohsovUC4eAOuzFxE6b+RXvBU3UZ9o0YhUTkA==} - - core-js-pure@3.41.0: - resolution: {integrity: sha512-71Gzp96T9YPk63aUvE5Q5qP+DryB4ZloUZPSOebGM88VNw8VNfvdA7z6kGA8iGOTEzAomsRidp4jXSmUIJsL+Q==} - - core-js@3.25.5: - resolution: {integrity: sha512-nbm6eZSjm+ZuBQxCUPQKQCoUEfFOXjUZ8dTTyikyKaWrTYmAVbykQfwsKE5dBK88u3QCkCrzsx/PPlKfhsvgpw==} - - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - - cosmiconfig@6.0.0: - resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==} - engines: {node: '>=8'} - - cosmiconfig@7.0.1: - resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==} - engines: {node: '>=10'} - - cross-fetch@3.2.0: - resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - crypto-random-string@2.0.0: - resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} - engines: {node: '>=8'} - - css-declaration-sorter@6.3.1: - resolution: {integrity: sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==} - engines: {node: ^10 || ^12 || >=14} - peerDependencies: - postcss: ^8.0.9 - - css-loader@6.7.3: - resolution: {integrity: sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^5.0.0 - - css-minimizer-webpack-plugin@4.2.2: - resolution: {integrity: sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA==} - engines: {node: '>= 14.15.0'} - peerDependencies: - '@parcel/css': '*' - '@swc/css': '*' - clean-css: '*' - csso: '*' - esbuild: '*' - lightningcss: '*' - webpack: ^5.0.0 - peerDependenciesMeta: - '@parcel/css': - optional: true - '@swc/css': - optional: true - clean-css: - optional: true - csso: - optional: true - esbuild: - optional: true - lightningcss: - optional: true - - css-select@4.3.0: - resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} - - css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} - - css-tree@1.1.3: - resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} - engines: {node: '>=8.0.0'} - - css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - cssnano-preset-advanced@5.3.9: - resolution: {integrity: sha512-njnh4pp1xCsibJcEHnWZb4EEzni0ePMqPuPNyuWT4Z+YeXmsgqNuTPIljXFEXhxGsWs9183JkXgHxc1TcsahIg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - cssnano-preset-default@5.2.13: - resolution: {integrity: sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - cssnano-utils@3.1.0: - resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - cssnano@5.1.13: - resolution: {integrity: sha512-S2SL2ekdEz6w6a2epXn4CmMKU4K3KpcyXLKfAYc9UQQqJRkD/2eLUG0vJ3Db/9OvO5GuAdgXw3pFbR6abqghDQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - csso@4.2.0: - resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} - engines: {node: '>=8.0.0'} - - csstype@3.1.1: - resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} - - data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} - - data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} - - data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} - - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - deepmerge@4.2.2: - resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} - engines: {node: '>=0.10.0'} - - default-gateway@6.0.3: - resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} - engines: {node: '>= 10'} - - defer-to-connect@2.0.1: - resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} - engines: {node: '>=10'} - - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - - del@6.1.1: - resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} - engines: {node: '>=10'} - - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - depd@1.1.2: - resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} - engines: {node: '>= 0.6'} - - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - detab@2.0.4: - resolution: {integrity: sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g==} - - detect-node@2.1.0: - resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - - detect-port-alt@1.1.6: - resolution: {integrity: sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==} - engines: {node: '>= 4.2.1'} - hasBin: true - - detect-port@1.5.1: - resolution: {integrity: sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==} - hasBin: true - - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - - dns-equal@1.0.0: - resolution: {integrity: sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==} - - dns-packet@5.4.0: - resolution: {integrity: sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==} - engines: {node: '>=6'} - - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - - dom-converter@0.2.0: - resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} - - dom-serializer@1.4.1: - resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} - - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@4.3.1: - resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} - engines: {node: '>= 4'} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@2.8.0: - resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} - - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - - dot-case@3.0.4: - resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} - - dot-prop@5.3.0: - resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} - engines: {node: '>=8'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - duplexer@0.1.2: - resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - - electron-to-chromium@1.5.286: - resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - emojis-list@3.0.0: - resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} - engines: {node: '>= 4'} - - emoticon@3.2.0: - resolution: {integrity: sha512-SNujglcLTTg+lDAcApPNgEdudaqQFiAbJCqzjNxJkvN9vAwCGi0uu8IUVvx+f16h+V44KCY6Y2yboroc9pilHg==} - - encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - - encoding-sniffer@0.2.0: - resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} - - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - - enhanced-resolve@5.19.0: - resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} - engines: {node: '>=10.13.0'} - - enquirer@2.3.6: - resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} - engines: {node: '>=8.6'} - - entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - - es-abstract@1.23.9: - resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} - engines: {node: '>= 0.4'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-iterator-helpers@1.2.1: - resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} - engines: {node: '>= 0.4'} - - es-module-lexer@2.0.0: - resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} - engines: {node: '>= 0.4'} - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-goat@2.1.1: - resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==} - engines: {node: '>=8'} - - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-plugin-react@7.37.4: - resolution: {integrity: sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - - eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} - - eslint-utils@2.1.0: - resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} - engines: {node: '>=6'} - - eslint-visitor-keys@1.3.0: - resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} - engines: {node: '>=4'} - - eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - - eslint@7.32.0: - resolution: {integrity: sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==} - engines: {node: ^10.12.0 || >=12.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - - espree@7.3.1: - resolution: {integrity: sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==} - engines: {node: ^10.12.0 || >=12.0.0} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - esquery@1.4.0: - resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - eta@2.2.0: - resolution: {integrity: sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==} - engines: {node: '>=6.0.0'} - - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - eval@0.1.8: - resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} - engines: {node: '>= 0.8'} - - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - - express@4.21.2: - resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} - engines: {node: '>= 0.10.0'} - - extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-glob@3.2.12: - resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} - engines: {node: '>=8.6.0'} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fast-uri@3.0.6: - resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} - - fastq@1.13.0: - resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} - - faye-websocket@0.11.4: - resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} - engines: {node: '>=0.8.0'} - - fbemitter@3.0.0: - resolution: {integrity: sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==} - - fbjs-css-vars@1.0.2: - resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} - - fbjs@3.0.5: - resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} - - feed@4.2.2: - resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} - engines: {node: '>=0.4.0'} - - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - - file-loader@6.2.0: - resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} - engines: {node: '>= 10.13.0'} - peerDependencies: - webpack: ^4.0.0 || ^5.0.0 - - filesize@8.0.7: - resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==} - engines: {node: '>= 0.4.0'} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - finalhandler@1.3.1: - resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} - engines: {node: '>= 0.8'} - - find-cache-dir@3.3.2: - resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} - engines: {node: '>=8'} - - find-up@3.0.0: - resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} - engines: {node: '>=6'} - - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} - engines: {node: ^10.12.0 || >=12.0.0} - - flatted@3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} - - flux@4.0.4: - resolution: {integrity: sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw==} - peerDependencies: - react: ^15.0.2 || ^16.0.0 || ^17.0.0 - - follow-redirects@1.15.11: - resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - - fork-ts-checker-webpack-plugin@6.5.2: - resolution: {integrity: sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==} - engines: {node: '>=10', yarn: '>=1.0.0'} - peerDependencies: - eslint: '>= 6' - typescript: '>= 2.7' - vue-template-compiler: '*' - webpack: '>= 4' - peerDependenciesMeta: - eslint: - optional: true - vue-template-compiler: - optional: true - - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fraction.js@4.2.0: - resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} - - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - - fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} - - fs-extra@9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} - - fs-monkey@1.0.3: - resolution: {integrity: sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} - engines: {node: '>= 0.4'} - - functional-red-black-tree@1.0.1: - resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-own-enumerable-property-symbols@3.0.2: - resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - - get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - - github-slugger@1.4.0: - resolution: {integrity: sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ==} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - - global-dirs@3.0.0: - resolution: {integrity: sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==} - engines: {node: '>=10'} - - global-modules@2.0.0: - resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} - engines: {node: '>=6'} - - global-prefix@3.0.0: - resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} - engines: {node: '>=6'} - - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - - globals@13.17.0: - resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==} - engines: {node: '>=8'} - - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - - globby@13.1.3: - resolution: {integrity: sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - got@11.8.6: - resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} - engines: {node: '>=10.19.0'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - - gzip-size@6.0.0: - resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} - engines: {node: '>=10'} - - handle-thing@2.0.1: - resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} - - has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - has-yarn@2.1.0: - resolution: {integrity: sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==} - engines: {node: '>=8'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - hast-to-hyperscript@9.0.1: - resolution: {integrity: sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==} - - hast-util-from-parse5@6.0.1: - resolution: {integrity: sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA==} - - hast-util-is-element@1.1.0: - resolution: {integrity: sha512-oUmNua0bFbdrD/ELDSSEadRVtWZOf3iF6Lbv81naqsIV99RnSCieTbWuWCY8BAeEfKJTKl0gRdokv+dELutHGQ==} - - hast-util-parse-selector@2.2.5: - resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} - - hast-util-raw@6.0.1: - resolution: {integrity: sha512-ZMuiYA+UF7BXBtsTBNcLBF5HzXzkyE6MLzJnL605LKE8GJylNjGc4jjxazAHUtcwT5/CEt6afRKViYB4X66dig==} - - hast-util-to-parse5@6.0.0: - resolution: {integrity: sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ==} - - hast-util-to-text@2.0.1: - resolution: {integrity: sha512-8nsgCARfs6VkwH2jJU9b8LNTuR4700na+0h3PqCaEk4MAnMDeu5P0tP8mjk9LLNGxIeQRLbiDbZVw6rku+pYsQ==} - - hastscript@6.0.0: - resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} - - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - - history@4.10.1: - resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} - - hoist-non-react-statics@3.3.2: - resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - - hpack.js@2.1.6: - resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} - - html-entities@2.3.3: - resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} - - html-minifier-terser@6.1.0: - resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} - engines: {node: '>=12'} - hasBin: true - - html-tags@3.2.0: - resolution: {integrity: sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==} - engines: {node: '>=8'} - - html-void-elements@1.0.5: - resolution: {integrity: sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w==} - - html-webpack-plugin@5.5.0: - resolution: {integrity: sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==} - engines: {node: '>=10.13.0'} - peerDependencies: - webpack: ^5.20.0 - - htmlparser2@6.1.0: - resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} - - htmlparser2@9.1.0: - resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} - - http-cache-semantics@4.2.0: - resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - - http-deceiver@1.2.7: - resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} - - http-errors@1.6.3: - resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} - engines: {node: '>= 0.6'} - - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - - http-parser-js@0.5.8: - resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} - - http-proxy-middleware@2.0.9: - resolution: {integrity: sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@types/express': ^4.17.13 - peerDependenciesMeta: - '@types/express': - optional: true - - http-proxy@1.18.1: - resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} - engines: {node: '>=8.0.0'} - - http2-wrapper@1.0.3: - resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} - engines: {node: '>=10.19.0'} - - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - icss-utils@5.1.0: - resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - ignore@4.0.6: - resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} - engines: {node: '>= 4'} - - ignore@5.2.4: - resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} - engines: {node: '>= 4'} - - image-size@1.0.2: - resolution: {integrity: sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==} - engines: {node: '>=14.0.0'} - hasBin: true - - immer@9.0.17: - resolution: {integrity: sha512-+hBruaLSQvkPfxRiTLK/mi4vLH+/VQS6z2KJahdoxlleFOI8ARqzOF17uy12eFDlqWmPoygwc5evgwcp+dlHhg==} - - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - - import-lazy@2.1.0: - resolution: {integrity: sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==} - engines: {node: '>=4'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - - infima@0.2.0-alpha.43: - resolution: {integrity: sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ==} - engines: {node: '>=12'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.3: - resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - - ini@2.0.0: - resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} - engines: {node: '>=10'} - - inline-style-parser@0.1.1: - resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} - - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - - interpret@1.4.0: - resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} - engines: {node: '>= 0.10'} - - invariant@2.2.4: - resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - - ipaddr.js@2.0.1: - resolution: {integrity: sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==} - engines: {node: '>= 10'} - - is-alphabetical@1.0.4: - resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} - - is-alphanumerical@1.0.4: - resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} - - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} - - is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - - is-buffer@2.0.5: - resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} - engines: {node: '>=4'} - - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - - is-ci@2.0.0: - resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} - hasBin: true - - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} - - is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - - is-decimal@1.0.4: - resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} - - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - - is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-generator-function@1.1.0: - resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} - engines: {node: '>= 0.4'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-hexadecimal@1.0.4: - resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} - - is-installed-globally@0.4.0: - resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} - engines: {node: '>=10'} - - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - - is-npm@5.0.0: - resolution: {integrity: sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==} - engines: {node: '>=10'} - - is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-obj@1.0.1: - resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} - engines: {node: '>=0.10.0'} - - is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - - is-path-cwd@2.2.0: - resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} - engines: {node: '>=6'} - - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - - is-plain-obj@2.1.0: - resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} - engines: {node: '>=8'} - - is-plain-obj@3.0.0: - resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} - engines: {node: '>=10'} - - is-plain-object@2.0.4: - resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} - engines: {node: '>=0.10.0'} - - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - - is-regexp@1.0.0: - resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} - engines: {node: '>=0.10.0'} - - is-root@2.1.0: - resolution: {integrity: sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==} - engines: {node: '>=6'} - - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - - is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - - is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - - is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - - is-typedarray@1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - - is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} - - is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - - is-whitespace-character@1.0.4: - resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==} - - is-word-character@1.0.4: - resolution: {integrity: sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==} - - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - - is-yarn-global@0.3.0: - resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==} - - isarray@0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - isobject@3.0.1: - resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} - engines: {node: '>=0.10.0'} - - iterator.prototype@1.1.5: - resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} - engines: {node: '>= 0.4'} - - jest-util@29.3.1: - resolution: {integrity: sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-worker@27.5.1: - resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} - engines: {node: '>= 10.13.0'} - - jest-worker@29.3.1: - resolution: {integrity: sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - joi@17.13.3: - resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsesc@0.5.0: - resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} - hasBin: true - - jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - - jsx-ast-utils@3.3.3: - resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} - engines: {node: '>=4.0'} - - katex@0.16.28: - resolution: {integrity: sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==} - hasBin: true - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - - klona@2.0.6: - resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} - engines: {node: '>= 8'} - - latest-version@5.1.0: - resolution: {integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==} - engines: {node: '>=8'} - - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lilconfig@2.0.6: - resolution: {integrity: sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==} - engines: {node: '>=10'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - loader-runner@4.3.1: - resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} - engines: {node: '>=6.11.5'} - - loader-utils@2.0.4: - resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} - engines: {node: '>=8.9.0'} - - loader-utils@3.2.1: - resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==} - engines: {node: '>= 12.13.0'} - - locate-path@3.0.0: - resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} - engines: {node: '>=6'} - - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.curry@4.1.1: - resolution: {integrity: sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==} - - lodash.debounce@4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - - lodash.flow@3.5.0: - resolution: {integrity: sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==} - - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lodash.truncate@4.4.2: - resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} - - lodash.uniq@4.5.0: - resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - - lower-case@2.0.2: - resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - - lowercase-keys@2.0.0: - resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} - engines: {node: '>=8'} - - make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - - markdown-escapes@1.0.4: - resolution: {integrity: sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - mdast-squeeze-paragraphs@4.0.0: - resolution: {integrity: sha512-zxdPn69hkQ1rm4J+2Cs2j6wDEv7O17TfXTJ33tl/+JPIoEmtV9t2ZzBM5LPHE8QlHsmVD8t3vPKCyY3oH+H8MQ==} - - mdast-util-definitions@4.0.0: - resolution: {integrity: sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==} - - mdast-util-to-hast@10.0.1: - resolution: {integrity: sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA==} - - mdast-util-to-string@2.0.0: - resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} - - mdn-data@2.0.14: - resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} - - mdurl@1.0.1: - resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} - - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - - memfs@3.4.12: - resolution: {integrity: sha512-BcjuQn6vfqP+k100e0E9m61Hyqa//Brp+I3f0OBmN0ATHlFA8vx3Lt8z57R3u2bPqe3WGDBC+nF72fTH7isyEw==} - engines: {node: '>= 4.0.0'} - - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mime-db@1.33.0: - resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==} - engines: {node: '>= 0.6'} - - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.18: - resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - - mimic-response@1.0.1: - resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} - engines: {node: '>=4'} - - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - - mini-css-extract-plugin@2.7.2: - resolution: {integrity: sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^5.0.0 - - minimalistic-assert@1.0.1: - resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - mrmime@1.0.1: - resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} - engines: {node: '>=10'} - - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - multicast-dns@7.2.5: - resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} - hasBin: true - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - - no-case@3.0.4: - resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - - node-emoji@1.11.0: - resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - - node-forge@1.3.1: - resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} - engines: {node: '>= 6.13.0'} - - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - - normalize-url@6.1.0: - resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} - engines: {node: '>=10'} - - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - - nprogress@0.2.0: - resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} - - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - - object.entries@1.1.9: - resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - - obuf@1.1.2: - resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - on-headers@1.0.2: - resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - open@8.4.0: - resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} - engines: {node: '>=12'} - - opener@1.5.2: - resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} - hasBin: true - - optionator@0.9.1: - resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} - engines: {node: '>= 0.8.0'} - - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} - - p-cancelable@2.1.1: - resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} - engines: {node: '>=8'} - - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@3.0.0: - resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} - engines: {node: '>=6'} - - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - p-map@4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} - - p-retry@4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} - engines: {node: '>=8'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - package-json@6.5.0: - resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} - engines: {node: '>=8'} - - param-case@3.0.4: - resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-entities@2.0.0: - resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - parse-numeric-range@1.3.0: - resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} - - parse5-htmlparser2-tree-adapter@7.1.0: - resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} - - parse5-parser-stream@7.1.2: - resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} - - parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - - parse5@7.2.1: - resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} - - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - - pascal-case@3.1.2: - resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} - - path-exists@3.0.0: - resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} - engines: {node: '>=4'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-is-inside@1.0.2: - resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-to-regexp@0.1.12: - resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} - - path-to-regexp@1.9.0: - resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} - - path-to-regexp@3.3.0: - resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} - - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - - pkg-up@3.1.0: - resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} - engines: {node: '>=8'} - - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - - postcss-calc@8.2.4: - resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} - peerDependencies: - postcss: ^8.2.2 - - postcss-colormin@5.3.0: - resolution: {integrity: sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-convert-values@5.1.3: - resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-comments@5.1.2: - resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-duplicates@5.1.0: - resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-empty@5.1.1: - resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-overridden@5.1.0: - resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-discard-unused@5.1.0: - resolution: {integrity: sha512-KwLWymI9hbwXmJa0dkrzpRbSJEh0vVUd7r8t0yOGPcfKzyJJxFM8kLyC5Ev9avji6nY95pOp1W6HqIrfT+0VGw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-loader@4.3.0: - resolution: {integrity: sha512-M/dSoIiNDOo8Rk0mUqoj4kpGq91gcxCfb9PoyZVdZ76/AuhxylHDYZblNE8o+EQ9AMSASeMFEKxZf5aU6wlx1Q==} - engines: {node: '>= 10.13.0'} - peerDependencies: - postcss: ^7.0.0 || ^8.0.1 - webpack: ^4.0.0 || ^5.0.0 - - postcss-loader@7.0.2: - resolution: {integrity: sha512-fUJzV/QH7NXUAqV8dWJ9Lg4aTkDCezpTS5HgJ2DvqznexTbSTxgi/dTECvTZ15BwKTtk8G/bqI/QTu2HPd3ZCg==} - engines: {node: '>= 14.15.0'} - peerDependencies: - postcss: ^7.0.0 || ^8.0.1 - webpack: ^5.0.0 - - postcss-merge-idents@5.1.1: - resolution: {integrity: sha512-pCijL1TREiCoog5nQp7wUe+TUonA2tC2sQ54UGeMmryK3UFGIYKqDyjnqd6RcuI4znFn9hWSLNN8xKE/vWcUQw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-merge-longhand@5.1.7: - resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-merge-rules@5.1.3: - resolution: {integrity: sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-font-values@5.1.0: - resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-gradients@5.1.1: - resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-params@5.1.4: - resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-minify-selectors@5.2.1: - resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-modules-extract-imports@3.0.0: - resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-local-by-default@4.0.0: - resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-scope@3.0.0: - resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-values@4.0.0: - resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-normalize-charset@5.1.0: - resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-display-values@5.1.0: - resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-positions@5.1.1: - resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-repeat-style@5.1.1: - resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-string@5.1.0: - resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-timing-functions@5.1.0: - resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-unicode@5.1.1: - resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-url@5.1.0: - resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-normalize-whitespace@5.1.1: - resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-ordered-values@5.1.3: - resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-reduce-idents@5.2.0: - resolution: {integrity: sha512-BTrLjICoSB6gxbc58D5mdBK8OhXRDqud/zodYfdSi52qvDHdMwk+9kB9xsM8yJThH/sZU5A6QVSmMmaN001gIg==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-reduce-initial@5.1.1: - resolution: {integrity: sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-reduce-transforms@5.1.0: - resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-selector-parser@6.0.10: - resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} - engines: {node: '>=4'} - - postcss-sort-media-queries@4.3.0: - resolution: {integrity: sha512-jAl8gJM2DvuIJiI9sL1CuiHtKM4s5aEIomkU8G3LFvbP+p8i7Sz8VV63uieTgoewGqKbi+hxBTiOKJlB35upCg==} - engines: {node: '>=10.0.0'} - peerDependencies: - postcss: ^8.4.16 - - postcss-svgo@5.1.0: - resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-unique-selectors@5.1.1: - resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - - postcss-zindex@5.1.0: - resolution: {integrity: sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - postcss@8.5.3: - resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} - engines: {node: ^10 || ^12 || >=14} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - - pretty-error@4.0.0: - resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} - - pretty-time@1.1.0: - resolution: {integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==} - engines: {node: '>=4'} - - prism-react-renderer@1.3.5: - resolution: {integrity: sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg==} - peerDependencies: - react: '>=0.14.9' - - prismjs@1.30.0: - resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} - engines: {node: '>=6'} - - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - - promise@7.3.1: - resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} - - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - - prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - - property-information@5.6.0: - resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} - - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - - pump@3.0.2: - resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} - - punycode@2.1.1: - resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} - engines: {node: '>=6'} - - pupa@2.1.1: - resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==} - engines: {node: '>=8'} - - pure-color@1.3.0: - resolution: {integrity: sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==} - - qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} - engines: {node: '>=0.6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - queue@6.0.2: - resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} - - quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - - range-parser@1.2.0: - resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} - engines: {node: '>= 0.6'} - - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} - engines: {node: '>= 0.8'} - - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - - react-base16-styling@0.6.0: - resolution: {integrity: sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==} - - react-dev-utils@12.0.1: - resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=2.7' - webpack: '>=4' - peerDependenciesMeta: - typescript: - optional: true - - react-dom@16.14.0: - resolution: {integrity: sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==} - peerDependencies: - react: ^16.14.0 - - react-error-overlay@6.0.11: - resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==} - - react-fast-compare@3.2.0: - resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==} - - react-helmet-async@1.3.0: - resolution: {integrity: sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==} - peerDependencies: - react: ^16.6.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0 - - react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - - react-json-view@1.21.3: - resolution: {integrity: sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==} - peerDependencies: - react: ^17.0.0 || ^16.3.0 || ^15.5.4 - react-dom: ^17.0.0 || ^16.3.0 || ^15.5.4 - - react-lifecycles-compat@3.0.4: - resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} - - react-loadable-ssr-addon-v5-slorber@1.0.1: - resolution: {integrity: sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==} - engines: {node: '>=10.13.0'} - peerDependencies: - react-loadable: '*' - webpack: '>=4.41.1 || 5.x' - - react-router-config@5.1.1: - resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==} - peerDependencies: - react: '>=15' - react-router: '>=5' - - react-router-dom@5.3.4: - resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} - peerDependencies: - react: '>=15' - - react-router@5.3.4: - resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} - peerDependencies: - react: '>=15' - - react-textarea-autosize@8.5.8: - resolution: {integrity: sha512-iUiIj70JefrTuSJ4LbVFiSqWiHHss5L63L717bqaWHMgkm9sz6eEvro4vZ3uQfGJbevzwT6rHOszHKA8RkhRMg==} - engines: {node: '>=10'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - react@16.14.0: - resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==} - engines: {node: '>=0.10.0'} - - readable-stream@2.3.7: - resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} - - readable-stream@3.6.0: - resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} - engines: {node: '>= 6'} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - reading-time@1.5.0: - resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} - - rechoir@0.6.2: - resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} - engines: {node: '>= 0.10'} - - recursive-readdir@2.2.3: - resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==} - engines: {node: '>=6.0.0'} - - reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - - regenerate-unicode-properties@10.1.0: - resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} - engines: {node: '>=4'} - - regenerate@1.4.2: - resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - - regenerator-transform@0.15.0: - resolution: {integrity: sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==} - - regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - - regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - - regexpu-core@5.2.1: - resolution: {integrity: sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ==} - engines: {node: '>=4'} - - registry-auth-token@4.2.2: - resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==} - engines: {node: '>=6.0.0'} - - registry-url@5.1.0: - resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==} - engines: {node: '>=8'} - - regjsgen@0.7.1: - resolution: {integrity: sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==} - - regjsparser@0.9.1: - resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} - hasBin: true - - rehype-katex@4.0.0: - resolution: {integrity: sha512-0mgBqYugQyIW0eUl6RDOZ28Cat2YzrnWGaYgKCMQnJw6ClmKgLqXBnkDAPGh2mwxvkkKwQOUMUpSLpA5rt7rzA==} - - rehype-parse@7.0.1: - resolution: {integrity: sha512-fOiR9a9xH+Le19i4fGzIEowAbwG7idy2Jzs4mOrFWBSJ0sNUgy0ev871dwWnbOo371SjgjG4pwzrbgSVrKxecw==} - - relateurl@0.2.7: - resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} - engines: {node: '>= 0.10'} - - remark-emoji@2.2.0: - resolution: {integrity: sha512-P3cj9s5ggsUvWw5fS2uzCHJMGuXYRb0NnZqYlNecewXt8QBU9n5vW3DUUKOhepS8F9CwdMx9B8a3i7pqFWAI5w==} - - remark-footnotes@2.0.0: - resolution: {integrity: sha512-3Clt8ZMH75Ayjp9q4CorNeyjwIxHFcTkaektplKGl2A1jNGEUey8cKL0ZC5vJwfcD5GFGsNLImLG/NGzWIzoMQ==} - - remark-math@3.0.1: - resolution: {integrity: sha512-epT77R/HK0x7NqrWHdSV75uNLwn8g9qTyMqCRCDujL0vj/6T6+yhdrR7mjELWtkse+Fw02kijAaBuVcHBor1+Q==} - - remark-mdx@1.6.22: - resolution: {integrity: sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ==} - - remark-parse@8.0.3: - resolution: {integrity: sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==} - - remark-squeeze-paragraphs@4.0.0: - resolution: {integrity: sha512-8qRqmL9F4nuLPIgl92XUuxI3pFxize+F1H0e/W3llTk0UsjJaj01+RrirkMw7P21RKe4X6goQhYRSvNWX+70Rw==} - - renderkid@3.0.0: - resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} - - repeat-string@1.6.1: - resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} - engines: {node: '>=0.10'} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - require-like@0.1.2: - resolution: {integrity: sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==} - - requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - - resolve-alpn@1.2.1: - resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - resolve-pathname@3.0.0: - resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} - - resolve@1.22.1: - resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} - hasBin: true - - resolve@2.0.0-next.5: - resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} - hasBin: true - - responselike@2.0.1: - resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} - - retry@0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} - engines: {node: '>= 4'} - - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - rtl-detect@1.0.4: - resolution: {integrity: sha512-EBR4I2VDSSYr7PkBmFy04uhycIpDKp+21p/jARYXlCSjQksTBQcJ0HFUPOO79EPPH5JS6VAhiIQbycf0O3JAxQ==} - - rtlcss@3.5.0: - resolution: {integrity: sha512-wzgMaMFHQTnyi9YOwsx9LjOxYXJPzS8sYnFaKm6R5ysvTkwzHiB0vxnbHwchHQT65PTdBjDG21/kQBWI7q9O7A==} - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - rxjs@7.8.2: - resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} - engines: {node: '>=0.4'} - - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} - - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - sax@1.4.1: - resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} - - scheduler@0.19.1: - resolution: {integrity: sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==} - - schema-utils@2.7.0: - resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==} - engines: {node: '>= 8.9.0'} - - schema-utils@2.7.1: - resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} - engines: {node: '>= 8.9.0'} - - schema-utils@3.1.1: - resolution: {integrity: sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==} - engines: {node: '>= 10.13.0'} - - schema-utils@4.3.3: - resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} - engines: {node: '>= 10.13.0'} - - section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - - select-hose@2.0.0: - resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} - - selfsigned@2.1.1: - resolution: {integrity: sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==} - engines: {node: '>=10'} - - semver-diff@3.1.1: - resolution: {integrity: sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==} - engines: {node: '>=8'} - - semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} - engines: {node: '>=10'} - hasBin: true - - send@0.19.0: - resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} - engines: {node: '>= 0.8.0'} - - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - - serve-handler@6.1.6: - resolution: {integrity: sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==} - - serve-index@1.9.1: - resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} - engines: {node: '>= 0.8.0'} - - serve-static@1.16.2: - resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} - engines: {node: '>= 0.8.0'} - - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - - set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - - set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} - - setimmediate@1.0.5: - resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - - setprototypeof@1.1.0: - resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - - shallow-clone@3.0.1: - resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} - engines: {node: '>=8'} - - shallowequal@1.1.0: - resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - shell-quote@1.7.4: - resolution: {integrity: sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==} - - shelljs@0.8.5: - resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} - engines: {node: '>=4'} - hasBin: true - - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - sirv@1.0.19: - resolution: {integrity: sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==} - engines: {node: '>= 10'} - - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - - sitemap@7.1.2: - resolution: {integrity: sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==} - engines: {node: '>=12.0.0', npm: '>=5.6.0'} - hasBin: true - - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - - slash@4.0.0: - resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} - engines: {node: '>=12'} - - slice-ansi@4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} - - sockjs@0.3.24: - resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} - - sort-css-media-queries@2.1.0: - resolution: {integrity: sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA==} - engines: {node: '>= 6.3.0'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - - source-map@0.5.7: - resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} - engines: {node: '>=0.10.0'} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - space-separated-tokens@1.1.5: - resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} - - spdy-transport@3.0.0: - resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} - - spdy@4.0.2: - resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} - engines: {node: '>=6.0.0'} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - stable@0.1.8: - resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} - deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' - - state-toggle@1.0.3: - resolution: {integrity: sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==} - - statuses@1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} - - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - - std-env@3.3.0: - resolution: {integrity: sha512-cNNS+VYsXIs5gI6gJipO4qZ8YYT274JHvNnQ1/R/x8Q8mdP0qj0zoMchRXmBNPqp/0eOEhX+3g7g6Fgb7meLIQ==} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - string.prototype.matchall@4.0.12: - resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} - engines: {node: '>= 0.4'} - - string.prototype.repeat@1.0.0: - resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} - - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} - engines: {node: '>= 0.4'} - - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} - - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - - stringify-object@3.3.0: - resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} - engines: {node: '>=4'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.0.1: - resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} - engines: {node: '>=12'} - - strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - style-to-object@0.3.0: - resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==} - - stylehacks@5.1.1: - resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} - engines: {node: ^10 || ^12 || >=14.0} - peerDependencies: - postcss: ^8.2.15 - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - svg-parser@2.0.4: - resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} - - svgo@2.8.0: - resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} - engines: {node: '>=10.13.0'} - hasBin: true - - table@6.8.0: - resolution: {integrity: sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==} - engines: {node: '>=10.0.0'} - - tapable@1.1.3: - resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} - engines: {node: '>=6'} - - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} - engines: {node: '>=6'} - - terser-webpack-plugin@5.3.16: - resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true - - terser@5.39.0: - resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==} - engines: {node: '>=10'} - hasBin: true - - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - - thunky@1.1.0: - resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} - - tiny-invariant@1.3.1: - resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} - - tiny-warning@1.0.3: - resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - - totalist@1.1.0: - resolution: {integrity: sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==} - engines: {node: '>=6'} - - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - trim-trailing-lines@1.1.4: - resolution: {integrity: sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==} - - trim@0.0.3: - resolution: {integrity: sha512-h82ywcYhHK7veeelXrCScdH7HkWfbIT1D/CgYO+nmDarz3SGNssVBMws6jU16Ga60AJCRAvPV6w6RLuNerQqjg==} - deprecated: Use String.prototype.trim() instead - - trough@1.0.5: - resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==} - - tslib@2.4.0: - resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - - type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} - - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - - typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} - - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} - - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} - engines: {node: '>= 0.4'} - - typedarray-to-buffer@3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} - - typescript@5.8.2: - resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} - engines: {node: '>=14.17'} - hasBin: true - - ua-parser-js@1.0.40: - resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==} - hasBin: true - - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - - undici@6.21.2: - resolution: {integrity: sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==} - engines: {node: '>=18.17'} - - unherit@1.1.3: - resolution: {integrity: sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==} - - unicode-canonical-property-names-ecmascript@2.0.0: - resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} - engines: {node: '>=4'} - - unicode-match-property-ecmascript@2.0.0: - resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} - engines: {node: '>=4'} - - unicode-match-property-value-ecmascript@2.0.0: - resolution: {integrity: sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==} - engines: {node: '>=4'} - - unicode-property-aliases-ecmascript@2.1.0: - resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} - engines: {node: '>=4'} - - unified@9.2.0: - resolution: {integrity: sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==} - - unified@9.2.2: - resolution: {integrity: sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==} - - unique-string@2.0.0: - resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} - engines: {node: '>=8'} - - unist-builder@2.0.3: - resolution: {integrity: sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==} - - unist-util-find-after@3.0.0: - resolution: {integrity: sha512-ojlBqfsBftYXExNu3+hHLfJQ/X1jYY/9vdm4yZWjIbf0VuWF6CRufci1ZyoD/wV2TYMKxXUoNuoqwy+CkgzAiQ==} - - unist-util-generated@1.1.6: - resolution: {integrity: sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==} - - unist-util-is@4.1.0: - resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} - - unist-util-position@3.1.0: - resolution: {integrity: sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==} - - unist-util-remove-position@2.0.1: - resolution: {integrity: sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==} - - unist-util-remove@2.1.0: - resolution: {integrity: sha512-J8NYPyBm4baYLdCbjmf1bhPu45Cr1MWTm77qd9istEkzWpnN6O9tMsEbB2JhNnBCqGENRqEWomQ+He6au0B27Q==} - - unist-util-stringify-position@2.0.3: - resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} - - unist-util-visit-parents@3.1.1: - resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} - - unist-util-visit@2.0.3: - resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==} - - universalify@2.0.0: - resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} - engines: {node: '>= 10.0.0'} - - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - update-notifier@5.1.0: - resolution: {integrity: sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==} - engines: {node: '>=10'} - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - url-loader@4.1.1: - resolution: {integrity: sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==} - engines: {node: '>= 10.13.0'} - peerDependencies: - file-loader: '*' - webpack: ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - file-loader: - optional: true - - use-composed-ref@1.4.0: - resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - use-isomorphic-layout-effect@1.2.0: - resolution: {integrity: sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - use-latest@1.3.0: - resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - use-sync-external-store@1.4.0: - resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - utila@0.4.0: - resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} - - utility-types@3.10.0: - resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==} - engines: {node: '>= 4'} - - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - - v8-compile-cache@2.3.0: - resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} - - value-equal@1.0.1: - resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - - vfile-location@3.2.0: - resolution: {integrity: sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==} - - vfile-message@2.0.4: - resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==} - - vfile@4.2.1: - resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==} - - wait-on@8.0.3: - resolution: {integrity: sha512-nQFqAFzZDeRxsu7S3C7LbuxslHhk+gnJZHyethuGKAn2IVleIbTB9I3vJSQiSR+DifUqmdzfPMoMPJfLqMF2vw==} - engines: {node: '>=12.0.0'} - hasBin: true - - watchpack@2.5.1: - resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} - engines: {node: '>=10.13.0'} - - wbuf@1.7.3: - resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} - - web-namespaces@1.1.4: - resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==} - - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - webpack-bundle-analyzer@4.6.1: - resolution: {integrity: sha512-oKz9Oz9j3rUciLNfpGFjOb49/jEpXNmWdVH8Ls//zNcnLlQdTGXQQMsBbb/gR7Zl8WNLxVCq+0Hqbx3zv6twBw==} - engines: {node: '>= 10.13.0'} - hasBin: true - - webpack-dev-middleware@5.3.4: - resolution: {integrity: sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^4.0.0 || ^5.0.0 - - webpack-dev-server@4.11.1: - resolution: {integrity: sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==} - engines: {node: '>= 12.13.0'} - hasBin: true - peerDependencies: - webpack: ^4.37.0 || ^5.0.0 - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - - webpack-merge@5.8.0: - resolution: {integrity: sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==} - engines: {node: '>=10.0.0'} - - webpack-sources@3.3.3: - resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} - engines: {node: '>=10.13.0'} - - webpack@5.105.0: - resolution: {integrity: sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - - webpackbar@5.0.2: - resolution: {integrity: sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ==} - engines: {node: '>=12'} - peerDependencies: - webpack: 3 || 4 || 5 - - websocket-driver@0.7.4: - resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} - engines: {node: '>=0.8.0'} - - websocket-extensions@0.1.4: - resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} - engines: {node: '>=0.8.0'} - - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation - - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - - which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - - which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} - - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - - which-typed-array@1.1.19: - resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} - engines: {node: '>= 0.4'} - - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - widest-line@3.1.0: - resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} - engines: {node: '>=8'} - - widest-line@4.0.1: - resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} - engines: {node: '>=12'} - - wildcard@2.0.0: - resolution: {integrity: sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==} - - word-wrap@1.2.4: - resolution: {integrity: sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==} - engines: {node: '>=0.10.0'} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.0.1: - resolution: {integrity: sha512-QFF+ufAqhoYHvoHdajT/Po7KoXVBPXS2bgjIam5isfWJPfIOnQZ50JtUiVvCv/sjgacf3yRrt2ZKUZ/V4itN4g==} - engines: {node: '>=12'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - write-file-atomic@3.0.3: - resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} - - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - ws@8.18.1: - resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xdg-basedir@4.0.0: - resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} - engines: {node: '>=8'} - - xml-js@1.6.11: - resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} - hasBin: true - - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - - yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - zwitch@1.0.5: - resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} - -snapshots: - - '@algolia/autocomplete-core@1.7.1': - dependencies: - '@algolia/autocomplete-shared': 1.7.1 - - '@algolia/autocomplete-preset-algolia@1.7.1(@algolia/client-search@4.14.2)(algoliasearch@4.14.2)': - dependencies: - '@algolia/autocomplete-shared': 1.7.1 - '@algolia/client-search': 4.14.2 - algoliasearch: 4.14.2 - - '@algolia/autocomplete-shared@1.7.1': {} - - '@algolia/cache-browser-local-storage@4.14.2': - dependencies: - '@algolia/cache-common': 4.14.2 - - '@algolia/cache-common@4.14.2': {} - - '@algolia/cache-in-memory@4.14.2': - dependencies: - '@algolia/cache-common': 4.14.2 - - '@algolia/client-account@4.14.2': - dependencies: - '@algolia/client-common': 4.14.2 - '@algolia/client-search': 4.14.2 - '@algolia/transporter': 4.14.2 - - '@algolia/client-analytics@4.14.2': - dependencies: - '@algolia/client-common': 4.14.2 - '@algolia/client-search': 4.14.2 - '@algolia/requester-common': 4.14.2 - '@algolia/transporter': 4.14.2 - - '@algolia/client-common@4.14.2': - dependencies: - '@algolia/requester-common': 4.14.2 - '@algolia/transporter': 4.14.2 - - '@algolia/client-personalization@4.14.2': - dependencies: - '@algolia/client-common': 4.14.2 - '@algolia/requester-common': 4.14.2 - '@algolia/transporter': 4.14.2 - - '@algolia/client-search@4.14.2': - dependencies: - '@algolia/client-common': 4.14.2 - '@algolia/requester-common': 4.14.2 - '@algolia/transporter': 4.14.2 - - '@algolia/events@4.0.1': {} - - '@algolia/logger-common@4.14.2': {} - - '@algolia/logger-console@4.14.2': - dependencies: - '@algolia/logger-common': 4.14.2 - - '@algolia/requester-browser-xhr@4.14.2': - dependencies: - '@algolia/requester-common': 4.14.2 - - '@algolia/requester-common@4.14.2': {} - - '@algolia/requester-node-http@4.14.2': - dependencies: - '@algolia/requester-common': 4.14.2 - - '@algolia/transporter@4.14.2': - dependencies: - '@algolia/cache-common': 4.14.2 - '@algolia/logger-common': 4.14.2 - '@algolia/requester-common': 4.14.2 - - '@ampproject/remapping@2.2.0': - dependencies: - '@jridgewell/gen-mapping': 0.1.1 - '@jridgewell/trace-mapping': 0.3.25 - - '@babel/code-frame@7.12.11': - dependencies: - '@babel/highlight': 7.22.20 - - '@babel/code-frame@7.26.2': - dependencies: - '@babel/helper-validator-identifier': 7.25.9 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.19.4': {} - - '@babel/core@7.12.9': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.23.0 - '@babel/helper-module-transforms': 7.19.6 - '@babel/helpers': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/template': 7.27.0 - '@babel/traverse': 7.23.2 - '@babel/types': 7.27.0 - convert-source-map: 1.9.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - lodash: 4.17.21 - resolve: 1.22.1 - semver: 5.7.2 - source-map: 0.5.7 - transitivePeerDependencies: - - supports-color - - '@babel/core@7.19.6': - dependencies: - '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.23.0 - '@babel/helper-compilation-targets': 7.19.3(@babel/core@7.19.6) - '@babel/helper-module-transforms': 7.19.6 - '@babel/helpers': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/template': 7.27.0 - '@babel/traverse': 7.23.2 - '@babel/types': 7.27.0 - convert-source-map: 1.9.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.23.0': - dependencies: - '@babel/types': 7.27.0 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 - - '@babel/helper-annotate-as-pure@7.18.6': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-builder-binary-assignment-operator-visitor@7.18.9': - dependencies: - '@babel/helper-explode-assignable-expression': 7.18.6 - '@babel/types': 7.27.0 - - '@babel/helper-compilation-targets@7.19.3(@babel/core@7.19.6)': - dependencies: - '@babel/compat-data': 7.19.4 - '@babel/core': 7.19.6 - '@babel/helper-validator-option': 7.18.6 - browserslist: 4.28.1 - semver: 6.3.1 - - '@babel/helper-create-class-features-plugin@7.19.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-member-expression-to-functions': 7.18.9 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-replace-supers': 7.19.1 - '@babel/helper-split-export-declaration': 7.22.6 - transitivePeerDependencies: - - supports-color - - '@babel/helper-create-regexp-features-plugin@7.19.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - regexpu-core: 5.2.1 - - '@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-compilation-targets': 7.19.3(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - debug: 4.3.4 - lodash.debounce: 4.0.8 - resolve: 1.22.1 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/helper-environment-visitor@7.22.20': {} - - '@babel/helper-explode-assignable-expression@7.18.6': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-function-name@7.23.0': - dependencies: - '@babel/template': 7.27.0 - '@babel/types': 7.27.0 - - '@babel/helper-hoist-variables@7.22.5': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-member-expression-to-functions@7.18.9': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-module-imports@7.18.6': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-module-transforms@7.19.6': - dependencies: - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-simple-access': 7.19.4 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/template': 7.27.0 - '@babel/traverse': 7.23.2 - '@babel/types': 7.27.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-optimise-call-expression@7.18.6': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-plugin-utils@7.10.4': {} - - '@babel/helper-plugin-utils@7.20.2': {} - - '@babel/helper-remap-async-to-generator@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-wrap-function': 7.19.0 - '@babel/types': 7.27.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-replace-supers@7.19.1': - dependencies: - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-member-expression-to-functions': 7.18.9 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/traverse': 7.23.2 - '@babel/types': 7.27.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-simple-access@7.19.4': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-skip-transparent-expression-wrappers@7.18.9': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-split-export-declaration@7.22.6': - dependencies: - '@babel/types': 7.27.0 - - '@babel/helper-string-parser@7.25.9': {} - - '@babel/helper-validator-identifier@7.25.9': {} - - '@babel/helper-validator-option@7.18.6': {} - - '@babel/helper-wrap-function@7.19.0': - dependencies: - '@babel/helper-function-name': 7.23.0 - '@babel/template': 7.27.0 - '@babel/traverse': 7.23.2 - '@babel/types': 7.27.0 - transitivePeerDependencies: - - supports-color - - '@babel/helpers@7.27.0': - dependencies: - '@babel/template': 7.27.0 - '@babel/types': 7.27.0 - - '@babel/highlight@7.22.20': - dependencies: - '@babel/helper-validator-identifier': 7.25.9 - chalk: 2.4.2 - js-tokens: 4.0.0 - - '@babel/parser@7.27.0': - dependencies: - '@babel/types': 7.27.0 - - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - '@babel/plugin-proposal-optional-chaining': 7.18.9(@babel/core@7.19.6) - - '@babel/plugin-proposal-async-generator-functions@7.19.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-class-static-block@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.19.6) - - '@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.19.6) - - '@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.19.6) - - '@babel/plugin-proposal-logical-assignment-operators@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.19.6) - - '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.19.6) - - '@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.19.6) - - '@babel/plugin-proposal-object-rest-spread@7.12.1(@babel/core@7.12.9)': - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.12.9) - '@babel/plugin-transform-parameters': 7.18.8(@babel/core@7.12.9) - - '@babel/plugin-proposal-object-rest-spread@7.19.4(@babel/core@7.19.6)': - dependencies: - '@babel/compat-data': 7.19.4 - '@babel/core': 7.19.6 - '@babel/helper-compilation-targets': 7.19.3(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-transform-parameters': 7.18.8(@babel/core@7.19.6) - - '@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.19.6) - - '@babel/plugin-proposal-optional-chaining@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.19.6) - - '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-private-property-in-object@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-regexp-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-import-assertions@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-jsx@7.12.1(@babel/core@7.12.9)': - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.12.9)': - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-syntax-typescript@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-arrow-functions@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-async-to-generator@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-block-scoped-functions@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-block-scoping@7.19.4(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-classes@7.19.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-compilation-targets': 7.19.3(@babel/core@7.19.6) - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-optimise-call-expression': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-replace-supers': 7.19.1 - '@babel/helper-split-export-declaration': 7.22.6 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-computed-properties@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-destructuring@7.19.4(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-dotall-regex@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-regexp-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-duplicate-keys@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-exponentiation-operator@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-for-of@7.18.8(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-function-name@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-compilation-targets': 7.19.3(@babel/core@7.19.6) - '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-literals@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-member-expression-literals@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-modules-amd@7.19.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-module-transforms': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-modules-commonjs@7.19.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-module-transforms': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-simple-access': 7.19.4 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-modules-systemjs@7.19.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-identifier': 7.25.9 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-modules-umd@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-module-transforms': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-named-capturing-groups-regex@7.19.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-regexp-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-new-target@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-object-super@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-replace-supers': 7.19.1 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-parameters@7.18.8(@babel/core@7.12.9)': - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-parameters@7.18.8(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-property-literals@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-react-constant-elements@7.20.2(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-react-display-name@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-react-jsx-development@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/plugin-transform-react-jsx': 7.19.0(@babel/core@7.19.6) - - '@babel/plugin-transform-react-jsx@7.19.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.19.6) - '@babel/types': 7.27.0 - - '@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-annotate-as-pure': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-regenerator@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - regenerator-transform: 0.15.0 - - '@babel/plugin-transform-reserved-words@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-runtime@7.19.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-plugin-utils': 7.20.2 - babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.19.6) - babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.19.6) - babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.19.6) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-shorthand-properties@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-spread@7.19.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-skip-transparent-expression-wrappers': 7.18.9 - - '@babel/plugin-transform-sticky-regex@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-template-literals@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-typeof-symbol@7.18.9(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-typescript@7.19.3(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-syntax-typescript': 7.18.6(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-unicode-escapes@7.18.10(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/plugin-transform-unicode-regex@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-create-regexp-features-plugin': 7.19.0(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - - '@babel/preset-env@7.19.4(@babel/core@7.19.6)': - dependencies: - '@babel/compat-data': 7.19.4 - '@babel/core': 7.19.6 - '@babel/helper-compilation-targets': 7.19.3(@babel/core@7.19.6) - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-proposal-async-generator-functions': 7.19.1(@babel/core@7.19.6) - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-class-static-block': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-dynamic-import': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-proposal-json-strings': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-logical-assignment-operators': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-object-rest-spread': 7.19.4(@babel/core@7.19.6) - '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-optional-chaining': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-private-property-in-object': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.19.6) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.19.6) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.19.6) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-import-assertions': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.19.6) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.19.6) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.19.6) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.19.6) - '@babel/plugin-transform-arrow-functions': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-async-to-generator': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-block-scoped-functions': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-block-scoping': 7.19.4(@babel/core@7.19.6) - '@babel/plugin-transform-classes': 7.19.0(@babel/core@7.19.6) - '@babel/plugin-transform-computed-properties': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-transform-destructuring': 7.19.4(@babel/core@7.19.6) - '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-duplicate-keys': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-transform-exponentiation-operator': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-for-of': 7.18.8(@babel/core@7.19.6) - '@babel/plugin-transform-function-name': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-transform-literals': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-transform-member-expression-literals': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-modules-amd': 7.19.6(@babel/core@7.19.6) - '@babel/plugin-transform-modules-commonjs': 7.19.6(@babel/core@7.19.6) - '@babel/plugin-transform-modules-systemjs': 7.19.6(@babel/core@7.19.6) - '@babel/plugin-transform-modules-umd': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-named-capturing-groups-regex': 7.19.1(@babel/core@7.19.6) - '@babel/plugin-transform-new-target': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-object-super': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-parameters': 7.18.8(@babel/core@7.19.6) - '@babel/plugin-transform-property-literals': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-regenerator': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-reserved-words': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-shorthand-properties': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-spread': 7.19.0(@babel/core@7.19.6) - '@babel/plugin-transform-sticky-regex': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-template-literals': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-transform-typeof-symbol': 7.18.9(@babel/core@7.19.6) - '@babel/plugin-transform-unicode-escapes': 7.18.10(@babel/core@7.19.6) - '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.19.6) - '@babel/preset-modules': 0.1.5(@babel/core@7.19.6) - '@babel/types': 7.27.0 - babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.19.6) - babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.19.6) - babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.19.6) - core-js-compat: 3.25.5 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/preset-modules@0.1.5(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.19.6) - '@babel/types': 7.27.0 - esutils: 2.0.3 - - '@babel/preset-react@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-react-jsx': 7.19.0(@babel/core@7.19.6) - '@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.19.6) - '@babel/plugin-transform-react-pure-annotations': 7.18.6(@babel/core@7.19.6) - - '@babel/preset-typescript@7.18.6(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-plugin-utils': 7.20.2 - '@babel/helper-validator-option': 7.18.6 - '@babel/plugin-transform-typescript': 7.19.3(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - '@babel/runtime-corejs3@7.27.0': - dependencies: - core-js-pure: 3.41.0 - regenerator-runtime: 0.14.1 - - '@babel/runtime@7.27.0': - dependencies: - regenerator-runtime: 0.14.1 - - '@babel/template@7.27.0': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 - - '@babel/traverse@7.23.2': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.23.0 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.27.0': - dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - - '@colors/colors@1.5.0': - optional: true - - '@docsearch/css@3.2.2': {} - - '@docsearch/react@3.2.2(@algolia/client-search@4.14.2)(@types/react@18.0.26)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)': - dependencies: - '@algolia/autocomplete-core': 1.7.1 - '@algolia/autocomplete-preset-algolia': 1.7.1(@algolia/client-search@4.14.2)(algoliasearch@4.14.2) - '@docsearch/css': 3.2.2 - algoliasearch: 4.14.2 - optionalDependencies: - '@types/react': 18.0.26 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - transitivePeerDependencies: - - '@algolia/client-search' - - '@docusaurus/core@2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@babel/core': 7.19.6 - '@babel/generator': 7.23.0 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.19.6) - '@babel/plugin-transform-runtime': 7.19.6(@babel/core@7.19.6) - '@babel/preset-env': 7.19.4(@babel/core@7.19.6) - '@babel/preset-react': 7.18.6(@babel/core@7.19.6) - '@babel/preset-typescript': 7.18.6(@babel/core@7.19.6) - '@babel/runtime': 7.27.0 - '@babel/runtime-corejs3': 7.27.0 - '@babel/traverse': 7.23.2 - '@docusaurus/cssnano-preset': 2.4.3 - '@docusaurus/logger': 2.4.3 - '@docusaurus/mdx-loader': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/react-loadable': 5.5.2(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@slorber/static-site-generator-webpack-plugin': 4.0.7 - '@svgr/webpack': 6.5.1 - autoprefixer: 10.4.12(postcss@8.5.3) - babel-loader: 8.2.5(@babel/core@7.19.6)(webpack@5.105.0) - babel-plugin-dynamic-import-node: 2.3.3 - boxen: 6.2.1 - chalk: 4.1.2 - chokidar: 3.5.3 - clean-css: 5.3.1 - cli-table3: 0.6.3 - combine-promises: 1.1.0 - commander: 5.1.0 - copy-webpack-plugin: 11.0.0(webpack@5.105.0) - core-js: 3.25.5 - css-loader: 6.7.3(webpack@5.105.0) - css-minimizer-webpack-plugin: 4.2.2(clean-css@5.3.1)(webpack@5.105.0) - cssnano: 5.1.13(postcss@8.5.3) - del: 6.1.1 - detect-port: 1.5.1 - escape-html: 1.0.3 - eta: 2.2.0 - file-loader: 6.2.0(webpack@5.105.0) - fs-extra: 10.1.0 - html-minifier-terser: 6.1.0 - html-tags: 3.2.0 - html-webpack-plugin: 5.5.0(webpack@5.105.0) - import-fresh: 3.3.0 - leven: 3.1.0 - lodash: 4.17.21 - mini-css-extract-plugin: 2.7.2(webpack@5.105.0) - postcss: 8.5.3 - postcss-loader: 7.0.2(postcss@8.5.3)(webpack@5.105.0) - prompts: 2.4.2 - react: 16.14.0 - react-dev-utils: 12.0.1(eslint@7.32.0)(typescript@5.8.2)(webpack@5.105.0) - react-dom: 16.14.0(react@16.14.0) - react-helmet-async: 1.3.0(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - react-loadable: '@docusaurus/react-loadable@5.5.2(react@16.14.0)' - react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@5.5.2(react@16.14.0))(webpack@5.105.0) - react-router: 5.3.4(react@16.14.0) - react-router-config: 5.1.1(react-router@5.3.4(react@16.14.0))(react@16.14.0) - react-router-dom: 5.3.4(react@16.14.0) - rtl-detect: 1.0.4 - semver: 7.7.1 - serve-handler: 6.1.6 - shelljs: 0.8.5 - terser-webpack-plugin: 5.3.16(webpack@5.105.0) - tslib: 2.4.0 - update-notifier: 5.1.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.105.0))(webpack@5.105.0) - wait-on: 8.0.3 - webpack: 5.105.0 - webpack-bundle-analyzer: 4.6.1 - webpack-dev-server: 4.11.1(webpack@5.105.0) - webpack-merge: 5.8.0 - webpackbar: 5.0.2(webpack@5.105.0) - transitivePeerDependencies: - - '@docusaurus/types' - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/cssnano-preset@2.4.3': - dependencies: - cssnano-preset-advanced: 5.3.9(postcss@8.5.3) - postcss: 8.5.3 - postcss-sort-media-queries: 4.3.0(postcss@8.5.3) - tslib: 2.4.0 - - '@docusaurus/logger@2.4.3': - dependencies: - chalk: 4.1.2 - tslib: 2.4.0 - - '@docusaurus/mdx-loader@2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0)': - dependencies: - '@babel/parser': 7.27.0 - '@babel/traverse': 7.23.2 - '@docusaurus/logger': 2.4.3 - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@mdx-js/mdx': 1.6.22 - escape-html: 1.0.3 - file-loader: 6.2.0(webpack@5.105.0) - fs-extra: 10.1.0 - image-size: 1.0.2 - mdast-util-to-string: 2.0.0 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - remark-emoji: 2.2.0 - stringify-object: 3.3.0 - tslib: 2.4.0 - unified: 9.2.2 - unist-util-visit: 2.0.3 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.105.0))(webpack@5.105.0) - webpack: 5.105.0 - transitivePeerDependencies: - - '@docusaurus/types' - - '@swc/core' - - esbuild - - supports-color - - uglify-js - - webpack-cli - - '@docusaurus/module-type-aliases@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)': - dependencies: - '@docusaurus/react-loadable': 5.5.2(react@16.14.0) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@types/history': 4.7.11 - '@types/react': 18.0.26 - '@types/react-router-config': 5.0.11 - '@types/react-router-dom': 5.3.3 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - react-helmet-async: 1.3.0(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - react-loadable: '@docusaurus/react-loadable@5.5.2(react@16.14.0)' - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - - webpack-cli - - '@docusaurus/plugin-content-blog@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/logger': 2.4.3 - '@docusaurus/mdx-loader': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - cheerio: 1.0.0 - feed: 4.2.2 - fs-extra: 10.1.0 - lodash: 4.17.21 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - reading-time: 1.5.0 - tslib: 2.4.0 - unist-util-visit: 2.0.3 - utility-types: 3.10.0 - webpack: 5.105.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-content-docs@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/logger': 2.4.3 - '@docusaurus/mdx-loader': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/module-type-aliases': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@types/react-router-config': 5.0.11 - combine-promises: 1.1.0 - fs-extra: 10.1.0 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - lodash: 4.17.21 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - utility-types: 3.10.0 - webpack: 5.105.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-content-pages@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/mdx-loader': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - fs-extra: 10.1.0 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - webpack: 5.105.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-debug@2.4.3(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - fs-extra: 10.1.0 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - react-json-view: 1.21.3(@types/react@18.0.26)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - tslib: 2.4.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - '@types/react' - - bufferutil - - csso - - debug - - encoding - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-google-analytics@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-google-gtag@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-google-tag-manager@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/plugin-sitemap@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/logger': 2.4.3 - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - fs-extra: 10.1.0 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - sitemap: 7.1.2 - tslib: 2.4.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/preset-classic@2.4.3(@algolia/client-search@4.14.2)(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-blog': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-docs': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-pages': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-debug': 2.4.3(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-google-analytics': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-google-gtag': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-google-tag-manager': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-sitemap': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-classic': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-search-algolia': 2.4.3(@algolia/client-search@4.14.2)(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - transitivePeerDependencies: - - '@algolia/client-search' - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - '@types/react' - - bufferutil - - csso - - debug - - encoding - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/react-loadable@5.5.2(react@16.14.0)': - dependencies: - '@types/react': 18.0.26 - prop-types: 15.8.1 - react: 16.14.0 - - '@docusaurus/theme-classic@2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/mdx-loader': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/module-type-aliases': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/plugin-content-blog': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-docs': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-pages': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-translations': 2.4.3 - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@mdx-js/react': 1.6.22(react@16.14.0) - clsx: 1.2.1 - copy-text-to-clipboard: 3.2.0 - infima: 0.2.0-alpha.43 - lodash: 4.17.21 - nprogress: 0.2.0 - postcss: 8.5.3 - prism-react-renderer: 1.3.5(react@16.14.0) - prismjs: 1.30.0 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - react-router-dom: 5.3.4(react@16.14.0) - rtlcss: 3.5.0 - tslib: 2.4.0 - utility-types: 3.10.0 - transitivePeerDependencies: - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/theme-common@2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docusaurus/mdx-loader': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/module-type-aliases': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/plugin-content-blog': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-docs': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/plugin-content-pages': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@types/history': 4.7.11 - '@types/react': 18.0.26 - '@types/react-router-config': 5.0.11 - clsx: 1.2.1 - parse-numeric-range: 1.3.0 - prism-react-renderer: 1.3.5(react@16.14.0) - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - use-sync-external-store: 1.4.0(react@16.14.0) - utility-types: 3.10.0 - transitivePeerDependencies: - - '@docusaurus/types' - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/theme-search-algolia@2.4.3(@algolia/client-search@4.14.2)(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(@types/react@18.0.26)(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2)': - dependencies: - '@docsearch/react': 3.2.2(@algolia/client-search@4.14.2)(@types/react@18.0.26)(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - '@docusaurus/core': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/logger': 2.4.3 - '@docusaurus/plugin-content-docs': 2.4.3(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-common': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))(eslint@7.32.0)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.8.2) - '@docusaurus/theme-translations': 2.4.3 - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - '@docusaurus/utils-validation': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - algoliasearch: 4.14.2 - algoliasearch-helper: 3.11.1(algoliasearch@4.14.2) - clsx: 1.2.1 - eta: 2.2.0 - fs-extra: 10.1.0 - lodash: 4.17.21 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - tslib: 2.4.0 - utility-types: 3.10.0 - transitivePeerDependencies: - - '@algolia/client-search' - - '@docusaurus/types' - - '@parcel/css' - - '@swc/core' - - '@swc/css' - - '@types/react' - - bufferutil - - csso - - debug - - esbuild - - eslint - - lightningcss - - supports-color - - typescript - - uglify-js - - utf-8-validate - - vue-template-compiler - - webpack-cli - - '@docusaurus/theme-translations@2.4.3': - dependencies: - fs-extra: 10.1.0 - tslib: 2.4.0 - - '@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)': - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.0.26 - commander: 5.1.0 - joi: 17.13.3 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - react-helmet-async: 1.3.0(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - utility-types: 3.10.0 - webpack: 5.105.0 - webpack-merge: 5.8.0 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - - webpack-cli - - '@docusaurus/utils-common@2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))': - dependencies: - tslib: 2.4.0 - optionalDependencies: - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - - '@docusaurus/utils-validation@2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))': - dependencies: - '@docusaurus/logger': 2.4.3 - '@docusaurus/utils': 2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0)) - joi: 17.13.3 - js-yaml: 4.1.0 - tslib: 2.4.0 - transitivePeerDependencies: - - '@docusaurus/types' - - '@swc/core' - - esbuild - - supports-color - - uglify-js - - webpack-cli - - '@docusaurus/utils@2.4.3(@docusaurus/types@2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0))': - dependencies: - '@docusaurus/logger': 2.4.3 - '@svgr/webpack': 6.5.1 - escape-string-regexp: 4.0.0 - file-loader: 6.2.0(webpack@5.105.0) - fs-extra: 10.1.0 - github-slugger: 1.4.0 - globby: 11.1.0 - gray-matter: 4.0.3 - js-yaml: 4.1.0 - lodash: 4.17.21 - micromatch: 4.0.8 - resolve-pathname: 3.0.0 - shelljs: 0.8.5 - tslib: 2.4.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.105.0))(webpack@5.105.0) - webpack: 5.105.0 - optionalDependencies: - '@docusaurus/types': 2.4.3(react-dom@16.14.0(react@16.14.0))(react@16.14.0) - transitivePeerDependencies: - - '@swc/core' - - esbuild - - supports-color - - uglify-js - - webpack-cli - - '@eslint/eslintrc@0.4.3': - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 7.3.1 - globals: 13.17.0 - ignore: 4.0.6 - import-fresh: 3.3.0 - js-yaml: 3.14.1 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@hapi/hoek@9.3.0': {} - - '@hapi/topo@5.1.0': - dependencies: - '@hapi/hoek': 9.3.0 - - '@humanwhocodes/config-array@0.5.0': - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@humanwhocodes/object-schema@1.2.1': {} - - '@jest/schemas@29.0.0': - dependencies: - '@sinclair/typebox': 0.24.51 - - '@jest/types@29.3.1': - dependencies: - '@jest/schemas': 29.0.0 - '@types/istanbul-lib-coverage': 2.0.4 - '@types/istanbul-reports': 3.0.1 - '@types/node': 18.11.3 - '@types/yargs': 17.0.19 - chalk: 4.1.2 - - '@jridgewell/gen-mapping@0.1.1': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.14 - - '@jridgewell/gen-mapping@0.3.8': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/resolve-uri@3.1.0': {} - - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/source-map@0.3.6': - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/sourcemap-codec@1.4.14': {} - - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - - '@leichtgewicht/ip-codec@2.0.4': {} - - '@mdx-js/mdx@1.6.22': - dependencies: - '@babel/core': 7.12.9 - '@babel/plugin-syntax-jsx': 7.12.1(@babel/core@7.12.9) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.12.9) - '@mdx-js/util': 1.6.22 - babel-plugin-apply-mdx-type-prop: 1.6.22(@babel/core@7.12.9) - babel-plugin-extract-import-names: 1.6.22 - camelcase-css: 2.0.1 - detab: 2.0.4 - hast-util-raw: 6.0.1 - lodash.uniq: 4.5.0 - mdast-util-to-hast: 10.0.1 - remark-footnotes: 2.0.0 - remark-mdx: 1.6.22 - remark-parse: 8.0.3 - remark-squeeze-paragraphs: 4.0.0 - style-to-object: 0.3.0 - unified: 9.2.0 - unist-builder: 2.0.3 - unist-util-visit: 2.0.3 - transitivePeerDependencies: - - supports-color - - '@mdx-js/react@1.6.22(react@16.14.0)': - dependencies: - react: 16.14.0 - - '@mdx-js/util@1.6.22': {} - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.13.0 - - '@polka/url@1.0.0-next.21': {} - - '@sideway/address@4.1.5': - dependencies: - '@hapi/hoek': 9.3.0 - - '@sideway/formula@3.0.1': {} - - '@sideway/pinpoint@2.0.0': {} - - '@sinclair/typebox@0.24.51': {} - - '@sindresorhus/is@4.6.0': {} - - '@slorber/static-site-generator-webpack-plugin@4.0.7': - dependencies: - eval: 0.1.8 - p-map: 4.0.0 - webpack-sources: 3.3.3 - - '@svgr/babel-plugin-add-jsx-attribute@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-remove-jsx-attribute@6.5.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-remove-jsx-empty-expression@6.5.0(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-replace-jsx-attribute-value@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-svg-dynamic-title@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-svg-em-dimensions@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-transform-react-native-svg@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-plugin-transform-svg-component@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - - '@svgr/babel-preset@6.5.1(@babel/core@7.19.6)': - dependencies: - '@babel/core': 7.19.6 - '@svgr/babel-plugin-add-jsx-attribute': 6.5.1(@babel/core@7.19.6) - '@svgr/babel-plugin-remove-jsx-attribute': 6.5.0(@babel/core@7.19.6) - '@svgr/babel-plugin-remove-jsx-empty-expression': 6.5.0(@babel/core@7.19.6) - '@svgr/babel-plugin-replace-jsx-attribute-value': 6.5.1(@babel/core@7.19.6) - '@svgr/babel-plugin-svg-dynamic-title': 6.5.1(@babel/core@7.19.6) - '@svgr/babel-plugin-svg-em-dimensions': 6.5.1(@babel/core@7.19.6) - '@svgr/babel-plugin-transform-react-native-svg': 6.5.1(@babel/core@7.19.6) - '@svgr/babel-plugin-transform-svg-component': 6.5.1(@babel/core@7.19.6) - - '@svgr/core@6.5.1': - dependencies: - '@babel/core': 7.19.6 - '@svgr/babel-preset': 6.5.1(@babel/core@7.19.6) - '@svgr/plugin-jsx': 6.5.1(@svgr/core@6.5.1) - camelcase: 6.3.0 - cosmiconfig: 7.0.1 - transitivePeerDependencies: - - supports-color - - '@svgr/hast-util-to-babel-ast@6.5.1': - dependencies: - '@babel/types': 7.27.0 - entities: 4.5.0 - - '@svgr/plugin-jsx@6.5.1(@svgr/core@6.5.1)': - dependencies: - '@babel/core': 7.19.6 - '@svgr/babel-preset': 6.5.1(@babel/core@7.19.6) - '@svgr/core': 6.5.1 - '@svgr/hast-util-to-babel-ast': 6.5.1 - svg-parser: 2.0.4 - transitivePeerDependencies: - - supports-color - - '@svgr/plugin-svgo@6.5.1(@svgr/core@6.5.1)': - dependencies: - '@svgr/core': 6.5.1 - cosmiconfig: 7.0.1 - deepmerge: 4.2.2 - svgo: 2.8.0 - - '@svgr/webpack@6.5.1': - dependencies: - '@babel/core': 7.19.6 - '@babel/plugin-transform-react-constant-elements': 7.20.2(@babel/core@7.19.6) - '@babel/preset-env': 7.19.4(@babel/core@7.19.6) - '@babel/preset-react': 7.18.6(@babel/core@7.19.6) - '@babel/preset-typescript': 7.18.6(@babel/core@7.19.6) - '@svgr/core': 6.5.1 - '@svgr/plugin-jsx': 6.5.1(@svgr/core@6.5.1) - '@svgr/plugin-svgo': 6.5.1(@svgr/core@6.5.1) - transitivePeerDependencies: - - supports-color - - '@szmarczak/http-timer@4.0.6': - dependencies: - defer-to-connect: 2.0.1 - - '@trysound/sax@0.2.0': {} - - '@types/body-parser@1.19.2': - dependencies: - '@types/connect': 3.4.35 - '@types/node': 18.11.3 - - '@types/bonjour@3.5.10': - dependencies: - '@types/node': 18.11.3 - - '@types/cacheable-request@6.0.3': - dependencies: - '@types/http-cache-semantics': 4.0.4 - '@types/keyv': 3.1.4 - '@types/node': 18.11.3 - '@types/responselike': 1.0.3 - - '@types/connect-history-api-fallback@1.3.5': - dependencies: - '@types/express-serve-static-core': 4.17.32 - '@types/node': 18.11.3 - - '@types/connect@3.4.35': - dependencies: - '@types/node': 18.11.3 - - '@types/eslint-scope@3.7.7': - dependencies: - '@types/eslint': 9.6.1 - '@types/estree': 1.0.8 - - '@types/eslint@9.6.1': - dependencies: - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - - '@types/estree@1.0.8': {} - - '@types/express-serve-static-core@4.17.32': - dependencies: - '@types/node': 18.11.3 - '@types/qs': 6.9.7 - '@types/range-parser': 1.2.4 - - '@types/express@4.17.15': - dependencies: - '@types/body-parser': 1.19.2 - '@types/express-serve-static-core': 4.17.32 - '@types/qs': 6.9.7 - '@types/serve-static': 1.15.0 - - '@types/hast@2.3.4': - dependencies: - '@types/unist': 2.0.6 - - '@types/history@4.7.11': {} - - '@types/html-minifier-terser@6.1.0': {} - - '@types/http-cache-semantics@4.0.4': {} - - '@types/http-proxy@1.17.9': - dependencies: - '@types/node': 18.11.3 - - '@types/istanbul-lib-coverage@2.0.4': {} - - '@types/istanbul-lib-report@3.0.0': - dependencies: - '@types/istanbul-lib-coverage': 2.0.4 - - '@types/istanbul-reports@3.0.1': - dependencies: - '@types/istanbul-lib-report': 3.0.0 - - '@types/json-schema@7.0.15': {} - - '@types/katex@0.11.1': {} - - '@types/keyv@3.1.4': - dependencies: - '@types/node': 18.11.3 - - '@types/mdast@3.0.10': - dependencies: - '@types/unist': 2.0.6 - - '@types/mime@3.0.1': {} - - '@types/node@17.0.45': {} - - '@types/node@18.11.3': {} - - '@types/parse-json@4.0.0': {} - - '@types/parse5@5.0.3': {} - - '@types/prop-types@15.7.5': {} - - '@types/qs@6.9.7': {} - - '@types/range-parser@1.2.4': {} - - '@types/react-router-config@5.0.11': - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.0.26 - '@types/react-router': 5.1.20 - - '@types/react-router-dom@5.3.3': - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.0.26 - '@types/react-router': 5.1.20 - - '@types/react-router@5.1.20': - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.0.26 - - '@types/react@18.0.26': - dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.2 - csstype: 3.1.1 - - '@types/responselike@1.0.3': - dependencies: - '@types/node': 18.11.3 - - '@types/retry@0.12.0': {} - - '@types/sax@1.2.7': - dependencies: - '@types/node': 18.11.3 - - '@types/scheduler@0.16.2': {} - - '@types/serve-index@1.9.1': - dependencies: - '@types/express': 4.17.15 - - '@types/serve-static@1.15.0': - dependencies: - '@types/mime': 3.0.1 - '@types/node': 18.11.3 - - '@types/sockjs@0.3.33': - dependencies: - '@types/node': 18.11.3 - - '@types/unist@2.0.6': {} - - '@types/ws@8.18.0': - dependencies: - '@types/node': 18.11.3 - - '@types/yargs-parser@21.0.0': {} - - '@types/yargs@17.0.19': - dependencies: - '@types/yargs-parser': 21.0.0 - - '@webassemblyjs/ast@1.14.1': - dependencies: - '@webassemblyjs/helper-numbers': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - - '@webassemblyjs/floating-point-hex-parser@1.13.2': {} - - '@webassemblyjs/helper-api-error@1.13.2': {} - - '@webassemblyjs/helper-buffer@1.14.1': {} - - '@webassemblyjs/helper-numbers@1.13.2': - dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.13.2 - '@webassemblyjs/helper-api-error': 1.13.2 - '@xtuc/long': 4.2.2 - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} - - '@webassemblyjs/helper-wasm-section@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/wasm-gen': 1.14.1 - - '@webassemblyjs/ieee754@1.13.2': - dependencies: - '@xtuc/ieee754': 1.2.0 - - '@webassemblyjs/leb128@1.13.2': - dependencies: - '@xtuc/long': 4.2.2 - - '@webassemblyjs/utf8@1.13.2': {} - - '@webassemblyjs/wasm-edit@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/helper-wasm-section': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-opt': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - '@webassemblyjs/wast-printer': 1.14.1 - - '@webassemblyjs/wasm-gen@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wasm-opt@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - - '@webassemblyjs/wasm-parser@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-api-error': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wast-printer@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@xtuc/long': 4.2.2 - - '@xtuc/ieee754@1.2.0': {} - - '@xtuc/long@4.2.2': {} - - accepts@1.3.8: - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - - acorn-import-phases@1.0.4(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn-jsx@5.3.2(acorn@7.4.1): - dependencies: - acorn: 7.4.1 - - acorn-walk@8.2.0: {} - - acorn@7.4.1: {} - - acorn@8.15.0: {} - - address@1.2.1: {} - - aggregate-error@3.1.0: - dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 - - ajv-formats@2.1.1(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 - - ajv-keywords@3.5.2(ajv@6.12.6): - dependencies: - ajv: 6.12.6 - - ajv-keywords@5.1.0(ajv@8.17.1): - dependencies: - ajv: 8.17.1 - fast-deep-equal: 3.1.3 - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ajv@8.17.1: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.0.6 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - algoliasearch-helper@3.11.1(algoliasearch@4.14.2): - dependencies: - '@algolia/events': 4.0.1 - algoliasearch: 4.14.2 - - algoliasearch@4.14.2: - dependencies: - '@algolia/cache-browser-local-storage': 4.14.2 - '@algolia/cache-common': 4.14.2 - '@algolia/cache-in-memory': 4.14.2 - '@algolia/client-account': 4.14.2 - '@algolia/client-analytics': 4.14.2 - '@algolia/client-common': 4.14.2 - '@algolia/client-personalization': 4.14.2 - '@algolia/client-search': 4.14.2 - '@algolia/logger-common': 4.14.2 - '@algolia/logger-console': 4.14.2 - '@algolia/requester-browser-xhr': 4.14.2 - '@algolia/requester-common': 4.14.2 - '@algolia/requester-node-http': 4.14.2 - '@algolia/transporter': 4.14.2 - - ansi-align@3.0.1: - dependencies: - string-width: 4.2.3 - - ansi-colors@4.1.3: {} - - ansi-html-community@0.0.8: {} - - ansi-regex@5.0.1: {} - - ansi-regex@6.0.1: {} - - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@6.2.1: {} - - anymatch@3.1.2: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - arg@5.0.2: {} - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - - argparse@2.0.1: {} - - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - array-flatten@1.1.1: {} - - array-flatten@2.1.2: {} - - array-includes@3.1.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - - array-union@2.1.0: {} - - array.prototype.findlast@1.2.5: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-shim-unscopables: 1.1.0 - - array.prototype.tosorted@1.1.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-shim-unscopables: 1.1.0 - - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - asap@2.0.6: {} - - astral-regex@2.0.0: {} - - async-function@1.0.0: {} - - asynckit@0.4.0: {} - - at-least-node@1.0.0: {} - - autoprefixer@10.4.12(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - caniuse-lite: 1.0.30001768 - fraction.js: 4.2.0 - normalize-range: 0.1.2 - picocolors: 1.1.1 - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - - axios@1.13.5: - dependencies: - follow-redirects: 1.15.11 - form-data: 4.0.5 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - - babel-eslint@10.1.0(eslint@7.32.0): - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.27.0 - '@babel/traverse': 7.23.2 - '@babel/types': 7.27.0 - eslint: 7.32.0 - eslint-visitor-keys: 1.3.0 - resolve: 1.22.1 - transitivePeerDependencies: - - supports-color - - babel-loader@8.2.5(@babel/core@7.19.6)(webpack@5.105.0): - dependencies: - '@babel/core': 7.19.6 - find-cache-dir: 3.3.2 - loader-utils: 2.0.4 - make-dir: 3.1.0 - schema-utils: 2.7.1 - webpack: 5.105.0 - - babel-plugin-apply-mdx-type-prop@1.6.22(@babel/core@7.12.9): - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.10.4 - '@mdx-js/util': 1.6.22 - - babel-plugin-dynamic-import-node@2.3.3: - dependencies: - object.assign: 4.1.7 - - babel-plugin-extract-import-names@1.6.22: - dependencies: - '@babel/helper-plugin-utils': 7.10.4 - - babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.19.6): - dependencies: - '@babel/compat-data': 7.19.4 - '@babel/core': 7.19.6 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.19.6) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - babel-plugin-polyfill-corejs3@0.6.0(@babel/core@7.19.6): - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.19.6) - core-js-compat: 3.25.5 - transitivePeerDependencies: - - supports-color - - babel-plugin-polyfill-regenerator@0.4.1(@babel/core@7.19.6): - dependencies: - '@babel/core': 7.19.6 - '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.19.6) - transitivePeerDependencies: - - supports-color - - bail@1.0.5: {} - - balanced-match@1.0.2: {} - - base16@1.0.0: {} - - baseline-browser-mapping@2.9.19: {} - - batch@0.6.1: {} - - big.js@5.2.2: {} - - binary-extensions@2.2.0: {} - - body-parser@1.20.3: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.13.0 - raw-body: 2.5.2 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - - bonjour-service@1.0.14: - dependencies: - array-flatten: 2.1.2 - dns-equal: 1.0.0 - fast-deep-equal: 3.1.3 - multicast-dns: 7.2.5 - - boolbase@1.0.0: {} - - boxen@5.1.2: - dependencies: - ansi-align: 3.0.1 - camelcase: 6.3.0 - chalk: 4.1.2 - cli-boxes: 2.2.1 - string-width: 4.2.3 - type-fest: 0.20.2 - widest-line: 3.1.0 - wrap-ansi: 7.0.0 - - boxen@6.2.1: - dependencies: - ansi-align: 3.0.1 - camelcase: 6.3.0 - chalk: 4.1.2 - cli-boxes: 3.0.0 - string-width: 5.1.2 - type-fest: 2.19.0 - widest-line: 4.0.1 - wrap-ansi: 8.0.1 - - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.28.1: - dependencies: - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001768 - electron-to-chromium: 1.5.286 - node-releases: 2.0.27 - update-browserslist-db: 1.2.3(browserslist@4.28.1) - - buffer-from@1.1.2: {} - - bytes@3.0.0: {} - - bytes@3.1.2: {} - - cacheable-lookup@5.0.4: {} - - cacheable-request@7.0.4: - dependencies: - clone-response: 1.0.3 - get-stream: 5.2.0 - http-cache-semantics: 4.2.0 - keyv: 4.5.4 - lowercase-keys: 2.0.0 - normalize-url: 6.1.0 - responselike: 2.0.1 - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bind@1.0.8: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - callsites@3.1.0: {} - - camel-case@4.1.2: - dependencies: - pascal-case: 3.1.2 - tslib: 2.4.0 - - camelcase-css@2.0.1: {} - - camelcase@6.3.0: {} - - caniuse-api@3.0.0: - dependencies: - browserslist: 4.28.1 - caniuse-lite: 1.0.30001768 - lodash.memoize: 4.1.2 - lodash.uniq: 4.5.0 - - caniuse-lite@1.0.30001768: {} - - ccount@1.1.0: {} - - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - character-entities-legacy@1.1.4: {} - - character-entities@1.2.4: {} - - character-reference-invalid@1.1.4: {} - - cheerio-select@2.1.0: - dependencies: - boolbase: 1.0.0 - css-select: 5.1.0 - css-what: 6.1.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - - cheerio@1.0.0: - dependencies: - cheerio-select: 2.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - domutils: 3.2.2 - encoding-sniffer: 0.2.0 - htmlparser2: 9.1.0 - parse5: 7.2.1 - parse5-htmlparser2-tree-adapter: 7.1.0 - parse5-parser-stream: 7.1.2 - undici: 6.21.2 - whatwg-mimetype: 4.0.0 - - chokidar@3.5.3: - dependencies: - anymatch: 3.1.2 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - chrome-trace-event@1.0.3: {} - - ci-info@2.0.0: {} - - ci-info@3.7.1: {} - - clean-css@5.3.1: - dependencies: - source-map: 0.6.1 - - clean-stack@2.2.0: {} - - cli-boxes@2.2.1: {} - - cli-boxes@3.0.0: {} - - cli-table3@0.6.3: - dependencies: - string-width: 4.2.3 - optionalDependencies: - '@colors/colors': 1.5.0 - - clone-deep@4.0.1: - dependencies: - is-plain-object: 2.0.4 - kind-of: 6.0.3 - shallow-clone: 3.0.1 - - clone-response@1.0.3: - dependencies: - mimic-response: 1.0.1 - - clsx@1.2.1: {} - - collapse-white-space@1.0.6: {} - - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.3: {} - - color-name@1.1.4: {} - - colord@2.9.3: {} - - colorette@2.0.19: {} - - combine-promises@1.1.0: {} - - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - - comma-separated-tokens@1.0.8: {} - - commander@2.20.3: {} - - commander@5.1.0: {} - - commander@7.2.0: {} - - commander@8.3.0: {} - - commondir@1.0.1: {} - - compressible@2.0.18: - dependencies: - mime-db: 1.52.0 - - compression@1.7.4: - dependencies: - accepts: 1.3.8 - bytes: 3.0.0 - compressible: 2.0.18 - debug: 2.6.9 - on-headers: 1.0.2 - safe-buffer: 5.1.2 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - concat-map@0.0.1: {} - - configstore@5.0.1: - dependencies: - dot-prop: 5.3.0 - graceful-fs: 4.2.11 - make-dir: 3.1.0 - unique-string: 2.0.0 - write-file-atomic: 3.0.3 - xdg-basedir: 4.0.0 - - connect-history-api-fallback@2.0.0: {} - - consola@2.15.3: {} - - content-disposition@0.5.2: {} - - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 - - content-type@1.0.5: {} - - convert-source-map@1.9.0: {} - - cookie-signature@1.0.6: {} - - cookie@0.7.1: {} - - copy-text-to-clipboard@3.2.0: {} - - copy-webpack-plugin@11.0.0(webpack@5.105.0): - dependencies: - fast-glob: 3.2.12 - glob-parent: 6.0.2 - globby: 13.1.3 - normalize-path: 3.0.0 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - webpack: 5.105.0 - - core-js-compat@3.25.5: - dependencies: - browserslist: 4.28.1 - - core-js-pure@3.41.0: {} - - core-js@3.25.5: {} - - core-util-is@1.0.3: {} - - cosmiconfig@6.0.0: - dependencies: - '@types/parse-json': 4.0.0 - import-fresh: 3.3.0 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 - - cosmiconfig@7.0.1: - dependencies: - '@types/parse-json': 4.0.0 - import-fresh: 3.3.0 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 - - cross-fetch@3.2.0: - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - crypto-random-string@2.0.0: {} - - css-declaration-sorter@6.3.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - css-loader@6.7.3(webpack@5.105.0): - dependencies: - icss-utils: 5.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-modules-extract-imports: 3.0.0(postcss@8.5.3) - postcss-modules-local-by-default: 4.0.0(postcss@8.5.3) - postcss-modules-scope: 3.0.0(postcss@8.5.3) - postcss-modules-values: 4.0.0(postcss@8.5.3) - postcss-value-parser: 4.2.0 - semver: 7.7.1 - webpack: 5.105.0 - - css-minimizer-webpack-plugin@4.2.2(clean-css@5.3.1)(webpack@5.105.0): - dependencies: - cssnano: 5.1.13(postcss@8.5.3) - jest-worker: 29.3.1 - postcss: 8.5.3 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - source-map: 0.6.1 - webpack: 5.105.0 - optionalDependencies: - clean-css: 5.3.1 - - css-select@4.3.0: - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 4.3.1 - domutils: 2.8.0 - nth-check: 2.1.1 - - css-select@5.1.0: - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 5.0.3 - domutils: 3.2.2 - nth-check: 2.1.1 - - css-tree@1.1.3: - dependencies: - mdn-data: 2.0.14 - source-map: 0.6.1 - - css-what@6.1.0: {} - - cssesc@3.0.0: {} - - cssnano-preset-advanced@5.3.9(postcss@8.5.3): - dependencies: - autoprefixer: 10.4.12(postcss@8.5.3) - cssnano-preset-default: 5.2.13(postcss@8.5.3) - postcss: 8.5.3 - postcss-discard-unused: 5.1.0(postcss@8.5.3) - postcss-merge-idents: 5.1.1(postcss@8.5.3) - postcss-reduce-idents: 5.2.0(postcss@8.5.3) - postcss-zindex: 5.1.0(postcss@8.5.3) - - cssnano-preset-default@5.2.13(postcss@8.5.3): - dependencies: - css-declaration-sorter: 6.3.1(postcss@8.5.3) - cssnano-utils: 3.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-calc: 8.2.4(postcss@8.5.3) - postcss-colormin: 5.3.0(postcss@8.5.3) - postcss-convert-values: 5.1.3(postcss@8.5.3) - postcss-discard-comments: 5.1.2(postcss@8.5.3) - postcss-discard-duplicates: 5.1.0(postcss@8.5.3) - postcss-discard-empty: 5.1.1(postcss@8.5.3) - postcss-discard-overridden: 5.1.0(postcss@8.5.3) - postcss-merge-longhand: 5.1.7(postcss@8.5.3) - postcss-merge-rules: 5.1.3(postcss@8.5.3) - postcss-minify-font-values: 5.1.0(postcss@8.5.3) - postcss-minify-gradients: 5.1.1(postcss@8.5.3) - postcss-minify-params: 5.1.4(postcss@8.5.3) - postcss-minify-selectors: 5.2.1(postcss@8.5.3) - postcss-normalize-charset: 5.1.0(postcss@8.5.3) - postcss-normalize-display-values: 5.1.0(postcss@8.5.3) - postcss-normalize-positions: 5.1.1(postcss@8.5.3) - postcss-normalize-repeat-style: 5.1.1(postcss@8.5.3) - postcss-normalize-string: 5.1.0(postcss@8.5.3) - postcss-normalize-timing-functions: 5.1.0(postcss@8.5.3) - postcss-normalize-unicode: 5.1.1(postcss@8.5.3) - postcss-normalize-url: 5.1.0(postcss@8.5.3) - postcss-normalize-whitespace: 5.1.1(postcss@8.5.3) - postcss-ordered-values: 5.1.3(postcss@8.5.3) - postcss-reduce-initial: 5.1.1(postcss@8.5.3) - postcss-reduce-transforms: 5.1.0(postcss@8.5.3) - postcss-svgo: 5.1.0(postcss@8.5.3) - postcss-unique-selectors: 5.1.1(postcss@8.5.3) - - cssnano-utils@3.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - cssnano@5.1.13(postcss@8.5.3): - dependencies: - cssnano-preset-default: 5.2.13(postcss@8.5.3) - lilconfig: 2.0.6 - postcss: 8.5.3 - yaml: 1.10.2 - - csso@4.2.0: - dependencies: - css-tree: 1.1.3 - - csstype@3.1.1: {} - - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - debug@2.6.9: - dependencies: - ms: 2.0.0 - - debug@4.3.4: - dependencies: - ms: 2.1.2 - - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - - deep-extend@0.6.0: {} - - deep-is@0.1.4: {} - - deepmerge@4.2.2: {} - - default-gateway@6.0.3: - dependencies: - execa: 5.1.1 - - defer-to-connect@2.0.1: {} - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-lazy-prop@2.0.0: {} - - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - - del@6.1.1: - dependencies: - globby: 11.1.0 - graceful-fs: 4.2.11 - is-glob: 4.0.3 - is-path-cwd: 2.2.0 - is-path-inside: 3.0.3 - p-map: 4.0.0 - rimraf: 3.0.2 - slash: 3.0.0 - - delayed-stream@1.0.0: {} - - depd@1.1.2: {} - - depd@2.0.0: {} - - destroy@1.2.0: {} - - detab@2.0.4: - dependencies: - repeat-string: 1.6.1 - - detect-node@2.1.0: {} - - detect-port-alt@1.1.6: - dependencies: - address: 1.2.1 - debug: 2.6.9 - transitivePeerDependencies: - - supports-color - - detect-port@1.5.1: - dependencies: - address: 1.2.1 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - - dns-equal@1.0.0: {} - - dns-packet@5.4.0: - dependencies: - '@leichtgewicht/ip-codec': 2.0.4 - - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - - dom-converter@0.2.0: - dependencies: - utila: 0.4.0 - - dom-serializer@1.4.1: - dependencies: - domelementtype: 2.3.0 - domhandler: 4.3.1 - entities: 2.2.0 - - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@4.3.1: - dependencies: - domelementtype: 2.3.0 - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@2.8.0: - dependencies: - dom-serializer: 1.4.1 - domelementtype: 2.3.0 - domhandler: 4.3.1 - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - - dot-case@3.0.4: - dependencies: - no-case: 3.0.4 - tslib: 2.4.0 - - dot-prop@5.3.0: - dependencies: - is-obj: 2.0.0 - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - duplexer@0.1.2: {} - - eastasianwidth@0.2.0: {} - - ee-first@1.1.1: {} - - electron-to-chromium@1.5.286: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - emojis-list@3.0.0: {} - - emoticon@3.2.0: {} - - encodeurl@1.0.2: {} - - encodeurl@2.0.0: {} - - encoding-sniffer@0.2.0: - dependencies: - iconv-lite: 0.6.3 - whatwg-encoding: 3.1.1 - - end-of-stream@1.4.4: - dependencies: - once: 1.4.0 - - enhanced-resolve@5.19.0: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.0 - - enquirer@2.3.6: - dependencies: - ansi-colors: 4.1.3 - - entities@2.2.0: {} - - entities@4.5.0: {} - - error-ex@1.3.2: - dependencies: - is-arrayish: 0.2.1 - - es-abstract@1.23.9: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-regex: 1.2.1 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.19 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-iterator-helpers@1.2.1: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-set-tostringtag: 2.1.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - iterator.prototype: 1.1.5 - safe-array-concat: 1.1.3 - - es-module-lexer@2.0.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.2 - - es-to-primitive@1.3.0: - dependencies: - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - - escalade@3.2.0: {} - - escape-goat@2.1.1: {} - - escape-html@1.0.3: {} - - escape-string-regexp@1.0.5: {} - - escape-string-regexp@4.0.0: {} - - eslint-plugin-react@7.37.4(eslint@7.32.0): - dependencies: - array-includes: 3.1.8 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.1 - eslint: 7.32.0 - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.3 - minimatch: 3.1.2 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - - eslint-scope@5.1.1: - dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - - eslint-utils@2.1.0: - dependencies: - eslint-visitor-keys: 1.3.0 - - eslint-visitor-keys@1.3.0: {} - - eslint-visitor-keys@2.1.0: {} - - eslint@7.32.0: - dependencies: - '@babel/code-frame': 7.12.11 - '@eslint/eslintrc': 0.4.3 - '@humanwhocodes/config-array': 0.5.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.3.4 - doctrine: 3.0.0 - enquirer: 2.3.6 - escape-string-regexp: 4.0.0 - eslint-scope: 5.1.1 - eslint-utils: 2.1.0 - eslint-visitor-keys: 2.1.0 - espree: 7.3.1 - esquery: 1.4.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - functional-red-black-tree: 1.0.1 - glob-parent: 5.1.2 - globals: 13.17.0 - ignore: 4.0.6 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - js-yaml: 3.14.1 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.1 - progress: 2.0.3 - regexpp: 3.2.0 - semver: 7.7.1 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - table: 6.8.0 - text-table: 0.2.0 - v8-compile-cache: 2.3.0 - transitivePeerDependencies: - - supports-color - - espree@7.3.1: - dependencies: - acorn: 7.4.1 - acorn-jsx: 5.3.2(acorn@7.4.1) - eslint-visitor-keys: 1.3.0 - - esprima@4.0.1: {} - - esquery@1.4.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@4.3.0: {} - - estraverse@5.3.0: {} - - esutils@2.0.3: {} - - eta@2.2.0: {} - - etag@1.8.1: {} - - eval@0.1.8: - dependencies: - '@types/node': 18.11.3 - require-like: 0.1.2 - - eventemitter3@4.0.7: {} - - events@3.3.0: {} - - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - express@4.21.2: - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.3 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.7.1 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.3.1 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.3 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.12 - proxy-addr: 2.0.7 - qs: 6.13.0 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.0 - serve-static: 1.16.2 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - extend-shallow@2.0.1: - dependencies: - is-extendable: 0.1.1 - - extend@3.0.2: {} - - fast-deep-equal@3.1.3: {} - - fast-glob@3.2.12: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fast-uri@3.0.6: {} - - fastq@1.13.0: - dependencies: - reusify: 1.0.4 - - faye-websocket@0.11.4: - dependencies: - websocket-driver: 0.7.4 - - fbemitter@3.0.0: - dependencies: - fbjs: 3.0.5 - transitivePeerDependencies: - - encoding - - fbjs-css-vars@1.0.2: {} - - fbjs@3.0.5: - dependencies: - cross-fetch: 3.2.0 - fbjs-css-vars: 1.0.2 - loose-envify: 1.4.0 - object-assign: 4.1.1 - promise: 7.3.1 - setimmediate: 1.0.5 - ua-parser-js: 1.0.40 - transitivePeerDependencies: - - encoding - - feed@4.2.2: - dependencies: - xml-js: 1.6.11 - - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.0.4 - - file-loader@6.2.0(webpack@5.105.0): - dependencies: - loader-utils: 2.0.4 - schema-utils: 3.1.1 - webpack: 5.105.0 - - filesize@8.0.7: {} - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - finalhandler@1.3.1: - dependencies: - debug: 2.6.9 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - - find-cache-dir@3.3.2: - dependencies: - commondir: 1.0.1 - make-dir: 3.1.0 - pkg-dir: 4.2.0 - - find-up@3.0.0: - dependencies: - locate-path: 3.0.0 - - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@3.0.4: - dependencies: - flatted: 3.2.7 - rimraf: 3.0.2 - - flatted@3.2.7: {} - - flux@4.0.4(react@16.14.0): - dependencies: - fbemitter: 3.0.0 - fbjs: 3.0.5 - react: 16.14.0 - transitivePeerDependencies: - - encoding - - follow-redirects@1.15.11: {} - - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - - fork-ts-checker-webpack-plugin@6.5.2(eslint@7.32.0)(typescript@5.8.2)(webpack@5.105.0): - dependencies: - '@babel/code-frame': 7.26.2 - '@types/json-schema': 7.0.15 - chalk: 4.1.2 - chokidar: 3.5.3 - cosmiconfig: 6.0.0 - deepmerge: 4.2.2 - fs-extra: 9.1.0 - glob: 7.2.3 - memfs: 3.4.12 - minimatch: 3.1.2 - schema-utils: 2.7.0 - semver: 7.7.1 - tapable: 1.1.3 - typescript: 5.8.2 - webpack: 5.105.0 - optionalDependencies: - eslint: 7.32.0 - - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - - forwarded@0.2.0: {} - - fraction.js@4.2.0: {} - - fresh@0.5.2: {} - - fs-extra@10.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.0 - - fs-extra@9.1.0: - dependencies: - at-least-node: 1.0.0 - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.0 - - fs-monkey@1.0.3: {} - - fs.realpath@1.0.0: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - function.prototype.name@1.1.8: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - functions-have-names: 1.2.3 - hasown: 2.0.2 - is-callable: 1.2.7 - - functional-red-black-tree@1.0.1: {} - - functions-have-names@1.2.3: {} - - gensync@1.0.0-beta.2: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-own-enumerable-property-symbols@3.0.2: {} - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - get-stream@5.2.0: - dependencies: - pump: 3.0.2 - - get-stream@6.0.1: {} - - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - - github-slugger@1.4.0: {} - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob-to-regexp@0.4.1: {} - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - global-dirs@3.0.0: - dependencies: - ini: 2.0.0 - - global-modules@2.0.0: - dependencies: - global-prefix: 3.0.0 - - global-prefix@3.0.0: - dependencies: - ini: 1.3.8 - kind-of: 6.0.3 - which: 1.3.1 - - globals@11.12.0: {} - - globals@13.17.0: - dependencies: - type-fest: 0.20.2 - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.2.12 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 3.0.0 - - globby@13.1.3: - dependencies: - dir-glob: 3.0.1 - fast-glob: 3.2.12 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 4.0.0 - - gopd@1.2.0: {} - - got@11.8.6: - dependencies: - '@sindresorhus/is': 4.6.0 - '@szmarczak/http-timer': 4.0.6 - '@types/cacheable-request': 6.0.3 - '@types/responselike': 1.0.3 - cacheable-lookup: 5.0.4 - cacheable-request: 7.0.4 - decompress-response: 6.0.0 - http2-wrapper: 1.0.3 - lowercase-keys: 2.0.0 - p-cancelable: 2.1.1 - responselike: 2.0.1 - - graceful-fs@4.2.11: {} - - gray-matter@4.0.3: - dependencies: - js-yaml: 3.14.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - - gzip-size@6.0.0: - dependencies: - duplexer: 0.1.2 - - handle-thing@2.0.1: {} - - has-bigints@1.1.0: {} - - has-flag@3.0.0: {} - - has-flag@4.0.0: {} - - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - has-yarn@2.1.0: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - hast-to-hyperscript@9.0.1: - dependencies: - '@types/unist': 2.0.6 - comma-separated-tokens: 1.0.8 - property-information: 5.6.0 - space-separated-tokens: 1.1.5 - style-to-object: 0.3.0 - unist-util-is: 4.1.0 - web-namespaces: 1.1.4 - - hast-util-from-parse5@6.0.1: - dependencies: - '@types/parse5': 5.0.3 - hastscript: 6.0.0 - property-information: 5.6.0 - vfile: 4.2.1 - vfile-location: 3.2.0 - web-namespaces: 1.1.4 - - hast-util-is-element@1.1.0: {} - - hast-util-parse-selector@2.2.5: {} - - hast-util-raw@6.0.1: - dependencies: - '@types/hast': 2.3.4 - hast-util-from-parse5: 6.0.1 - hast-util-to-parse5: 6.0.0 - html-void-elements: 1.0.5 - parse5: 6.0.1 - unist-util-position: 3.1.0 - vfile: 4.2.1 - web-namespaces: 1.1.4 - xtend: 4.0.2 - zwitch: 1.0.5 - - hast-util-to-parse5@6.0.0: - dependencies: - hast-to-hyperscript: 9.0.1 - property-information: 5.6.0 - web-namespaces: 1.1.4 - xtend: 4.0.2 - zwitch: 1.0.5 - - hast-util-to-text@2.0.1: - dependencies: - hast-util-is-element: 1.1.0 - repeat-string: 1.6.1 - unist-util-find-after: 3.0.0 - - hastscript@6.0.0: - dependencies: - '@types/hast': 2.3.4 - comma-separated-tokens: 1.0.8 - hast-util-parse-selector: 2.2.5 - property-information: 5.6.0 - space-separated-tokens: 1.1.5 - - he@1.2.0: {} - - history@4.10.1: - dependencies: - '@babel/runtime': 7.27.0 - loose-envify: 1.4.0 - resolve-pathname: 3.0.0 - tiny-invariant: 1.3.1 - tiny-warning: 1.0.3 - value-equal: 1.0.1 - - hoist-non-react-statics@3.3.2: - dependencies: - react-is: 16.13.1 - - hpack.js@2.1.6: - dependencies: - inherits: 2.0.4 - obuf: 1.1.2 - readable-stream: 2.3.7 - wbuf: 1.7.3 - - html-entities@2.3.3: {} - - html-minifier-terser@6.1.0: - dependencies: - camel-case: 4.1.2 - clean-css: 5.3.1 - commander: 8.3.0 - he: 1.2.0 - param-case: 3.0.4 - relateurl: 0.2.7 - terser: 5.39.0 - - html-tags@3.2.0: {} - - html-void-elements@1.0.5: {} - - html-webpack-plugin@5.5.0(webpack@5.105.0): - dependencies: - '@types/html-minifier-terser': 6.1.0 - html-minifier-terser: 6.1.0 - lodash: 4.17.21 - pretty-error: 4.0.0 - tapable: 2.3.0 - webpack: 5.105.0 - - htmlparser2@6.1.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 4.3.1 - domutils: 2.8.0 - entities: 2.2.0 - - htmlparser2@9.1.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 4.5.0 - - http-cache-semantics@4.2.0: {} - - http-deceiver@1.2.7: {} - - http-errors@1.6.3: - dependencies: - depd: 1.1.2 - inherits: 2.0.3 - setprototypeof: 1.1.0 - statuses: 1.5.0 - - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - - http-parser-js@0.5.8: {} - - http-proxy-middleware@2.0.9(@types/express@4.17.15): - dependencies: - '@types/http-proxy': 1.17.9 - http-proxy: 1.18.1 - is-glob: 4.0.3 - is-plain-obj: 3.0.0 - micromatch: 4.0.8 - optionalDependencies: - '@types/express': 4.17.15 - transitivePeerDependencies: - - debug - - http-proxy@1.18.1: - dependencies: - eventemitter3: 4.0.7 - follow-redirects: 1.15.11 - requires-port: 1.0.0 - transitivePeerDependencies: - - debug - - http2-wrapper@1.0.3: - dependencies: - quick-lru: 5.1.1 - resolve-alpn: 1.2.1 - - human-signals@2.1.0: {} - - iconv-lite@0.4.24: - dependencies: - safer-buffer: 2.1.2 - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - icss-utils@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - ignore@4.0.6: {} - - ignore@5.2.4: {} - - image-size@1.0.2: - dependencies: - queue: 6.0.2 - - immer@9.0.17: {} - - import-fresh@3.3.0: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - import-lazy@2.1.0: {} - - imurmurhash@0.1.4: {} - - indent-string@4.0.0: {} - - infima@0.2.0-alpha.43: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.3: {} - - inherits@2.0.4: {} - - ini@1.3.8: {} - - ini@2.0.0: {} - - inline-style-parser@0.1.1: {} - - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 - - interpret@1.4.0: {} - - invariant@2.2.4: - dependencies: - loose-envify: 1.4.0 - - ipaddr.js@1.9.1: {} - - ipaddr.js@2.0.1: {} - - is-alphabetical@1.0.4: {} - - is-alphanumerical@1.0.4: - dependencies: - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-arrayish@0.2.1: {} - - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.2.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-buffer@2.0.5: {} - - is-callable@1.2.7: {} - - is-ci@2.0.0: - dependencies: - ci-info: 2.0.0 - - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-decimal@1.0.4: {} - - is-docker@2.2.1: {} - - is-extendable@0.1.1: {} - - is-extglob@2.1.1: {} - - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-fullwidth-code-point@3.0.0: {} - - is-generator-function@1.1.0: - dependencies: - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-hexadecimal@1.0.4: {} - - is-installed-globally@0.4.0: - dependencies: - global-dirs: 3.0.0 - is-path-inside: 3.0.3 - - is-map@2.0.3: {} - - is-npm@5.0.0: {} - - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-number@7.0.0: {} - - is-obj@1.0.1: {} - - is-obj@2.0.0: {} - - is-path-cwd@2.2.0: {} - - is-path-inside@3.0.3: {} - - is-plain-obj@2.1.0: {} - - is-plain-obj@3.0.0: {} - - is-plain-object@2.0.4: - dependencies: - isobject: 3.0.1 - - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - - is-regexp@1.0.0: {} - - is-root@2.1.0: {} - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - - is-stream@2.0.1: {} - - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.19 - - is-typedarray@1.0.0: {} - - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-whitespace-character@1.0.4: {} - - is-word-character@1.0.4: {} - - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - - is-yarn-global@0.3.0: {} - - isarray@0.0.1: {} - - isarray@1.0.0: {} - - isarray@2.0.5: {} - - isexe@2.0.0: {} - - isobject@3.0.1: {} - - iterator.prototype@1.1.5: - dependencies: - define-data-property: 1.1.4 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - has-symbols: 1.1.0 - set-function-name: 2.0.2 - - jest-util@29.3.1: - dependencies: - '@jest/types': 29.3.1 - '@types/node': 18.11.3 - chalk: 4.1.2 - ci-info: 3.7.1 - graceful-fs: 4.2.11 - picomatch: 2.3.1 - - jest-worker@27.5.1: - dependencies: - '@types/node': 18.11.3 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - jest-worker@29.3.1: - dependencies: - '@types/node': 18.11.3 - jest-util: 29.3.1 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - joi@17.13.3: - dependencies: - '@hapi/hoek': 9.3.0 - '@hapi/topo': 5.1.0 - '@sideway/address': 4.1.5 - '@sideway/formula': 3.0.1 - '@sideway/pinpoint': 2.0.0 - - js-tokens@4.0.0: {} - - js-yaml@3.14.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - jsesc@0.5.0: {} - - jsesc@2.5.2: {} - - json-buffer@3.0.1: {} - - json-parse-even-better-errors@2.3.1: {} - - json-schema-traverse@0.4.1: {} - - json-schema-traverse@1.0.0: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@2.2.3: {} - - jsonfile@6.1.0: - dependencies: - universalify: 2.0.0 - optionalDependencies: - graceful-fs: 4.2.11 - - jsx-ast-utils@3.3.3: - dependencies: - array-includes: 3.1.8 - object.assign: 4.1.7 - - katex@0.16.28: - dependencies: - commander: 8.3.0 - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - kind-of@6.0.3: {} - - kleur@3.0.3: {} - - klona@2.0.6: {} - - latest-version@5.1.0: - dependencies: - package-json: 6.5.0 - - leven@3.1.0: {} - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lilconfig@2.0.6: {} - - lines-and-columns@1.2.4: {} - - loader-runner@4.3.1: {} - - loader-utils@2.0.4: - dependencies: - big.js: 5.2.2 - emojis-list: 3.0.0 - json5: 2.2.3 - - loader-utils@3.2.1: {} - - locate-path@3.0.0: - dependencies: - p-locate: 3.0.0 - path-exists: 3.0.0 - - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.curry@4.1.1: {} - - lodash.debounce@4.0.8: {} - - lodash.flow@3.5.0: {} - - lodash.memoize@4.1.2: {} - - lodash.merge@4.6.2: {} - - lodash.truncate@4.4.2: {} - - lodash.uniq@4.5.0: {} - - lodash@4.17.21: {} - - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - - lower-case@2.0.2: - dependencies: - tslib: 2.4.0 - - lowercase-keys@2.0.0: {} - - make-dir@3.1.0: - dependencies: - semver: 6.3.1 - - markdown-escapes@1.0.4: {} - - math-intrinsics@1.1.0: {} - - mdast-squeeze-paragraphs@4.0.0: - dependencies: - unist-util-remove: 2.1.0 - - mdast-util-definitions@4.0.0: - dependencies: - unist-util-visit: 2.0.3 - - mdast-util-to-hast@10.0.1: - dependencies: - '@types/mdast': 3.0.10 - '@types/unist': 2.0.6 - mdast-util-definitions: 4.0.0 - mdurl: 1.0.1 - unist-builder: 2.0.3 - unist-util-generated: 1.1.6 - unist-util-position: 3.1.0 - unist-util-visit: 2.0.3 - - mdast-util-to-string@2.0.0: {} - - mdn-data@2.0.14: {} - - mdurl@1.0.1: {} - - media-typer@0.3.0: {} - - memfs@3.4.12: - dependencies: - fs-monkey: 1.0.3 - - merge-descriptors@1.0.3: {} - - merge-stream@2.0.0: {} - - merge2@1.4.1: {} - - methods@1.1.2: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - mime-db@1.33.0: {} - - mime-db@1.52.0: {} - - mime-types@2.1.18: - dependencies: - mime-db: 1.33.0 - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - mime@1.6.0: {} - - mimic-fn@2.1.0: {} - - mimic-response@1.0.1: {} - - mimic-response@3.1.0: {} - - mini-css-extract-plugin@2.7.2(webpack@5.105.0): - dependencies: - schema-utils: 4.3.3 - webpack: 5.105.0 - - minimalistic-assert@1.0.1: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - minimist@1.2.8: {} - - mrmime@1.0.1: {} - - ms@2.0.0: {} - - ms@2.1.2: {} - - ms@2.1.3: {} - - multicast-dns@7.2.5: - dependencies: - dns-packet: 5.4.0 - thunky: 1.1.0 - - nanoid@3.3.11: {} - - natural-compare@1.4.0: {} - - negotiator@0.6.3: {} - - neo-async@2.6.2: {} - - no-case@3.0.4: - dependencies: - lower-case: 2.0.2 - tslib: 2.4.0 - - node-emoji@1.11.0: - dependencies: - lodash: 4.17.21 - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - - node-forge@1.3.1: {} - - node-releases@2.0.27: {} - - normalize-path@3.0.0: {} - - normalize-range@0.1.2: {} - - normalize-url@6.1.0: {} - - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - - nprogress@0.2.0: {} - - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 - - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - object.entries@1.1.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - obuf@1.1.2: {} - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - on-headers@1.0.2: {} - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - open@8.4.0: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - - opener@1.5.2: {} - - optionator@0.9.1: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.4 - - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - - p-cancelable@2.1.1: {} - - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@3.0.0: - dependencies: - p-limit: 2.3.0 - - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - p-map@4.0.0: - dependencies: - aggregate-error: 3.1.0 - - p-retry@4.6.2: - dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - - p-try@2.2.0: {} - - package-json@6.5.0: - dependencies: - got: 11.8.6 - registry-auth-token: 4.2.2 - registry-url: 5.1.0 - semver: 6.3.1 - - param-case@3.0.4: - dependencies: - dot-case: 3.0.4 - tslib: 2.4.0 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse-entities@2.0.0: - dependencies: - character-entities: 1.2.4 - character-entities-legacy: 1.1.4 - character-reference-invalid: 1.1.4 - is-alphanumerical: 1.0.4 - is-decimal: 1.0.4 - is-hexadecimal: 1.0.4 - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.26.2 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - parse-numeric-range@1.3.0: {} - - parse5-htmlparser2-tree-adapter@7.1.0: - dependencies: - domhandler: 5.0.3 - parse5: 7.2.1 - - parse5-parser-stream@7.1.2: - dependencies: - parse5: 7.2.1 - - parse5@6.0.1: {} - - parse5@7.2.1: - dependencies: - entities: 4.5.0 - - parseurl@1.3.3: {} - - pascal-case@3.1.2: - dependencies: - no-case: 3.0.4 - tslib: 2.4.0 - - path-exists@3.0.0: {} - - path-exists@4.0.0: {} - - path-is-absolute@1.0.1: {} - - path-is-inside@1.0.2: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - path-to-regexp@0.1.12: {} - - path-to-regexp@1.9.0: - dependencies: - isarray: 0.0.1 - - path-to-regexp@3.3.0: {} - - path-type@4.0.0: {} - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 - - pkg-up@3.1.0: - dependencies: - find-up: 3.0.0 - - possible-typed-array-names@1.1.0: {} - - postcss-calc@8.2.4(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - postcss-value-parser: 4.2.0 - - postcss-colormin@5.3.0(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - caniuse-api: 3.0.0 - colord: 2.9.3 - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-convert-values@5.1.3(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-discard-comments@5.1.2(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-discard-duplicates@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-discard-empty@5.1.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-discard-overridden@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-discard-unused@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - - postcss-loader@4.3.0(postcss@8.5.3)(webpack@5.105.0): - dependencies: - cosmiconfig: 7.0.1 - klona: 2.0.6 - loader-utils: 2.0.4 - postcss: 8.5.3 - schema-utils: 3.1.1 - semver: 7.7.1 - webpack: 5.105.0 - - postcss-loader@7.0.2(postcss@8.5.3)(webpack@5.105.0): - dependencies: - cosmiconfig: 7.0.1 - klona: 2.0.6 - postcss: 8.5.3 - semver: 7.7.1 - webpack: 5.105.0 - - postcss-merge-idents@5.1.1(postcss@8.5.3): - dependencies: - cssnano-utils: 3.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-merge-longhand@5.1.7(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - stylehacks: 5.1.1(postcss@8.5.3) - - postcss-merge-rules@5.1.3(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - caniuse-api: 3.0.0 - cssnano-utils: 3.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - - postcss-minify-font-values@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-minify-gradients@5.1.1(postcss@8.5.3): - dependencies: - colord: 2.9.3 - cssnano-utils: 3.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-minify-params@5.1.4(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - cssnano-utils: 3.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-minify-selectors@5.2.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - - postcss-modules-extract-imports@3.0.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-modules-local-by-default@4.0.0(postcss@8.5.3): - dependencies: - icss-utils: 5.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - postcss-value-parser: 4.2.0 - - postcss-modules-scope@3.0.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - - postcss-modules-values@4.0.0(postcss@8.5.3): - dependencies: - icss-utils: 5.1.0(postcss@8.5.3) - postcss: 8.5.3 - - postcss-normalize-charset@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-normalize-display-values@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-positions@5.1.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-repeat-style@5.1.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-string@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-timing-functions@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-unicode@5.1.1(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-url@5.1.0(postcss@8.5.3): - dependencies: - normalize-url: 6.1.0 - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-normalize-whitespace@5.1.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-ordered-values@5.1.3(postcss@8.5.3): - dependencies: - cssnano-utils: 3.1.0(postcss@8.5.3) - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-reduce-idents@5.2.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-reduce-initial@5.1.1(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - caniuse-api: 3.0.0 - postcss: 8.5.3 - - postcss-reduce-transforms@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - - postcss-selector-parser@6.0.10: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss-sort-media-queries@4.3.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - sort-css-media-queries: 2.1.0 - - postcss-svgo@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-value-parser: 4.2.0 - svgo: 2.8.0 - - postcss-unique-selectors@5.1.1(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - - postcss-value-parser@4.2.0: {} - - postcss-zindex@5.1.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss@8.5.3: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prelude-ls@1.2.1: {} - - prettier@2.8.8: {} - - pretty-error@4.0.0: - dependencies: - lodash: 4.17.21 - renderkid: 3.0.0 - - pretty-time@1.1.0: {} - - prism-react-renderer@1.3.5(react@16.14.0): - dependencies: - react: 16.14.0 - - prismjs@1.30.0: {} - - process-nextick-args@2.0.1: {} - - progress@2.0.3: {} - - promise@7.3.1: - dependencies: - asap: 2.0.6 - - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - - prop-types@15.8.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - - property-information@5.6.0: - dependencies: - xtend: 4.0.2 - - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - proxy-from-env@1.1.0: {} - - pump@3.0.2: - dependencies: - end-of-stream: 1.4.4 - once: 1.4.0 - - punycode@2.1.1: {} - - pupa@2.1.1: - dependencies: - escape-goat: 2.1.1 - - pure-color@1.3.0: {} - - qs@6.13.0: - dependencies: - side-channel: 1.1.0 - - queue-microtask@1.2.3: {} - - queue@6.0.2: - dependencies: - inherits: 2.0.4 - - quick-lru@5.1.1: {} - - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - - range-parser@1.2.0: {} - - range-parser@1.2.1: {} - - raw-body@2.5.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - - react-base16-styling@0.6.0: - dependencies: - base16: 1.0.0 - lodash.curry: 4.1.1 - lodash.flow: 3.5.0 - pure-color: 1.3.0 - - react-dev-utils@12.0.1(eslint@7.32.0)(typescript@5.8.2)(webpack@5.105.0): - dependencies: - '@babel/code-frame': 7.26.2 - address: 1.2.1 - browserslist: 4.28.1 - chalk: 4.1.2 - cross-spawn: 7.0.6 - detect-port-alt: 1.1.6 - escape-string-regexp: 4.0.0 - filesize: 8.0.7 - find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 6.5.2(eslint@7.32.0)(typescript@5.8.2)(webpack@5.105.0) - global-modules: 2.0.0 - globby: 11.1.0 - gzip-size: 6.0.0 - immer: 9.0.17 - is-root: 2.1.0 - loader-utils: 3.2.1 - open: 8.4.0 - pkg-up: 3.1.0 - prompts: 2.4.2 - react-error-overlay: 6.0.11 - recursive-readdir: 2.2.3 - shell-quote: 1.7.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - webpack: 5.105.0 - optionalDependencies: - typescript: 5.8.2 - transitivePeerDependencies: - - eslint - - supports-color - - vue-template-compiler - - react-dom@16.14.0(react@16.14.0): - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - prop-types: 15.8.1 - react: 16.14.0 - scheduler: 0.19.1 - - react-error-overlay@6.0.11: {} - - react-fast-compare@3.2.0: {} - - react-helmet-async@1.3.0(react-dom@16.14.0(react@16.14.0))(react@16.14.0): - dependencies: - '@babel/runtime': 7.27.0 - invariant: 2.2.4 - prop-types: 15.8.1 - react: 16.14.0 - react-dom: 16.14.0(react@16.14.0) - react-fast-compare: 3.2.0 - shallowequal: 1.1.0 - - react-is@16.13.1: {} - - react-json-view@1.21.3(@types/react@18.0.26)(react-dom@16.14.0(react@16.14.0))(react@16.14.0): - dependencies: - flux: 4.0.4(react@16.14.0) - react: 16.14.0 - react-base16-styling: 0.6.0 - react-dom: 16.14.0(react@16.14.0) - react-lifecycles-compat: 3.0.4 - react-textarea-autosize: 8.5.8(@types/react@18.0.26)(react@16.14.0) - transitivePeerDependencies: - - '@types/react' - - encoding - - react-lifecycles-compat@3.0.4: {} - - react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@5.5.2(react@16.14.0))(webpack@5.105.0): - dependencies: - '@babel/runtime': 7.27.0 - react-loadable: '@docusaurus/react-loadable@5.5.2(react@16.14.0)' - webpack: 5.105.0 - - react-router-config@5.1.1(react-router@5.3.4(react@16.14.0))(react@16.14.0): - dependencies: - '@babel/runtime': 7.27.0 - react: 16.14.0 - react-router: 5.3.4(react@16.14.0) - - react-router-dom@5.3.4(react@16.14.0): - dependencies: - '@babel/runtime': 7.27.0 - history: 4.10.1 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 16.14.0 - react-router: 5.3.4(react@16.14.0) - tiny-invariant: 1.3.1 - tiny-warning: 1.0.3 - - react-router@5.3.4(react@16.14.0): - dependencies: - '@babel/runtime': 7.27.0 - history: 4.10.1 - hoist-non-react-statics: 3.3.2 - loose-envify: 1.4.0 - path-to-regexp: 1.9.0 - prop-types: 15.8.1 - react: 16.14.0 - react-is: 16.13.1 - tiny-invariant: 1.3.1 - tiny-warning: 1.0.3 - - react-textarea-autosize@8.5.8(@types/react@18.0.26)(react@16.14.0): - dependencies: - '@babel/runtime': 7.27.0 - react: 16.14.0 - use-composed-ref: 1.4.0(@types/react@18.0.26)(react@16.14.0) - use-latest: 1.3.0(@types/react@18.0.26)(react@16.14.0) - transitivePeerDependencies: - - '@types/react' - - react@16.14.0: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - prop-types: 15.8.1 - - readable-stream@2.3.7: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - - readable-stream@3.6.0: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - reading-time@1.5.0: {} - - rechoir@0.6.2: - dependencies: - resolve: 1.22.1 - - recursive-readdir@2.2.3: - dependencies: - minimatch: 3.1.2 - - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - - regenerate-unicode-properties@10.1.0: - dependencies: - regenerate: 1.4.2 - - regenerate@1.4.2: {} - - regenerator-runtime@0.14.1: {} - - regenerator-transform@0.15.0: - dependencies: - '@babel/runtime': 7.27.0 - - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - - regexpp@3.2.0: {} - - regexpu-core@5.2.1: - dependencies: - regenerate: 1.4.2 - regenerate-unicode-properties: 10.1.0 - regjsgen: 0.7.1 - regjsparser: 0.9.1 - unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.0.0 - - registry-auth-token@4.2.2: - dependencies: - rc: 1.2.8 - - registry-url@5.1.0: - dependencies: - rc: 1.2.8 - - regjsgen@0.7.1: {} - - regjsparser@0.9.1: - dependencies: - jsesc: 0.5.0 - - rehype-katex@4.0.0: - dependencies: - '@types/katex': 0.11.1 - hast-util-to-text: 2.0.1 - katex: 0.16.28 - rehype-parse: 7.0.1 - unified: 9.2.2 - unist-util-visit: 2.0.3 - - rehype-parse@7.0.1: - dependencies: - hast-util-from-parse5: 6.0.1 - parse5: 6.0.1 - - relateurl@0.2.7: {} - - remark-emoji@2.2.0: - dependencies: - emoticon: 3.2.0 - node-emoji: 1.11.0 - unist-util-visit: 2.0.3 - - remark-footnotes@2.0.0: {} - - remark-math@3.0.1: {} - - remark-mdx@1.6.22: - dependencies: - '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.10.4 - '@babel/plugin-proposal-object-rest-spread': 7.12.1(@babel/core@7.12.9) - '@babel/plugin-syntax-jsx': 7.12.1(@babel/core@7.12.9) - '@mdx-js/util': 1.6.22 - is-alphabetical: 1.0.4 - remark-parse: 8.0.3 - unified: 9.2.0 - transitivePeerDependencies: - - supports-color - - remark-parse@8.0.3: - dependencies: - ccount: 1.1.0 - collapse-white-space: 1.0.6 - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - is-whitespace-character: 1.0.4 - is-word-character: 1.0.4 - markdown-escapes: 1.0.4 - parse-entities: 2.0.0 - repeat-string: 1.6.1 - state-toggle: 1.0.3 - trim: 0.0.3 - trim-trailing-lines: 1.1.4 - unherit: 1.1.3 - unist-util-remove-position: 2.0.1 - vfile-location: 3.2.0 - xtend: 4.0.2 - - remark-squeeze-paragraphs@4.0.0: - dependencies: - mdast-squeeze-paragraphs: 4.0.0 - - renderkid@3.0.0: - dependencies: - css-select: 4.3.0 - dom-converter: 0.2.0 - htmlparser2: 6.1.0 - lodash: 4.17.21 - strip-ansi: 6.0.1 - - repeat-string@1.6.1: {} - - require-from-string@2.0.2: {} - - require-like@0.1.2: {} - - requires-port@1.0.0: {} - - resolve-alpn@1.2.1: {} - - resolve-from@4.0.0: {} - - resolve-pathname@3.0.0: {} - - resolve@1.22.1: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - resolve@2.0.0-next.5: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - responselike@2.0.1: - dependencies: - lowercase-keys: 2.0.0 - - retry@0.13.1: {} - - reusify@1.0.4: {} - - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - - rtl-detect@1.0.4: {} - - rtlcss@3.5.0: - dependencies: - find-up: 5.0.0 - picocolors: 1.1.1 - postcss: 8.5.3 - strip-json-comments: 3.1.1 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - rxjs@7.8.2: - dependencies: - tslib: 2.4.0 - - safe-array-concat@1.1.3: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - - safe-buffer@5.1.2: {} - - safe-buffer@5.2.1: {} - - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - - safer-buffer@2.1.2: {} - - sax@1.4.1: {} - - scheduler@0.19.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - - schema-utils@2.7.0: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) - - schema-utils@2.7.1: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) - - schema-utils@3.1.1: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) - - schema-utils@4.3.3: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - ajv-keywords: 5.1.0(ajv@8.17.1) - - section-matter@1.0.0: - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - - select-hose@2.0.0: {} - - selfsigned@2.1.1: - dependencies: - node-forge: 1.3.1 - - semver-diff@3.1.1: - dependencies: - semver: 6.3.1 - - semver@5.7.2: {} - - semver@6.3.1: {} - - semver@7.7.1: {} - - send@0.19.0: - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - - serve-handler@6.1.6: - dependencies: - bytes: 3.0.0 - content-disposition: 0.5.2 - mime-types: 2.1.18 - minimatch: 3.1.2 - path-is-inside: 1.0.2 - path-to-regexp: 3.3.0 - range-parser: 1.2.0 - - serve-index@1.9.1: - dependencies: - accepts: 1.3.8 - batch: 0.6.1 - debug: 2.6.9 - escape-html: 1.0.3 - http-errors: 1.6.3 - mime-types: 2.1.35 - parseurl: 1.3.3 - transitivePeerDependencies: - - supports-color - - serve-static@1.16.2: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.19.0 - transitivePeerDependencies: - - supports-color - - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - - setimmediate@1.0.5: {} - - setprototypeof@1.1.0: {} - - setprototypeof@1.2.0: {} - - shallow-clone@3.0.1: - dependencies: - kind-of: 6.0.3 - - shallowequal@1.1.0: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - shell-quote@1.7.4: {} - - shelljs@0.8.5: - dependencies: - glob: 7.2.3 - interpret: 1.4.0 - rechoir: 0.6.2 - - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - signal-exit@3.0.7: {} - - sirv@1.0.19: - dependencies: - '@polka/url': 1.0.0-next.21 - mrmime: 1.0.1 - totalist: 1.1.0 - - sisteransi@1.0.5: {} - - sitemap@7.1.2: - dependencies: - '@types/node': 17.0.45 - '@types/sax': 1.2.7 - arg: 5.0.2 - sax: 1.4.1 - - slash@3.0.0: {} - - slash@4.0.0: {} - - slice-ansi@4.0.0: - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - - sockjs@0.3.24: - dependencies: - faye-websocket: 0.11.4 - uuid: 8.3.2 - websocket-driver: 0.7.4 - - sort-css-media-queries@2.1.0: {} - - source-map-js@1.2.1: {} - - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.5.7: {} - - source-map@0.6.1: {} - - space-separated-tokens@1.1.5: {} - - spdy-transport@3.0.0: - dependencies: - debug: 4.3.4 - detect-node: 2.1.0 - hpack.js: 2.1.6 - obuf: 1.1.2 - readable-stream: 3.6.0 - wbuf: 1.7.3 - transitivePeerDependencies: - - supports-color - - spdy@4.0.2: - dependencies: - debug: 4.3.4 - handle-thing: 2.0.1 - http-deceiver: 1.2.7 - select-hose: 2.0.0 - spdy-transport: 3.0.0 - transitivePeerDependencies: - - supports-color - - sprintf-js@1.0.3: {} - - stable@0.1.8: {} - - state-toggle@1.0.3: {} - - statuses@1.5.0: {} - - statuses@2.0.1: {} - - std-env@3.3.0: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.0.1 - - string.prototype.matchall@4.0.12: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.4 - set-function-name: 2.0.2 - side-channel: 1.1.0 - - string.prototype.repeat@1.0.0: - dependencies: - define-properties: 1.2.1 - es-abstract: 1.23.9 - - string.prototype.trim@1.2.10: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.23.9 - es-object-atoms: 1.1.1 - has-property-descriptors: 1.0.2 - - string.prototype.trimend@1.0.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - - stringify-object@3.3.0: - dependencies: - get-own-enumerable-property-symbols: 3.0.2 - is-obj: 1.0.1 - is-regexp: 1.0.0 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.0.1: - dependencies: - ansi-regex: 6.0.1 - - strip-bom-string@1.0.0: {} - - strip-final-newline@2.0.0: {} - - strip-json-comments@2.0.1: {} - - strip-json-comments@3.1.1: {} - - style-to-object@0.3.0: - dependencies: - inline-style-parser: 0.1.1 - - stylehacks@5.1.1(postcss@8.5.3): - dependencies: - browserslist: 4.28.1 - postcss: 8.5.3 - postcss-selector-parser: 6.0.10 - - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - svg-parser@2.0.4: {} - - svgo@2.8.0: - dependencies: - '@trysound/sax': 0.2.0 - commander: 7.2.0 - css-select: 4.3.0 - css-tree: 1.1.3 - csso: 4.2.0 - picocolors: 1.1.1 - stable: 0.1.8 - - table@6.8.0: - dependencies: - ajv: 8.17.1 - lodash.truncate: 4.4.2 - slice-ansi: 4.0.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - tapable@1.1.3: {} - - tapable@2.3.0: {} - - terser-webpack-plugin@5.3.16(webpack@5.105.0): - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.39.0 - webpack: 5.105.0 - - terser@5.39.0: - dependencies: - '@jridgewell/source-map': 0.3.6 - acorn: 8.15.0 - commander: 2.20.3 - source-map-support: 0.5.21 - - text-table@0.2.0: {} - - thunky@1.1.0: {} - - tiny-invariant@1.3.1: {} - - tiny-warning@1.0.3: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - toidentifier@1.0.1: {} - - totalist@1.1.0: {} - - tr46@0.0.3: {} - - trim-trailing-lines@1.1.4: {} - - trim@0.0.3: {} - - trough@1.0.5: {} - - tslib@2.4.0: {} - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-fest@0.20.2: {} - - type-fest@2.19.0: {} - - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.7: - dependencies: - call-bind: 1.0.8 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - - typedarray-to-buffer@3.1.5: - dependencies: - is-typedarray: 1.0.0 - - typescript@5.8.2: {} - - ua-parser-js@1.0.40: {} - - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - - undici@6.21.2: {} - - unherit@1.1.3: - dependencies: - inherits: 2.0.4 - xtend: 4.0.2 - - unicode-canonical-property-names-ecmascript@2.0.0: {} - - unicode-match-property-ecmascript@2.0.0: - dependencies: - unicode-canonical-property-names-ecmascript: 2.0.0 - unicode-property-aliases-ecmascript: 2.1.0 - - unicode-match-property-value-ecmascript@2.0.0: {} - - unicode-property-aliases-ecmascript@2.1.0: {} - - unified@9.2.0: - dependencies: - '@types/unist': 2.0.6 - bail: 1.0.5 - extend: 3.0.2 - is-buffer: 2.0.5 - is-plain-obj: 2.1.0 - trough: 1.0.5 - vfile: 4.2.1 - - unified@9.2.2: - dependencies: - '@types/unist': 2.0.6 - bail: 1.0.5 - extend: 3.0.2 - is-buffer: 2.0.5 - is-plain-obj: 2.1.0 - trough: 1.0.5 - vfile: 4.2.1 - - unique-string@2.0.0: - dependencies: - crypto-random-string: 2.0.0 - - unist-builder@2.0.3: {} - - unist-util-find-after@3.0.0: - dependencies: - unist-util-is: 4.1.0 - - unist-util-generated@1.1.6: {} - - unist-util-is@4.1.0: {} - - unist-util-position@3.1.0: {} - - unist-util-remove-position@2.0.1: - dependencies: - unist-util-visit: 2.0.3 - - unist-util-remove@2.1.0: - dependencies: - unist-util-is: 4.1.0 - - unist-util-stringify-position@2.0.3: - dependencies: - '@types/unist': 2.0.6 - - unist-util-visit-parents@3.1.1: - dependencies: - '@types/unist': 2.0.6 - unist-util-is: 4.1.0 - - unist-util-visit@2.0.3: - dependencies: - '@types/unist': 2.0.6 - unist-util-is: 4.1.0 - unist-util-visit-parents: 3.1.1 - - universalify@2.0.0: {} - - unpipe@1.0.0: {} - - update-browserslist-db@1.2.3(browserslist@4.28.1): - dependencies: - browserslist: 4.28.1 - escalade: 3.2.0 - picocolors: 1.1.1 - - update-notifier@5.1.0: - dependencies: - boxen: 5.1.2 - chalk: 4.1.2 - configstore: 5.0.1 - has-yarn: 2.1.0 - import-lazy: 2.1.0 - is-ci: 2.0.0 - is-installed-globally: 0.4.0 - is-npm: 5.0.0 - is-yarn-global: 0.3.0 - latest-version: 5.1.0 - pupa: 2.1.1 - semver: 7.7.1 - semver-diff: 3.1.1 - xdg-basedir: 4.0.0 - - uri-js@4.4.1: - dependencies: - punycode: 2.1.1 - - url-loader@4.1.1(file-loader@6.2.0(webpack@5.105.0))(webpack@5.105.0): - dependencies: - loader-utils: 2.0.4 - mime-types: 2.1.35 - schema-utils: 3.1.1 - webpack: 5.105.0 - optionalDependencies: - file-loader: 6.2.0(webpack@5.105.0) - - use-composed-ref@1.4.0(@types/react@18.0.26)(react@16.14.0): - dependencies: - react: 16.14.0 - optionalDependencies: - '@types/react': 18.0.26 - - use-isomorphic-layout-effect@1.2.0(@types/react@18.0.26)(react@16.14.0): - dependencies: - react: 16.14.0 - optionalDependencies: - '@types/react': 18.0.26 - - use-latest@1.3.0(@types/react@18.0.26)(react@16.14.0): - dependencies: - react: 16.14.0 - use-isomorphic-layout-effect: 1.2.0(@types/react@18.0.26)(react@16.14.0) - optionalDependencies: - '@types/react': 18.0.26 - - use-sync-external-store@1.4.0(react@16.14.0): - dependencies: - react: 16.14.0 - - util-deprecate@1.0.2: {} - - utila@0.4.0: {} - - utility-types@3.10.0: {} - - utils-merge@1.0.1: {} - - uuid@8.3.2: {} - - v8-compile-cache@2.3.0: {} - - value-equal@1.0.1: {} - - vary@1.1.2: {} - - vfile-location@3.2.0: {} - - vfile-message@2.0.4: - dependencies: - '@types/unist': 2.0.6 - unist-util-stringify-position: 2.0.3 - - vfile@4.2.1: - dependencies: - '@types/unist': 2.0.6 - is-buffer: 2.0.5 - unist-util-stringify-position: 2.0.3 - vfile-message: 2.0.4 - - wait-on@8.0.3: - dependencies: - axios: 1.13.5 - joi: 17.13.3 - lodash: 4.17.21 - minimist: 1.2.8 - rxjs: 7.8.2 - transitivePeerDependencies: - - debug - - watchpack@2.5.1: - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - - wbuf@1.7.3: - dependencies: - minimalistic-assert: 1.0.1 - - web-namespaces@1.1.4: {} - - webidl-conversions@3.0.1: {} - - webpack-bundle-analyzer@4.6.1: - dependencies: - acorn: 8.15.0 - acorn-walk: 8.2.0 - chalk: 4.1.2 - commander: 7.2.0 - gzip-size: 6.0.0 - lodash: 4.17.21 - opener: 1.5.2 - sirv: 1.0.19 - ws: 7.5.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - webpack-dev-middleware@5.3.4(webpack@5.105.0): - dependencies: - colorette: 2.0.19 - memfs: 3.4.12 - mime-types: 2.1.35 - range-parser: 1.2.1 - schema-utils: 4.3.3 - webpack: 5.105.0 - - webpack-dev-server@4.11.1(webpack@5.105.0): - dependencies: - '@types/bonjour': 3.5.10 - '@types/connect-history-api-fallback': 1.3.5 - '@types/express': 4.17.15 - '@types/serve-index': 1.9.1 - '@types/serve-static': 1.15.0 - '@types/sockjs': 0.3.33 - '@types/ws': 8.18.0 - ansi-html-community: 0.0.8 - bonjour-service: 1.0.14 - chokidar: 3.5.3 - colorette: 2.0.19 - compression: 1.7.4 - connect-history-api-fallback: 2.0.0 - default-gateway: 6.0.3 - express: 4.21.2 - graceful-fs: 4.2.11 - html-entities: 2.3.3 - http-proxy-middleware: 2.0.9(@types/express@4.17.15) - ipaddr.js: 2.0.1 - open: 8.4.0 - p-retry: 4.6.2 - rimraf: 3.0.2 - schema-utils: 4.3.3 - selfsigned: 2.1.1 - serve-index: 1.9.1 - sockjs: 0.3.24 - spdy: 4.0.2 - webpack: 5.105.0 - webpack-dev-middleware: 5.3.4(webpack@5.105.0) - ws: 8.18.1 - transitivePeerDependencies: - - bufferutil - - debug - - supports-color - - utf-8-validate - - webpack-merge@5.8.0: - dependencies: - clone-deep: 4.0.1 - wildcard: 2.0.0 - - webpack-sources@3.3.3: {} - - webpack@5.105.0: - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.28.1 - chrome-trace-event: 1.0.3 - enhanced-resolve: 5.19.0 - es-module-lexer: 2.0.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.3 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(webpack@5.105.0) - watchpack: 2.5.1 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - - webpackbar@5.0.2(webpack@5.105.0): - dependencies: - chalk: 4.1.2 - consola: 2.15.3 - pretty-time: 1.1.0 - std-env: 3.3.0 - webpack: 5.105.0 - - websocket-driver@0.7.4: - dependencies: - http-parser-js: 0.5.8 - safe-buffer: 5.2.1 - websocket-extensions: 0.1.4 - - websocket-extensions@0.1.4: {} - - whatwg-encoding@3.1.1: - dependencies: - iconv-lite: 0.6.3 - - whatwg-mimetype@4.0.0: {} - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.1.8 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.0 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.19 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-typed-array@1.1.19: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.8 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - - which@1.3.1: - dependencies: - isexe: 2.0.0 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - widest-line@3.1.0: - dependencies: - string-width: 4.2.3 - - widest-line@4.0.1: - dependencies: - string-width: 5.1.2 - - wildcard@2.0.0: {} - - word-wrap@1.2.4: {} - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.0.1: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.0.1 - - wrappy@1.0.2: {} - - write-file-atomic@3.0.3: - dependencies: - imurmurhash: 0.1.4 - is-typedarray: 1.0.0 - signal-exit: 3.0.7 - typedarray-to-buffer: 3.1.5 - - ws@7.5.10: {} - - ws@8.18.1: {} - - xdg-basedir@4.0.0: {} - - xml-js@1.6.11: - dependencies: - sax: 1.4.1 - - xtend@4.0.2: {} - - yaml@1.10.2: {} - - yocto-queue@0.1.0: {} - - zwitch@1.0.5: {} diff --git a/docs/set-solana-release-tag.sh b/docs/set-solana-release-tag.sh deleted file mode 100755 index af8426526a8..00000000000 --- a/docs/set-solana-release-tag.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -e - -cd "$(dirname "$0")" - -eval "$(../ci/channel-info.sh)" - -if [[ -n $BETA_CHANNEL_LATEST_TAG ]]; then - LATEST_AGAVE_RELEASE_VERSION=$BETA_CHANNEL_LATEST_TAG -else - LATEST_AGAVE_RELEASE_VERSION=$STABLE_CHANNEL_LATEST_TAG -fi -VERSION_FOR_DOCS_RS="${LATEST_AGAVE_RELEASE_VERSION:1}" - -set -x -if [[ -n $CI ]]; then - sed --version || { echo "Error: Incompatible version of sed, use gnu sed"; exit 1; } - find src/ -name \*.md -exec sed -i "s/LATEST_AGAVE_RELEASE_VERSION/$LATEST_AGAVE_RELEASE_VERSION/g" {} \; - find src/ -name \*.md -exec sed -i "s/VERSION_FOR_DOCS_RS/$VERSION_FOR_DOCS_RS/g" {} \; -fi diff --git a/docs/sidebars.js b/docs/sidebars.js deleted file mode 100644 index e60502edfb8..00000000000 --- a/docs/sidebars.js +++ /dev/null @@ -1,130 +0,0 @@ -module.exports = { - masterSidebar: [ - "home", - { - type: "category", - label: "Introduction", - collapsed: false, - items: [ - "what-is-a-validator", - "what-is-an-rpc-node", - "faq", - "backwards-compatibility", - ], - }, - { - type: "category", - label: "Command Line Tools", - // collapsed: false, - items: [ - { - type: "autogenerated", - dirName: "cli", - }, - ], - }, - { - type: "category", - label: "Architecture", - // collapsed: false, - items: [ - "architecture", - { - type: "category", - label: "Clusters", - // collapsed: false, - items: [ - { - type: "autogenerated", - dirName: "clusters", - }, - ], - }, - { - type: "category", - label: "Consensus", - // collapsed: false, - items: [ - { - type: "autogenerated", - dirName: "consensus", - }, - ], - }, - { - type: "category", - label: "Runtime", - // collapsed: false, - items: [ - { - type: "autogenerated", - dirName: "runtime", - }, - ], - }, - { - type: "category", - label: "Validators", - // collapsed: false, - items: [ - { - type: "autogenerated", - dirName: "validator", - }, - ], - }, - ], - }, - { - type: "category", - label: "Operating a Validator", - // collapsed: false, - items: [ - { - type: "autogenerated", - dirName: "operations", - }, - ], - }, - ], - proposalsSidebar: [ - { - type: "category", - label: "System Design Proposals", - collapsed: false, - items: [ - "proposals", - { - type: "category", - label: "Accepted Proposals", - collapsed: true, - link: { - type: "doc", - id: "proposals/accepted-design-proposals", - }, - items: [ - { - type: "autogenerated", - dirName: "proposals", - }, - ], - }, - { - type: "category", - label: "Implemented Proposals", - collapsed: true, - link: { - type: "doc", - id: "implemented-proposals/index", - }, - items: [ - { - type: "autogenerated", - dirName: "implemented-proposals", - }, - ], - }, - ], - }, - ], -}; diff --git a/docs/src/architecture.md b/docs/src/architecture.md deleted file mode 100644 index 798860f9adf..00000000000 --- a/docs/src/architecture.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Agave Validator Architecture -sidebar_position: 0 -sidebar_label: Overview -pagination_label: Agave Validator Architecture ---- - -In this section, we will describe the architecture of the Agave Validator. diff --git a/docs/src/backwards-compatibility.md b/docs/src/backwards-compatibility.md deleted file mode 100644 index 7a8ef3caab2..00000000000 --- a/docs/src/backwards-compatibility.md +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: Backward Compatibility Policy ---- - -As the Solana developer ecosystem grows, so does the need for clear expectations around -breaking API and behavior changes affecting applications and tooling built for Solana by Anza. -In a perfect world, Solana development could continue at a very fast pace without ever -causing issues for existing developers. However, some compromises will need to be made -and so this document attempts to clarify and codify the process for new releases. Furthermore, -there will be a growing number of validator clients maintained separately by distinct teams. -Coordinating across these teams to ensure the reliability of the network will require ongoing -communication. - -### Expectations - -- Agave software releases include APIs, SDKs, and CLI tooling (with a few [exceptions](#exceptions)). -- Agave software releases follow semantic versioning, more details below. -- Software for a `MINOR` version release will be compatible across all software on the - same `MAJOR` version. - -### Deprecation Process - -1. In any `PATCH` or `MINOR` release, a feature, API, endpoint, etc. could be marked as deprecated. -2. According to code upgrade difficulty, some features will remain deprecated for a few release - cycles. -3. In a future `MAJOR` release, deprecated features will be removed in an incompatible way. - -### Release Cadence - -The Solana RPC API, Rust SDK, CLI tooling, and SBF Program SDK are all updated and shipped -along with each Solana software release and should always be compatible between `PATCH` -updates of a particular `MINOR` version release. - -#### Release Channels - -- `edge` software that contains cutting-edge features with no backward compatibility policy -- `beta` software that runs on the Solana Testnet cluster -- `stable` software that run on the Solana Mainnet Beta and Devnet clusters - -#### Major Releases (x.0.0) - -`MAJOR` version releases (e.g. 2.0.0) may contain breaking changes and removal of previously -deprecated features. Client SDKs and tooling will begin using new features and endpoints -that were enabled in the previous `MAJOR` version. - -#### Minor Releases (1.x.0) - -New features and proposal implementations are added to _new_ `MINOR` version -releases (e.g. 1.4.0) and are first run on Solana's Testnet cluster. While running -on the testnet, `MINOR` versions are considered to be in the `beta` release channel. After -those changes have been patched as needed and proven to be reliable, the `MINOR` version will -be upgraded to the `stable` release channel and deployed to the Mainnet Beta cluster. - -#### Patch Releases (1.0.x) - -Low risk features, non-breaking changes, and security and bug fixes are shipped as part -of `PATCH` version releases (e.g. 1.0.11). Patches may be applied to both `beta` and `stable` -release channels. - -### RPC API - -Patch releases: - -- Bug fixes -- Security fixes -- Endpoint / feature deprecation - -Minor releases: - -- New RPC endpoints and features - -Major releases: - -- Removal of deprecated features - -### Rust Crates - -- [`solana-sdk`](https://docs.rs/solana-sdk/) - Rust SDK for creating transactions and parsing account state -- [`solana-program`](https://docs.rs/solana-program/) - Rust SDK for writing programs -- [`solana-client`](https://docs.rs/solana-client/) - Rust client for connecting to RPC API -- [`solana-cli-config`](https://docs.rs/solana-cli-config/) - Rust client for managing Solana CLI config files -- [`agave-geyser-plugin-interface`](https://docs.rs/agave-geyser-plugin-interface/) - Rust interface for developing Solana Geyser plugins. - -Patch releases: - -- Bug fixes -- Security fixes -- Performance improvements - -Minor releases: - -- New APIs - -Major releases - -- Removal of deprecated APIs -- Backwards incompatible behavior changes - -### CLI Tools - -Patch releases: - -- Bug and security fixes -- Performance improvements -- Subcommand / argument deprecation - -Minor releases: - -- New subcommands - -Major releases: - -- Switch to new RPC API endpoints / configuration introduced in the previous major version. -- Removal of deprecated features - -### Runtime Features - -New Agave runtime features are feature-switched and manually activated. Runtime features -include: the introduction of new native programs, sysvars, and syscalls; and changes to -their behavior. Feature activation is cluster agnostic, allowing confidence to be built on -Testnet before activation on Mainnet-beta. - -The release process is as follows: - -1. New runtime feature is included in a new release, deactivated by default -2. Once sufficient staked validators upgrade to the new release, the runtime feature switch - is activated manually with an instruction -3. The feature takes effect at the beginning of the next epoch - -### Infrastructure Changes - -#### Local cluster scripts and Docker images - -Breaking changes will be limited to `MAJOR` version updates. `MINOR` and `PATCH` updates should always -be backwards compatible. - -### Exceptions - -#### Web3 JavaScript SDK - -The Web3.JS SDK also follows semantic versioning specifications but is shipped separately from Solana -software releases. - -#### Attack Vectors - -If a new attack vector is discovered in existing code, the above processes may be -circumvented in order to rapidly deploy a fix, depending on the severity of the issue. - -#### CLI Tooling Output - -CLI tooling json output (`output --json`) compatibility will be preserved; however, output directed -for a human reader is subject to change. This includes output as well as potential help, warning, or -error messages. diff --git a/docs/src/cli/.usage.md.header b/docs/src/cli/.usage.md.header deleted file mode 100644 index 07cdfec744a..00000000000 --- a/docs/src/cli/.usage.md.header +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Solana CLI Reference and Usage -pagination_label: Solana CLI Reference and Usage -sidebar_label: Reference & Usage -sidebar_position: 3 ---- - -The [solana-cli crate](https://crates.io/crates/solana-cli) provides a command-line interface tool for Solana - -## Examples - -### Get Pubkey - -```bash -// Command -$ solana-keygen pubkey - -// Return - -``` - -### Airdrop SOL/Lamports - -```bash -// Command -$ solana airdrop 1 - -// Return -"1 SOL" -``` - -### Get Balance - -```bash -// Command -$ solana balance - -// Return -"3.00050001 SOL" -``` - -### Confirm Transaction - -```bash -// Command -$ solana confirm - -// Return -"Confirmed" / "Not found" / "Transaction failed with error " -``` - -### Deploy program - -```bash -// Command -$ solana program deploy - -// Return - -``` - -## Usage diff --git a/docs/src/cli/examples/_category_.json b/docs/src/cli/examples/_category_.json deleted file mode 100644 index 9a1a43e3072..00000000000 --- a/docs/src/cli/examples/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "position": 4.5, - "label": "Command Examples", - "collapsible": true, - "collapsed": false, - "link": null -} diff --git a/docs/src/cli/examples/choose-a-cluster.md b/docs/src/cli/examples/choose-a-cluster.md deleted file mode 100644 index c554c39ab2a..00000000000 --- a/docs/src/cli/examples/choose-a-cluster.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Connecting to a Cluster with the Solana CLI -pagination_label: "Solana CLI: Connecting to a Cluster" -sidebar_label: Connecting to a Cluster ---- - -See [Solana Clusters](../../clusters/available.md) for general information about the -available clusters. - -## Configure the command-line tool - -You can check what cluster the Solana command-line tool (CLI) is currently targeting by -running the following command: - -```bash -solana config get -``` - -Use `solana config set` command to target a particular cluster. After setting -a cluster target, any future subcommands will send/receive information from that -cluster. - -For example to target the Devnet cluster, run: - -```bash -solana config set --url https://api.devnet.solana.com -``` - -## Ensure Versions Match - -Though not strictly necessary, the CLI will generally work best when its version -matches the software version running on the cluster. To get the locally-installed -CLI version, run: - -```bash -solana --version -``` - -To get the cluster version, run: - -```bash -solana cluster-version -``` - -Ensure the local CLI version is greater than or equal to the cluster version. diff --git a/docs/src/cli/examples/delegate-stake.md b/docs/src/cli/examples/delegate-stake.md deleted file mode 100644 index 161d4aa8776..00000000000 --- a/docs/src/cli/examples/delegate-stake.md +++ /dev/null @@ -1,250 +0,0 @@ ---- -title: Staking SOL with the Solana CLI -pagination_label: "Solana CLI: Staking" -sidebar_label: Staking ---- - -After you have [received SOL](./transfer-tokens.md), you might consider putting it -to use by delegating _stake_ to a validator. Stake is what we call tokens in a -_stake account_. Solana weights validator votes by the amount of stake delegated -to them, which gives those validators more influence in determining the next -valid block of transactions in the blockchain. Solana then generates new SOL -periodically to reward stakers and validators. You earn more rewards the more -stake you delegate. - -:::info -For an overview of staking, read first the -[Staking and Inflation FAQ](https://solana.com/staking). -::: - -## Create a Stake Account - -To delegate stake, you will need to transfer some tokens into a stake account. -To create an account, you will need a keypair. Its public key will be used as -the -[stake account address](https://solana.com/docs/references/staking/stake-accounts#account-address). -No need for a password or encryption here; this keypair will be discarded right -after creating the stake account. - -```bash -solana-keygen new --no-passphrase -o stake-account.json -``` - -The output will contain the public key after the text `pubkey:`. - -```text -pubkey: GKvqsuNcnwWqPzzuhLmGi4rzzh55FhJtGizkhHaEJqiV -``` - -Copy the public key and store it for safekeeping. You will need it any time you -want to perform an action on the stake account you create next. - -Now, create a stake account: - -```bash -solana create-stake-account --from stake-account.json \ - --stake-authority --withdraw-authority \ - --fee-payer -``` - -`` tokens are transferred from the account at the "from" `` to -a new stake account at the public key of stake-account.json. - -Instead of a numeric amount, you can also use the keyword `ALL` to send all available SOL from the `--from` account. - -The stake-account.json file can now be discarded. To authorize additional -actions, you will use the `--stake-authority` or `--withdraw-authority` keypair, -not stake-account.json. - -View the new stake account with the `solana stake-account` command: - -```bash -solana stake-account -``` - -The output will look similar to this: - -```text -Total Stake: 5000 SOL -Stake account is undelegated -Stake Authority: EXU95vqs93yPeCeAU7mPPu6HbRUmTFPEiGug9oCdvQ5F -Withdraw Authority: EXU95vqs93yPeCeAU7mPPu6HbRUmTFPEiGug9oCdvQ5F -``` - -### Set Stake and Withdraw Authorities - -[Stake and withdraw authorities](https://solana.com/docs/references/staking/stake-accounts#understanding-account-authorities) -can be set when creating an account via the `--stake-authority` and -`--withdraw-authority` options, or afterward with the `solana stake-authorize` -command. For example, to set a new stake authority, run: - -```bash -solana stake-authorize \ - --stake-authority --new-stake-authority \ - --fee-payer -``` - -This will use the existing stake authority `` to authorize a new stake -authority `` on the stake account ``. - -### Advanced: Derive Stake Account Addresses - -When you delegate stake, you delegate all tokens in the stake account to a -single validator. To delegate to multiple validators, you will need multiple -stake accounts. Creating a new keypair for each account and managing those -addresses can be cumbersome. Fortunately, you can derive stake addresses using -the `--seed` option: - -```bash -solana create-stake-account --from --seed \ - --stake-authority --withdraw-authority --fee-payer -``` - -`` is an arbitrary string up to 32 bytes, but will typically be a number -corresponding to which derived account this is. The first account might be "0", -then "1", and so on. The public key of `` acts as the -base address. The command derives a new address from the base address and seed -string. To see what stake address the command will derive, use -`solana create-address-with-seed`: - -```bash -solana create-address-with-seed --from STAKE -``` - -`` is the public key of the `` passed to -`solana create-stake-account`. - -The command will output a derived address, which can be used for the -`` argument in staking operations. - -## Delegate Stake - -To delegate your stake to a validator, you will need its vote account address. -Find it by querying the cluster for the list of all validators and their vote -accounts with the `solana validators` command: - -```bash -solana validators -``` - -The first column of each row contains the validator's identity and the second is -the vote account address. Choose a validator and use its vote account address in -`solana delegate-stake`: - -```bash -solana delegate-stake --stake-authority \ - --fee-payer -``` - -The stake authority `` authorizes the operation on the account with -address ``. The stake is delegated to the vote account -with address ``. - -After delegating stake, use `solana stake-account` to observe the changes to the -stake account: - -```bash -solana stake-account -``` - -You will see new fields "Delegated Stake" and "Delegated Vote Account Address" -in the output. The output will look similar to this: - -```text -Total Stake: 5000 SOL -Credits Observed: 147462 -Delegated Stake: 4999.99771712 SOL -Delegated Vote Account Address: CcaHc2L43ZWjwCHART3oZoJvHLAe9hzT2DJNUpBzoTN1 -Stake activates starting from epoch: 42 -Stake Authority: EXU95vqs93yPeCeAU7mPPu6HbRUmTFPEiGug9oCdvQ5F -Withdraw Authority: EXU95vqs93yPeCeAU7mPPu6HbRUmTFPEiGug9oCdvQ5F -``` - -## Deactivate Stake - -Once delegated, you can undelegate stake with the `solana deactivate-stake` -command: - -```bash -solana deactivate-stake --stake-authority \ - --fee-payer -``` - -The stake authority `` authorizes the operation on the account with -address ``. - -If the stake account was created at a derived address, pass the same seed when -deactivating: - -```bash -solana deactivate-stake --stake-authority --seed \ - --fee-payer -``` - -To deactivate stake delegated to a delinquent validator, use: - -```bash -solana deactivate-stake --stake-authority --delinquent \ - --fee-payer -``` - -Note that stake takes several epochs to "cool down". Attempts to delegate stake -in the cool down period will fail. - -## Withdraw Stake - -Transfer tokens out of a stake account with the `solana withdraw-stake` command: - -```bash -solana withdraw-stake --withdraw-authority \ - --fee-payer -``` - -`` is the existing stake account, the `` provided -as `--withdraw-authority` is the withdraw authority, and `` is the number -of tokens to transfer to ``. - -`` may be a numeric value in SOL or one of the keywords: - -- `ALL` – withdraw the maximum amount allowed from the stake account -- `AVAILABLE` – withdraw only the portion that is currently withdrawable. - -The exact amount that can be withdrawn depends on whether the stake is active, -cooling down, or fully inactive, and on any lockup that may apply. - -## Split Stake - -You may want to delegate stake to additional validators while your existing -stake is not eligible for withdrawal. It might not be eligible because it is -currently staked, cooling down, or locked up. To transfer tokens from an -existing stake account to a new one, use the `solana split-stake` command: - -```bash -solana split-stake --stake-authority \ - --fee-payer -``` - -`` is the existing stake account, the stake authority -`` is the stake authority, `` is the keypair -for the new account, and `` is the number of tokens to transfer to the -new account. - -When splitting stake, the amount moved into the new stake account must be at least -the cluster's minimum delegation amount, otherwise the command will fail. - -The new stake account must also be rent-exempt. When signing the transaction -offline, you must provide the rent-exempt reserve explicitly using the -`--rent-exempt-reserve-sol` flag: - -```bash -solana split-stake --stake-authority \ - --rent-exempt-reserve-sol --fee-payer -``` - -`` is the additional SOL sent to the new stake account to make -it rent-exempt, in addition to `` of stake being split. - -To split a stake account into a derived account address, use the `--seed` -option. See -[Derive Stake Account Addresses](#advanced-derive-stake-account-addresses) for -details. diff --git a/docs/src/cli/examples/deploy-a-program.md b/docs/src/cli/examples/deploy-a-program.md deleted file mode 100644 index a5e0eb60d2c..00000000000 --- a/docs/src/cli/examples/deploy-a-program.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Deploy a Solana Program with the CLI -pagination_label: "Solana CLI: Deploy a Program" -sidebar_label: Deploy a Program ---- - -[Redirect](https://solana.com/docs/programs/deploying) diff --git a/docs/src/cli/examples/durable-nonce.md b/docs/src/cli/examples/durable-nonce.md deleted file mode 100644 index 11c90c3936b..00000000000 --- a/docs/src/cli/examples/durable-nonce.md +++ /dev/null @@ -1,256 +0,0 @@ ---- -title: Durable Transaction Nonces in the Solana CLI -pagination_label: "Solana CLI: Durable Transaction Nonces" -sidebar_label: Durable Transaction Nonces ---- - -Durable transaction nonces are a mechanism for getting around the typical short -lifetime of a transaction's -[`recent_blockhash`](https://solana.com/docs/core/transactions#recent-blockhash). -They are implemented as a Solana Program, the mechanics of which can be read -about in the [proposal](../../implemented-proposals/durable-tx-nonces.md). - -## Usage Examples - -Full usage details for durable nonce CLI commands can be found in the -[CLI reference](../usage.md). - -### Nonce Authority - -Authority over a nonce account can optionally be assigned to another account. In -doing so the new authority inherits full control over the nonce account from the -previous authority, including the account creator. This feature enables the -creation of more complex account ownership arrangements and derived account -addresses not associated with a keypair. The -`--nonce-authority ` argument is used to specify this account -and is supported by the following commands - -- `create-nonce-account` -- `new-nonce` -- `withdraw-from-nonce-account` -- `authorize-nonce-account` - -### Nonce Account Creation - -The durable transaction nonce feature uses an account to store the next nonce -value. Durable nonce accounts must be -[rent-exempt](../../implemented-proposals/rent.md#two-tiered-rent-regime), so need -to carry the minimum balance to achieve this. - -A nonce account is created by first generating a new keypair, then create the -account on chain - -- Command - -```bash -solana-keygen new -o nonce-keypair.json -solana create-nonce-account nonce-keypair.json 1 -``` - -- Output - -```text -2SymGjGV4ksPdpbaqWFiDoBz8okvtiik4KE9cnMQgRHrRLySSdZ6jrEcpPifW4xUpp4z66XM9d9wM48sA7peG2XL -``` - -> To keep the keypair entirely offline, use the -> [Paper Wallet](../wallets/paper.md) keypair generation -> [instructions](../wallets/paper.md#seed-phrase-generation) instead - -> [Full usage documentation](../usage.md#solana-create-nonce-account) - -### Querying the Stored Nonce Value - -Creating a durable nonce transaction requires passing the stored nonce value as -the value to the `--blockhash` argument upon signing and submission. Obtain the -presently stored nonce value with - -- Command - -```bash -solana nonce nonce-keypair.json -``` - -- Output - -```text -8GRipryfxcsxN8mAGjy8zbFo9ezaUsh47TsPzmZbuytU -``` - -> [Full usage documentation](../usage.md#solana-get-nonce) - -### Advancing the Stored Nonce Value - -While not typically needed outside a more useful transaction, the stored nonce -value can be advanced by - -- Command - -```bash -solana new-nonce nonce-keypair.json -``` - -- Output - -```text -44jYe1yPKrjuYDmoFTdgPjg8LFpYyh1PFKJqm5SC1PiSyAL8iw1bhadcAX1SL7KDmREEkmHpYvreKoNv6fZgfvUK -``` - -> [Full usage documentation](../usage.md#solana-new-nonce) - -### Display Nonce Account - -Inspect a nonce account in a more human friendly format with - -- Command - -```bash -solana nonce-account nonce-keypair.json -``` - -- Output - -```text -balance: 0.5 SOL -minimum balance required: 0.00136416 SOL -nonce: DZar6t2EaCFQTbUP4DHKwZ1wT8gCPW2aRfkVWhydkBvS -``` - -> [Full usage documentation](../usage.md#solana-nonce-account) - -### Withdraw Funds from a Nonce Account - -Withdraw funds from a nonce account with - -- Command - -```bash -solana withdraw-from-nonce-account nonce-keypair.json ~/.config/solana/id.json 0.5 -``` - -- Output - -```text -3foNy1SBqwXSsfSfTdmYKDuhnVheRnKXpoPySiUDBVeDEs6iMVokgqm7AqfTjbk7QBE8mqomvMUMNQhtdMvFLide -``` - -> Close a nonce account by withdrawing the full balance - -> [Full usage documentation](../usage.md#solana-withdraw-from-nonce-account) - -### Assign a New Authority to a Nonce Account - -Reassign the authority of a nonce account after creation with - -- Command - -```bash -solana authorize-nonce-account nonce-keypair.json nonce-authority.json -``` - -- Output - -```text -3F9cg4zN9wHxLGx4c3cUKmqpej4oa67QbALmChsJbfxTgTffRiL3iUehVhR9wQmWgPua66jPuAYeL1K2pYYjbNoT -``` - -> [Full usage documentation](../usage.md#solana-authorize-nonce-account) - -## Other Commands Supporting Durable Nonces - -To make use of durable nonces with other CLI subcommands, two arguments must be -supported. - -- `--nonce`, specifies the account storing the nonce value -- `--nonce-authority`, specifies an optional [nonce authority](#nonce-authority) - -The following subcommands have received this treatment so far - -- [`pay`](../usage.md#solana-pay) -- [`delegate-stake`](../usage.md#solana-delegate-stake) -- [`deactivate-stake`](../usage.md#solana-deactivate-stake) - -### Example Pay Using Durable Nonce - -Here we demonstrate Alice paying Bob 1 SOL using a durable nonce. The procedure -is the same for all subcommands supporting durable nonces - -#### - Create accounts - -First we need some accounts for Alice, Alice's nonce and Bob - -```bash -$ solana-keygen new -o alice.json -$ solana-keygen new -o nonce.json -$ solana-keygen new -o bob.json -``` - -#### - Fund Alice's account - -Alice will need some funds to create a nonce account and send to Bob. Airdrop -her some SOL - -```bash -$ solana airdrop -k alice.json 1 -1 SOL -``` - -#### - Create Alice's nonce account - -Now Alice needs a nonce account. Create one - -> Here, no separate [nonce authority](#nonce-authority) is employed, so -> `alice.json` has full authority over the nonce account - -```bash -$ solana create-nonce-account -k alice.json nonce.json 0.1 -3KPZr96BTsL3hqera9up82KAU462Gz31xjqJ6eHUAjF935Yf8i1kmfEbo6SVbNaACKE5z6gySrNjVRvmS8DcPuwV -``` - -#### - A failed first attempt to pay Bob - -Alice attempts to pay Bob, but takes too long to sign. The specified blockhash -expires and the transaction fails - -```bash -$ solana transfer -k alice.json --blockhash expiredDTaxfagttWjQweib42b6ZHADSx94Tw8gHx11 bob.json 0.01 -[2020-01-02T18:48:28.462911000Z ERROR solana_cli::cli] Io(Custom { kind: Other, error: "Transaction \"33gQQaoPc9jWePMvDAeyJpcnSPiGUAdtVg8zREWv4GiKjkcGNufgpcbFyRKRrA25NkgjZySEeKue5rawyeH5TzsV\" failed: None" }) -Error: Io(Custom { kind: Other, error: "Transaction \"33gQQaoPc9jWePMvDAeyJpcnSPiGUAdtVg8zREWv4GiKjkcGNufgpcbFyRKRrA25NkgjZySEeKue5rawyeH5TzsV\" failed: None" }) -``` - -#### - Nonce to the rescue! - -Alice retries the transaction, this time specifying her nonce account and the -blockhash stored there - -> Remember, `alice.json` is the [nonce authority](#nonce-authority) in this -> example - -```bash -$ solana nonce-account nonce.json -balance: 0.1 SOL -minimum balance required: 0.00136416 SOL -nonce: F7vmkY3DTaxfagttWjQweib42b6ZHADSx94Tw8gHx3W7 -``` - -```bash -$ solana transfer -k alice.json --blockhash F7vmkY3DTaxfagttWjQweib42b6ZHADSx94Tw8gHx3W7 --nonce nonce.json bob.json 0.01 -HR1368UKHVZyenmH7yVz5sBAijV6XAPeWbEiXEGVYQorRMcoijeNAbzZqEZiH8cDB8tk65ckqeegFjK8dHwNFgQ -``` - -#### - Success! - -The transaction succeeds! Bob receives 0.01 SOL from Alice and Alice's stored -nonce advances to a new value - -```bash -$ solana balance -k bob.json -0.01 SOL -``` - -```bash -$ solana nonce-account nonce.json -balance: 0.1 SOL -minimum balance required: 0.00136416 SOL -nonce: 6bjroqDcZgTv6Vavhqf81oBHTv3aMnX19UTB51YhAZnN -``` diff --git a/docs/src/cli/examples/offline-signing.md b/docs/src/cli/examples/offline-signing.md deleted file mode 100644 index 28b54561732..00000000000 --- a/docs/src/cli/examples/offline-signing.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: Offline Transaction Signing with the Solana CLI -pagination_label: "Solana CLI: Offline Transaction Signing" -sidebar_label: Offline Transaction Signing ---- - -Some security models require keeping signing keys, and thus the signing -process, separated from transaction creation and network broadcast. Examples -include: - -- Collecting signatures from geographically disparate signers in a - [multi-signature scheme](https://spl.solana.com/token#multisig-usage) -- Signing transactions using an [air-gapped]() - signing device - -This document describes using Solana's CLI to separately sign and submit a -transaction. - -## Commands Supporting Offline Signing - -At present, the following commands support offline signing: - -- [`create-stake-account`](../usage.md#solana-create-stake-account) -- [`create-stake-account-checked`](../usage.md#solana-create-stake-account-checked) -- [`deactivate-stake`](../usage.md#solana-deactivate-stake) -- [`delegate-stake`](../usage.md#solana-delegate-stake) -- [`split-stake`](../usage.md#solana-split-stake) -- [`stake-authorize`](../usage.md#solana-stake-authorize) -- [`stake-authorize-checked`](../usage.md#solana-stake-authorize-checked) -- [`stake-set-lockup`](../usage.md#solana-stake-set-lockup) -- [`stake-set-lockup-checked`](../usage.md#solana-stake-set-lockup-checked) -- [`transfer`](../usage.md#solana-transfer) -- [`withdraw-stake`](../usage.md#solana-withdraw-stake) - -- [`create-vote-account`](../usage.md#solana-create-vote-account) -- [`vote-authorize-voter`](../usage.md#solana-vote-authorize-voter) -- [`vote-authorize-voter-checked`](../usage.md#solana-vote-authorize-voter-checked) -- [`vote-authorize-withdrawer`](../usage.md#solana-vote-authorize-withdrawer) -- [`vote-authorize-withdrawer-checked`](../usage.md#solana-vote-authorize-withdrawer-checked) -- [`vote-update-commission`](../usage.md#solana-vote-update-commission) -- [`vote-update-validator`](../usage.md#solana-vote-update-validator) -- [`withdraw-from-vote-account`](../usage.md#solana-withdraw-from-vote-account) - -## Signing Transactions Offline - -To sign a transaction offline, pass the following arguments on the command line - -1. `--sign-only`, prevents the client from submitting the signed transaction - to the network. Instead, the pubkey/signature pairs are printed to stdout. -2. `--blockhash BASE58_HASH`, allows the caller to specify the value used to - fill the transaction's `recent_blockhash` field. This serves a number of - purposes, namely: - _ Eliminates the need to connect to the network and query a recent blockhash - via RPC - _ Enables the signers to coordinate the blockhash in a multiple-signature - scheme - -### Example: Offline Signing a Payment - -Command - -```bash -solana@offline$ solana transfer --sign-only --blockhash 5Tx8F3jgSHx21CbtjwmdaKPLM5tWmreWAnPrbqHomSJF \ - recipient-keypair.json 1 -``` - -Output - -```text - -Blockhash: 5Tx8F3jgSHx21CbtjwmdaKPLM5tWmreWAnPrbqHomSJF -Signers (Pubkey=Signature): - FhtzLVsmcV7S5XqGD79ErgoseCLhZYmEZnz9kQg1Rp7j=4vC38p4bz7XyiXrk6HtaooUqwxTWKocf45cstASGtmrD398biNJnmTcUCVEojE7wVQvgdYbjHJqRFZPpzfCQpmUN - -{"blockhash":"5Tx8F3jgSHx21CbtjwmdaKPLM5tWmreWAnPrbqHomSJF","signers":["FhtzLVsmcV7S5XqGD79ErgoseCLhZYmEZnz9kQg1Rp7j=4vC38p4bz7XyiXrk6HtaooUqwxTWKocf45cstASGtmrD398biNJnmTcUCVEojE7wVQvgdYbjHJqRFZPpzfCQpmUN"]}' -``` - -## Submitting Offline Signed Transactions to the Network - -To submit a transaction that has been signed offline to the network, pass the -following arguments on the command line - -1. `--blockhash BASE58_HASH`, must be the same blockhash as was used to sign -2. `--signer BASE58_PUBKEY=BASE58_SIGNATURE`, one for each offline signer. This - includes the pubkey/signature pairs directly in the transaction rather than - signing it with any local keypair(s) - -### Example: Submitting an Offline Signed Payment - -Command - -```bash -solana@online$ solana transfer --blockhash 5Tx8F3jgSHx21CbtjwmdaKPLM5tWmreWAnPrbqHomSJF \ - --signer FhtzLVsmcV7S5XqGD79ErgoseCLhZYmEZnz9kQg1Rp7j=4vC38p4bz7XyiXrk6HtaooUqwxTWKocf45cstASGtmrD398biNJnmTcUCVEojE7wVQvgdYbjHJqRFZPpzfCQpmUN - recipient-keypair.json 1 -``` - -Output - -```text -4vC38p4bz7XyiXrk6HtaooUqwxTWKocf45cstASGtmrD398biNJnmTcUCVEojE7wVQvgdYbjHJqRFZPpzfCQpmUN -``` - -## Offline Signing Over Multiple Sessions - -Offline signing can also take place over multiple sessions. In this scenario, -pass the absent signer's public key for each role. All pubkeys that were specified, -but no signature was generated for will be listed as absent in the offline signing -output - -### Example: Transfer with Two Offline Signing Sessions - -Command (Offline Session #1) - -```text -solana@offline1$ solana transfer Fdri24WUGtrCXZ55nXiewAj6RM18hRHPGAjZk3o6vBut 10 \ - --blockhash 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc \ - --sign-only \ - --keypair fee_payer.json \ - --from 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL -``` - -Output (Offline Session #1) - -```text -Blockhash: 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc -Signers (Pubkey=Signature): - 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy=ohGKvpRC46jAduwU9NW8tP91JkCT5r8Mo67Ysnid4zc76tiiV1Ho6jv3BKFSbBcr2NcPPCarmfTLSkTHsJCtdYi -Absent Signers (Pubkey): - 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL -``` - -Command (Offline Session #2) - -```text -solana@offline2$ solana transfer Fdri24WUGtrCXZ55nXiewAj6RM18hRHPGAjZk3o6vBut 10 \ - --blockhash 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc \ - --sign-only \ - --keypair from.json \ - --fee-payer 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy -``` - -Output (Offline Session #2) - -```text -Blockhash: 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc -Signers (Pubkey=Signature): - 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL=3vJtnba4dKQmEAieAekC1rJnPUndBcpvqRPRMoPWqhLEMCty2SdUxt2yvC1wQW6wVUa5putZMt6kdwCaTv8gk7sQ -Absent Signers (Pubkey): - 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy -``` - -Command (Online Submission) - -```text -solana@online$ solana transfer Fdri24WUGtrCXZ55nXiewAj6RM18hRHPGAjZk3o6vBut 10 \ - --blockhash 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc \ - --from 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL \ - --signer 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL=3vJtnba4dKQmEAieAekC1rJnPUndBcpvqRPRMoPWqhLEMCty2SdUxt2yvC1wQW6wVUa5putZMt6kdwCaTv8gk7sQ \ - --fee-payer 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy \ - --signer 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy=ohGKvpRC46jAduwU9NW8tP91JkCT5r8Mo67Ysnid4zc76tiiV1Ho6jv3BKFSbBcr2NcPPCarmfTLSkTHsJCtdYi -``` - -Output (Online Submission) - -```text -ohGKvpRC46jAduwU9NW8tP91JkCT5r8Mo67Ysnid4zc76tiiV1Ho6jv3BKFSbBcr2NcPPCarmfTLSkTHsJCtdYi -``` - -## Buying More Time to Sign - -Typically a Solana transaction must be signed and accepted by the network within -a number of slots from the blockhash in its `recent_blockhash` field (~1min at -the time of this writing). If your signing procedure takes longer than this, a -[Durable Transaction Nonce](./durable-nonce.md) can give you the extra time you -need. diff --git a/docs/src/cli/examples/sign-offchain-message.md b/docs/src/cli/examples/sign-offchain-message.md deleted file mode 100644 index bb0b44b35e1..00000000000 --- a/docs/src/cli/examples/sign-offchain-message.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Off-Chain Message Signing with the Solana CLI -pagination_label: "Solana CLI: Off-Chain Message Signing" -sidebar_label: Off-Chain Message Signing ---- - -Off-chain message signing is a method of signing non-transaction messages with -a Solana wallet. This feature can be used to authenticate users or provide -proof of wallet ownership. - -## Sign Off-Chain Message - -To sign an arbitrary off-chain message, run the following command: - -```bash -solana sign-offchain-message -``` - -The message will be encoded and signed with CLI's default private key and -signature printed to the output. If you want to sign it with another key, just -use the `-k/--keypair` option: - -```bash -solana sign-offchain-message -k -``` - -By default, the messages constructed are version 0, the only version currently -supported. When other versions become available, you can override the default -value with the `--version` option: - -```bash -solana sign-offchain-message -k --version -``` - -The message format is determined automatically based on the version and text -of the message. - -Version `0` headers specify three message formats allowing for trade-offs -between compatibility and composition of messages: - -| ID | Encoding | Maximum Length | Hardware Wallet Support | -| :-: | :-----------------: | :------------: | :---------------------: | -| 0 | Restricted ASCII \* | 1212 | Yes | -| 1 | UTF-8 | 1212 | Blind sign only | -| 2 | UTF-8 | 65515 | No | - -\* Those characters for which [`isprint(3)`](https://linux.die.net/man/3/isprint) -returns true. That is, `0x20..=0x7e`. - -Formats `0` and `1` are motivated by hardware wallet support where both RAM to -store the payload and font character support are limited. - -To sign an off-chain message with Ledger, ensure your Ledger is running latest -firmware and Solana Ledger App version 1.3.0 or later. After Ledger is -unlocked and Solana Ledger App is open, run: - -```bash -solana sign-offchain-message -k usb://ledger -``` - -For more information on how to setup and work with the ledger device see this -[link](../wallets/hardware/ledger.md). - -Please note that UTF-8 encoded messages require `Allow blind sign` option -enabled in Solana Ledger App. Also, due to the lack of UTF-8 support in Ledger -devices, only the hash of the message will be displayed in such cases. - -If `Display mode` is set to `Expert`, Ledger will display technical -information about the message to be signed. - -## Verify Off-Chain Message Signature - -To verify the off-chain message signature, run the following command: - -```bash -solana verify-offchain-signature -``` - -The public key of the default CLI signer will be used. You can specify another -key with the `--signer` option: - -```bash -solana verify-offchain-signature --signer -``` - -If the signed message has a version different from the default, you need to -specify the matching version explicitly: - -```bash -solana verify-offchain-signature --version -``` - -## Protocol Specification - -To ensure that off-chain messages are not valid transactions, they are encoded -with a fixed prefix: `\xffsolana offchain`, where first byte is chosen such -that it is implicitly illegal as the first byte in a transaction -`MessageHeader` today. More details about the payload format and other -considerations are available in the -[proposal](https://github.com/anza-xyz/agave/blob/master/docs/src/proposals/off-chain-message-signing.md). diff --git a/docs/src/cli/examples/test-validator.md b/docs/src/cli/examples/test-validator.md deleted file mode 100644 index dc8a484d6c7..00000000000 --- a/docs/src/cli/examples/test-validator.md +++ /dev/null @@ -1,171 +0,0 @@ ---- -title: Solana Test Validator -pagination_label: "Solana CLI: Test Validator" -sidebar_label: Test Validator ---- - -During early stage development, it is often convenient to target a cluster with -fewer restrictions and more configuration options than the public offerings -provide. This is easily achieved with the `solana-test-validator` binary, which -starts a full-featured, single-node cluster on the developer's workstation. - -## Advantages - -- No RPC rate-limits -- No airdrop limits -- Direct [on-chain program](https://solana.com/docs/programs/rust) deployment - (`--bpf-program ...`) -- Clone accounts from a public cluster, including programs (`--clone ...`) -- Load accounts from files -- Configurable transaction history retention (`--limit-ledger-size ...`) -- Configurable epoch length (`--slots-per-epoch ...`) -- Jump to an arbitrary slot (`--warp-slot ...`) - -## Installation - -The `solana-test-validator` binary ships with the Solana CLI Tool Suite. -[Install](../install.md) before continuing. - -## Running - -First take a look at the configuration options - -``` -solana-test-validator --help -``` - -Next start the test validator - -``` -solana-test-validator -``` - -By default, basic status information is printed while the process is running. -See [Appendix I](#appendix-i-status-output) for details - -``` -Ledger location: test-ledger -Log: test-ledger/validator.log -Identity: EPhgPANa5Rh2wa4V2jxt7YbtWa3Uyw4sTeZ13cQjDDB8 -Genesis Hash: 4754oPEMhAKy14CZc8GzQUP93CB4ouELyaTs4P8ittYn -Version: 1.6.7 -Shred Version: 13286 -Gossip Address: 127.0.0.1:1024 -TPU Address: 127.0.0.1:1027 -JSON RPC URL: http://127.0.0.1:8899 -⠈ 00:36:02 | Processed Slot: 5142 | Confirmed Slot: 5142 | Finalized Slot: 5110 | Snapshot Slot: 5100 | Transactions: 5142 | ◎499.974295000 -``` - -Leave `solana-test-validator` running in its own terminal. When it is no longer -needed, it can be stopped with ctrl-c. - -## Interacting - -Open a new terminal to interact with a [running](#running) `solana-test-validator` -instance using other binaries from the Solana CLI Tool Suite or your own client -software. - -#### Configure the CLI Tool Suite to target a local cluster by default - -``` -solana config set --url http://127.0.0.1:8899 -``` - -#### Verify the CLI Tool Suite configuration - -``` -solana genesis-hash -``` - -- **NOTE:** The result should match the `Genesis Hash:` field in the - `solana-test-validator` status output - -#### Check the wallet balance - -``` -solana balance -``` - -- **NOTE:** `Error: No such file or directory (os error 2)` means that the default - wallet does not yet exist. Create it with `solana-keygen new`. -- **NOTE:** If the wallet has a zero SOL balance, airdrop some localnet SOL with - `solana airdrop 10` - -#### Perform a basic transfer transaction - -``` -solana transfer EPhgPANa5Rh2wa4V2jxt7YbtWa3Uyw4sTeZ13cQjDDB8 1 -``` - -#### Monitor `msg!()` output from on-chain programs - -``` -solana logs -``` - -- **NOTE:** This command needs to be running when the target transaction is - executed. Run it in its own terminal - -## Appendix I: Status Output - -``` -Ledger location: test-ledger -``` - -- File path of the ledger storage directory. This directory can get large. Store - less transaction history with `--limit-ledger-size ...` or relocate it with - `--ledger ...` - -``` -Log: test-ledger/validator.log -``` - -- File path of the validator text log file. The log can also be streamed by - passing `--log`. Status output is suppressed in this case. - -``` -Identity: EPhgPANa5Rh2wa4V2jxt7YbtWa3Uyw4sTeZ13cQjDDB8 -``` - -- The validator's identity in the [gossip network](../../validator/gossip.md#gossip-overview) - -``` -Version: 1.6.7 -``` - -- The software version - -``` -Gossip Address: 127.0.0.1:1024 -TPU Address: 127.0.0.1:1027 -JSON RPC URL: http://127.0.0.1:8899 -``` - -- The network address of the [Gossip](../../validator/gossip.md#gossip-overview), - [Transaction Processing Unit](../../validator/tpu.md) and [JSON RPC](https://solana.com/docs/rpc) - service, respectively - -``` -⠈ 00:36:02 | Processed Slot: 5142 | Confirmed Slot: 5142 | Finalized Slot: 5110 | Snapshot Slot: 5100 | Transactions: 5142 | ◎499.974295000 -``` - -- Session running time, current slot of the three block - [commitment levels](https://solana.com/docs/rpc#configuring-state-commitment), - slot height of the last snapshot, transaction count, - [voting authority](../../operations/guides/vote-accounts.md#vote-authority) balance - -## Appendix II: Runtime Features - -By default, the test validator runs with all [runtime features](https://solana.com/docs/core/runtime#features) activated. - -You can verify this using the [Solana command-line tools](../install.md): - -```bash -solana feature status -ul -``` - -Since this may not always be desired, especially when testing programs meant for deployment to mainnet, the CLI provides an option to deactivate specific features: - -```bash -solana-test-validator --deactivate-feature --deactivate-feature -``` diff --git a/docs/src/cli/examples/transfer-tokens.md b/docs/src/cli/examples/transfer-tokens.md deleted file mode 100644 index 28c933be666..00000000000 --- a/docs/src/cli/examples/transfer-tokens.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: Send and Receive Tokens with the Solana CLI -pagination_label: "Solana CLI: Send and Receive Tokens" -sidebar_label: Send and Receive Tokens ---- - -This page describes how to receive and send SOL tokens using the command line -tools with a command line wallet such as a [paper wallet](../wallets/paper.md), -a [file system wallet](../wallets/file-system.md), or a -[hardware wallet](../wallets/hardware/index.md). Before you begin, make sure -you have created a wallet and have access to its address (pubkey) and the -signing keypair. Check out our -[conventions for entering keypairs for different wallet types](../intro.md#keypair-conventions). - -## Testing your Wallet - -Before sharing your public key with others, you may want to first ensure the -key is valid and that you indeed hold the corresponding private key. - -In this example, we will create a second wallet in addition to your first wallet, -and then transfer some tokens to it. This will confirm that you can send and -receive tokens on your wallet type of choice. - -This test example uses our Developer Testnet, called devnet. Tokens issued -on devnet have **no** value, so don't worry if you lose them. - -#### Airdrop some tokens to get started - -First, _airdrop_ yourself some play tokens on the devnet. - -```bash -solana airdrop 1 --url https://api.devnet.solana.com -``` - -where you replace the text `` with your base58-encoded -public key/wallet address. - -A response with the signature of the transaction will be returned. If the balance -of the address does not change by the expected amount, run the following command -for more information on what potentially went wrong: - -```bash -solana confirm -v -``` - -#### Check your balance - -Confirm the airdrop was successful by checking the account's balance. -It should output `1 SOL`: - -```bash -solana balance --url https://api.devnet.solana.com -``` - -#### Create a second wallet address - -We will need a new address to receive our tokens. Create a second -keypair and record its pubkey: - -```bash -solana-keygen new --no-passphrase --no-outfile -``` - -The output will contain the address after the text `pubkey:`. Copy the -address. We will use it in the next step. - -```text -pubkey: GKvqsuNcnwWqPzzuhLmGi4rzzh55FhJtGizkhHaEJqiV -``` - -You can also create a second (or more) wallet of any type: -[paper](../wallets/paper.md#creating-multiple-paper-wallet-addresses), -[file system](../wallets/file-system.md#creating-multiple-file-system-wallet-addresses), -or [hardware](../wallets/hardware/index.md#multiple-addresses-on-a-single-hardware-wallet). - -#### Transfer tokens from your first wallet to the second address - -Next, prove that you own the airdropped tokens by transferring them. -The Solana cluster will only accept the transfer if you sign the transaction -with the private keypair corresponding to the sender's public key in the -transaction. - -```bash -solana transfer --from 0.5 --allow-unfunded-recipient --url https://api.devnet.solana.com --fee-payer -``` - -where you replace `` with the path to a keypair in your first wallet, -and replace `` with the address of your second -wallet. - -Confirm the updated balances with `solana balance`: - -```bash -solana balance --url http://api.devnet.solana.com -``` - -where `` is either the public key from your keypair or the -recipient's public key. - -#### Full example of test transfer - -```bash -$ solana-keygen new --outfile my_solana_wallet.json # Creating my first wallet, a file system wallet -Generating a new keypair -For added security, enter a passphrase (empty for no passphrase): -Wrote new keypair to my_solana_wallet.json -========================================================================== -pubkey: DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK # Here is the address of the first wallet -========================================================================== -Save this seed phrase to recover your new keypair: -width enhance concert vacant ketchup eternal spy craft spy guard tag punch # If this was a real wallet, never share these words on the internet like this! -========================================================================== - -$ solana airdrop 1 DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK --url https://api.devnet.solana.com # Airdropping 1 SOL to my wallet's address/pubkey -Requesting airdrop of 1 SOL from 35.233.193.70:9900 -1 SOL - -$ solana balance DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK --url https://api.devnet.solana.com # Check the address's balance -1 SOL - -$ solana-keygen new --no-outfile # Creating a second wallet, a paper wallet -Generating a new keypair -For added security, enter a passphrase (empty for no passphrase): -==================================================================== -pubkey: 7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv # Here is the address of the second, paper, wallet. -==================================================================== -Save this seed phrase to recover your new keypair: -clump panic cousin hurt coast charge engage fall eager urge win love # If this was a real wallet, never share these words on the internet like this! -==================================================================== - -$ solana transfer --from my_solana_wallet.json 7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv 0.5 --allow-unfunded-recipient --url https://api.devnet.solana.com --fee-payer my_solana_wallet.json # Transferring tokens to the public address of the paper wallet -3gmXvykAd1nCQQ7MjosaHLf69Xyaqyq1qw2eu1mgPyYXd5G4v1rihhg1CiRw35b9fHzcftGKKEu4mbUeXY2pEX2z # This is the transaction signature - -$ solana balance DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK --url https://api.devnet.solana.com -0.499995 SOL # The sending account has slightly less than 0.5 SOL remaining due to the 0.000005 SOL transaction fee payment - -$ solana balance 7S3P4HxJpyyigGzodYwHtCxZyUQe9JiBMHyRWXArAaKv --url https://api.devnet.solana.com -0.5 SOL # The second wallet has now received the 0.5 SOL transfer from the first wallet - -``` - -## Receive Tokens - -To receive tokens, you will need an address for others to send tokens to. In -Solana, the wallet address is the public key of a keypair. There are a variety -of techniques for generating keypairs. The method you choose will depend on how -you choose to store keypairs. Keypairs are stored in wallets. Before receiving -tokens, you will need to [create a wallet](../wallets/index.md). -Once completed, you should have a public key -for each keypair you generated. The public key is a long string of base58 -characters. Its length varies from 32 to 44 characters. - -## Send Tokens - -If you already hold SOL and want to send tokens to someone, you will need -a path to your keypair, their base58-encoded public key, and a number of -tokens to transfer. Once you have that collected, you can transfer tokens -with the `solana transfer` command: - -```bash -solana transfer --from --fee-payer -``` - -Confirm the updated balances with `solana balance`: - -```bash -solana balance -``` diff --git a/docs/src/cli/index.md b/docs/src/cli/index.md deleted file mode 100644 index 27deffd485d..00000000000 --- a/docs/src/cli/index.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Solana CLI Tool Suite -sidebar_position: 0 -sidebar_label: Overview -pagination_label: Solana CLI Tool Suite ---- - -In this section, we will describe how to use the Solana command-line tools to -create a _wallet_, to send and receive SOL tokens, and to participate in the -cluster by delegating stake. - -To interact with a Solana cluster, we will use its command-line interface, also -known as the CLI. We use the command-line because it is the first place the -Anza core team deploys new functionality. The command-line interface is not -necessarily the easiest to use, but it provides the most direct, flexible, and -secure access to your Solana accounts. - -## Getting Started - -To get started using the Solana Command Line (CLI) tools: - -- [Install the Solana CLI Tool Suite](./install.md) -- [Introduction to our CLI conventions](./intro.md) -- [Create a Wallet using the CLI](./wallets/index.md) -- [Choose a Cluster to connect to using the CLI](./examples/choose-a-cluster.md) diff --git a/docs/src/cli/install.md b/docs/src/cli/install.md deleted file mode 100644 index 01548d20baa..00000000000 --- a/docs/src/cli/install.md +++ /dev/null @@ -1,257 +0,0 @@ ---- -title: Install the Solana CLI -pagination_label: Install the Solana CLI -sidebar_label: Installation -sidebar_position: 1 ---- - -There are multiple ways to install the Solana tools on your computer depending -on your preferred workflow: - -- [Use the Solana Install Tool (Simplest option)](#use-solanas-install-tool) -- [Download Prebuilt Binaries](#download-prebuilt-binaries) -- [Build from Source](#build-from-source) -- [Use Homebrew](#use-homebrew) - -## Use The Solana Install Tool - -### MacOS & Linux - -- Open your favorite Terminal application - -- Install the Agave release - [LATEST_AGAVE_RELEASE_VERSION](https://github.com/anza-xyz/agave/releases/tag/LATEST_AGAVE_RELEASE_VERSION) - on your machine by running: - -```bash -sh -c "$(curl -sSfL https://release.anza.xyz/LATEST_AGAVE_RELEASE_VERSION/install)" -``` - -- You can replace `LATEST_AGAVE_RELEASE_VERSION` with the release tag matching - the software version of your desired release, or use one of the three symbolic - channel names: `stable`, `beta`, or `edge`. - -- The following output indicates a successful update: - -```text -downloading LATEST_AGAVE_RELEASE_VERSION installer -Configuration: /home/solana/.config/solana/install/config.yml -Active release directory: /home/solana/.local/share/solana/install/active_release -* Release version: LATEST_AGAVE_RELEASE_VERSION -* Release URL: https://github.com/anza-xyz/agave/releases/download/LATEST_AGAVE_RELEASE_VERSION/solana-release-x86_64-unknown-linux-gnu.tar.bz2 -Update successful -``` - -- Depending on your system, the end of the installer messaging may prompt you to - -```bash -Please update your PATH environment variable to include the solana programs: -``` - -- If you get the above message, copy and paste the recommended command below it - to update `PATH` -- Confirm you have the desired version of `solana` installed by running: - -```bash -solana --version -``` - -- After a successful install, `agave-install update` may be used to easily - update the Solana software to a newer version at any time. - ---- - -### Windows - -- Open a Command Prompt (`cmd.exe`) as an Administrator - - - Search for Command Prompt in the Windows search bar. When the Command Prompt - app appears, right-click and select "Open as Administrator". If you are - prompted by a pop-up window asking "Do you want to allow this app to make - changes to your device?", click Yes. - -- Copy and paste the following command, then press Enter to download the Solana - installer into a temporary directory: - -```bash -cmd /c "curl https://release.anza.xyz/LATEST_AGAVE_RELEASE_VERSION/agave-install-init-x86_64-pc-windows-msvc.exe --output C:\agave-install-tmp\agave-install-init.exe --create-dirs" -``` - -- Copy and paste the following command, then press Enter to install the latest - version of Solana. If you see a security pop-up by your system, please select - to allow the program to run. - -```bash -C:\agave-install-tmp\agave-install-init.exe LATEST_AGAVE_RELEASE_VERSION -``` - -- When the installer is finished, press Enter. - -- Close the command prompt window and re-open a new command prompt window as a - normal user - - Search for "Command Prompt" in the search bar, then left click on the - Command Prompt app icon, no need to run as Administrator) -- Confirm you have the desired version of `solana` installed by entering: - -```bash -solana --version -``` - -- After a successful install, `agave-install update` may be used to easily - update the Solana software to a newer version at any time. - -## Download Prebuilt Binaries - -If you would rather not use `agave-install` to manage the install, you can -manually download and install the binaries. - -### Linux - -Download the binaries by navigating to -[https://github.com/anza-xyz/agave/releases/latest](https://github.com/anza-xyz/agave/releases/latest), -download **solana-release-x86_64-unknown-linux-gnu.tar.bz2**, then extract the -archive: - -```bash -tar jxf solana-release-x86_64-unknown-linux-gnu.tar.bz2 -cd solana-release/ -export PATH=$PWD/bin:$PATH -``` - -### MacOS - -Download the binaries by navigating to -[https://github.com/anza-xyz/agave/releases/latest](https://github.com/anza-xyz/agave/releases/latest), -download **solana-release-x86_64-apple-darwin.tar.bz2**, then extract the -archive: - -```bash -tar jxf solana-release-x86_64-apple-darwin.tar.bz2 -cd solana-release/ -export PATH=$PWD/bin:$PATH -``` - -### Windows - -- Download the binaries by navigating to - [https://github.com/anza-xyz/agave/releases/latest](https://github.com/anza-xyz/agave/releases/latest), - download **solana-release-x86_64-pc-windows-msvc.tar.bz2**, then extract the - archive using WinZip or similar. - -- Open a Command Prompt and navigate to the directory into which you extracted - the binaries and run: - -```bash -cd solana-release/ -set PATH=%cd%/bin;%PATH% -``` - -## Build From Source - -If you are unable to use the prebuilt binaries or prefer to build it yourself -from source, follow these steps, ensuring you have the necessary prerequisites -installed on your system. - -### Prerequisites - -Before building from source, make sure to install the following prerequisites: - -#### Rust - -For all platforms, check "Install Rust" at -[https://www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) -for the latest installation instructions. - -#### For Debian and Other Linux Distributions: - -Install build dependencies: - -- Build essential -- Package config -- Udev & LLM & libclang -- Protocol buffers - -```bash -apt-get install \ - build-essential \ - pkg-config \ - libudev-dev llvm libclang-dev \ - protobuf-compiler -``` - -#### For Other Linux Distributions: - -Replace `apt` with your distribution's package manager (e.g., `yum`, `dnf`, -`pacman`) and adjust package names as needed. - -#### For macOS: - -Check "Install Homebrew" at [https://brew.sh/](https://brew.sh/) for the latest -installation instruction for Homebrew if not already installed. - -Then, install build dependencies with `brew`: - -```bash -brew install pkg-config libudev protobuf llvm coreutils -``` - -Follow the instructions given at the end of the brew install command about -`PATH` configurations. - -#### For Windows: - -- Download and install the Build Tools for Visual Studio (2019 or later) from - the - [Visual Studio downloads page](https://visualstudio.microsoft.com/downloads/). - Make sure to include the C++ build tools in the installation. -- Install LLVM: Download and install LLVM from the - [official LLVM download page](https://releases.llvm.org/download.html). -- Install Protocol Buffers Compiler (protoc): Download `protoc` from the - [GitHub releases page of Protocol Buffers](https://github.com/protocolbuffers/protobuf/releases), - and add it to your `PATH`. - -:::info - -Users on Windows 10 or 11 may need to install -[Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install) -(WSL) in order to be able to build from source. WSL provides a Linux environment -that runs inside your existing Windows installation. You can then run regular -Linux software, including the Linux versions of Solana CLI. - -After installed, run `wsl` from your Windows terminal, then continue through the -[Debian and Other Linux Distributions](#for-debian-and-other-linux-distributions) -above. - -::: - -### Building from Source - -After installing the prerequisites, proceed with building Solana from source, -navigate to the -[Agave GitHub releases page](https://github.com/anza-xyz/agave/releases/latest), -and download the **Source Code** archive. Extract the code and build the -binaries with: - -```bash -./scripts/cargo-install-all.sh . -export PATH=$PWD/bin:$PATH -``` - -## Use Homebrew - -This option requires you to have [Homebrew](https://brew.sh/) package manager on -your MacOS or Linux machine. - -### MacOS & Linux - -- Follow instructions at: https://formulae.brew.sh/formula/solana - -[Homebrew formulae](https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/s/solana.rb) -is updated after each `solana` release, however it is possible that the Homebrew -version is outdated. - -- Confirm you have the desired version of `solana` installed by entering: - -```bash -solana --version -``` diff --git a/docs/src/cli/intro.md b/docs/src/cli/intro.md deleted file mode 100644 index 436776ee718..00000000000 --- a/docs/src/cli/intro.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Introduction to the Solana CLI -pagination_label: Introduction to the Solana CLI -sidebar_label: Introduction -sidebar_position: 2 ---- - -Before running any Solana CLI commands, let's go over some conventions that -you will see across all commands. First, the Solana CLI is actually a collection -of different commands for each action you might want to take. You can view the list -of all possible commands by running: - -```bash -solana --help -``` - -To zoom in on how to use a particular command, run: - -```bash -solana --help -``` - -where you replace the text `` with the name of the command you want -to learn more about. - -The command's usage message will typically contain words such as ``, -`` or ``. Each word is a placeholder for the _type_ of -text you can execute the command with. For example, you can replace `` -with a number such as `42` or `100.42`. You can replace `` with -the base58 encoding of your public key, such as -`9grmKMwTiZwUHSExjtbFzHLPTdWoXgcg1bZkhvwTrTww`. - -## Keypair conventions - -Many commands using the CLI tools require a value for a ``. The value -you should use for the keypair depends on what type of -[command line wallet you created](./wallets/index.md). - -For example, the CLI help shows that the way to display any wallet's address -(also known as the keypair's pubkey), is: - -```bash -solana-keygen pubkey -``` - -Below, we show how to resolve what you should put in `` depending -on your wallet type. - -## Paper Wallet - -In a paper wallet, the keypair is securely derived from the seed words and -optional passphrase you entered when the wallet was created. To use a paper -wallet keypair anywhere the `` text is shown in examples or help -documents, enter the uri scheme `prompt://` and the program will prompt you to -enter your seed words when you run the command. - -To display the wallet address of a Paper Wallet: - -```bash -solana-keygen pubkey prompt:// -``` - -## File System Wallet - -With a file system wallet, the keypair is stored in a file on your computer. -Replace `` with the complete file path to the keypair file. - -For example, if the file system keypair file location is -`/home/solana/my_wallet.json`, to display the address, do: - -```bash -solana-keygen pubkey /home/solana/my_wallet.json -``` - -## Hardware Wallet - -If you chose a hardware wallet, use your -[keypair URL](./wallets/hardware/index.md#specify-a-hardware-wallet-key), -such as `usb://ledger?key=0`. - -```bash -solana-keygen pubkey usb://ledger?key=0 -``` diff --git a/docs/src/cli/wallets/_category_.json b/docs/src/cli/wallets/_category_.json deleted file mode 100644 index e420a33bb4a..00000000000 --- a/docs/src/cli/wallets/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "position": 3.5, - "label": "Command-line Wallets", - "collapsible": true, - "collapsed": false, - "link": null -} diff --git a/docs/src/cli/wallets/file-system.md b/docs/src/cli/wallets/file-system.md deleted file mode 100644 index dd21203c4e1..00000000000 --- a/docs/src/cli/wallets/file-system.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: File System Wallets using the CLI -pagination_label: File System Wallets using the CLI -sidebar_label: File System Wallets -sidebar_position: 2 ---- - -This document describes how to create and use a file system wallet with the -Solana CLI tools. A file system wallet exists as an unencrypted keypair file -on your computer system's filesystem. - -> File system wallets are the **least secure** method of storing SOL tokens. Storing large amounts of tokens in a file system wallet is **not recommended**. - -## Before you Begin - -Make sure you have -[installed the Solana Command Line Tools](../install.md) - -## Generate a File System Wallet Keypair - -Use Solana's command-line tool `solana-keygen` to generate keypair files. For -example, run the following from a command-line shell: - -```bash -mkdir ~/my-solana-wallet -solana-keygen new --outfile ~/my-solana-wallet/my-keypair.json -``` - -This file contains your **unencrypted** keypair. In fact, even if you specify -a password, that password applies to the recovery seed phrase, not the file. Do -not share this file with others. Anyone with access to this file will have access -to all tokens sent to its public key. Instead, you should share only its public -key. To display its public key, run: - -```bash -solana-keygen pubkey ~/my-solana-wallet/my-keypair.json -``` - -It will output a string of characters, such as: - -```text -ErRr1caKzK8L8nn4xmEWtimYRiTCAZXjBtVphuZ5vMKy -``` - -This is the public key corresponding to the keypair in -`~/my-solana-wallet/my-keypair.json`. The public key of the keypair file is -your _wallet address_. - -## Verify your Address against your Keypair file - -To verify you hold the private key for a given address, use -`solana-keygen verify`: - -```bash -solana-keygen verify ~/my-solana-wallet/my-keypair.json -``` - -where `` is replaced with your wallet address. -The command will output "Success" if the given address matches the -one in your keypair file, and "Failed" otherwise. - -## Creating Multiple File System Wallet Addresses - -You can create as many wallet addresses as you like. Simply re-run the -steps in [Generate a File System Wallet](#generate-a-file-system-wallet-keypair) -and make sure to use a new filename or path with the `--outfile` argument. -Multiple wallet addresses can be useful if you want to transfer tokens between -your own accounts for different purposes. diff --git a/docs/src/cli/wallets/hardware/_category_.json b/docs/src/cli/wallets/hardware/_category_.json deleted file mode 100644 index d000fb2312b..00000000000 --- a/docs/src/cli/wallets/hardware/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "position": 5, - "label": "Hardware Wallets", - "collapsible": false, - "collapsed": false, - "link": null -} diff --git a/docs/src/cli/wallets/hardware/index.md b/docs/src/cli/wallets/hardware/index.md deleted file mode 100644 index c2491e4b3de..00000000000 --- a/docs/src/cli/wallets/hardware/index.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: Using Hardware Wallets in the Solana CLI -pagination_label: "Using Hardware Wallets in the Solana CLI" -sidebar_label: Using in the Solana CLI -sidebar_position: 0 ---- - -Signing a transaction requires a private key, but storing a private -key on your personal computer or phone leaves it subject to theft. -Adding a password to your key adds security, but many people prefer -to take it a step further and move their private keys to a separate -physical device called a _hardware wallet_. A hardware wallet is a -small handheld device that stores private keys and provides some -interface for signing transactions. - -The Solana CLI has first class support for hardware wallets. Anywhere -you use a keypair filepath (denoted as `` in usage docs), you -can pass a _keypair URL_ that uniquely identifies a keypair in a -hardware wallet. - -## Supported Hardware Wallets - -The Solana CLI supports the following hardware wallets: - -- [Ledger Nano S, Nano S Plus, and Nano X](./ledger.md) -- [Trezor Model T, Safe 3, and Safe 5](./trezor.md) - -## Specify a Keypair URL - -Solana defines a keypair URL format to uniquely locate any Solana keypair on a -hardware wallet connected to your computer. - -The keypair URL has the following form, where square brackets denote optional -fields: - -```text -usb://[/][?key=] -``` - -`WALLET_ID` is a globally unique key used to disambiguate multiple devices. - -`DERVIATION_PATH` is used to navigate to Solana keys within your hardware wallet. -The path has the form `[/]`, where each `ACCOUNT` and `CHANGE` -are nonnegative integers. - -For example, a fully qualified URL for a Ledger device might be: - -```text -usb://ledger/BsNsvfXqQTtJnagwFWdBS7FBXgnsK8VZ5CmuznN85swK?key=0/0 -``` - -All derivation paths implicitly include the prefix `44'/501'`, which indicates -the path follows the [BIP44 specifications](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) -and that any derived keys are Solana keys (Coin type 501). The single quote -indicates a "hardened" derivation. Because Solana uses Ed25519 keypairs, all -derivations are hardened and therefore adding the quote is optional and -unnecessary. diff --git a/docs/src/cli/wallets/hardware/ledger.md b/docs/src/cli/wallets/hardware/ledger.md deleted file mode 100644 index e5a45c63df0..00000000000 --- a/docs/src/cli/wallets/hardware/ledger.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: Using Ledger Nano Hardware Wallets in the Solana CLI -pagination_label: "Hardware Wallets in the Solana CLI: Ledger Nano" -sidebar_label: Ledger Nano ---- - -This page describes how to use a Ledger Nano S, Nano S Plus, or Nano X to -interact with Solana using the command line tools. - -## Before You Begin - -- [Set up a Nano with the Solana App](https://support.ledger.com/hc/en-us/articles/360016265659-Solana-SOL-?docs=true) -- [Install the Solana command-line tools](../../install.md) - -## Use Ledger Nano with Solana CLI - -1. Ensure the Ledger Live application is closed -2. Plug your Nano into your computer's USB port -3. Enter your pin and start the Solana app on the Nano -4. Ensure the screen reads "Application is ready" - -### View your Wallet ID - -On your computer, run: - -```bash -solana-keygen pubkey usb://ledger -``` - -This confirms your Ledger device is connected properly and in the correct state -to interact with the Solana CLI. The command returns your Ledger's unique -_wallet ID_. When you have multiple Nano devices connected to the same computer, -you can use your wallet ID to specify which Ledger hardware wallet you want to -use. If you only plan to use a single Nano on your computer at a time, you don't -need to include the wallet ID. For information on using the wallet ID to use a -specific Ledger, see -[Manage Multiple Hardware Wallets](#manage-multiple-hardware-wallets). - -### View your Wallet Addresses - -Your Nano supports an arbitrary number of valid wallet addresses and signers. To -view any address, use the `solana-keygen pubkey` command, as shown below, -followed by a valid [keypair URL](./index.md#specify-a-keypair-url). - -Multiple wallet addresses can be useful if you want to transfer tokens between -your own accounts for different purposes, or use different keypairs on the -device as signing authorities for a stake account, for example. - -All of the following commands will display different addresses, associated with -the keypair path given. Try them out! - -```bash -solana-keygen pubkey usb://ledger -solana-keygen pubkey usb://ledger?key=0 -solana-keygen pubkey usb://ledger?key=1 -solana-keygen pubkey usb://ledger?key=2 -``` - -- NOTE: keypair url parameters are ignored in **zsh** -  [see troubleshooting for more info](#troubleshooting) - -You can use other values for the number after `key=` as well. Any of the -addresses displayed by these commands are valid Solana wallet addresses. The -private portion associated with each address is stored securely on the Nano, and -is used to sign transactions from this address. Just make a note of which -keypair URL you used to derive any address you will be using to receive tokens. - -If you are only planning to use a single address/keypair on your device, a good -easy-to-remember path might be to use the address at `key=0`. View this address -with: - -```bash -solana-keygen pubkey usb://ledger?key=0 -``` - -Now you have a wallet address (or multiple addresses), you can share any of -these addresses publicly to act as a receiving address, and you can use the -associated keypair URL as the signer for transactions from that address. - -### View your Balance - -To view the balance of any account, regardless of which wallet it uses, use the -`solana balance` command: - -```bash -solana balance SOME_WALLET_ADDRESS -``` - -For example, if your address is `7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri`, -then enter the following command to view the balance: - -```bash -solana balance 7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri -``` - -You can also view the balance of any account address on the Accounts tab in the -[Explorer](https://explorer.solana.com/accounts) and paste the address in the -box to view the balance in your web browser. - -Note: Any address with a balance of 0 SOL, such as a newly created one on your -Ledger, will show as "Not Found" in the explorer. Empty accounts and -non-existent accounts are treated the same in Solana. This will change when your -account address has some SOL in it. - -### Send SOL from a Nano - -To send some tokens from an address controlled by your Nano, you will need to -use the device to sign a transaction, using the same keypair URL you used to -derive the address. To do this, make sure your Nano is plugged in, unlocked with -the PIN, Ledger Live is not running, and the Solana App is open on the device, -showing "Application is Ready". - -The `solana transfer` command is used to specify to which address to send -tokens, how many tokens to send, and uses the `--keypair` argument to specify -which keypair is sending the tokens, which will sign the transaction, and the -balance from the associated address will decrease. - -```bash -solana transfer RECIPIENT_ADDRESS AMOUNT --keypair KEYPAIR_URL_OF_SENDER -``` - -Below is a full example. First, an address is viewed at a certain keypair URL. -Second, the balance of that address is checked. Lastly, a transfer transaction -is entered to send `1` SOL to the recipient address -`7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri`. When you hit Enter for a -transfer command, you will be prompted to approve the transaction details on -your Ledger device. On the device, use the right and left buttons to review the -transaction details. If they look correct, click both buttons on the "Approve" -screen, otherwise push both buttons on the "Reject" screen. - -```bash -~$ solana-keygen pubkey usb://ledger?key=42 -CjeqzArkZt6xwdnZ9NZSf8D1CNJN1rjeFiyd8q7iLWAV - -~$ solana balance CjeqzArkZt6xwdnZ9NZSf8D1CNJN1rjeFiyd8q7iLWAV -1.000005 SOL - -~$ solana transfer 7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri 1 --keypair usb://ledger?key=42 -Waiting for your approval on Ledger hardware wallet usb://ledger/2JT2Xvy6T8hSmT8g6WdeDbHUgoeGdj6bE2VueCZUJmyN -✅ Approved - -Signature: kemu9jDEuPirKNRKiHan7ycybYsZp7pFefAdvWZRq5VRHCLgXTXaFVw3pfh87MQcWX4kQY4TjSBmESrwMApom1V -``` - -After approving the transaction on your device, the program will display the -transaction signature, and wait for the maximum number of confirmations (32) -before returning. This only takes a few seconds, and then the transaction is -finalized on the Solana network. You can view details of this or any other -transaction by going to the Transaction tab in the -[Explorer](https://explorer.solana.com/transactions) and paste in the -transaction signature. - -## Advanced Operations - -### Manage Multiple Hardware Wallets - -It is sometimes useful to sign a transaction with keys from multiple hardware -wallets. Signing with multiple wallets requires _fully qualified keypair URLs_. -When the URL is not fully qualified, the Solana CLI will prompt you with the -fully qualified URLs of all connected hardware wallets, and ask you to choose -which wallet to use for each signature. - -Instead of using the interactive prompts, you can generate fully qualified URLs -using the Solana CLI `resolve-signer` command. For example, try connecting a -Nano to USB, unlock it with your pin, and running the following command: - -```text -solana resolve-signer usb://ledger?key=0/0 -``` - -You will see output similar to: - -```text -usb://ledger/BsNsvfXqQTtJnagwFWdBS7FBXgnsK8VZ5CmuznN85swK?key=0/0 -``` - -but where `BsNsvfXqQTtJnagwFWdBS7FBXgnsK8VZ5CmuznN85swK` is your `WALLET_ID`. - -With your fully qualified URL, you can connect multiple hardware wallets to the -same computer and uniquely identify a keypair from any of them. Use the output -from the `resolve-signer` command anywhere a `solana` command expects a -`` entry to use that resolved path as the signer for that part of the -given transaction. - -## Troubleshooting - -### Keypair URL parameters are ignored in zsh - -The question mark character is a special character in zsh. If that's not a -feature you use, add the following line to your `~/.zshrc` to treat it as a -normal character: - -```bash -unsetopt nomatch -``` - -Then either restart your shell window or run `~/.zshrc`: - -```bash -source ~/.zshrc -``` - -If you would prefer not to disable zsh's special handling of the question mark -character, you can disable it explicitly with a backslash in your keypair URLs. -For example: - -```bash -solana-keygen pubkey usb://ledger\?key=0 -``` - -## Support - -You can find additional support and get help on the -[Solana StackExchange](https://solana.stackexchange.com). - -Read more about [sending and receiving tokens](../../examples/transfer-tokens.md) and -[delegating stake](../../examples/delegate-stake.md). You can use your Ledger keypair -URL anywhere you see an option or argument that accepts a ``. diff --git a/docs/src/cli/wallets/hardware/trezor.md b/docs/src/cli/wallets/hardware/trezor.md deleted file mode 100644 index 4f372ca93e0..00000000000 --- a/docs/src/cli/wallets/hardware/trezor.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -title: Using Trezor Hardware Wallets in the Solana CLI -pagination_label: "Hardware Wallets in the Solana CLI: Trezor" -sidebar_label: Trezor ---- - -This page describes how to use a Trezor Model T, Safe 3, or Safe 5 device to -interact with Solana using the command line tools. - -## Before You Begin - -- [Install the Solana command-line tools](../../install.md) -- [Review Trezor and BIP-32](https://trezor.io/learn/a/what-is-bip32) -- [Review Trezor and BIP-44](https://trezor.io/learn/a/what-is-bip44) - -## Use Trezor Model T, Safe 3, or Safe 5 with Solana CLI - -1. Plug your Trezor device into your computer's USB port -2. Tap to connect the device -3. Enter your pin -3. Ensure the screen reads the name of your device - -### View your Wallet Addresses - -On your computer, run: - -```bash -solana-keygen pubkey usb://trezor?key=0/0 -``` - -This confirms your Trezor device is connected properly and in the correct state -to interact with the Solana CLI. The command returns your Trezor device's first -Solana account's external (receiving) wallet address using the -[BIP-32](https://trezor.io/learn/a/what-is-bip32) derivation path -`m/44'/501'/0'/0'`. - -Your Trezor device supports an arbitrary number of valid wallet addresses and signers. To -view any address, use the `solana-keygen pubkey` command, as shown below, -followed by a valid [keypair URL](./index.md#specify-a-keypair-url). - -Multiple wallet addresses can be useful if you want to transfer tokens between -your own accounts for different purposes, or use different keypairs on the -device as signing authorities for a stake account, for example. - -All of the following commands will display different addresses, associated with -the keypair path given. Try them out! - -```bash -solana-keygen pubkey usb://trezor?key=0/0 -solana-keygen pubkey usb://trezor?key=0/1 -solana-keygen pubkey usb://trezor?key=1/0 -solana-keygen pubkey usb://trezor?key=1/1 -``` - -- NOTE: keypair url parameters are ignored in **zsh** -  [see troubleshooting for more info](#troubleshooting) - -You can use other values for the number after `key=` as well. Any of the -addresses displayed by these commands are valid Solana wallet addresses. The -private portion associated with each address is stored securely on the Trezor device, and -is used to sign transactions from this address. Just make a note of which -keypair URL you used to derive any address you will be using to receive tokens. - -If you are only planning to use a single address/keypair on your device, a good -easy-to-remember path might be to use the address at `key=0/`. View this address -with: - -```bash -solana-keygen pubkey usb://trezor?key=0/0 -solana-keygen pubkey usb://trezor?key=0/1 -``` - -Now you have a wallet address (or multiple addresses), you can share any of -these addresses publicly to act as a receiving address, and you can use the -associated keypair URL as the signer for transactions from that address. - -### Wallet Operations - -To use the device for wallet operations, such as balance fetching or -transferring SOL, follow the guides for -[viewing balance](./ledger.md#view-your-balance) or -[sending SOL](./ledger.md#send-sol-from-a-nano), substituting `ledger` with -`trezor` and your key path. - -## Troubleshooting - -### Keypair URL parameters are ignored in zsh - -The question mark character is a special character in zsh. If that's not a -feature you use, add the following line to your `~/.zshrc` to treat it as a -normal character: - -```bash -unsetopt nomatch -``` - -Then either restart your shell window or run `~/.zshrc`: - -```bash -source ~/.zshrc -``` - -If you would prefer not to disable zsh's special handling of the question mark -character, you can disable it explicitly with a backslash in your keypair URLs. -For example: - -```bash -solana-keygen pubkey usb://trezor\?key=0/0 -``` - -## Support - -You can find additional support and get help on the -[Solana StackExchange](https://solana.stackexchange.com). - -Read more about [sending and receiving tokens](../../examples/transfer-tokens.md) and -[delegating stake](../../examples/delegate-stake.md). You can use your Ledger keypair -URL anywhere you see an option or argument that accepts a ``. diff --git a/docs/src/cli/wallets/index.md b/docs/src/cli/wallets/index.md deleted file mode 100644 index 33fc0f28488..00000000000 --- a/docs/src/cli/wallets/index.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Solana Wallets with the CLI -pagination_label: Command Line Wallets -sidebar_label: Overview -sidebar_position: 0 ---- - -Solana supports several different types of wallets that can be used to interface -directly with the Solana command-line tools. - -To use a Command Line Wallet, you must first [install the Solana CLI tools](../install.md) - -## File System Wallet - -A _file system wallet_, aka an FS wallet, is a directory in your computer's -file system. Each file in the directory holds a keypair. - -### File System Wallet Security - -A file system wallet is the most convenient and least secure form of wallet. It -is convenient because the keypair is stored in a simple file. You can generate as -many keys as you would like and trivially back them up by copying the files. It -is insecure because the keypair files are **unencrypted**. If you are the only -user of your computer and you are confident it is free of malware, an FS wallet -is a fine solution for small amounts of cryptocurrency. If, however, your -computer contains malware and is connected to the Internet, that malware may -upload your keys and use them to take your tokens. Likewise, because the -keypairs are stored on your computer as files, a skilled hacker with physical -access to your computer may be able to access it. Using an encrypted hard -drive, such as FileVault on MacOS, minimizes that risk. - -See [File System Wallets](./file-system.md) for more details. - -## Paper Wallet - -A _paper wallet_ is a collection of _seed phrases_ written on paper. A seed -phrase is some number of words (typically 12 or 24) that can be used to -regenerate a keypair on demand. - -### Paper Wallet Security - -In terms of convenience versus security, a paper wallet sits at the opposite -side of the spectrum from an FS wallet. It is terribly inconvenient to use, but -offers excellent security. That high security is further amplified when paper -wallets are used in conjunction with [offline signing](../examples/offline-signing.md). - -See [Paper Wallets](./paper.md) for more details - -## Hardware Wallet - -A hardware wallet is a small handheld device that stores keypairs and provides -some interface for signing transactions. - -### Hardware Wallet Security - -A hardware wallet, such as the -[Ledger hardware wallet](https://www.ledger.com/) or -[Trezor hardware wallet](https://trezor.io/), offers a great blend of -security and convenience for cryptocurrencies. It effectively automates the -process of offline signing while retaining nearly all the convenience of a file -system wallet. - -See [Hardware Wallets](./hardware/index.md) for more details diff --git a/docs/src/cli/wallets/paper.md b/docs/src/cli/wallets/paper.md deleted file mode 100644 index b90b1b9bfd3..00000000000 --- a/docs/src/cli/wallets/paper.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: Paper Wallets using the Solana CLI -pagination_label: Paper Wallets using the CLI -sidebar_label: Paper Wallets -sidebar_position: 1 ---- - -This document describes how to create and use a paper wallet with the Solana CLI -tools. - -> We do not intend to advise on how to _securely_ create or manage paper -> wallets. Please research the security concerns carefully. - -## Overview - -Solana provides a key generation tool to derive keys from -[BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)-compliant -seed phrases. Solana CLI commands for running a validator and staking tokens all -support keypair input via seed phrases. - -## Paper Wallet Usage - -Solana commands can be run without ever saving a keypair to disk on a machine. -If avoiding writing a private key to disk is a security concern of yours, you've -come to the right place. - -> Even using this secure input method, it's still possible that a private key -> gets written to disk by unencrypted memory swaps. It is the user's -> responsibility to protect against this scenario. - -## Before You Begin - -- [Install the Solana command-line tools](../install.md) - -### Check your installation - -Check that `solana-keygen` is installed correctly by running: - -```bash -solana-keygen --version -``` - -## Creating a Paper Wallet - -Using the `solana-keygen` tool, it is possible to generate new seed phrases as -well as derive a keypair from an existing seed phrase and (optional) passphrase. -The seed phrase and passphrase can be used together as a paper wallet. As long -as you keep your seed phrase and passphrase stored safely, you can use them to -access your account. - -> For more information about how seed phrases work, review this -> [Bitcoin Wiki page](https://en.bitcoin.it/wiki/Seed_phrase). - -### Seed Phrase Generation - -Generating a new keypair can be done using the `solana-keygen new` command. The -command will generate a random seed phrase, ask you to enter an optional -passphrase, and then will display the derived public key and the generated seed -phrase for your paper wallet. - -After copying down your seed phrase, you can use the -[public key derivation](#public-key-derivation) instructions to verify that you -have not made any errors. - -```bash -solana-keygen new --no-outfile -``` - -> If the `--no-outfile` flag is **omitted**, the default behavior is to write -> the keypair to `~/.config/solana/id.json`, resulting in a -> [file system wallet](./file-system.md). - -The output of this command will display a line like this: - -```bash -pubkey: 9ZNTfG4NyQgxy2SWjSiQoUyBPEvXT2xo7fKc5hPYYJ7b -``` - -The value shown after `pubkey:` is your _wallet address_. - -**Note:** In working with paper wallets and file system wallets, the terms -"pubkey" and "wallet address" are sometimes used interchangeably. - -> For added security, increase the seed phrase word count using the -> `--word-count` argument - -For full usage details, run: - -```bash -solana-keygen new --help -``` - -### Public Key Derivation - -Public keys can be derived from a seed phrase and a passphrase if you choose to -use one. This is useful for using an offline-generated seed phrase to derive a -valid public key. The `solana-keygen pubkey` command will walk you through how -to use your seed phrase (and a passphrase if you chose to use one) as a signer -with the solana command-line tools using the `prompt` URI scheme. - -```bash -solana-keygen pubkey prompt:// -``` - -> Note that you could potentially use different passphrases for the same seed -> phrase. Each unique passphrase will yield a different keypair. - -The `solana-keygen` tool uses the same BIP39 standard English word list as it -does to generate seed phrases. If your seed phrase was generated with another -tool that uses a different word list, you can still use `solana-keygen`, but -will need to pass the `--skip-seed-phrase-validation` argument and forego this -validation. - -```bash -solana-keygen pubkey prompt:// --skip-seed-phrase-validation -``` - -After entering your seed phrase with `solana-keygen pubkey prompt://` the -console will display a string of base-58 characters. This is the -[derived](#hierarchical-derivation) solana BIP44 _wallet address_ associated -with your seed phrase. - -> Copy the derived address to a USB stick for easy usage on networked computers - -If needed, you can access the legacy, raw keypair's pubkey by instead passing -the `ASK` keyword: - -```bash -solana-keygen pubkey ASK -``` - -> A common next step is to [check the balance](#checking-account-balance) of the -> account associated with a public key - -For full usage details, run: - -```bash -solana-keygen pubkey --help -``` - -### Hierarchical Derivation - -The solana-cli supports -[BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) and -[BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) -hierarchical derivation of private keys from your seed phrase and passphrase by -adding either the `?key=` query string or the `?full-path=` query string. - -By default, `prompt:` will derive solana's base derivation path `m/44'/501'`. To -derive a child key, supply the `?key=/` query string. - -```bash -solana-keygen pubkey 'prompt://?key=0/1' -``` - -To use a derivation path other than solana's standard BIP44, you can supply -`?full-path=m////`. - -```bash -solana-keygen pubkey 'prompt://?full-path=m/44/2017/0/1' -``` - -Because Solana uses Ed25519 keypairs, as per -[SLIP-0010](https://github.com/satoshilabs/slips/blob/master/slip-0010.md) all -derivation-path indexes will be promoted to hardened indexes -- eg. -`?key=0'/0'`, `?full-path=m/44'/2017'/0'/1'` -- regardless of whether ticks are -included in the query-string input. - -## Verifying the Keypair - -To verify you control the private key of a paper wallet address, use -`solana-keygen verify`: - -```bash -solana-keygen verify prompt:// -``` - -where `` is replaced with the wallet address and the keyword `prompt://` -tells the command to prompt you for the keypair's seed phrase; `key` and -`full-path` query-strings accepted. Note that for security reasons, your seed -phrase will not be displayed as you type. After entering your seed phrase, the -command will output "Success" if the given public key matches the keypair -generated from your seed phrase, and "Failed" otherwise. - -## Checking Account Balance - -All that is needed to check an account balance is the public key of an account. -To retrieve public keys securely from a paper wallet, follow the -[Public Key Derivation](#public-key-derivation) instructions on an -[air gapped computer](). -Public keys can then be typed manually or transferred via a USB stick to a -networked machine. - -Next, configure the `solana` CLI tool to -[connect to a particular cluster](../examples/choose-a-cluster.md): - -```bash -solana config set --url # (i.e. https://api.mainnet-beta.solana.com) -``` - -Finally, to check the balance, run the following command: - -```bash -solana balance -``` - -## Creating Multiple Paper Wallet Addresses - -You can create as many wallet addresses as you like. Simply re-run the steps in -[Seed Phrase Generation](#seed-phrase-generation) or -[Public Key Derivation](#public-key-derivation) to create a new address. -Multiple wallet addresses can be useful if you want to transfer tokens between -your own accounts for different purposes. - -## Support - -You can find additional support and get help on the -[Solana StackExchange](https://solana.stackexchange.com). diff --git a/docs/src/clusters/available.md b/docs/src/clusters/available.md deleted file mode 100644 index 498c77436ea..00000000000 --- a/docs/src/clusters/available.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: Available Solana Clusters -sidebar_label: Solana Clusters -pagination_label: Available Solana Clusters ---- - -Solana maintains several different clusters with different purposes. - -Before you begin, make sure you have first built the Solana command line tools. -See [Installing the Solana Command Line Tools](../cli/install.md#build-from-source). - -Explorers: - -- [http://explorer.solana.com/](https://explorer.solana.com/). -- [http://solanabeach.io/](http://solanabeach.io/). - -## Devnet - -- Devnet serves as a playground for anyone who wants to take Solana for a - test drive, as a user, token holder, app developer or validator. -- Application developers should target Devnet. -- Potential validators should first target Devnet. -- Key differences between Devnet and Mainnet Beta: - - Devnet tokens are **not real** - - Devnet includes a token faucet for airdrops for application testing - - Devnet may be subject to ledger resets - - Devnet typically runs the same software release branch version as Mainnet Beta, - but may run a newer minor release version than Mainnet Beta. -- Gossip entrypoint for Devnet: `entrypoint.devnet.solana.com:8001` -- Metrics environment variable for Devnet: - -```bash -export SOLANA_METRICS_CONFIG="host=https://metrics.solana.com:8086,db=devnet,u=scratch_writer,p=topsecret" -``` - -- RPC URL for Devnet: `https://api.devnet.solana.com` - -##### Example `solana` command-line configuration - -```bash -solana config set --url https://api.devnet.solana.com -``` - -##### Example `agave-validator` command-line - -```bash -$ agave-validator \ - --identity validator-keypair.json \ - --vote-account vote-account-keypair.json \ - --known-validator dv1ZAGvdsz5hHLwWXsVnM94hWf1pjbKVau1QVkaMJ92 \ - --known-validator dv2eQHeP4RFrJZ6UeiZWoc3XTtmtZCUKxxCApCDcRNV \ - --known-validator dv4ACNkpYPcE3aKmYDqZm9G5EB3J4MRoeE7WNDRBVJB \ - --known-validator dv3qDFk1DTF36Z62bNvrCXe9sKATA6xvVy6A798xxAS \ - --only-known-rpc \ - --ledger ledger \ - --rpc-port 8899 \ - --dynamic-port-range 8000-8020 \ - --entrypoint entrypoint.devnet.solana.com:8001 \ - --entrypoint entrypoint2.devnet.solana.com:8001 \ - --entrypoint entrypoint3.devnet.solana.com:8001 \ - --entrypoint entrypoint4.devnet.solana.com:8001 \ - --entrypoint entrypoint5.devnet.solana.com:8001 \ - --expected-genesis-hash EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG \ - --wal-recovery-mode skip_any_corrupted_record \ - --limit-ledger-size -``` - -The [`--known-validator`s](../operations/guides/validator-start.md#known-validators) -are operated by Anza - -## Testnet - -- Testnet is where the Solana core contributors stress test recent release features on a live - cluster, particularly focused on network performance, stability and validator - behavior. -- Testnet tokens are **not real** -- Testnet may be subject to ledger resets. -- Testnet includes a token faucet for airdrops for application testing -- Testnet typically runs a newer software release branch than both - Devnet and Mainnet Beta -- Gossip entrypoint for Testnet: `entrypoint.testnet.solana.com:8001` -- Metrics environment variable for Testnet: - -```bash -export SOLANA_METRICS_CONFIG="host=https://metrics.solana.com:8086,db=tds,u=testnet_write,p=c4fa841aa918bf8274e3e2a44d77568d9861b3ea" -``` - -- RPC URL for Testnet: `https://api.testnet.solana.com` - -##### Example `solana` command-line configuration - -```bash -solana config set --url https://api.testnet.solana.com -``` - -##### Example `agave-validator` command-line - -```bash -$ agave-validator \ - --identity validator-keypair.json \ - --vote-account vote-account-keypair.json \ - --known-validator 5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on \ - --known-validator dDzy5SR3AXdYWVqbDEkVFdvSPCtS9ihF5kJkHCtXoFs \ - --known-validator Ft5fbkqNa76vnsjYNwjDZUXoTWpP7VYm3mtsaQckQADN \ - --known-validator eoKpUABi59aT4rR9HGS3LcMecfut9x7zJyodWWP43YQ \ - --known-validator 9QxCLckBiJc783jnMvXZubK4wH86Eqqvashtrwvcsgkv \ - --only-known-rpc \ - --ledger ledger \ - --rpc-port 8899 \ - --dynamic-port-range 8000-8020 \ - --entrypoint entrypoint.testnet.solana.com:8001 \ - --entrypoint entrypoint2.testnet.solana.com:8001 \ - --entrypoint entrypoint3.testnet.solana.com:8001 \ - --expected-genesis-hash 4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY \ - --wal-recovery-mode skip_any_corrupted_record \ - --limit-ledger-size -``` - -The identities of the -[`--known-validator`s](../operations/guides/validator-start.md#known-validators) are: - -- `5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on` - Anza -- `dDzy5SR3AXdYWVqbDEkVFdvSPCtS9ihF5kJkHCtXoFs` - MonkeDAO -- `Ft5fbkqNa76vnsjYNwjDZUXoTWpP7VYm3mtsaQckQADN` - Certus One -- `eoKpUABi59aT4rR9HGS3LcMecfut9x7zJyodWWP43YQ` - SerGo -- `9QxCLckBiJc783jnMvXZubK4wH86Eqqvashtrwvcsgkv` - Algo|Stake - -## Mainnet Beta - -A permissionless, persistent cluster for Solana users, builders, validators and token holders. - -- Tokens that are issued on Mainnet Beta are **real** SOL -- Gossip entrypoint for Mainnet Beta: `entrypoint.mainnet-beta.solana.com:8001` -- Metrics environment variable for Mainnet Beta: - -```bash -export SOLANA_METRICS_CONFIG="host=https://metrics.solana.com:8086,db=mainnet-beta,u=mainnet-beta_write,p=password" -``` - -- RPC URL for Mainnet Beta: `https://api.mainnet-beta.solana.com` - -##### Example `solana` command-line configuration - -```bash -solana config set --url https://api.mainnet-beta.solana.com -``` - -##### Example `agave-validator` command-line - -```bash -$ agave-validator \ - --identity ~/validator-keypair.json \ - --vote-account ~/vote-account-keypair.json \ - --known-validator 7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2 \ - --known-validator GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ \ - --known-validator DE1bawNcRJB9rVm3buyMVfr8mBEoyyu73NBovf2oXJsJ \ - --known-validator CakcnaRDHka2gXyfbEd2d3xsvkJkqsLw2akB3zsN1D2S \ - --only-known-rpc \ - --ledger ledger \ - --rpc-port 8899 \ - --private-rpc \ - --dynamic-port-range 8000-8020 \ - --entrypoint entrypoint.mainnet-beta.solana.com:8001 \ - --entrypoint entrypoint2.mainnet-beta.solana.com:8001 \ - --entrypoint entrypoint3.mainnet-beta.solana.com:8001 \ - --entrypoint entrypoint4.mainnet-beta.solana.com:8001 \ - --entrypoint entrypoint5.mainnet-beta.solana.com:8001 \ - --expected-genesis-hash 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d \ - --wal-recovery-mode skip_any_corrupted_record \ - --limit-ledger-size -``` - -:::info -The above four [`--known-validator`s](../operations/guides/validator-start.md#known-validators) -are operated by Anza. -::: \ No newline at end of file diff --git a/docs/src/clusters/benchmark.md b/docs/src/clusters/benchmark.md deleted file mode 100644 index dbb7113cdf6..00000000000 --- a/docs/src/clusters/benchmark.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: Benchmark a Cluster ---- - -The Solana git repository contains all the scripts you might need to spin up your own local testnet. Depending on what you're looking to achieve, you may want to run a different variation, as the full-fledged, performance-enhanced multinode testnet is considerably more complex to set up than a Rust-only, singlenode testnode. If you are looking to develop high-level features, such as experimenting with smart contracts, save yourself some setup headaches and stick to the Rust-only singlenode demo. If you're doing performance optimization of the transaction pipeline, consider the enhanced singlenode demo. If you're doing consensus work, you'll need at least a Rust-only multinode demo. If you want to reproduce our TPS metrics, run the enhanced multinode demo. - -For all four variations, you'd need the latest Rust toolchain and the Solana source code: - -First, setup Rust, Cargo and system packages as described in the Solana [README](https://github.com/solana-labs/solana#1-install-rustc-cargo-and-rustfmt) - -Now checkout the code from github: - -```bash -git clone https://github.com/solana-labs/solana.git -cd solana -``` - -The demo code is sometimes broken between releases as we add new low-level features, so if this is your first time running the demo, you'll improve your odds of success if you check out the [latest release](https://github.com/solana-labs/solana/releases) before proceeding: - -```bash -TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) -git checkout $TAG -``` - -### Configuration Setup - -Ensure important programs such as the vote program are built before any nodes are started. Note that we are using the release build here for good performance. - -```bash -cargo build --release -``` - -Then set the following environment variable to enforce release builds across all commands we are going to use later: - -```bash -export CARGO_BUILD_PROFILE=release -``` - -If you want to profile the validator(s), you can use the `release-with-debug` build profile, which leverages the most of optimizations used in the `release` profile, while keeping the debug symbols. - -```bash -cargo build --profile release-with-debug -export CARGO_BUILD_PROFILE=release-with-debug -``` - -If you want the debug build without optimizations, use just `cargo build` and do not set any environment variables mentioned above. - -The network is initialized with a genesis ledger generated by running the following script. - -```bash -./multinode-demo/setup.sh -``` - -### Faucet - -In order for the validators and clients to work, we'll need to spin up a faucet to give out some test tokens. The faucet delivers Milton Friedman-style "air drops" \(free tokens to requesting clients\) to be used in test transactions. - -Start the faucet with: - -```bash -./multinode-demo/faucet.sh -``` - -### Singlenode Testnet - -Before you start a validator, make sure you know the IP address of the machine you want to be the bootstrap validator for the demo, and make sure that udp ports 8000-10000 are open on all the machines you want to test with. - -Now start the bootstrap validator in a separate shell: - -```bash -./multinode-demo/bootstrap-validator.sh -``` - -Wait a few seconds for the server to initialize. It will print "leader ready..." when it's ready to receive transactions. The leader will request some tokens from the faucet if it doesn't have any. The faucet does not need to be running for subsequent leader starts. - -### Multinode Testnet - -To run a multinode testnet, after starting a leader node, spin up some additional validators in separate shells: - -```bash -./multinode-demo/validator-x.sh -``` - - -### Testnet Client Demo - -Now that your singlenode or multinode testnet is up and running let's send it some transactions! - -In a separate shell start the client: - -```bash -./multinode-demo/bench-tps.sh # runs against localhost by default -``` - -What just happened? The client demo spins up several threads to send 500,000 transactions to the testnet as quickly as it can. The client then pings the testnet periodically to see how many transactions it processed in that time. Take note that the demo intentionally floods the network with UDP packets, such that the network will almost certainly drop a bunch of them. This ensures the testnet has an opportunity to reach 710k TPS. The client demo completes after it has convinced itself the testnet won't process any additional transactions. You should see several TPS measurements printed to the screen. In the multinode variation, you'll see TPS measurements for each validator node as well. - -### Testnet Debugging - -There are some useful debug messages in the code, you can enable them on a per-module and per-level basis. Before running a leader or validator set the normal RUST_LOG environment variable. - -For example - -- To enable `info` everywhere and `debug` only in the solana::banking_stage module: - - ```bash - export RUST_LOG=solana=info,solana::banking_stage=debug - ``` - -- To enable SBF program logging: - - ```bash - export RUST_LOG=solana_bpf_loader=trace - ``` - -Generally we are using `debug` for infrequent debug messages, `trace` for potentially frequent messages and `info` for performance-related logging. - -You can also attach to a running process with GDB. The leader's process is named _agave-validator_: - -```bash -sudo gdb -attach -set logging on -thread apply all bt -``` - -This will dump all the threads stack traces into gdb.txt - -## Developer Testnet - -In this example the client connects to our public testnet. To run validators on the testnet you would need to open udp ports `8000-10000`. - -```bash -./multinode-demo/bench-tps.sh --entrypoint entrypoint.devnet.solana.com:8001 --faucet api.devnet.solana.com:9900 --duration 60 --tx_count 50 -``` - -You can observe the effects of your client's transactions on our [metrics dashboard](https://metrics.solana.com:3000/d/monitor/cluster-telemetry?var-testnet=devnet) diff --git a/docs/src/clusters/index.md b/docs/src/clusters/index.md deleted file mode 100644 index e2d25c603b4..00000000000 --- a/docs/src/clusters/index.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Overview of a Solana Cluster -sidebar_position: 0 -sidebar_label: Overview -pagination_label: Overview of a Solana Cluster ---- - -A Solana cluster is a set of validators working together to serve client transactions and maintain the integrity of the ledger. Many clusters may coexist. When two clusters share a common genesis block, they attempt to converge. Otherwise, they simply ignore the existence of the other. Transactions sent to the wrong one are quietly rejected. In this section, we'll discuss how a cluster is created, how nodes join the cluster, how they share the ledger, how they ensure the ledger is replicated, and how they cope with buggy and malicious nodes. - -## Creating a Cluster - -Before starting any validators, one first needs to create a _genesis config_. The config references two public keys, a _mint_ and a _bootstrap validator_. The validator holding the bootstrap validator's private key is responsible for appending the first entries to the ledger. It initializes its internal state with the mint's account. That account will hold the number of native tokens defined by the genesis config. The second validator then contacts the bootstrap validator to register as a _validator_. Additional validators then register with any registered member of the cluster. - -A validator receives all entries from the leader and submits votes confirming those entries are valid. After voting, the validator is expected to store those entries. Once the validator observes a sufficient number of copies exist, it deletes its copy. - -## Joining a Cluster - -Validators enter the cluster via registration messages sent to its _control plane_. The control plane is implemented using a _gossip_ protocol, meaning that a node may register with any existing node, and expect its registration to propagate to all nodes in the cluster. The time it takes for all nodes to synchronize is proportional to the square of the number of nodes participating in the cluster. Algorithmically, that's considered very slow, but in exchange for that time, a node is assured that it eventually has all the same information as every other node, and that information cannot be censored by any one node. - -## Sending Transactions to a Cluster - -Clients send transactions to any validator's Transaction Processing Unit \(TPU\) port. If the node is in the validator role, it forwards the transaction to the designated leader. If in the leader role, the node bundles incoming transactions, timestamps them creating an _entry_, and pushes them onto the cluster's _data plane_. Once on the data plane, the transactions are validated by validator nodes, effectively appending them to the ledger. - -## Confirming Transactions - -A Solana cluster is capable of subsecond _confirmation_ for thousands of nodes with plans to scale up to hundreds of thousands of nodes. Confirmation times are expected to increase only with the logarithm of the number of validators, where the logarithm's base is very high. If the base is one thousand, for example, it means that for the first thousand nodes, confirmation will be the duration of three network hops plus the time it takes the slowest validator of a supermajority to vote. For the next million nodes, confirmation increases by only one network hop. - -Solana defines confirmation as the duration of time from when the leader timestamps a new entry to the moment when it recognizes a supermajority of ledger votes. - -Scalable confirmation can be achieved using the following combination of techniques: - -1. Timestamp transactions with a VDF sample and sign the timestamp. - -2. Split the transactions into batches, send each to separate nodes and have each node share its batch with its peers. - -3. Repeat the previous step recursively until all nodes have all batches. - -Solana rotates leaders at fixed intervals, called _slots_. Each leader may only produce entries during its allotted slot. The leader therefore timestamps transactions so that validators may lookup the public key of the designated leader. The leader then signs the timestamp so that a validator may verify the signature, proving the signer is owner of the designated leader's public key. - -Next, transactions are broken into batches so that a node can send transactions to multiple parties without making multiple copies. If, for example, the leader needed to send 60 transactions to 6 nodes, it would break that collection of 60 into batches of 10 transactions and send one to each node. This allows the leader to put 60 transactions on the wire, not 60 transactions for each node. Each node then shares its batch with its peers. Once the node has collected all 6 batches, it reconstructs the original set of 60 transactions. - -A batch of transactions can only be split so many times before it is so small that header information becomes the primary consumer of network bandwidth. At the time of this writing (December, 2021), the approach is scaling well up to about 1,250 validators. To scale up to hundreds of thousands of validators, each node can apply the same technique as the leader node to another set of nodes of equal size. We call the technique [_Turbine Block Propagation_](../consensus/turbine-block-propagation.md). diff --git a/docs/src/clusters/metrics.md b/docs/src/clusters/metrics.md deleted file mode 100644 index 3405fab4001..00000000000 --- a/docs/src/clusters/metrics.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Solana Cluster Performance Metrics -sidebar_label: Performance Metrics -pagination_label: Cluster Performance Metrics ---- - -Solana cluster performance is measured as average number of transactions per second that the network can sustain \(TPS\). And, how long it takes for a transaction to be confirmed by super majority of the cluster \(Confirmation Time\). - -Each cluster node maintains various counters that are incremented on certain events. These counters are periodically uploaded to a cloud based database. Solana's metrics dashboard fetches these counters, and computes the performance metrics and displays it on the dashboard. - -## TPS - -Each node's bank runtime maintains a count of transactions that it has processed. The dashboard first calculates the median count of transactions across all metrics enabled nodes in the cluster. The median cluster transaction count is then averaged over a 2-second period and displayed in the TPS time series graph. The dashboard also shows the Mean TPS, Max TPS and Total Transaction Count stats which are all calculated from the median transaction count. - -## Confirmation Time - -Each validator node maintains a list of active ledger forks that are visible to the node. A fork is considered to be frozen when the node has received and processed all entries corresponding to the fork. A fork is considered to be confirmed when it receives cumulative super majority vote, and when one of its children forks is frozen. - -The node assigns a timestamp to every new fork, and computes the time it took to confirm the fork. This time is reflected as validator confirmation time in performance metrics. The performance dashboard displays the average of each validator node's confirmation time as a time series graph. - -## Hardware setup - -The validator software is deployed to GCP n1-standard-16 instances with 1TB pd-ssd disk, and 2x Nvidia V100 GPUs. These are deployed in the us-west-1 region. - -solana-bench-tps is started after the network converges from a client machine with n1-standard-16 CPU-only instance with the following arguments: `--tx\_count=50000 --thread-batch-sleep 1000` - -TPS and confirmation metrics are captured from the dashboard numbers over a 5-minute average of when the bench-tps transfer stage begins. diff --git a/docs/src/clusters/testnet.md b/docs/src/clusters/testnet.md deleted file mode 100644 index 9a44a6ada1d..00000000000 --- a/docs/src/clusters/testnet.md +++ /dev/null @@ -1,202 +0,0 @@ -# Test Network Management -The `./net/` directory in the monorepo contains scripts useful for creation and manipulation of a test network. -The test network allows you to run a fully isolated set of validators and clients on a configurable hardware setup. -It's intended to be both dev and CD friendly. - - -### Cloud account prerequisites - -The test networks to be created can run in GCP, AWS or colo. Whichever cloud provider you choose, you will need the credentials set up on your machine. - -#### GCP -You will need a working `gcloud` command from google SDK, -if you do not have it follow the guide in [https://cloud.google.com/sdk?hl=en](https://cloud.google.com/sdk?hl=en) - -Before running any scripts, authenticate with -```bash -$ gcloud auth login -``` -If you are running the scripts on a headless machine, you can use curl to issue requests to confirm your auth. - -If you are doing it the first time, you might need to set up project -```bash -gcloud config set project principal-lane-200702 -``` - -#### AWS -Obtain your credentials from the AWS IAM Console and configure the AWS CLI with -```bash -$ aws configure -``` -More information on AWS CLI configuration can be found [here](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-quick-configuration) - -## Metrics configuration (Optional) -Metrics collection relies on 2 environment variables that are patched to the remote nodes by net.sh: - * `RUST_LOG` to enable metrics reporting in principle - * `SOLANA_METRICS_CONFIG` to tell agave where to log the metrics - -### Preparation -> [!NOTE] -> Anza employees should follow the guide in notion to set up the influxDB account. - - * Ensure that `${host}` is the host name of the InfluxDB you can access, for example `https://internal-metrics.solana.com:8086` - * Ensure that `${user}` is the name of an InfluxDB user account with enough -rights to create a new InfluxDB database, for example `solana`. - -### To set up the metrics -You will normally only need to do this once. Once this is done, you will be able to save the metrics configuration and load it later from the environment. - -* Go to ./net/ in agave repo -* Run `./init-metrics.sh -c testnet-dev-${user} ${user} ` - * Script will ask for a password, it is the same one you’ve created when making a user in the InfluxDB UI - * Put the username you have used in preparation, not your login user name - * If you need to set influxDb host, edit the script -* The script will configure the database (recreating one if necessary) and append a config line in the very end of `net/config/config` file like the following: - * `export SOLANA_METRICS_CONFIG="host=${host},db=testnet-dev-${user},u=${user},p=some_secret"` - * You can store that line somewhere and append it to the config file when you need to reuse the database. - * You can also store it into your shell’s environment so you can run `./init-metrics.sh -e` to quickly load it - * Alternatively, you'll need to run `./init-metrics.sh` with appropriate arguments every time you set up a new cluster -* Assuming no errors, your influxDB setup is now done. -* For simple cases, storing `SOLANA_METRICS_CONFIG` in your env is appropriate, but you may want to use different databases for different runs of net.sh - * You can call ./init-metrics.sh before you call net.sh start, this will change the metrics config for a particular run. - * You can manually write `SOLANA_METRICS_CONFIG` in the `./net/config/config` file -* By default, metrics are only logged by agave if `RUST_LOG` is set to `info` or higher. You can provide it as environment for `./net.sh start` command, or set this in your shell environment. - ```bash - RUST_LOG="solana_metrics=info" - ``` - -### To validate that your database and metrics environment variables are set up 100% correctly - -Note: this only works if you store `SOLANA_METRICS_CONFIG` in your shell environment - -```bash - cd ./scripts/ - source ./configure-metrics.sh - INFLUX_HOST=https://internal-metrics.solana.com:8086 - INFLUX_DATABASE=testnet-dev-solana - INFLUX_USERNAME=solana - INFLUX_PASSWORD=******** - ./metrics-write-datapoint.sh "testnet-deploy net-create-begin=1" - - ``` - * All commands should complete with no errors, this indicates your influxDB config is usable - * Ensure that `RUST_LOG` is set to `info` or `debug` - -## Quick Start - -NOTE: This example uses GCE. If you are using AWS EC2, replace `./gce.sh` with -`./ec2.sh` in the commands. - -```bash -# In Agave repo -cd net/ - -# Create a GCE testnet with 4 additional validator nodes (beyond the bootstrap node) and 1 client (billing starts here) -./gce.sh create -n 4 -c 1 - -# Configure the metrics database and validate credentials using environment variable `SOLANA_METRICS_CONFIG` (skip this if you are not using metrics) -./init-metrics.sh -c testnet-dev-${USER} ${USER} - -# Deploy the network from the local workspace and start processes on all nodes including bench-tps on the client node -RUST_LOG=info ./net.sh start - -# Show a help to ssh into any testnet node to access logs/etc -./ssh.sh - -# Stop running processes on all nodes -./net.sh stop - -# Dispose of the network (billing stops here) -./gce.sh delete -``` - -## Full guide -* If you expect metrics to work, make sure you have configured them before proceeding -* Go to `./net/` directory in agave repo -* `./gce.sh` command controls creation and destruction of the nodes in the test net. It does not actually run any software. - * `./gce.sh create \-n 4 \-c 2` creates cluster with 4 validators and 1 node for load generation, this is minimal viable setup for all solana features to work - * If the creation succeeds, `net/config/config` will contain the config file of the testnet just created - * If you do not have `SOLANA_METRICS_CONFIG` set in your shell env, `gce.sh` may complain about metrics not being configured, this is perfectly fine - * `./gce.sh info` lists active test cluster nodes, this allows you to get their IP addresses for SSH access and/or debugging - * `./gce.sh delete` destroys the nodes (save the electricity and $$$ - destroy your test nets the moment you no longer need them). - * On GCE, if you do not delete nodes, they will self-destruct in 8 hours anyway, you can configure self-destruct timer by supplying `--self-destruct-hours=N` argument to `gce.sh` - * On other cloud platforms the testnet will not self-destruct! -* To enable metrics in the testnet, at this point you need to either: - * `./init-metrics.sh -c testnet-dev-${user} ${user}` to create a new metrics database from scratch - * Manually set `SOLANA_METRICS_CONFIG` in `./net/config/config` (which is exactly what `init-metrics.sh` does for you) - * `./init-metrics.sh -e` to load metrics config from `SOLANA_METRICS_CONFIG` into the testnet config file or -* `./net.sh` controls the payload on the testnet nodes, i.e. bootstrapping, the validators and bench-tps. In principle, you can run everything by hand, but `./net.sh` makes it easier. - * `./net.sh start` to actually run the test network. - * This will actually upload your current sources to the bootstrap host, build them there and upload the result to all the nodes - * The script will take 5-10 of minutes to run, in the end it should print something like - ``` - --- Deployment Successful - Bootstrap validator deployment took 164 seconds - ``` - * You can also make sure it logs successful test transfers: - ```✅ 1 lamport(s) transferred: seq=0 time= 402ms signature=33uJtPJM6ekBGrWCgWHKw1TTQJVrLxYMe3sp2PUmSRVb21LyXn3nDbQmzsgQyihE7VP2zD2iR66Du8aDUnSSd6pb``` - * `./net.sh start bench-tps=2="--tx_count 2500"` will start 2 clients with bench-tps workload sending 2500 transactions per batch. - * --tx_count argument is passed to the bench-tps program, see its manual for more options - * `./net.sh sanity` to test the deployment, it is also run by start command - * `./net.sh stop` to stop the validators and client. This does not kill the machines, so you can study the logs etc. - * `./net.sh start --nobuild` will skip the source compilation, you will generally want that if you are only changing configuration files rather than code, or just want to re-run the last test. -* To connect to the nodes: - * `./gce.sh info ` to get the public IPs - * `./ssh.sh ` to get a shell on the node - * `sudo su` will give you root access on the nodes - * Nodes run latest ubuntu LTS image -* You can also interact with the nodes using solana cli: -```bash -# source ip list use as ${validatorIpList[4]} -source net/config/config - -# airdrop -../target/release/solana -u http://${validatorIpList[1]}:8899 airdrop 1 - -# check feature -../target/release/solana -u http://${validatorIpList[1]}:8899 feature status - -# activate a feature -../target/release/solana -u http://${validatorIpList[1]}:8899 feature activate - -# check the stakes on current validators -../target/release/solana --url http://${validatorIpList[0]}:8899 validators -``` - -## Tips - -### Automation -You will want to have a script like this pretty much immediately to avoid making mistakes in the init process: -```bash -# Create the testnet with reasonable node sizes for a small test -# This particular one will have 7 nodes: 1 bootstrap validator, 4 regular validators, and 2 clients -./gce.sh create -n4 -c2 --custom-machine-type "--machine-type n1-standard-16" --client-machine-type "--machine-type n1-standard-4" -# Patch metrics config from env into config file -./init-metrics.sh -e -# Enable metrics and start the network (this will also build software) -RUST_LOG=info ./net.sh start -c bench-tps=2="--tx_count 25000" -``` - -### Inscrutable "nothing works everything times out state" - Note that net.sh and `gce.sh info` commands do not actually check if all the nodes are still alive in gcloud, - they just assume the config file information is correct. So if your nodes got killed/timed out they will lie to you. In such case, just use `gce.sh delete` to reset. - -### Running the network over public IP addresses -By default private IP addresses are used with all instances in the same -availability zone to avoid GCE network egress charges. However to run the -network over public IP addresses: -```bash -$ ./gce.sh create -P ... -``` -or -```bash -$ ./ec2.sh create -P ... -``` - -### Deploying a tarball-based network -To deploy the latest pre-built `edge` channel tarball (ie, latest from the `master` -branch), once the testnet has been created run: - -```bash -$ ./net.sh start -t edge -``` diff --git a/docs/src/consensus/commitments.md b/docs/src/consensus/commitments.md deleted file mode 100644 index 404f41dc7f5..00000000000 --- a/docs/src/consensus/commitments.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Solana Commitment Status -sidebar_label: Commitment Status -pagination_label: Consensus Commitment Status -description: - "Processed, confirmed, and finalized. Learn the differences between the - different commitment statuses on the Solana blockchain." -keywords: - - processed - - confirmed - - finalized - - stake level - - block - - blockhash ---- - -The [commitment](https://solana.com/docs/terminology#commitment) metric gives -clients a standard measure of the network confirmation for the block. Clients -can then use this information to derive their own measures of commitment. - -There are three specific commitment statuses: - -- Processed -- Confirmed -- Finalized - -| Property | Processed | Confirmed | Finalized | -| ------------------------------------- | --------- | --------- | --------- | -| Received block | X | X | X | -| Block on majority fork | X | X | X | -| Block contains target tx | X | X | X | -| 66%+ stake voted on block | - | X | X | -| 31+ confirmed blocks built atop block | - | - | X | diff --git a/docs/src/consensus/fork-generation.md b/docs/src/consensus/fork-generation.md deleted file mode 100644 index c62cacdfee2..00000000000 --- a/docs/src/consensus/fork-generation.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: Fork Generation -description: - "A fork is created when validators do not agree on a newly produced block. - Using a consensus algorithm validators vote on which will be finalized." ---- - -The Solana protocol doesn’t wait for all validators to agree on a newly produced -block before the next block is produced. Because of that, it’s quite common for -two different blocks to be chained to the same parent block. In those -situations, we call each conflicting chain a ["fork."](./fork-generation.md) - -Solana validators need to vote on one of these forks and reach agreement on -which one to use through a consensus algorithm (that is beyond the scope of this -article). The main point you need to remember is that when there are competing -forks, only one fork will be finalized by the cluster and the abandoned blocks -in competing forks are all discarded. - -This section describes how forks naturally occur as a consequence of -[leader rotation](./leader-rotation.md). - -## Overview - -Nodes take turns being [leader](https://solana.com/docs/terminology#leader) and -generating the PoH that encodes state changes. The cluster can tolerate loss of -connection to any leader by synthesizing what the leader _**would**_ have -generated had it been connected but not ingesting any state changes. - -The possible number of forks is thereby limited to a "there/not-there" skip list -of forks that may arise on leader rotation slot boundaries. At any given slot, -only a single leader's transactions will be accepted. - -### Forking example - -The table below illustrates what competing forks could look like. Time -progresses from left to right and each slot is assigned to a validator that -temporarily becomes the cluster "leader" and may produce a block for that slot. - -In this example, the leader for slot 3 chose to chain its "Block 3" directly to -"Block 1" and in doing so skipped "Block 2". Similarly, the leader for slot 5 -chose to chain "Block 5" directly to "Block 3" and skipped "Block 4". - -> Note that across different forks, the block produced for a given slot is -> _always_ the same because producing two different blocks for the same slot is -> a slashable offense. So the conflicting forks above can be distinguished from -> each other by which slots they have _skipped_. - -| | Slot 1 | Slot 2 | Slot 3 | Slot 4 | Slot 5 | -| ------ | ------- | ------- | ------- | ------- | ------- | -| Fork 1 | Block 1 | | Block 3 | | Block 5 | -| Fork 2 | Block 1 | | Block 3 | Block 4 | | -| Fork 3 | Block 1 | Block 2 | | | | - -## Message Flow - -1. Transactions are ingested by the current leader. -2. Leader filters valid transactions. -3. Leader executes valid transactions updating its state. -4. Leader packages transactions into entries based off its current PoH slot. -5. Leader transmits the entries to validator nodes \(in signed shreds\) - 1. The PoH stream includes ticks; empty entries that indicate liveness of the - leader and the passage of time on the cluster. - 2. A leader's stream begins with the tick entries necessary to complete PoH - back to the leader's most recently observed prior leader slot. -6. Validators retransmit entries to peers in their set and to further downstream - nodes. -7. Validators validate the transactions and execute them on their state. -8. Validators compute the hash of the state. -9. At specific times, i.e. specific PoH tick counts, validators transmit votes - to the leader. - 1. Votes are signatures of the hash of the computed state at that PoH tick - count. - 2. Votes are also propagated via gossip. -10. Leader executes the votes, the same as any other transaction, and broadcasts - them to the cluster. -11. Validators observe their votes and all the votes from the cluster. - -## Partitions, Forks - -Forks can arise at PoH tick counts that correspond to a vote. The next leader -may not have observed the last vote slot and may start their slot with generated -virtual PoH entries. These empty ticks are generated by all nodes in the cluster -at a cluster-configured rate for hashes/per/tick `Z`. - -There are only two possible versions of the PoH during a voting slot: PoH with -`T` ticks and entries generated by the current leader, or PoH with just ticks. -The "just ticks" version of the PoH can be thought of as a virtual ledger, one -that all nodes in the cluster can derive from the last tick in the previous -slot. - -Validators can ignore forks at other points \(e.g. from the wrong leader\), or -slash the leader responsible for the fork. - -Validators vote based on a greedy choice to maximize their reward described in -[Tower BFT](../implemented-proposals/tower-bft.md). - -### Validator's View - -#### Time Progression - -The diagram below represents a validator's view of the PoH stream with possible -forks over time. L1, L2, etc. are leader slots, and `E`s represent entries from -that leader during that leader's slot. The `x`s represent ticks only, and time -flows downwards in the diagram. - -![Fork generation](/img/fork-generation.svg) - -Note that an `E` appearing on 2 forks at the same slot is a slashable condition, -so a validator observing `E3` and `E3'` can slash L3 and safely choose `x` for -that slot. Once a validator commits to a fork, other forks can be discarded -below that tick count. For any slot, validators need only consider a single "has -entries" chain or a "ticks only" chain to be proposed by a leader. But multiple -virtual entries may overlap as they link back to the previous slot. - -#### Time Division - -It's useful to consider leader rotation over PoH tick count as time division of -the job of encoding state for the cluster. The following table presents the -above tree of forks as a time-divided ledger. - -| leader slot | L1 | L2 | L3 | L4 | L5 | -| :--------------- | :-- | :-- | :-- | :-- | :-- | -| data | E1 | E2 | E3 | E4 | E5 | -| ticks since prev | | | | x | xx | - -Note that only data from leader L3 will be accepted during leader slot L3. Data -from L3 may include "catchup" ticks back to a slot other than L2 if L3 did not -observe L2's data. L4 and L5's transmissions include the "ticks to prev" PoH -entries. - -This arrangement of the network data streams permits nodes to save exactly this -to the ledger for replay, restart, and checkpoints. - -### Leader's View - -When a new leader begins a slot, it must first transmit any PoH \(ticks\) -required to link the new slot with the most recently observed and voted slot. -The fork the leader proposes would link the current slot to a previous fork that -the leader has voted on with virtual ticks. diff --git a/docs/src/consensus/leader-rotation.md b/docs/src/consensus/leader-rotation.md deleted file mode 100644 index 8e6088e397f..00000000000 --- a/docs/src/consensus/leader-rotation.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Solana Leader Rotation -sidebar_label: Leader Rotation -pagination_label: Leader Rotation ---- - -At any given moment, a cluster expects only one validator to produce ledger entries. By having only one leader at a time, all validators are able to replay identical copies of the ledger. The drawback of only one leader at a time, however, is that a malicious leader is capable of censoring votes and transactions. Since censoring cannot be distinguished from the network dropping packets, the cluster cannot simply elect a single node to hold the leader role indefinitely. Instead, the cluster minimizes the influence of a malicious leader by rotating which node takes the lead. - -Each validator selects the expected leader using the same algorithm, described below. When the validator receives a new signed ledger entry, it can be certain that an entry was produced by the expected leader. The order of slots which each leader is assigned a slot is called a _leader schedule_. - -## Leader Schedule Rotation - -A validator rejects blocks that are not signed by the _slot leader_. The list of identities of all slot leaders is called a _leader schedule_. The leader schedule is recomputed locally and periodically. It assigns slot leaders for a duration of time called an _epoch_. The schedule must be computed far in advance of the slots it assigns, such that the ledger state it uses to compute the schedule is finalized. That duration is called the _leader schedule offset_. Solana sets the offset to the duration of slots until the next epoch. That is, the leader schedule for an epoch is calculated from the ledger state at the start of the previous epoch. The offset of one epoch is fairly arbitrary and assumed to be sufficiently long such that all validators will have finalized their ledger state before the next schedule is generated. A cluster may choose to shorten the offset to reduce the time between stake changes and leader schedule updates. - -While operating without partitions lasting longer than an epoch, the schedule only needs to be generated when the root fork crosses the epoch boundary. Since the schedule is for the next epoch, any new stakes committed to the root fork will not be active until the next epoch. The block used for generating the leader schedule is the first block to cross the epoch boundary. - -Without a partition lasting longer than an epoch, the cluster will work as follows: - -1. A validator continuously updates its own root fork as it votes. -2. The validator updates its leader schedule each time the slot height crosses an epoch boundary. - -For example: - -Let's assume an epoch duration of 100 slots, which in reality is magnitudes higher. The root fork is updated from fork computed at slot height 99 to a fork computed at slot height 102. Forks with slots at height 100, 101 were skipped because of failures. The new leader schedule is computed using fork at slot height 102. It is active from slot 200 until it is updated again. - -No inconsistency can exist because every validator that is voting with the cluster has skipped 100 and 101 when its root passes 102. All validators, regardless of voting pattern, would be committing to a root that is either 102, or a descendant of 102. - -### Leader Schedule Rotation with Epoch Sized Partitions. - -The duration of the leader schedule offset has a direct relationship to the likelihood of a cluster having an inconsistent view of the correct leader schedule. - -Consider the following scenario: - -Two partitions that are generating half of the blocks each. Neither is coming to a definitive supermajority fork. Both will cross epoch 100 and 200 without actually committing to a root and therefore a cluster-wide commitment to a new leader schedule. - -In this unstable scenario, multiple valid leader schedules exist. - -- A leader schedule is generated for every fork whose direct parent is in the previous epoch. -- The leader schedule is valid after the start of the next epoch for descendant forks until it is updated. - -Each partition's schedule will diverge after the partition lasts more than an epoch. For this reason, the epoch duration should be selected to be much larger then slot time and the expected length for a fork to be committed to root. - -After observing the cluster for a sufficient amount of time, the leader schedule offset can be selected based on the median partition duration and its standard deviation. For example, an offset longer then the median partition duration plus six standard deviations would reduce the likelihood of an inconsistent ledger schedule in the cluster to 1 in 1 million. - -## Leader Schedule Generation at Genesis - -The genesis config declares the first leader for the first epoch. This leader ends up scheduled for the first two epochs because the leader schedule is also generated at slot 0 for the next epoch. The length of the first two epochs can be specified in the genesis config as well. The minimum length of the first epochs must be greater than or equal to the maximum rollback depth as defined in [Tower BFT](../implemented-proposals/tower-bft.md). - -## Leader Schedule Generation Algorithm - -Leader schedule is generated using a predefined seed. The process is as follows: - -1. Periodically use the PoH tick height \(a monotonically increasing counter\) to seed a stable pseudo-random algorithm. -2. At that height, sample the bank for all the staked accounts with leader identities that have voted within a cluster-configured number of ticks. The sample is called the _active set_. -3. Sort the active set by stake weight. -4. Use the random seed to select nodes weighted by stake to create a stake-weighted ordering. -5. This ordering becomes valid after a cluster-configured number of ticks. - -## Schedule Attack Vectors - -### Seed - -The seed that is selected is predictable but unbiasable. There is no grinding attack to influence its outcome. - -### Active Set - -A leader can bias the active set by censoring validator votes. Two possible ways exist for leaders to censor the active set: - -- Ignore votes from validators -- Refuse to vote for blocks with votes from validators - -To reduce the likelihood of censorship, the active set is calculated at the leader schedule offset boundary over an _active set sampling duration_. The active set sampling duration is long enough such that votes will have been collected by multiple leaders. - -### Staking - -Leaders can censor new staking transactions or refuse to validate blocks with new stakes. This attack is similar to censorship of validator votes. - -### Validator operational key loss - -Leaders and validators are expected to use ephemeral keys for operation, and stake owners authorize the validators to do work with their stake via delegation. - -The cluster should be able to recover from the loss of all the ephemeral keys used by leaders and validators, which could occur through a common software vulnerability shared by all the nodes. Stake owners should be able to vote directly by co-signing a validator vote even though the stake is currently delegated to a validator. - -## Appending Entries - -The lifetime of a leader schedule is called an _epoch_. The epoch is split into _slots_, where each slot has a duration of `T` PoH ticks. - -A leader transmits entries during its slot. After `T` ticks, all the validators switch to the next scheduled leader. Validators must ignore entries sent outside a leader's assigned slot. - -All `T` ticks must be observed by the next leader for it to build its own entries on. If entries are not observed \(leader is down\) or entries are invalid \(leader is buggy or malicious\), the next leader must produce ticks to fill the previous leader's slot. Note that the next leader should do repair requests in parallel, and postpone sending ticks until it is confident other validators also failed to observe the previous leader's entries. If a leader incorrectly builds on its own ticks, the leader following it must replace all its ticks. diff --git a/docs/src/consensus/managing-forks.md b/docs/src/consensus/managing-forks.md deleted file mode 100644 index 291d67d83f9..00000000000 --- a/docs/src/consensus/managing-forks.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Managing Forks ---- - -The ledger is permitted to fork at slot boundaries. The resulting data structure forms a tree called a _blockstore_. When the validator interprets the blockstore, it must maintain state for each fork in the chain. It is the responsibility of a validator to weigh those forks, such that it may eventually select a fork. Details for selection and voting on these forks can be found in [Tower Bft](../implemented-proposals/tower-bft.md) - -## Forks - -A fork is as a sequence of slots originating from some root. For example: - -``` - 2 - 4 - 6 - 8 - / -0 - 1 12 - 13 - \ / - 3 - 5 - \ - 7 - 9 - 10 - 11 -``` - -The following sequences are forks: - -``` -- {0, 1, 2, 4, 6, 8} -- {0, 1, 3, 5, 12, 13} -- {0, 1, 3, 5, 7, 9, 10, 11} -``` - -## Pruning and Squashing - -As the chain grows, storing the local forks view becomes detrimental to performance. Fortunately we can take advantage of the properties of tower bft roots to prune this data structure. Recall a root is a slot that has reached the max lockout depth. The assumption is that this slot has accrued enough lockout that it would be impossible to roll this slot back. - -Thus, the validator prunes forks that do not originate from its local root, and then takes the opportunity to minimize its memory usage by squashing any nodes it can into the root. Although not necessary for consensus, to enable some RPC use cases the validator chooses to keep ancestors of its local root up until the last slot rooted by the super majority of the cluster. We call this the super majority root (SMR). - -Starting from the above example imagine a max lockout depth of 3. Our validator votes on slots `0, 1, 3, 5, 7, 9`. Upon the final vote at `9`, our local root is `3`. Assume the latest super majority root is `0`. After pruning this is our local fork view. - -``` -SMR - 0 - 1 12 - 13 - \ / - 3 - 5 - ROOT \ - 7 - 9 - 10 - 11 -``` - -Now imagine we vote on `10`, which roots `5`. At the same time the cluster catches up and the latest super majority root is now `3`. After pruning this is our local fork view. - -``` - 12 - 13 - / - 3 - 5 ROOT - SMR \ - 7 - 9 - 10 - 11 -``` - -Finally a vote on `11` will root `7`, pruning the final fork -``` - 3 - 5 - 7 - 9 - 10 - 11 - SMR ROOT -``` diff --git a/docs/src/consensus/stake-delegation-and-rewards.md b/docs/src/consensus/stake-delegation-and-rewards.md deleted file mode 100644 index 8d2f6b31c61..00000000000 --- a/docs/src/consensus/stake-delegation-and-rewards.md +++ /dev/null @@ -1,339 +0,0 @@ ---- -title: Stake Delegation and Rewards ---- - -Stakers are rewarded for helping to validate the ledger. They do this by -delegating their stake to validator nodes. Those validators do the legwork of -replaying the ledger and sending votes to a per-node vote account to which -stakers can delegate their stakes. The rest of the cluster uses those -stake-weighted votes to select a block when forks arise. Both the validator and -staker need some economic incentive to play their part. The validator needs to -be compensated for its hardware and the staker needs to be compensated for the -risk of getting its stake slashed. The economics are covered in -[staking rewards](../implemented-proposals/staking-rewards.md). This section, on -the other hand, describes the underlying mechanics of its implementation. - -## Basic Design - -The general idea is that the validator owns a Vote account. The Vote account -tracks validator votes, counts validator generated credits, and provides any -additional validator specific state. The Vote account is not aware of any stakes -delegated to it and has no staking weight. - -A separate Stake account \(created by a staker\) names a Vote account to which -the stake is delegated. Rewards generated are proportional to the amount of -lamports staked. The Stake account is owned by the staker only. Some portion of -the lamports stored in this account are the stake. - -## Passive Delegation - -Any number of Stake accounts can delegate to a single Vote account without an -interactive action from the identity controlling the Vote account or submitting -votes to the account. - -The total stake allocated to a Vote account can be calculated by the sum of all -the Stake accounts that have the Vote account pubkey as the -`StakeStateV2::Stake::voter_pubkey`. - -## Vote and Stake accounts - -The rewards process is split into two on-chain programs. The Vote program solves -the problem of making stakes slashable. The Stake program acts as custodian of -the rewards pool and provides for passive delegation. The Stake program is -responsible for paying rewards to staker and voter when shown that a staker's -delegate has participated in validating the ledger. - -### VoteState - -VoteState is the current state of all the votes the validator has submitted to -the network. VoteState contains the following state information: - -- `votes` - The submitted votes data structure. -- `credits` - The total number of rewards this Vote program has generated over - its lifetime. -- `root_slot` - The last slot to reach the full lockout commitment necessary for - rewards. -- `commission` - The commission taken by this VoteState for any rewards claimed - by staker's Stake accounts. This is the percentage ceiling of the reward. -- Account::lamports - The accumulated lamports from the commission. These do not - count as stakes. -- `authorized_voter` - Only this identity is authorized to submit votes. This - field can only modified by this identity. -- `node_pubkey` - The Solana node that votes in this account. -- `authorized_withdrawer` - the identity of the entity in charge of the lamports - of this account, separate from the account's address and the authorized vote - signer. - -### VoteInstruction::Initialize\(VoteInit\) - -- `account[0]` - RW - The VoteState. - - `VoteInit` carries the new vote account's `node_pubkey`, `authorized_voter`, - `authorized_withdrawer`, and `commission`. - - other VoteState members defaulted. - -### VoteInstruction::Authorize\(Pubkey, VoteAuthorize\) - -Updates the account with a new authorized voter or withdrawer, according to the -VoteAuthorize parameter \(`Voter` or `Withdrawer`\). The transaction must be -signed by the Vote account's current `authorized_voter` or -`authorized_withdrawer`. - -- `account[0]` - RW - The VoteState. `VoteState::authorized_voter` or - `authorized_withdrawer` is set to `Pubkey`. - -### VoteInstruction::AuthorizeWithSeed\(VoteAuthorizeWithSeedArgs\) - -Updates the account with a new authorized voter or withdrawer, according to the -VoteAuthorize parameter \(`Voter` or `Withdrawer`\). Unlike -`VoteInstruction::Authorize` this instruction is for use when the Vote account's -current `authorized_voter` or `authorized_withdrawer` is a derived key. The -transaction must be signed by someone who can sign for the base key of that -derived key. - -- `account[0]` - RW - The VoteState. `VoteState::authorized_voter` or - `authorized_withdrawer` is set to `Pubkey`. - -### VoteInstruction::Vote\(Vote\) - -- `account[0]` - RW - The VoteState. `VoteState::lockouts` and - `VoteState::credits` are updated according to voting lockout rules see - [Tower BFT](../implemented-proposals/tower-bft.md). -- `account[1]` - RO - `sysvar::slot_hashes` A list of some N most recent slots - and their hashes for the vote to be verified against. -- `account[2]` - RO - `sysvar::clock` The current network time, expressed in - slots, epochs. - -### StakeStateV2 - -A StakeStateV2 takes one of four forms, StakeStateV2::Uninitialized, -StakeStateV2::Initialized, StakeStateV2::Stake, and StakeStateV2::RewardsPool. -Only the first three forms are used in staking, but only StakeStateV2::Stake is -interesting. All RewardsPools are created at genesis. - -### StakeStateV2::Stake - -StakeStateV2::Stake is the current delegation preference of the **staker** and -contains the following state information: - -- Account::lamports - The lamports available for staking. -- `stake` - the staked amount \(subject to warmup and cooldown\) for generating - rewards, always less than or equal to Account::lamports. -- `voter_pubkey` - The pubkey of the VoteState instance the lamports are - delegated to. -- `credits_observed` - The total credits claimed over the lifetime of the - program. -- `activated` - the epoch at which this stake was activated/delegated. The full - stake will be counted after warmup. -- `deactivated` - the epoch at which this stake was de-activated, some cooldown - epochs are required before the account is fully deactivated, and the stake - available for withdrawal. -- `authorized_staker` - the pubkey of the entity that must sign delegation, - activation, and deactivation transactions. -- `authorized_withdrawer` - the identity of the entity in charge of the lamports - of this account, separate from the account's address, and the authorized - staker. - -### StakeStateV2::RewardsPool - -To avoid a single network-wide lock or contention in redemption, 256 -RewardsPools are part of genesis under pre-determined keys, each with -std::u64::MAX credits to be able to satisfy redemptions according to point -value. - -The Stakes and the RewardsPool are accounts that are owned by the same `Stake` -program. - -### StakeInstruction::DelegateStake - -The Stake account is moved from Initialized to StakeStateV2::Stake form, or from -a deactivated (i.e. fully cooled-down) StakeStateV2::Stake to activated -StakeStateV2::Stake. This is how stakers choose the vote account and validator -node to which their stake account lamports are delegated. The transaction must -be signed by the stake's `authorized_staker`. - -- `account[0]` - RW - The StakeStateV2::Stake instance. - `StakeStateV2::Stake::credits_observed` is initialized to - `VoteState::credits`, `StakeStateV2::Stake::voter_pubkey` is initialized to - `account[1]`. If this is the initial delegation of stake, - `StakeStateV2::Stake::stake` is initialized to the account's balance in - lamports, `StakeStateV2::Stake::activated` is initialized to the current Bank - epoch, and `StakeStateV2::Stake::deactivated` is initialized to std::u64::MAX -- `account[1]` - R - The VoteState instance. -- `account[2]` - R - sysvar::clock account, carries information about current - Bank epoch. -- `account[3]` - R - sysvar::stakehistory account, carries information about - stake history. -- `account[4]` - R - stake::Config account, carries warmup, cooldown, and - slashing configuration. - -### StakeInstruction::Authorize\(Pubkey, StakeAuthorize\) - -Updates the account with a new authorized staker or withdrawer, according to the -StakeAuthorize parameter \(`Staker` or `Withdrawer`\). The transaction must be -by signed by the Stakee account's current `authorized_staker` or -`authorized_withdrawer`. Any stake lock-up must have expired, or the lock-up -custodian must also sign the transaction. - -- `account[0]` - RW - The StakeStateV2. - - `StakeStateV2::authorized_staker` or `authorized_withdrawer` is set to - `Pubkey`. - -### StakeInstruction::Deactivate - -A staker may wish to withdraw from the network. To do so he must first -deactivate his stake, and wait for cooldown. The transaction must be signed by -the stake's `authorized_staker`. - -- `account[0]` - RW - The StakeStateV2::Stake instance that is deactivating. -- `account[1]` - R - sysvar::clock account from the Bank that carries current - epoch. - -StakeStateV2::Stake::deactivated is set to the current epoch + cooldown. The -account's stake will ramp down to zero by that epoch, and Account::lamports will -be available for withdrawal. - -### StakeInstruction::Withdraw\(u64\) - -Lamports build up over time in a Stake account and any excess over activated -stake can be withdrawn. The transaction must be signed by the stake's -`authorized_withdrawer`. - -- `account[0]` - RW - The StakeStateV2::Stake from which to withdraw. -- `account[1]` - RW - Account that should be credited with the withdrawn - lamports. -- `account[2]` - R - sysvar::clock account from the Bank that carries current - epoch, to calculate stake. -- `account[3]` - R - sysvar::stake_history account from the Bank that carries - stake warmup/cooldown history. - -## Benefits of the design - -- Single vote for all the stakers. -- Clearing of the credit variable is not necessary for claiming rewards. -- Each delegated stake can claim its rewards independently. -- Commission for the work is deposited when a reward is claimed by the delegated - stake. - -## Example Callflow - -![Passive Staking Callflow](/img/passive-staking-callflow.png) - -## Staking Rewards - -The specific mechanics and rules of the validator rewards regime is outlined -here. Rewards are earned by delegating stake to a validator that is voting -correctly. Voting incorrectly exposes that validator's stakes to -[slashing](../proposals/slashing.md). - -### Basics - -The network pays rewards from a portion of network -[inflation](https://solana.com/docs/terminology#inflation). The number of -lamports available to pay rewards for an epoch is fixed and must be evenly -divided among all staked nodes according to their relative stake weight and -participation. The weighting unit is called a -[point](https://solana.com/docs/terminology#point). - -Rewards for an epoch are not available until the end of that epoch. - -At the end of each epoch, the total number of points earned during the epoch is -summed and used to divide the rewards portion of epoch inflation to arrive at a -point value. This value is recorded in the bank in a -[sysvar](https://solana.com/docs/terminology#sysvar) that maps epochs to point -values. - -During redemption, the stake program counts the points earned by the stake for -each epoch, multiplies that by the epoch's point value, and transfers lamports -in that amount from a rewards account into the stake and vote accounts according -to the vote account's commission setting. - -### Economics - -Point value for an epoch depends on aggregate network participation. If -participation in an epoch drops off, point values are higher for those that do -participate. - -### Earning credits - -Validators earn one vote credit for every correct vote that exceeds maximum -lockout, i.e. every time the validator's vote account retires a slot from its -lockout list, making that vote a root for the node. - -Stakers who have delegated to that validator earn points in proportion to their -stake. Points earned is the product of vote credits and stake. - -### Stake warmup, cooldown, withdrawal - -Stakes, once delegated, do not become effective immediately. They must first -pass through a warmup period. During this period some portion of the stake is -considered "effective", the rest is considered "activating". Changes occur on -epoch boundaries. - -The stake program limits the rate of change to total network stake, reflected in -the stake program's `config::warmup_rate` \(set to 25% per epoch in the current -implementation\). - -The amount of stake that can be warmed up each epoch is a function of the -previous epoch's total effective stake, total activating stake, and the stake -program's configured warmup rate. - -Cooldown works the same way. Once a stake is deactivated, some part of it is -considered "effective", and also "deactivating". As the stake cools down, it -continues to earn rewards and be exposed to slashing, but it also becomes -available for withdrawal. - -Bootstrap stakes are not subject to warmup. - -Rewards are paid against the "effective" portion of the stake for that epoch. - -#### Warmup example - -Consider the situation of a single stake of 1,000 activated at epoch N, with -network warmup rate of 20%, and a quiescent total network stake at epoch N of -2,000. - -At epoch N+1, the amount available to be activated for the network is 400 \(20% -of 2000\), and at epoch N, this example stake is the only stake activating, and -so is entitled to all of the warmup room available. - -| epoch | effective | activating | total effective | total activating | -| :---- | --------: | ---------: | --------------: | ---------------: | -| N-1 | | | 2,000 | 0 | -| N | 0 | 1,000 | 2,000 | 1,000 | -| N+1 | 400 | 600 | 2,400 | 600 | -| N+2 | 880 | 120 | 2,880 | 120 | -| N+3 | 1000 | 0 | 3,000 | 0 | - -Were 2 stakes \(X and Y\) to activate at epoch N, they would be awarded a -portion of the 20% in proportion to their stakes. At each epoch effective and -activating for each stake is a function of the previous epoch's state. - -| epoch | X eff | X act | Y eff | Y act | total effective | total activating | -| :---- | ----: | ----: | ----: | ----: | --------------: | ---------------: | -| N-1 | | | | | 2,000 | 0 | -| N | 0 | 1,000 | 0 | 200 | 2,000 | 1,200 | -| N+1 | 333 | 667 | 67 | 133 | 2,400 | 800 | -| N+2 | 733 | 267 | 146 | 54 | 2,880 | 321 | -| N+3 | 1000 | 0 | 200 | 0 | 3,200 | 0 | - -### Withdrawal - -Only lamports in excess of effective+activating stake may be withdrawn at any -time. This means that during warmup, effectively no stake can be withdrawn. -During cooldown, any tokens in excess of effective stake may be withdrawn -\(activating == 0\). Because earned rewards are automatically added to stake, -withdrawal is generally only possible after deactivation. - -### Lock-up - -Stake accounts support the notion of lock-up, wherein the stake account balance -is unavailable for withdrawal until a specified time. Lock-up is specified as an -epoch height, i.e. the minimum epoch height that must be reached by the network -before the stake account balance is available for withdrawal, unless the -transaction is also signed by a specified custodian. This information is -gathered when the stake account is created, and stored in the Lockup field of -the stake account's state. Changing the authorized staker or withdrawer is also -subject to lock-up, as such an operation is effectively a transfer. diff --git a/docs/src/consensus/synchronization.md b/docs/src/consensus/synchronization.md deleted file mode 100644 index 3eb763e4daf..00000000000 --- a/docs/src/consensus/synchronization.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Synchronization ---- - -Fast, reliable synchronization is the biggest reason Solana is able to achieve such high throughput. Traditional blockchains synchronize on large chunks of transactions called blocks. By synchronizing on blocks, a transaction cannot be processed until a duration, called "block time", has passed. In Proof of Work consensus, these block times need to be very large \(~10 minutes\) to minimize the odds of multiple validators producing a new valid block at the same time. There's no such constraint in Proof of Stake consensus, but without reliable timestamps, a validator cannot determine the order of incoming blocks. The popular workaround is to tag each block with a [wallclock timestamp](https://en.bitcoin.it/wiki/Block_timestamp). Because of clock drift and variance in network latencies, the timestamp is only accurate within an hour or two. To workaround the workaround, these systems lengthen block times to provide reasonable certainty that the median timestamp on each block is always increasing. - -Solana takes a very different approach, which it calls _Proof of History_ or _PoH_. Leader nodes "timestamp" blocks with cryptographic proofs that some duration of time has passed since the last proof. All data hashed into the proof most certainly have occurred before the proof was generated. The node then shares the new block with validator nodes, which are able to verify those proofs. The blocks can arrive at validators in any order or even could be replayed years later. With such reliable synchronization guarantees, Solana is able to break blocks into smaller batches of transactions called _entries_. Entries are streamed to validators in realtime, before any notion of block consensus. - -Solana technically never sends a _block_, but uses the term to describe the sequence of entries that validators vote on to achieve _confirmation_. In that way, Solana's confirmation times can be compared apples to apples to block-based systems. The current implementation sets block time to 800ms. - -What's happening under the hood is that entries are streamed to validators as quickly as a leader node can batch a set of valid transactions into an entry. Validators process those entries long before it is time to vote on their validity. By processing the transactions optimistically, there is effectively no delay between the time the last entry is received and the time when the node can vote. In the event consensus is **not** achieved, a node simply rolls back its state. This optimistic processing technique was introduced in 1981 and called [Optimistic Concurrency Control](https://en.wikipedia.org/wiki/Optimistic_concurrency_control). It can be applied to blockchain architecture where a cluster votes on a hash that represents the full ledger up to some _block height_. In Solana, it is implemented trivially using the last entry's PoH hash. - -## Relationship to VDFs - -The Proof of History technique was first described for use in blockchain by Solana in November of 2017. In June of the following year, a similar technique was described at Stanford and called a [verifiable delay function](https://eprint.iacr.org/2018/601.pdf) or _VDF_. - -A desirable property of a VDF is that verification time is very fast. Solana's approach to verifying its delay function is proportional to the time it took to create it. Split over a 4000 core GPU, it is sufficiently fast for Solana's needs, but if you asked the authors of the paper cited above, they might tell you \([and have](https://github.com/solana-labs/solana/issues/388)\) that Solana's approach is algorithmically slow and it shouldn't be called a VDF. We argue the term VDF should represent the category of verifiable delay functions and not just the subset with certain performance characteristics. Until that's resolved, Solana will likely continue using the term PoH for its application-specific VDF. - -Another difference between PoH and VDFs is that a VDF is used only for tracking duration. PoH's hash chain, on the other hand, includes hashes of any data the application observed. That data is a double-edged sword. On one side, the data "proves history" - that the data most certainly existed before hashes after it. On the other side, it means the application can manipulate the hash chain by changing _when_ the data is hashed. The PoH chain therefore does not serve as a good source of randomness whereas a VDF without that data could. Solana's [leader rotation algorithm](./leader-rotation.md), for example, is derived only from the VDF _height_ and not its hash at that height. - -## Relationship to Consensus Mechanisms - -Proof of History is not a consensus mechanism, but it is used to improve the performance of Solana's Proof of Stake consensus. It is also used to improve the performance of the data plane protocols. - -## More on Proof of History - -- [water clock analogy](https://medium.com/solana-labs/proof-of-history-explained-by-a-water-clock-e682183417b8) -- [Proof of History overview](https://medium.com/solana-labs/proof-of-history-a-clock-for-blockchain-cf47a61a9274) diff --git a/docs/src/consensus/turbine-block-propagation.md b/docs/src/consensus/turbine-block-propagation.md deleted file mode 100644 index f822a6a4f52..00000000000 --- a/docs/src/consensus/turbine-block-propagation.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: Turbine Block Propagation ---- - -A Solana cluster uses a multi-layer block propagation mechanism called _Turbine_ -to broadcast ledger entries to all nodes. The cluster divides itself into layers -of nodes, and each node in a given layer is responsible for propagating any data -it receives on to a small set of nodes in the next downstream layer. This way -each node only has to communicate with a small number of nodes. - -## Layer Structure - -The leader communicates with a special root node. The root can be thought of as -layer 0 and communicates with layer 1, which is made up of at most -`DATA_PLANE_FANOUT` nodes. If the number of nodes in the cluster is greater than -layer 1, then the data plane fanout mechanism adds layers below. The number of -nodes in each additional layer grows by a factor of `DATA_PLANE_FANOUT`. - -A good way to think about this is, layer 0 starts with a single node, layer 1 -starts with fanout nodes, and layer 2 will have `fanout * number of nodes in -layer 1` and so on. - -### Layer Assignment - Weighted Selection - -In order for data plane fanout to work, the entire cluster must agree on how the -cluster is divided into layers. To achieve this, all the recognized validator -nodes \(the TVU peers\) are shuffled with a stake weighting and stored in a -list. This list is then indexed in different ways to figure out layer boundaries -and retransmit peers - referred to as the \(turbine tree\). For example, the -list is shuffled and leader selects the first node to be the root node, and the -root node selects the next `DATA_PLANE_FANOUT` nodes to make up layer 1. The -shuffle is biased towards higher staked nodes, allowing heavier votes to come -back to the leader first. Layer 2 and lower-layer nodes use the same logic to -find their next layer peers. - -To reduce the possibility of attack vectors, the list is shuffled and indexed on -every shred. The turbine tree is generated from the set of validator nodes for -each shred using a seed derived from the slot leader id, slot, shred index, and -shred type. - -### Configuration Values - -`DATA_PLANE_FANOUT` - Determines the size of layer 1. Subsequent layers grow by -a factor of `DATA_PLANE_FANOUT`. Layers will fill to capacity before new ones are -added, i.e if a layer isn't full, it _must_ be the last one. - -Currently, configuration is set when the cluster is launched. In the future, -these parameters may be hosted on-chain, allowing modification on the fly as the -cluster sizes change. - -## Shred Propagation Flow - -During its slot, the leader node makes its initial broadcasts to a special root -node \(layer 0\) sitting atop the turbine tree. This root node is rotated every -shred based on the weighted shuffle previously mentioned. The root shares data -with layer 1. Nodes in this layer then retransmit shreds to a subset of nodes in -the next layer \(layer 2\). In general, every node in layer-1 retransmits to a -unique subset of nodes in the next layer, etc, until all nodes in the cluster -have received all the shreds. - -To prevent redundant transmission, each node uses the deterministically -generated turbine tree, its own index in the tree, and `DATA_PLANE_FANOUT` to -iterate through the tree and identify downstream nodes. Each node in a layer -only has to broadcast its shreds to a maximum of `DATA_PLANE_FANOUT` nodes in -the next layer instead of to every TVU peer in the cluster. - -The following diagram shows how shreds propagate through a cluster with 15 nodes -and a fanout of 3. - -![Shred propagation through 15 node cluster with fanout of 3](/img/data-plane-propagation.png) - -## Calculating the required FEC rate - -Turbine relies on retransmission of packets between validators. Due to -retransmission, any network wide packet loss is compounded, and the probability -of the packet failing to reach its destination increases on each hop. The FEC -rate needs to take into account the network wide packet loss, and the -propagation depth. - -A shred group is the set of data and coding packets that can be used to -reconstruct each other. Each shred group has a chance of failure, based on the -likelihood of the number of packets failing that exceeds the FEC rate. If a -validator fails to reconstruct the shred group, then the block cannot be -reconstructed, and the validator has to rely on repair to fixup the blocks. - -The probability of the shred group failing can be computed using the binomial -distribution. If the FEC rate is `16:4`, then the group size is 20, and at least -5 of the shreds must fail for the group to fail. Which is equal to the sum of -the probability of 5 or more trials failing out of 20. - -Probability of a block succeeding in turbine: - -- Probability of packet failure: `P = 1 - (1 - network_packet_loss_rate)^2` -- FEC rate: `K:M` -- Number of trials: `N = K + M` -- Shred group failure rate: `S = 1 - (SUM of i=0 -> M for binomial(prob_failure = P, trials = N, failures = i))` -- Shreds per block: `G` -- Block success rate: `B = (1 - S) ^ (G / N)` -- Binomial distribution for exactly `i` results with probability of P in N trials is defined as `(N choose i) * P^i * (1 - P)^(N-i)` - -For example: - -- Network packet loss rate is 15%. -- 50k tps network generates 6400 shreds per second. -- FEC rate increases the total shreds per block by the FEC ratio. - -With a FEC rate: `16:4` - -- `G = 8000` -- `P = 1 - 0.85 * 0.85 = 1 - 0.7225 = 0.2775` -- `S = 1 - (SUM of i=0 -> 4 for binomial(prob_failure = 0.2775, trials = 20, failures = i)) = 0.689414` -- `B = (1 - 0.689) ^ (8000 / 20) = 10^-203` - -With FEC rate of `16:16` - -- `G = 12800` -- `S = 1 - (SUM of i=0 -> 16 for binomial(prob_failure = 0.2775, trials = 32, failures = i)) = 0.002132` -- `B = (1 - 0.002132) ^ (12800 / 32) = 0.42583` - -With FEC rate of `32:32` - -- `G = 12800` -- `S = 1 - (SUM of i=0 -> 32 for binomial(prob_failure = 0.2775, trials = 64, failures = i)) = 0.000048` -- `B = (1 - 0.000048) ^ (12800 / 64) = 0.99045` diff --git a/docs/src/consensus/vote-signing.md b/docs/src/consensus/vote-signing.md deleted file mode 100644 index 0d4df08b4af..00000000000 --- a/docs/src/consensus/vote-signing.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Secure Vote Signing ---- - -A validator receives entries from the current leader and submits votes confirming those entries are valid. This vote submission presents a security challenge, because forged votes that violate consensus rules could be used to slash the validator's stake. - -The validator votes on its chosen fork by submitting a transaction that uses an asymmetric key to sign the result of its validation work. Other entities can verify this signature using the validator's public key. If the validator's key is used to sign incorrect data \(e.g. votes on multiple forks of the ledger\), the node's stake or its resources could be compromised. - -## Validators, Vote Signers, and Stakeholders - -When a validator receives multiple blocks for the same slot, it tracks all possible forks until it can determine a "best" one. A validator selects the best fork by submitting a vote to it. - -A stakeholder is an identity that has control of the staked capital. The stakeholder can delegate its stake to the vote signer. Once a stake is delegated, the vote signer's votes represent the voting weight of all the delegated stakes, and produce rewards for all the delegated stakes. - -## Validator voting - -A validator node, at startup, creates a new vote account and registers it with the cluster via gossip. The other nodes on the cluster include the new validator in the active set. Subsequently, the validator submits a "new vote" transaction signed with the validator's voting private key on each voting event. diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css deleted file mode 100644 index 35cc4a0d71f..00000000000 --- a/docs/src/css/custom.css +++ /dev/null @@ -1,130 +0,0 @@ -/* stylelint-disable docusaurus/copyright-header */ -/** - * Any CSS included here will be global. The classic template - * bundles Infima by default. Infima is a CSS framework designed to - * work well for content-centric websites. - */ - -/* You can override the default Infima variables here. */ - -@import url("https://fonts.googleapis.com/css2?family=Roboto"); - -:root { - --ifm-color-primary: #25c2a0; - --ifm-color-primary-dark: #409088; - --ifm-color-primary-darker: #387462; - --ifm-color-primary-darkest: #1b4e3f; - --ifm-color-primary-light: #42ba96; - --ifm-color-primary-lighter: #86b8b6; - --ifm-color-primary-lightest: #abd5c6; - --ifm-code-font-size: 95%; - --ifm-spacing-horizontal: 1em; - --ifm-font-family-base: "Roboto", system-ui, -apple-system, Segoe UI, Roboto, - Ubuntu, Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, "Segoe UI", - Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", - "Segoe UI Symbol"; - --ifm-footer-background-color: #232323; -} - -@keyframes fadeInUp { - /* 0% { - opacity: 0; - transform: translateY(1.5rem); - } */ -} - -main { - margin: 1rem 0 5rem 0; -} - -.docusaurus-highlight-code-line { - background-color: rgb(72, 77, 91); - display: block; - margin: 0 calc(-1 * var(--ifm-pre-padding)); - padding: 0 var(--ifm-pre-padding); -} - -.button { - background-color: var(--ifm-link-color); - color: var(--ifm-background-surface-color) !important; - padding: 0.5em 0.8em; - border: 0px solid red; - font-size: 1em !important; -} - -.container__spacer { - margin: 0em 1em 3em 1em; -} - -.cards__container { - /* margin-bottom: 0; */ -} - -.cards__container .col { - margin-bottom: 3em; -} - -.card { - padding: 1rem; - margin-top: 0rem; - animation: fadeInUp 400ms backwards; - animation-delay: 150ms; - transition-property: all; - transition-duration: 200ms; - align-items: start; - height: 100%; - /* box-shadow: 0 8px 15px 2px rgba(86, 91, 115, 0.1); */ - border: 1px solid #ccc; -} - -[data-theme="dark"] .card { - border-color: #444; -} - -.card a { - text-decoration: none; -} - -.card:hover { - /* transform: translate(0px, -5px); */ - border-color: var(--ifm-color-primary); -} - -.footer--dark { - background-color: #232323 !important; -} - -footer .text--center { - padding: 2rem 0 0 0; -} - -.card__header h3 { - color: #1dd79b; -} - -.header-link-icon:before { - content: ""; - display: flex; - height: 24px; - width: 24px; - background-position: center center; -} -.header-link-icon { - padding: 0.4em !important; -} -[data-theme="dark"] .header-github-link:before { - background: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23fff' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") - no-repeat; -} -.header-github-link:before { - background: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") - no-repeat; -} -[data-theme="dark"] .header-discord-link:before { - background: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 640 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23fff' d='M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z'/%3E%3C/svg%3E") - no-repeat center; -} -.header-discord-link:before { - background: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 640 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z'/%3E%3C/svg%3E") - no-repeat center; -} diff --git a/docs/src/faq.md b/docs/src/faq.md deleted file mode 100644 index be6365dba0a..00000000000 --- a/docs/src/faq.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Validator Frequently Asked Questions -sidebar_label: Frequently Asked Questions ---- - -### What is Agave? How is it different from other possible Solana validators? - -Solana is an open source, decentralized, proof-of-stake blockchain. It is therefore possible for multiple distinct teams to fork and maintain their own validator software. The original Solana validator was maintained by Solana Labs. A new organization, Anza, was formed in 2024 consisting of former Solana Labs core engineering members. Anza forked the Solana validator and renamed it to Agave (this project). Agave is the version of the original Solana validator maintained by the team at Anza. As of the writing of this FAQ, Agave is the most popular validator client for the Solana network, but it is likely in the future there may be several validators running in parallel to help support the network. We recommend checking the community and doing research before making a selection. - -### What is a validator? - -A validator is a computer that runs a software program to verify transactions that are added to the Solana blockchain. A validator can be a voting validator or a non voting validator. To learn more, see [what is a validator](./what-is-a-validator.md). - -### What is an RPC node? - -An RPC node is also a computer that runs the validator software. Typically, an RPC node does not vote on the network. Instead the RPC node's job is to respond to API requests. See [what is an rpc node](./what-is-an-rpc-node.md) for more information. - -### What is a cluster? - -For a definition and an overview of the topic, see [what is a cluster?](./clusters/index.md). Solana maintains several clusters. For details on each, see [Solana clusters](./clusters/available.md). - -### What is Proof of Stake? - -Proof of Stake (PoS) is a blockchain architecture. Solana is a Proof of Stake blockchain. To read more, see [Proof of Stake](./what-is-a-validator.md#proof-of-stake). - -### What is Proof of Work? Is running a Solana validator the same as mining? - -No, a Solana validator uses Proof of Stake. It does not use Proof of Work (often called mining). See [Proof of Work: For Contrast](./what-is-a-validator.md#proof-of-stake). - -### Who can operate a validator? - -Anyone can operate a validator. All Solana clusters are permissionless. A new operator can choose to join at any time. - -### Is there a validator set or limited number of validators that can operate? - -No, all Solana clusters are permissionless. There is no limit to the number of active validators that can participate in consensus. Validators participating in consensus (voting validators) incur transaction fees for each vote. A voting validator can expect to incur up to 1.1 SOL per day in vote transaction fees. - -### What are the hardware requirements for running a validator? - -See [validator requirements](./operations/requirements.md). - -### Can I run my validator at home? - -Anyone can join the cluster including home users. You must make sure that your system can perform well and keep up with the cluster. Many home internet connections are not suitable to run a Solana validator. Most operators choose to operate their validator in a data center either by using a server provider or by supplying your own hardware at a colocation data center. - -See the [validator requirements](./operations/requirements.md) for more information. - -### What skills does a Solana validator operator need? - -See [Solana validator prerequisites](./operations/prerequisites.md). - -### What are the economics of running a validator? - -See [economics of running a validator](./operations/validator-or-rpc-node.md#economics-of-running-a-consensus-validator). diff --git a/docs/src/implemented-proposals/abi-management.md b/docs/src/implemented-proposals/abi-management.md deleted file mode 100644 index 88e27c9a1a9..00000000000 --- a/docs/src/implemented-proposals/abi-management.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -title: Solana ABI management process ---- - -This document proposes the Solana ABI management process. The ABI management -process is an engineering practice and a supporting technical framework to avoid -introducing unintended incompatible ABI changes. - -# Problem - -The Solana ABI (binary interface to the cluster) is currently only defined -implicitly by the implementation and requires a very careful eye to notice -breaking changes. This makes it extremely difficult to upgrade the software -on an existing cluster without rebooting the ledger. - -# Requirements and objectives - -- Unintended ABI changes can be detected as CI failures mechanically. -- Newer implementation must be able to process the oldest data (since genesis) - once we go mainnet. -- The objective of this proposal is to protect the ABI while sustaining rather - rapid development by opting for a mechanical process rather than a very long - human-driven auditing process. -- Once signed cryptographically, data blob must be identical, so no - in-place data format update is possible regardless of inbound and outbound of - the online system. Also, considering the sheer volume of transactions we're - aiming to handle, retrospective in-place update is undesirable at best. - -# Solution - -Instead of natural human's eye due-diligence, which should be assumed to fail -regularly, we need a systematic assurance of not breaking the cluster when -changing the source code. - -For that purpose, we introduce a mechanism of marking every ABI-related things -in source code (`struct`s, `enum`s) with the new `#[frozen_abi]` attribute. This -takes hard-coded digest value derived from types of its fields via -`ser::Serialize`. And the attribute automatically generates a unit test to try -to detect any unsanctioned changes to the marked ABI-related things. - -However, the detection cannot be complete; no matter how hard we statically -analyze the source code, it's still possible to break ABI. For example, this -includes not-`derive`d hand-written `ser::Serialize`, underlying library's -implementation changes (for example `bincode`), CPU architecture differences. -The detection of these possible ABI incompatibilities is out-of-scope for this -ABI management. - -# Definitions - -ABI item/type: various types to be used for serialization, which collectively -comprises the whole ABI for any system components. For example, those types -include `struct`s and `enum`s. - -ABI item digest: Some fixed hash derived from type information of ABI item's -fields. - -# Example - -```patch -+#[frozen_abi(digest="eXSMM7b89VY72V...")] - #[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone)] - pub struct Vote { - /// A stack of votes starting with the oldest vote - pub slots: Vec, - /// signature of the bank's state at the last slot - pub hash: Hash, - } -``` - -# Developer's workflow - -To know the digest for new ABI items, developers can add `frozen_abi` with a -random digest value and run the unit tests and replace it with the correct -digest from the assertion test error message. - -Run unit tests using the following command to generate digest values: -``` -SOLANA_ABI_DUMP_DIR=. cargo +nightly test abi -``` - -In general, once we add `frozen_abi` and its change is published in the stable -release channel, its digest should never change. If such a change is needed, we -should opt for defining a new `struct` like `FooV1`. And special release flow -like hard forks should be approached. - -# Implementation remarks - -We use some degree of macro machinery to automatically generate unit tests -and calculate a digest from ABI items. This is doable by clever use of -`serde::Serialize` (`[1]`) and `any::type_name` (`[2]`). For a precedent for similar -implementation, `ink` from the Parity Technologies `[3]` could be informational. - -# Implementation details - -The implementation's goal is to detect unintended ABI changes automatically as -much as possible. To that end, the digest of structural ABI information is -calculated with best-effort accuracy and stability. - -When the ABI digest check is run, it dynamically computes an ABI digest by -recursively digesting the ABI of fields of the ABI item, by re-using the -`serde`'s serialization functionality, proc macro and generic specialization. -And then, the check `assert!`s that its finalized digest value is identical as -what is specified in the `frozen_abi` attribute. - -To realize that, it creates an example instance of the type and a custom -`Serializer` instance for `serde` to recursively traverse its fields as if -serializing the example for real. This traversing must be done via `serde` to -really capture what kinds of data actually would be serialized by `serde`, even -considering custom non-`derive`d `Serialize` trait implementations. - -# The ABI digesting process - -This part is a bit complex. There is three inter-depending parts: `AbiExample`, -`AbiDigester` and `AbiEnumVisitor`. - -First, the generated test creates an example instance of the digested type with -a trait called `AbiExample`, which should be implemented for all of digested -types like the `Serialize` and return `Self` like the `Default` trait. Usually, -it's provided via generic trait specialization for most of common types. Also -it is possible to `derive` for `struct` and `enum` and can be hand-written if -needed. - -The custom `Serializer` is called `AbiDigester`. And when it's called by `serde` -to serialize some data, it recursively collects ABI information as much as -possible. `AbiDigester`'s internal state for the ABI digest is updated -differentially depending on the type of data. This logic is specifically -redirected via with a trait called `AbiEnumVisitor` for each `enum` type. As the -name suggests, there is no need to implement `AbiEnumVisitor` for other types. - -To summarize this interplay, `serde` handles the recursive serialization control -flow in tandem with `AbiDigester`. The initial entry point in tests and child -`AbiDigester`s use `AbiExample` recursively to create an example object -hierarchical graph. And `AbiDigester` uses `AbiEnumVisitor` to inquiry the actual -ABI information using the constructed sample. - -`Default` isn't enough for `AbiExample`. Various collection's `::default()` is -empty, yet, we want to digest them with actual items. And, ABI digesting can't -be realized only with `AbiEnumVisitor`. `AbiExample` is required because an -actual instance of type is needed to actually traverse the data via `serde`. - -On the other hand, ABI digesting can't be done only with `AbiExample`, either. -`AbiEnumVisitor` is required because all variants of an `enum` cannot be -traversed just with a single variant of it as a ABI example. - -Digestible information: - -- rust's type name -- `serde`'s data type name -- all fields in `struct` -- all variants in `enum` -- `struct`: normal(`struct {...}`) and tuple-style (`struct(...)`) -- `enum`: normal variants and `struct`- and `tuple`- styles. -- attributes: `serde(serialize_with=...)` and `serde(skip)` - -Not digestible information: - -- Any custom serialize code path not touched by the sample provided by - `AbiExample`. (technically not possible) -- generics (must be a concrete type; use `frozen_abi` on concrete type - aliases) - -# References - -1. [(De)Serialization with type info · Issue #1095 · serde-rs/serde](https://github.com/serde-rs/serde/issues/1095#issuecomment-345483479) -2. [`std::any::type_name` - Rust](https://doc.rust-lang.org/std/any/fn.type_name.html) -3. [Parity's ink to write smart contracts](https://github.com/paritytech/ink) diff --git a/docs/src/implemented-proposals/bank-timestamp-correction.md b/docs/src/implemented-proposals/bank-timestamp-correction.md deleted file mode 100644 index f4ba7a0efb8..00000000000 --- a/docs/src/implemented-proposals/bank-timestamp-correction.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Bank Timestamp Correction ---- - -Each Bank has a timestamp that is stashed in the Clock sysvar and used to assess -time-based stake account lockups. However, since genesis, this value has been -based on a theoretical slots-per-second instead of reality, so it's quite -inaccurate. This poses a problem for lockups, since the accounts will not -register as lockup-free on (or anytime near) the date the lockup is set to -expire. - -Block times are already being estimated to cache in Blockstore and long-term -storage using a [validator timestamp oracle](validator-timestamp-oracle.md); -this data provides an opportunity to align the bank timestamp more closely with -real-world time. - -The general outline of the proposed implementation is as follows: - -- Correct each Bank timestamp using the validator-provided timestamp. -- Update the validator-provided timestamp calculation to use a stake-weighted - median, rather than a stake-weighted mean. -- Bound the timestamp correction so that it cannot deviate too far from the - expected theoretical estimate - -## Timestamp Correction - -On every new Bank, the runtime calculates a realistic timestamp estimate using -validator timestamp-oracle data. The Bank timestamp is corrected to this value -if it is greater than or equal to the previous Bank's timestamp. That is, time -should not ever go backward, so that locked up accounts may be released by the -correction, but once released, accounts can never be relocked by a time -correction. - -### Calculating Stake-Weighted Median Timestamp - -In order to calculate the estimated timestamp for a particular Bank, the runtime -first needs to get the most recent vote timestamps from the active validator -set. The `Bank::vote_accounts()` method provides the vote accounts state, and -these can be filtered to all accounts whose most recent timestamp was provided -within the last epoch. - -From each vote timestamp, an estimate for the current Bank is calculated using -the epoch's target ns_per_slot for any delta between the Bank slot and the -timestamp slot. Each timestamp estimate is associated with the stake delegated -to that vote account, and all the timestamps are collected to create a -stake-weighted timestamp distribution. - -From this set, the stake-weighted median timestamp -- that is, the timestamp at -which 50% of the stake estimates a greater-or-equal timestamp and 50% of the -stake estimates a lesser-or-equal timestamp -- is selected as the potential -corrected timestamp. - -This stake-weighted median timestamp is preferred over the stake-weighted mean -because the multiplication of stake by proposed timestamp in the mean -calculation allows a node with very small stake to still have a large effect on -the resulting timestamp by proposing a timestamp that is very large or very -small. For example, using the previous `calculate_stake_weighted_timestamp()` -method, a node with 0.00003% of the stake proposing a timestamp of `i64::MAX` -can shift the timestamp forward 97k years! - -### Bounding Timestamps - -In addition to preventing time moving backward, we can prevent malicious -activity by bounding the corrected timestamp to an acceptable level of deviation -from the theoretical expected time. - -This proposal suggests that each timestamp be allowed to deviate up to 25% from -the expected time since the start of the epoch. - -In order to calculate the timestamp deviation, each Bank needs to log the -`epoch_start_timestamp` in the Clock sysvar. This value is set to the -`Clock::unix_timestamp` on the first slot of each epoch. - -Then, the runtime compares the expected elapsed time since the start of the -epoch with the proposed elapsed time based on the corrected timestamp. If the -corrected elapsed time is within +/- 25% of expected, the corrected timestamp is -accepted. Otherwise, it is bounded to the acceptable deviation. diff --git a/docs/src/implemented-proposals/commitment.md b/docs/src/implemented-proposals/commitment.md deleted file mode 100644 index 43b6f8a7fd8..00000000000 --- a/docs/src/implemented-proposals/commitment.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Commitment ---- - -The commitment metric aims to give clients a measure of the network confirmation -and stake levels on a particular block. Clients can then use this information to -derive their own [measures of commitment](../consensus/commitments.md). - -# Calculation RPC - -Clients can request commitment metrics from a validator for a signature `s` -through `get_block_commitment(s: Signature) -> BlockCommitment` over RPC. The -`BlockCommitment` struct contains an array of u64 `[u64, MAX_CONFIRMATIONS]`. This -array represents the commitment metric for the particular block `N` that -contains the signature `s` as of the last block `M` that the validator voted on. - -An entry `s` at index `i` in the `BlockCommitment` array implies that the -validator observed `s` total stake in the cluster reaching `i` confirmations on -block `N` as observed in some block `M`. There will be `MAX_CONFIRMATIONS` elements in -this array, representing all the possible number of confirmations from 1 to -`MAX_CONFIRMATIONS`. - -# Computation of commitment metric - -Building this `BlockCommitment` struct leverages the computations already being -performed for building consensus. The `collect_vote_lockouts` function in -`consensus.rs` builds a HashMap, where each entry is of the form `(b, s)` -where `s` is the amount of stake on a bank `b`. - -This computation is performed on a votable candidate bank `b` as follows. - -```text - let output: HashMap = HashMap::new(); - for vote_account in b.vote_accounts { - for v in vote_account.vote_stack { - for a in ancestors(v) { - f(*output.get_mut(a), vote_account, v); - } - } - } -``` - -Where `f` is some accumulation function that modifies the `Stake` entry -for slot `a` with some data derivable from vote `v` and `vote_account` -(stake, lockout, etc.). Note here that the `ancestors` here only includes -slots that are present in the current status cache. Signatures for banks earlier -than those present in the status cache would not be queryable anyway, so those -banks are not included in the commitment calculations here. - -Now we can naturally augment the above computation to also build a -`BlockCommitment` array for every bank `b` by: - -1. Adding a `ForkCommitmentCache` to collect the `BlockCommitment` structs -2. Replacing `f` with `f'` such that the above computation also builds this - `BlockCommitment` for every bank `b`. - -We will proceed with the details of 2) as 1) is trivial. - -Before continuing, it is noteworthy that for some validator's vote account `a`, -the number of local confirmations for that validator on slot `s` is -`v.num_confirmations`, where `v` is the smallest vote in the stack of votes -`a.votes` such that `v.slot >= s` (i.e. there is no need to look at any -votes > v as the number of confirmations will be lower). - -Now more specifically, we augment the above computation to: - -```text - let output: HashMap = HashMap::new(); - let fork_commitment_cache = ForkCommitmentCache::default(); - for vote_account in b.vote_accounts { - // vote stack is sorted from oldest vote to newest vote - for (v1, v2) in vote_account.vote_stack.windows(2) { - for a in ancestors(v1).difference(ancestors(v2)) { - f'(*output.get_mut(a), *fork_commitment_cache.get_mut(a), vote_account, v); - } - } - } -``` - -where `f'` is defined as: - -```text - fn f`( - stake: &mut Stake, - some_ancestor: &mut BlockCommitment, - vote_account: VoteAccount, - v: Vote, total_stake: u64 - ){ - f(stake, vote_account, v); - *some_ancestor.commitment[v.num_confirmations] += vote_account.stake; - } -``` diff --git a/docs/src/implemented-proposals/durable-tx-nonces.md b/docs/src/implemented-proposals/durable-tx-nonces.md deleted file mode 100644 index d2de019db5c..00000000000 --- a/docs/src/implemented-proposals/durable-tx-nonces.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: Durable Transaction Nonces ---- - -## Problem - -To prevent replay, Solana transactions contain a nonce field populated with a -"recent" blockhash value. A transaction containing a blockhash that is too old -(~2min as of this writing) is rejected by the network as invalid. Unfortunately -certain use cases, such as custodial services, require more time to produce a -signature for the transaction. A mechanism is needed to enable these potentially -offline network participants. - -## Requirements - -1. The transaction's signature needs to cover the nonce value -2. The nonce must not be reusable, even in the case of signing key disclosure - -## A Contract-based Solution - -Here we describe a contract-based solution to the problem, whereby a client can -"stash" a nonce value for future use in a transaction's `recent_blockhash` -field. This approach is akin to the Compare and Swap atomic instruction, -implemented by some CPU ISAs. - -When making use of a durable nonce, the client must first query its value from -account data. A transaction is now constructed in the normal way, but with the -following additional requirements: - -1. The durable nonce value is used in the `recent_blockhash` field -2. An `AdvanceNonceAccount` instruction is the first issued in the transaction - -### Contract Mechanics - -TODO: svgbob this into a flowchart - -```text -Start -Create Account - state = Uninitialized -NonceInstruction - if state == Uninitialized - if account.balance < rent_exempt - error InsufficientFunds - state = Initialized - elif state != Initialized - error BadState - if sysvar.recent_blockhashes.is_empty() - error EmptyRecentBlockhashes - if !sysvar.recent_blockhashes.contains(stored_nonce) - error NotReady - stored_hash = sysvar.recent_blockhashes[0] - success -WithdrawInstruction(to, lamports) - if state == Uninitialized - if !signers.contains(owner) - error MissingRequiredSignatures - elif state == Initialized - if !sysvar.recent_blockhashes.contains(stored_nonce) - error NotReady - if lamports != account.balance && lamports + rent_exempt > account.balance - error InsufficientFunds - account.balance -= lamports - to.balance += lamports - success -``` - -A client wishing to use this feature starts by creating a nonce account under -the system program. This account will be in the `Uninitialized` state with no -stored hash, and thus unusable. - -To initialize a newly created account, an `InitializeNonceAccount` instruction must be -issued. This instruction takes one parameter, the `Pubkey` of the account's -[authority](../cli/examples/durable-nonce.md#nonce-authority). Nonce accounts -must be [rent-exempt](./rent.md#two-tiered-rent-regime) to meet the data-persistence -requirements of the feature, and as such, require that sufficient lamports be -deposited before they can be initialized. Upon successful initialization, the -cluster's most recent blockhash is stored along with specified nonce authority -`Pubkey`. - -The `AdvanceNonceAccount` instruction is used to manage the account's stored nonce -value. It stores the cluster's most recent blockhash in the account's state data, -failing if that matches the value already stored there. This check prevents -replaying transactions within the same block. - -Due to nonce accounts' [rent-exempt](./rent.md#two-tiered-rent-regime) requirement, -a custom withdraw instruction is used to move funds out of the account. -The `WithdrawNonceAccount` instruction takes a single argument, lamports to withdraw, -and enforces rent-exemption by preventing the account's balance from falling -below the rent-exempt minimum. An exception to this check is if the final balance -would be zero lamports, which makes the account eligible for deletion. This -account closure detail has an additional requirement that the stored nonce value -must not match the cluster's most recent blockhash, as per `AdvanceNonceAccount`. - -The account's [nonce authority](../cli/examples/durable-nonce.md#nonce-authority) -can be changed using the `AuthorizeNonceAccount` instruction. It takes one parameter, -the `Pubkey` of the new authority. Executing this instruction grants full -control over the account and its balance to the new authority. - -> `AdvanceNonceAccount`, `WithdrawNonceAccount` and `AuthorizeNonceAccount` all require the current [nonce authority](../cli/examples/durable-nonce.md#nonce-authority) for the account to sign the transaction. - -### Runtime Support - -The contract alone is not sufficient for implementing this feature. To enforce -an extant `recent_blockhash` on the transaction and prevent fee theft via -failed transaction replay, runtime modifications are necessary. - -Any transaction failing the usual `check_hash_age` validation will be tested -for a Durable Transaction Nonce. This is signaled by including a `AdvanceNonceAccount` -instruction as the first instruction in the transaction. - -If the runtime determines that a Durable Transaction Nonce is in use, it will -take the following additional actions to validate the transaction: - -1. The `NonceAccount` specified in the `Nonce` instruction is loaded. -2. The `NonceState` is deserialized from the `NonceAccount`'s data field and - confirmed to be in the `Initialized` state. -3. The nonce value stored in the `NonceAccount` is tested to match against the - one specified in the transaction's `recent_blockhash` field. - -If all three of the above checks succeed, the transaction is allowed to continue -validation. - -Since transactions that fail with an `InstructionError` are charged a fee and -changes to their state rolled back, there is an opportunity for fee theft if an -`AdvanceNonceAccount` instruction is reverted. A malicious validator could replay the -failed transaction until the stored nonce is successfully advanced. Runtime -changes prevent this behavior. When a durable nonce transaction fails with an -`InstructionError` aside from the `AdvanceNonceAccount` instruction, the nonce account -is rolled back to its pre-execution state as usual. Then the runtime advances -its nonce value and the advanced nonce account stored as if it succeeded. diff --git a/docs/src/implemented-proposals/ed_overview/ed_economic_sustainability.md b/docs/src/implemented-proposals/ed_overview/ed_economic_sustainability.md deleted file mode 100644 index 6e407be262d..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_economic_sustainability.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Economic Sustainability ---- - -**Subject to change.** - -Long term economic sustainability is one of the guiding principles of Solana’s economic design. While it is impossible to predict how decentralized economies will develop over time, especially economies with flexible decentralized governances, we can arrange economic components such that, under certain conditions, a sustainable economy may take shape in the long term. In the case of Solana’s network, these components take the form of token issuance \(via inflation\) and token burning. - -The dominant remittances from the Solana mining pool are validator rewards. The disinflationary mechanism is a flat, protocol-specified and adjusted, % of each transaction fee. diff --git a/docs/src/implemented-proposals/ed_overview/ed_mvp.md b/docs/src/implemented-proposals/ed_overview/ed_mvp.md deleted file mode 100644 index bb7cc87bcc7..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_mvp.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Economic Design MVP ---- - -**Subject to change.** - -The preceding sections, outlined in the -[Economic Design Overview](ed_overview.md), -describe a long-term vision of a sustainable Solana economy. -Of course, we don't expect the final implementation to perfectly match what has -been described above. We intend to fully engage with network stakeholders -throughout the implementation phases \(i.e. pre-testnet, testnet, mainnet\) -to ensure the system supports, and is representative of, the various network -participants' interests. The first step toward this goal, however, is outlining -some desired MVP economic features to be available for early pre-testnet and -testnet participants. Below is a rough sketch outlining basic economic -functionality from which a more complete and functional system can be developed. - -## MVP Economic Features - -- Faucet to deliver testnet SOLs to validators for staking and application development. -- Mechanism by which validators are rewarded via network inflation. -- Ability to delegate tokens to validator nodes -- Validator set commission fees on interest from delegated tokens. diff --git a/docs/src/implemented-proposals/ed_overview/ed_overview.md b/docs/src/implemented-proposals/ed_overview/ed_overview.md deleted file mode 100644 index 000ff2adbe6..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_overview.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Cluster Economics ---- - -**Subject to change.** - -Solana’s crypto-economic system is designed to promote a healthy, long term self-sustaining economy with participant incentives aligned to the security and decentralization of the network. The main participants in this economy are validation-clients. Their contributions to the network, state validation, and their requisite incentive mechanisms are discussed below. - -The main channels of participant remittances are referred to as protocol-based (inflationary) rewards and transaction fees. Protocol-based rewards are issuances from a global, protocol-defined, inflation rate. These rewards will constitute the total reward delivered to validation clients, the remaining sourced from transaction fees. In the early days of the network, it is likely that protocol-based rewards, deployed based on predefined issuance schedule, will drive the majority of participant incentives to participate in the network. - -These protocol-based rewards, to be distributed across the actively staked tokens on the network, are to be a result of a global supply inflation rate, calculated per Solana epoch and distributed amongst the active validator set. As discussed further below, the per annum inflation rate is based on a pre-determined disinflationary schedule. This provides the network with monetary supply predictability which supports long term economic stability and security. - -Transaction fees are market-based participant-to-participant transfers, attached to network interactions as a necessary motivation and compensation for the inclusion and execution of a proposed transaction. A mechanism for long-term economic stability and forking protection through partial burning of each transaction fee is also discussed below. - -A high-level schematic of Solana’s crypto-economic design is shown below in **Figure 1**. The specifics of validation-client economics are described in sections: [Validation-client Economics](ed_validation_client_economics/ed_vce_overview.md), [Inflation Schedule](ed_validation_client_economics/ed_vce_state_validation_protocol_based_rewards.md), and [Transaction Fees](ed_validation_client_economics/ed_vce_state_validation_transaction_fees.md). Also, the section titled [Validation Stake Delegation](ed_validation_client_economics/ed_vce_validation_stake_delegation.md) closes with a discussion of validator delegation opportunities and marketplace. Additionally, in [Storage Rent Economics](ed_storage_rent_economics.md), we describe an implementation of storage rent to account for the externality costs of maintaining the active state of the ledger. An outline of features for an MVP economic design is discussed in the [Economic Design MVP](ed_mvp.md) section. - -![](/img/economic_design_infl_230719.png) - -**Figure 1**: Schematic overview of Solana economic incentive design. diff --git a/docs/src/implemented-proposals/ed_overview/ed_references.md b/docs/src/implemented-proposals/ed_overview/ed_references.md deleted file mode 100644 index 4c9047ae381..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_references.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: References ---- - -1. [https://blog.ethereum.org/2016/07/27/inflation-transaction-fees-cryptocurrency-monetary-policy/](https://blog.ethereum.org/2016/07/27/inflation-transaction-fees-cryptocurrency-monetary-policy/) -2. [https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281](https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281) -3. [https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281](https://medium.com/solana-labs/how-to-create-decentralized-storage-for-a-multi-petabyte-digital-ledger-2499a3a8c281) diff --git a/docs/src/implemented-proposals/ed_overview/ed_storage_rent_economics.md b/docs/src/implemented-proposals/ed_overview/ed_storage_rent_economics.md deleted file mode 100644 index 8abd88ad2c5..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_storage_rent_economics.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Storage Rent Economics ---- - -Each transaction that is submitted to the Solana ledger imposes costs. Transaction fees paid by the submitter, and collected by a validator, in theory, account for the acute, transactional, costs of validating and adding that data to the ledger. Unaccounted in this process is the mid-term storage of active ledger state, necessarily maintained by the rotating validator set. This type of storage imposes costs not only to validators but also to the broader network as active state grows so does data transmission and validation overhead. To account for these costs, we describe here our preliminary design and implementation of storage rent. - -Storage rent can be paid via one of two methods: - -Method 1: Set it and forget it - -With this approach, accounts with two-years worth of rent deposits secured are exempt from network rent charges. By maintaining this minimum-balance, the broader network benefits from reduced liquidity and the account holder can rest assured that their `Account::data` will be retained for continual access/usage. - -Method 2: Pay per byte - -If an account has less than two-years worth of deposited rent the network charges rent on a per-epoch basis, in credit for the next epoch. This rent is deducted at a rate specified in genesis, in lamports per kilobyte-year. - -For information on the technical implementation details of this design, see the [Rent](../rent.md) section. diff --git a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_overview.md b/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_overview.md deleted file mode 100644 index 89478eda517..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_overview.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Validation-client Economics ---- - -**Subject to change.** - -Validator-clients are eligible to charge commission on inflationary rewards distributed to staked tokens. This compensation is for providing compute resources to validate and vote on a given PoH state. These protocol-based rewards are determined through an algorithmic disinflationary schedule as a function of total token supply. The network is expected to launch with an annual inflation rate around 8%, set to decrease by 15% per year until a long-term stable rate of 1.5% is reached, however these parameters are yet to be finalized by the community. These issuances are to be split and distributed to participating validators, with around 95% of the issued tokens allocated for validator rewards initial (the remaining 5% reserved for Foundation operating expenses). Because the network will be distributing a fixed amount of inflation rewards across the stake-weighted validator set, the yield observed for staked tokens will be primarily a function of the amount of staked tokens in relation to the total token supply. - -Additionally, validator clients may earn revenue through fees via state-validation transactions. For clarity, we separately describe the design and motivation of these revenue distributions for validation-clients below: state-validation protocol-based rewards and state-validation transaction fees and rent. diff --git a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_protocol_based_rewards.md b/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_protocol_based_rewards.md deleted file mode 100644 index e5c3d10380b..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_protocol_based_rewards.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Inflation Schedule ---- - -**Subject to change.** - -Validator-clients have two functional roles in the Solana network: - -- Validate \(vote\) the current global state of their observed PoH. -- Be elected as ‘leader’ on a stake-weighted round-robin schedule during which time they are responsible for collecting outstanding transactions and incorporating them into their observed PoH, thus updating the global state of the network and providing chain continuity. - -Validator-client rewards for these services are to be distributed at the end of each Solana epoch. As previously discussed, compensation for validator-clients is provided via a commission charged on the protocol-based annual inflation rate dispersed in proportion to the stake-weight of each validator-node \(see below\) along with leader-claimed transaction fees available during each leader rotation. I.e. during the time a given validator-client is elected as leader, it has the opportunity to keep a portion of each transaction fee, less a protocol-specified amount that is destroyed \(see [Validation-client State Transaction Fees](ed_vce_state_validation_transaction_fees.md)\). - -The effective protocol-based annual staking yield \(%\) per epoch received by validation-clients is to be a function of: - -- the current global inflation rate, derived from the pre-determined disinflationary issuance schedule \(see [Validation-client Economics](ed_vce_overview.md)\) -- the fraction of staked SOLs out of the current total circulating supply, -- the commission charged by the validation service, -- the up-time/participation \[% of available slots that validator had opportunity to vote on\] of a given validator over the previous epoch. - -The first factor is a function of protocol parameters only \(i.e. independent of validator behavior in a given epoch\) and results in an inflation schedule designed to incentivize early participation, provide clear monetary stability and provide optimal security in the network. - -As a first step to understanding the impact of the _Inflation Schedule_ on the Solana economy, we’ve simulated the upper and lower ranges of what token issuance over time might look like given the current ranges of Inflation Schedule parameters under study. - -Specifically: - -- _Initial Inflation Rate_: 7-9% -- _Disinflation Rate_: -14-16% -- _Long-term Inflation Rate_: 1-2% - -Using these ranges to simulate a number of possible Inflation Schedules, we can explore inflation over time: - -![](/img/p_inflation_schedule_ranges_w_comments.png) - -In the above graph, the average values of the range are identified to illustrate the contribution of each parameter. -From these simulated _Inflation Schedules_, we can also project ranges for token issuance over time. - -![](/img/p_total_supply_ranges.png) - -Finally we can estimate the _Staked Yield_ on staked SOL, if we introduce an additional parameter, previously discussed, _% of Staked SOL_: - -$$ -\%~\text{SOL Staked} = \frac{\text{Total SOL Staked}}{\text{Total Current Supply}} -$$ - -In this case, because _% of Staked SOL_ is a parameter that must be estimated (unlike the _Inflation Schedule_ parameters), it is easier to use specific _Inflation Schedule_ parameters and explore a range of _% of Staked SOL_. For the below example, we’ve chosen the middle of the parameter ranges explored above: - -- _Initial Inflation Rate_: 8% -- _Disinflation Rate_: -15% -- _Long-term Inflation Rate_: 1.5% - -The values of _% of Staked SOL_ range from 60% - 90%, which we feel covers the likely range we expect to observe, based on feedback from the investor and validator communities as well as what is observed on comparable Proof-of-Stake protocols. - -![](/img/p_ex_staked_yields.png) - -Again, the above shows an example _Staked Yield_ that a staker might expect over time on the Solana network with the _Inflation Schedule_ as specified. This is an idealized _Staked Yield_ as it neglects validator uptime impact on rewards, validator commissions, potential yield throttling and potential slashing incidents. It additionally ignores that _% of Staked SOL_ is dynamic by design - the economic incentives set up by this _Inflation Schedule_. - -### Adjusted Staking Yield - -A complete appraisal of earning potential from staking tokens should take into account staked _Token Dilution_ and its impact on staking yield. For this, we define _adjusted staking yield_ as the change in fractional token supply ownership of staked tokens due to the distribution of inflation issuance. I.e. the positive dilutive effects of inflation. - -We can examine the _adjusted staking yield_ as a function of the inflation rate and the percent of staked tokens on the network. We can see this plotted for various staking fractions here: - -![](/img/p_ex_staked_dilution.png) diff --git a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_transaction_fees.md b/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_transaction_fees.md deleted file mode 100644 index 9022e42d7c0..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_state_validation_transaction_fees.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: State-validation Transaction Fees ---- - -**Subject to change.** - -Each transaction sent through the network, to be processed by the current leader validation-client and confirmed as a global state transaction, must contain a transaction fee. Transaction fees offer many benefits in the Solana economic design, for example they: - -- provide unit compensation to the validator network for the compute resources necessary to process the state transaction, -- reduce network spam by introducing real cost to transactions, -- open avenues for a transaction market to incentivize validation-client to collect and process submitted transactions in their function as leader, -- and provide potential long-term economic stability of the network through a protocol-captured minimum fee amount per transaction, as described below. - -Many current blockchain economies \(e.g. Bitcoin, Ethereum\), rely on protocol-based rewards to support the economy in the short term, with the assumption that the revenue generated through transaction fees will support the economy in the long term, when the protocol derived rewards expire. In an attempt to create a sustainable economy through protocol-based rewards and transaction fees, a fixed portion of each transaction fee is destroyed, with the remaining fee going to the current leader processing the transaction. A scheduled global inflation rate provides a source for rewards distributed to validation-clients, through the process described above. - -Transaction fees are set by the network cluster based on recent historical throughput, see [Congestion Driven Fees](../../transaction-fees.md#congestion-driven-fees). This minimum portion of each transaction fee can be dynamically adjusted depending on historical gas usage. In this way, the protocol can use the minimum fee to target a desired hardware utilization. By monitoring a protocol specified gas usage with respect to a desired, target usage amount, the minimum fee can be raised/lowered which should, in turn, lower/raise the actual gas usage per block until it reaches the target amount. This adjustment process can be thought of as similar to the difficulty adjustment algorithm in the Bitcoin protocol, however in this case it is adjusting the minimum transaction fee to guide the transaction processing hardware usage to a desired level. - -As mentioned, a fixed-proportion of each transaction fee is to be destroyed. The intent of this design is to retain leader incentive to include as many transactions as possible within the leader-slot time, while providing an inflation limiting mechanism that protects against "tax evasion" attacks \(i.e. side-channel fee payments\)[1](../ed_references.md). - -Additionally, the burnt fees can be a consideration in fork selection. In the case of a PoH fork with a malicious, censoring leader, we would expect the total fees destroyed to be less than a comparable honest fork, due to the fees lost from censoring. If the censoring leader is to compensate for these lost protocol fees, they would have to replace the burnt fees on their fork themselves, thus potentially reducing the incentive to censor in the first place. diff --git a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_validation_stake_delegation.md b/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_validation_stake_delegation.md deleted file mode 100644 index 64ab8779260..00000000000 --- a/docs/src/implemented-proposals/ed_overview/ed_validation_client_economics/ed_vce_validation_stake_delegation.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Validation Stake Delegation ---- - -**Subject to change.** - -Running a Solana validation-client required relatively modest upfront hardware capital investment. **Table 2** provides an example hardware configuration to support ~1M tx/s with estimated ‘off-the-shelf’ costs: - -| Component | Example | Estimated Cost | -| :---------------- | :----------------------------------------------- | :------------- | -| GPU | 2x 2080 Ti | \$2500 | -| or | 4x 1080 Ti | \$2800 | -| OS/Ledger Storage | Samsung 860 Evo 2TB | \$370 | -| Accounts storage | 2x Samsung 970 Pro M.2 512GB | \$340 | -| RAM | 32 Gb | \$300 | -| Motherboard | AMD x399 | \$400 | -| CPU | AMD Threadripper 2920x | \$650 | -| Case | | \$100 | -| Power supply | EVGA 1600W | \$300 | -| Network | > 500 mbps | | -| Network \(1\) | Google webpass business bay area 1gbps unlimited | \$5500/mo | -| Network \(2\) | Hurricane Electric bay area colo 1gbps | \$500/mo | - -**Table 2** example high-end hardware setup for running a Solana client. - -Despite the low-barrier to entry as a validation-client, from a capital investment perspective, as in any developing economy, there will be much opportunity and need for competent validation services as evidenced by node reliability, UX/UI, APIs and other software accessibility tools. Additionally, although Solana’s validator node startup costs are nominal when compared to similar networks, they may still be somewhat restrictive for some potential participants. In the spirit of developing a true decentralized, permissionless network, these interested parties can become involved in the Solana network/economy via delegation of previously acquired tokens with a reliable validation node to earn a portion of the interest generated. - -Delegation of tokens to validation-clients provides a way for passive Solana token holders to become part of the active Solana economy and earn interest rates proportional to the interest rate generated by the delegated validation-client. Additionally, this feature intends to create a healthy validation-client market, with potential validation-client nodes competing to build reliable, transparent and profitable delegation services. diff --git a/docs/src/implemented-proposals/epoch_accounts_hash.md b/docs/src/implemented-proposals/epoch_accounts_hash.md deleted file mode 100644 index ba16f3f869b..00000000000 --- a/docs/src/implemented-proposals/epoch_accounts_hash.md +++ /dev/null @@ -1,331 +0,0 @@ ---- -title: Epoch Accounts Hash ---- - -*Paraphrasing from https://github.com/solana-labs/solana/issues/26847* - -## Background - -Rent collection checks every account at least once per epoch. At each slot, a -deterministic set of accounts (based on the pubkey range) is loaded, checked -for rent collection, and stored back to the Accounts DB at this new slot. - -Accounts are stored (rewritten) _even if_ they are unchanged. This has a few -positive effects. - 1. Once an account is rewritten, the previous version at an older slot is now - dead. As entire slots and AppendVecs become full of only dead accounts, - they can then be dropped/recycled. - 2. Each account rewritten due to rent collection is included in that slot's - bank hash. Since the bank hash is part of what is voted on for consensus, - this means every account is verified by the network at least once per - epoch. - -However, there is a big downside to rewriting unchanged accounts: performance. -Storing accounts can be very expensive. And since accounts now are required to -be rent-exempt, the majority of accounts rewritten due to rent collection are -unchanged. What if unchanged accounts were no longer rewritten? This would -minimally be a big performance win. - - -## Problem - -If rent collection no longer is rewriting unchanged accounts, we lose the two -positive effects. Dealing with Positive Effect 1 (from above) will be handled -by _Ancient AppendVecs_, and will not be discussed here. So how do we still -get the security from Positive Effect 2? How can we still verify every account -at least once per epoch, *as part of consensus*, but without rewriting -accounts? - - -## Proposed Solution - -Perform a full accounts hash calculation once per epoch, and hash the result -into a bank's `hash`. This will be known as the _Epoch Accounts Hash_, or -_EAH_. - -This retains the Positive Effect 2 from rent collection by checking every -account at least once per epoch. Thus, any validators with missing, corrupt, -or extra accounts will identify those issues within 1-2 epochs. - - -### Implementation - -Performing a full accounts hash takes a relatively long time to complete. For -this reason, the EAH calculation must take place in the background. - -In order for all the validators to calculate the same accounts hash, the -calculation must be based on the same view of all accounts. This means the EAH -must be based on a predetermined slot. This will be known as the `start slot`. -The `start slot` is calculated as an offset into the epoch. This offset will -be known as the `start offset`. Formally, the `start slot` is the first root -*greater-than-or-equal-to* `first slot in epoch + start offset`. - -Similarly, all the validators must save the EAH into a bank at a predetermined -slot, and offset from the first slot of an epoch. This will be known as the -`stop slot` and `stop offset`, respectively. - -* The `start offset` will be set at one-quarter into the epoch. -* The `stop offset` will be set at three-quarters into the epoch. -* For epochs with 432,000 slots, the `start offset` will be 108,000 and the - `stop offset` will be 324,000. - -These constants may be changed in the future, or may be determined at runtime. -The main justifications for these values are: -1. Do not start the EAH calculation at the beginning of an epoch, as the - beginning of an epoch is already a time of contention and stress. There is - no reason to make this worse. -2. The bank to save the EAH into—the `stop offset`—should be sufficiently far - in the future to guarantee all validators are able to complete the accounts - hash calculation in time. -3. The `start offset` should be *after* the `rewarding interval` - (from [Partitioned Inflationary Rewards Distribution](https://github.com/solana-labs/solana/pull/27455)). - This ensures stake rewards have been distributed and stored into the - accounts for this epoch. - -Once the EAH calculation is complete, it must be saved somewhere. Since this -occurs in the background, there is not an associated `Bank` that would make -sense to save into. Instead, a new field will be added to `AccountsDb` that -will store the EAH. Later, the bank at slot `stop slot`†¹ will read the EAH from -`AccountsDb` and hash it into its own hash (aka _bank hash_). - -EAH calculation will use the existing _accounts background services_ (_ABS_) to -perform the actual calculation. Requests for EAH calculation will be sent from -`bank_forks::set_root()`†², with a new request type to distinguish an EAH request -from a Snapshot request. Since the EAH will be part of consensus, it is not -optional; EAH requests will have the highest priority in ABS, and will be -processed first/instead of other requests. - -†¹: More precisely, all banks where `bank slot >= stop slot` and `parent slot < - stop slot` will include the EAH in their _bank hash_. This ensures EAH - handles forking around `stop slot`, since only one of these banks will end - up rooted. - -†²: An EAH calculation will be requested when `root bank slot >= start slot` - and `root parent slot < start slot`. This handles the scenario where - validators call `bank_forks::set_root()` at different intervals. - - -#### Details - -#### Snapshots - -A snapshot contains all the state necessary to reconstruct the cluster as of a -certain slot. A snapshot may then need to contain the EAH so that the `stop -slot` can include it. Consider the following scenarios within an epoch where a -snapshot is requested for slot `X`: - - -##### 1. `X >= first slot in epoch` and `X < start slot` - -Since the `start slot` has not been reached yet, there is nothing special to do -in order to take a snapshot in this scenario. - - -##### 2. `X == start slot` - -The EAH *must* be included in the snapshot. Since the snapshot process always -calculates the accounts hash, no additional calculations are required. The -accounts hash calculation result will be used both to store in the snapshot as -the EAH, and for the snapshot hash (which is used at load-time for verification). - - -##### 3. `X > start slot` and `X < stop slot` - -If a snapshot is requested to be created *after* the `start slot` but *before* -the EAH calculation has completed, then it will be impossible to create a -snapshot with the correct EAH. The snapshot process will wait until the EAH -calculation has completed before proceeding. - - -##### 4. `X == stop slot` - -The EAH has been calculated for this epoch, and has been included in the `stop -slot` bank. No further handling is required; the snapshot does not need to -contain the EAH. - - -##### 5. `X > stop slot` and `X <= last slot in epoch` - -Same as (4). - - -#### Snapshot Verification - -If a snapshot archive includes an EAH, we want to verify the EAH is correct at -load time (instead of waiting until `stop slot`, which could be far in the -future). - -If the snapshot archive is for a slot within the `calculation window`†¹, then it -*must* include an EAH. The snapshot hash itself will now also incorporate the -EAH. In pseudo code: -```pseudo -if slot is in calculation window - let snapshot hash = hash(accounts hash, epoch accounts hash) -else - let snapshot hash = accounts hash -endif -``` -Since loading from a snapshot archive already verifies the snapshot archive's -hash against the deserialized bank, the EAH will be implicitly verified as -well. - -†¹: The `calculation window` is `[start slot, stop slot)`, based on the epoch - of the referenced `Bank`. - - -#### Corner Cases - -#### Minimum Slots per Epoch - -An EAH is requested by `BankForks::set_root()`, which happens while setting -*roots*. The EAH is stored into `Bank`s when they are *frozen*. Banks are -frozen *at least* 32 slots before they are rooted. For the expected behavior, -the EAH start slot really should be 32 slots before the stop slot. If the -number of slots per epoch is small, this can result in surprising behavior. - -Example: Assume there are 40 slots per epoch. The EAH start offset is 10, and -the EAH stop offset is 30. When Bank 30 is frozen it will include the EAH in -its hash. However, Bank 10 has not yet been rooted, so a new EAH has not been -calculated for this epoch. This means Bank 30 will have included the EAH *from -the previous epoch* in its hash. - -Later, when Bank 10 is rooted, it will request a new EAH be calculated. If a -snapshot is taken for Bank 12 (or any bank between 10 and 30), it will include -the EAH *from this epoch*. If a node loads the snapshot from Bank 12, once it -gets to freezing Bank 30, it will end up with a different bank hash since it -included the EAH from this epoch (versus the other node's Bank 30 included the -EAH from the previous epoch). Different bank hashes will result in consensus -failures. - -The above example is clearly bad. It can be observed that short epochs only -occur (1) during warmup, or (2) in tests. Real clusters have much longer -epochs (432,000 slots by default). - -Tests can be fixed as needed; that leaves fixing warmup. Since warmup is -transient, we disable EAH until slots-per-epoch is large enough. More -precisely, we disable EAH until the `calculation window` is big enough. During -warmup, slots-per-epoch doubles each epoch until reaching the desired number, -so only a few epochs will skip EAH (which also is a small total number of -slots). - - -#### Warping - -Warping introduces corner cases into EAH because many slots may be skipped, -including the entire range of `start slot` to `stop slot`. - -Now, a new EAH will always be calculated based on the parent. Thus, if the -warp slot is `stop slot` or greater (and the parent slot is less than `stop -slot`), the warp bank will include this newly-calculated EAH. This is safe -because warping cannot be used on a live cluster; only for a new cluster or -tests/debugging. Therefore _when_ the EAH was calculated is not germane. - -For specific examples, refer to Appendix A. - - -#### Implementation Alternatives - -##### Perform the EAH calculation in the foreground - -The accounts hash calculation takes around 15 seconds (median, on Mainnet-Beta -today). This is far beyond the slot time; this would be bad UX, and also -decrease network stability. - - -##### Remove `stop offset` - -Instead of having two offsets—one for `start` and one for `stop`—use a single -offset for both. This delays when the EAH is saved into a Bank and voted on. -The saved EAH is now the EAH from the previous epoch. This could work; would -reduce the number of "special" slots from two to one. No significant -advantages observed. - - -##### Send EAH requests when making a new bank, instead of a new root - -When a bank is created, we don't yet know if it will be finalized until it is -rooted, which could result in multiple EAH requests due to forking. This would -be bad for performance. - - -### Appendix A: All Warping Scenarios - -To enumerate how EAH interacts with warping, refer to the following diagram for -the scenarios below: - -```text - +---------+-----------------+-----------+---------+-----------------+-----------+ - | > < | > < | - | A > B < C | D > E < F | - | > < | > < | - +---------+-----------------+-----------+---------+-----------------+-----------+ - | | | | | | | - v v v v v v v - epoch 1 start slot 1 stop slot 1 epoch 2 start slot 2 stop slot 2 epoch 3 -``` - - -#### parent slot: `A`, warp slot: `A` - -No slots important to the EAH have been skipped, so no change in behavior. - - -#### parent slot: `A`, warp slot: `B` - -An EAH calculation will be requested when the warp slot is rooted, and then -will be included in the Bank at `slot slot 1`; behavior is unchanged. - - -#### parent slot: `A`, warp slot: `C` or `D` - -The entire EAH range has been skipped; no new EAH calculation will have been -requested for `start slot 1`. The warp slot will include the EAH calculated at -the parent slot. This is different from the normal behavior. - - -#### parent slot: `A`, warp slot: `E` - -Similar to `A -> C`, the warp slot will include the EAH calculated at the -parent slot. This behavior appears different. - -Then when the warp slot is rooted, a new EAH calculation will be requested, -which will be included in `stop slot 2`. This behavior is expected. - - -#### parent slot: `A`, warp slot: `F` - -Similar to `A -> C`, no new EAH calculation will be requested. The warp slot -will include the EAH calculated at the parent slot. This is different from the -normal behavior. - - -#### parent slot: `B`, warp slot: `B` - -A new EAH will be calculated at parent slot, which will be included in `stop -slot 1`, _not_ the previously-calculated EAH from `start slot 1`. This behavior -is different. - - -#### parent slot: `B`, warp slot: `C` or `D` - -The warp slot will include the EAH calculated at the parent slot, _not_ the -previously-calculated EAH from `start slot 1`. This is different from normal -behavior. - - -#### parent slot: `B`, warp slot: `E` - -Similar to `B -> C`, the warp slot will include the EAH calculated at the parent -slot, _not_ the previously-calculated EAH from `start slot 1`. This is -different from normal behavior. - -And similar to `A -> E`, when the warp slot is rooted, a new EAH calculation -will be requested, which will be included in `stop slot 2`. This behavior is -expected. - - -#### parent slot: `B`, warp slot: `F` - -Similar to `B -> C`, the warp slot will include the EAH calculated at the parent -slot, _not_ the previously-calculated EAH from `start slot 1`. This is -different from normal behavior. diff --git a/docs/src/implemented-proposals/index.md b/docs/src/implemented-proposals/index.md deleted file mode 100644 index 5dd1bc16fe0..00000000000 --- a/docs/src/implemented-proposals/index.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Implemented Design Proposals -sidebar_position: 1 -sidebar_label: Overview ---- - -These architectural proposals have been accepted and implemented by the Solana maintainers. Any designs that may be subject to future change are noted in the specific proposal page. - -## Not Yet Implemented - -Design proposals that have been accepted but not yet implemented are found in [Accepted Proposals](../proposals/accepted-design-proposals.md). - -## Submit a New Proposal - -To submit a new design proposal, consult this guide on [how to submit a design proposal](../proposals.md#submit-a-design-proposal). diff --git a/docs/src/implemented-proposals/installer.md b/docs/src/implemented-proposals/installer.md deleted file mode 100644 index f61c5fab844..00000000000 --- a/docs/src/implemented-proposals/installer.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: Cluster Software Installation and Updates ---- - -Currently users are required to build the solana cluster software themselves from the git repository and manually update it, which is error prone and inconvenient. - -This document proposes an easy to use software install and updater that can be used to deploy pre-built binaries for supported platforms. Users may elect to use binaries supplied by Solana or any other party provider. Deployment of updates is managed using an on-chain update manifest program. - -## Motivating Examples - -### Fetch and run a pre-built installer using a bootstrap curl/shell script - -The easiest install method for supported platforms: - -```bash -$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v1.0.0/install/agave-install-init.sh | sh -``` - -This script will check github for the latest tagged release and download and run the `agave-install-init` binary from there. - -If additional arguments need to be specified during the installation, the following shell syntax is used: - -```bash -$ init_args=.... # arguments for `agave-install-init ...` -$ curl -sSf https://raw.githubusercontent.com/solana-labs/solana/v1.0.0/install/agave-install-init.sh | sh -s - ${init_args} -``` - -### Fetch and run a pre-built installer from a Github release - -With a well-known release URL, a pre-built binary can be obtained for supported platforms: - -```bash -$ curl -o agave-install-init https://github.com/solana-labs/solana/releases/download/v1.0.0/agave-install-init-x86_64-apple-darwin -$ chmod +x ./agave-install-init -$ ./agave-install-init --help -``` - -### Build and run the installer from source - -If a pre-built binary is not available for a given platform, building the installer from source is always an option: - -```bash -$ git clone https://github.com/solana-labs/solana.git -$ cd solana/install -$ cargo run -- --help -``` - -### Deploy a new update to a cluster - -Given a solana release tarball \(as created by `ci/publish-tarball.sh`\) that has already been uploaded to a publicly accessible URL, the following commands will deploy the update: - -```bash -$ solana-keygen new -o update-manifest.json # <-- only generated once, the public key is shared with users -$ agave-install deploy http://example.com/path/to/solana-release.tar.bz2 update-manifest.json -``` - -### Run a validator node that auto updates itself - -```bash -$ agave-install init --pubkey 92DMonmBYXwEMHJ99c9ceRSpAmk9v6i3RdvDdXaVcrfj # <-- pubkey is obtained from whoever is deploying the updates -$ export PATH=~/.local/share/agave-install/bin:$PATH -$ solana-keygen ... # <-- runs the latest solana-keygen -$ agave-install run agave-validator ... # <-- runs a validator, restarting it as necessary when an update is applied -``` - -## On-chain Update Manifest - -An update manifest is used to advertise the deployment of new release tarballs on a solana cluster. The update manifest is stored using the `config` program, and each update manifest account describes a logical update channel for a given target triple \(eg, `x86_64-apple-darwin`\). The account public key is well-known between the entity deploying new updates and users consuming those updates. - -The update tarball itself is hosted elsewhere, off-chain and can be fetched from the specified `download_url`. - -```text -use solana_signature::Signature; - -/// Information required to download and apply a given update -pub struct UpdateManifest { - pub timestamp_secs: u64, // When the release was deployed in seconds since UNIX EPOCH - pub download_url: String, // Download URL to the release tar.bz2 - pub download_sha256: String, // SHA256 digest of the release tar.bz2 file -} - -/// Data of an Update Manifest program Account. -#[derive(Serialize, Deserialize, Default, Debug, PartialEq)] -pub struct SignedUpdateManifest { - pub manifest: UpdateManifest, - pub manifest_signature: Signature, -} -``` - -Note that the `manifest` field itself contains a corresponding signature \(`manifest_signature`\) to guard against man-in-the-middle attacks between the `agave-install` tool and the solana cluster RPC API. - -To guard against rollback attacks, `agave-install` will refuse to install an update with an older `timestamp_secs` than what is currently installed. - -## Release Archive Contents - -A release archive is expected to be a tar file compressed with bzip2 with the following internal structure: - -- `/version.yml` - a simple YAML file containing the field `"target"` - the - - target tuple. Any additional fields are ignored. - -- `/bin/` -- directory containing available programs in the release. - - `agave-install` will symlink this directory to - - `~/.local/share/agave-install/bin` for use by the `PATH` environment - - variable. - -- `...` -- any additional files and directories are permitted - -## agave-install Tool - -The `agave-install` tool is used by the user to install and update their cluster software. - -:::info -As of v3.0 `agave-install` does not install the `agave-validator` binary, which is required to run a validator node. -Validator operators are required to [build from source](../cli/install.md#build-from-source). - -::: - -It manages the following files and directories in the user's home directory: - -- `~/.config/solana/install/config.yml` - user configuration and information about the currently installed software version -- `~/.local/share/solana/install/bin` - a symlink to the current release. eg, `~/.local/share/solana-update/-/bin` -- `~/.local/share/solana/install/releases//` - contents of a release - -### Command-line Interface - -```text -agave-install 0.16.0 -The solana cluster software installer - -USAGE: - agave-install [OPTIONS] - -FLAGS: - -h, --help Prints help information - -V, --version Prints version information - -OPTIONS: - -c, --config Configuration file to use [default: .../Library/Preferences/solana/install.yml] - -SUBCOMMANDS: - deploy deploys a new update - help Prints this message or the help of the given subcommand(s) - info displays information about the current installation - init initializes a new installation - run Runs a program while periodically checking and applying software updates - update checks for an update, and if available downloads and applies it -``` - -```text -agave-install-init -initializes a new installation - -USAGE: - agave-install init [OPTIONS] - -FLAGS: - -h, --help Prints help information - -OPTIONS: - -d, --data_dir Directory to store install data [default: .../Library/Application Support/solana] - -u, --url JSON RPC URL for the solana cluster [default: http://api.devnet.solana.com] - -p, --pubkey Public key of the update manifest [default: 9XX329sPuskWhH4DQh6k16c87dHKhXLBZTL3Gxmve8Gp] -``` - -```text -agave-install info -displays information about the current installation - -USAGE: - agave-install info [FLAGS] - -FLAGS: - -h, --help Prints help information - -l, --local only display local information, don't check the cluster for new updates -``` - -```text -agave-install deploy -deploys a new update - -USAGE: - agave-install deploy - -FLAGS: - -h, --help Prints help information - -ARGS: - URL to the solana release archive - Keypair file for the update manifest (/path/to/keypair.json) -``` - -```text -agave-install update -checks for an update, and if available downloads and applies it - -USAGE: - agave-install update - -FLAGS: - -h, --help Prints help information -``` - -```text -agave-install run -Runs a program while periodically checking and applying software updates - -USAGE: - agave-install run [program_arguments]... - -FLAGS: - -h, --help Prints help information - -ARGS: - program to run - ... arguments to supply to the program - -The program will be restarted upon a successful software update -``` diff --git a/docs/src/implemented-proposals/instruction_introspection.md b/docs/src/implemented-proposals/instruction_introspection.md deleted file mode 100644 index 8854ad675f9..00000000000 --- a/docs/src/implemented-proposals/instruction_introspection.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: instruction introspection ---- - -## Problem - -Some smart contract programs may want to verify that another Instruction is present in a -given Message since that Instruction could be performing a verification of certain data, -in a precompiled function. (See secp256k1_instruction for an example). - -## Solution - -Add a new sysvar Sysvar1nstructions1111111111111111111111111 that a program can reference -and received the Message's instruction data inside, and also the index of the current instruction. - -Two helper functions to extract this data can be used: - -``` -fn load_current_index_checked(instruction_data: &[u8]) -> u16; -fn load_instruction_at_checked(instruction_index: usize, instruction_sysvar_account_info: &AccountInfo) -> Result; -``` - -The runtime will recognize this special instruction, serialize the Message instruction data -for it and also write the current instruction index and then the bpf program can extract the -necessary information from there. - -Note: custom serialization of instructions is used because bincode is about 10x slower -in native code and exceeds current SBF instruction limits. diff --git a/docs/src/implemented-proposals/leader-leader-transition.md b/docs/src/implemented-proposals/leader-leader-transition.md deleted file mode 100644 index f19036d92e2..00000000000 --- a/docs/src/implemented-proposals/leader-leader-transition.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Leader-to-Leader Transition ---- - -This design describes how leaders transition production of the PoH ledger between each other as each leader generates its own slot. - -## Challenges - -Current leader and the next leader are both racing to generate the final tick for the current slot. The next leader may arrive at that slot while still processing the current leader's entries. - -The ideal scenario would be that the next leader generated its own slot right after it was able to vote for the current leader. It is very likely that the next leader will arrive at their PoH slot height before the current leader finishes broadcasting the entire block. - -The next leader has to make the decision of attaching its own block to the last completed block, or wait to finalize the pending block. It is possible that the next leader will produce a block that proposes that the current leader failed, even though the rest of the network observes that block succeeding. - -The current leader has incentives to start its slot as early as possible to capture economic rewards. Those incentives need to be balanced by the leader's need to attach its block to a block that has the most commitment from the rest of the network. - -## Leader timeout - -While a leader is actively receiving entries for the previous slot, the leader can delay broadcasting the start of its block in real time. The delay is locally configurable by each leader, and can be dynamically based on the previous leader's behavior. If the previous leader's block is confirmed by the leader's TVU before the timeout, the PoH is reset to the start of the slot and this leader produces its block immediately. - -The downsides: - -- Leader delays its own slot, potentially allowing the next leader more time to - - catch up. - -The upsides compared to guards: - -- All the space in a block is used for entries. -- The timeout is not fixed. -- The timeout is local to the leader, and therefore can be clever. The leader's heuristic can take into account turbine performance. -- This design doesn't require a ledger hard fork to update. -- The previous leader can redundantly transmit the last entry in the block to the next leader, and the next leader can speculatively decide to trust it to generate its block without verification of the previous block. -- The leader can speculatively generate the last tick from the last received entry. -- The leader can speculatively process transactions and guess which ones are not going to be encoded by the previous leader. This is also a censorship attack vector. The current leader may withhold transactions that it receives from the clients so it can encode them into its own slot. Once processed, entries can be replayed into PoH quickly. - -## Alternative design options - -### Guard tick at the end of the slot - -A leader does not produce entries in its block after the _penultimate tick_, which is the last tick before the first tick of the next slot. The network votes on the _last tick_, so the time difference between the _penultimate tick_ and the _last tick_ is the forced delay for the entire network, as well as the next leader before a new slot can be generated. The network can produce the _last tick_ from the _penultimate tick_. - -If the next leader receives the _penultimate tick_ before it produces its own _first tick_, it will reset its PoH and produce the _first tick_ from the previous leader's _penultimate tick_. The rest of the network will also reset its PoH to produce the _last tick_ as the id to vote on. - -The downsides: - -- Every vote, and therefore confirmation, is delayed by a fixed timeout. 1 tick, or around 100ms. -- Average case confirmation time for a transaction would be at least 50ms worse. -- It is part of the ledger definition, so to change this behavior would require a hard fork. -- Not all the available space is used for entries. - -The upsides compared to leader timeout: - -- The next leader has received all the previous entries, so it can start processing transactions without recording them into PoH. -- The previous leader can redundantly transmit the last entry containing the _penultimate tick_ to the next leader. The next leader can speculatively generate the _last tick_ as soon as it receives the _penultimate tick_, even before verifying it. diff --git a/docs/src/implemented-proposals/leader-validator-transition.md b/docs/src/implemented-proposals/leader-validator-transition.md deleted file mode 100644 index e3281252f13..00000000000 --- a/docs/src/implemented-proposals/leader-validator-transition.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Leader-to-Validator Transition ---- - -A validator typically spends its time validating blocks. If, however, a staker -delegates its stake to a validator, it will occasionally be selected as a _slot -leader_. As a slot leader, the validator is responsible for producing blocks -during an assigned _slot_. A slot has a duration of some number of preconfigured -_ticks_. The duration of those ticks are estimated with a _PoH Recorder_ -described later in this document. - -## BankFork - -BankFork tracks changes to the bank state over a specific slot. Once the final -tick has been registered the state is frozen. Any attempts to write to are -rejected. - -## Validator - -A validator operates on many different concurrent forks of the bank state until -it generates a PoH hash with a height within its leader slot. - -## Slot Leader - -A slot leader builds blocks on top of only one fork, the one it last voted on. - -## PoH Recorder - -Slot leaders and validators use a PoH Recorder for both estimating slot height -and for recording transactions. - -### PoH Recorder when Validating - -The PoH Recorder acts as a simple VDF when validating. It tells the validator -when it needs to switch to the slot leader role. Every time the validator votes -on a fork, it should use the fork's latest -[blockhash](https://solana.com/docs/terminology#blockhash) to re-seed the VDF. -Re-seeding solves two problems. First, it synchronizes its VDF to the leader's, -allowing it to more accurately determine when its leader slot begins. Second, if -the previous leader goes down, all wallclock time is accounted for in the next -leader's PoH stream. For example, if one block is missing when the leader -starts, the block it produces should have a PoH duration of two blocks. The -longer duration ensures the following leader isn't attempting to snip all the -transactions from the previous leader's slot. - -### PoH Recorder when Leading - -A slot leader use the PoH Recorder to record transactions, locking their -positions in time. The PoH hash must be derived from a previous leader's last -block. If it isn't, its block will fail PoH verification and be rejected by the -cluster. - -The PoH Recorder also serves to inform the slot leader when its slot is over. -The leader needs to take care not to modify its bank if recording the -transaction would generate a PoH height outside its designated slot. The leader, -therefore, should not commit account changes until after it generates the -entry's PoH hash. When the PoH height falls outside its slot any transactions in -its pipeline may be dropped or forwarded to the next leader. Forwarding is -preferred, as it would minimize network congestion, allowing the cluster to -advertise higher TPS capacity. - -## Validator Loop - -The PoH Recorder manages the transition between modes. Once a ledger is -replayed, the validator can run until the recorder indicates it should be the -slot leader. As a slot leader, the node can then execute and record -transactions. - -The loop is synchronized to PoH and does a synchronous start and stop of the -slot leader functionality. After stopping, the validator's TVU should find -itself in the same state as if a different leader had sent it the same block. -The following is pseudocode for the loop: - -1. Query the LeaderScheduler for the next assigned slot. -2. Run the TVU over all the forks. 1. TVU will send votes to what it believes is - the "best" fork. 2. After each vote, restart the PoH Recorder to run until - the next assigned slot. -3. When time to be a slot leader, start the TPU. Point it to the last fork the - TVU voted on. -4. Produce entries until the end of the slot. 1. For the duration of the slot, - the TVU must not vote on other forks. 2. After the slot ends, the TPU freezes - its BankFork. After freezing, the TVU may resume voting. -5. Goto 1. diff --git a/docs/src/implemented-proposals/persistent-account-storage.md b/docs/src/implemented-proposals/persistent-account-storage.md deleted file mode 100644 index 038c37dc7c5..00000000000 --- a/docs/src/implemented-proposals/persistent-account-storage.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Persistent Account Storage ---- - -## Persistent Account Storage - -The set of accounts represent the current computed state of all the transactions that have been processed by a validator. Each validator needs to maintain this entire set. Each block that is proposed by the network represents a change to this set, and since each block is a potential rollback point, the changes need to be reversible. - -Persistent storage like NVMEs are 20 to 40 times cheaper than DDR. The problem with persistent storage is that write and read performance is much slower than DDR. Care must be taken in how data is read or written to. Both reads and writes can be split between multiple storage drives and accessed in parallel. This design proposes a data structure that allows for concurrent reads and concurrent writes of storage. Writes are optimized by using an AppendVec data structure, which allows a single writer to append while allowing access to many concurrent readers. The accounts index maintains a pointer to a spot where the account was appended to every fork, thus removing the need for explicit checkpointing of state. - -## AppendVec - -AppendVec is a data structure that allows for random reads concurrent with a single append-only writer. Growing or resizing the capacity of the AppendVec requires exclusive access. This is implemented with an atomic `offset`, which is updated at the end of a completed append. - -The underlying memory for an AppendVec is a memory-mapped file. Memory-mapped files allow for fast random access and paging is handled by the OS. - -## Account Index - -The account index is designed to support a single index for all the currently forked Accounts. - -```text -type AccountsFileId = usize; - -type Fork = u64; - -struct AccountMap(Hashmap); - -type AccountIndex = HashMap; -``` - -The index is a map of account Pubkeys to a map of Forks and the location of the Account data in an AppendVec. To get the version of an account for a specific Fork: - -```text -/// Load the account for the pubkey. -/// This function will load the account from the specified fork, falling back to the fork's parents -/// * fork - a virtual Accounts instance, keyed by Fork. Accounts keep track of their parents with Forks, -/// the persistent store -/// * pubkey - The Account's public key. -pub fn load_slow(&self, id: Fork, pubkey: &Pubkey) -> Option<&Account> -``` - -The read is satisfied by pointing to a memory-mapped location in the `AccountsFileId` at the stored offset. A reference can be returned without a copy. - -### Root Forks - -[Tower BFT](tower-bft.md) eventually selects a fork as a root fork and the fork is squashed. A squashed/root fork cannot be rolled back. - -When a fork is squashed, all accounts in its parents not already present in the fork are pulled up into the fork by updating the indexes. Accounts with zero balance in the squashed fork are removed from fork by updating the indexes. - -An account can be _garbage-collected_ when squashing makes it unreachable. - -Three possible options exist: - -- Maintain a HashSet of root forks. One is expected to be created every second. The entire tree can be garbage-collected later. Alternatively, if every fork keeps a reference count of accounts, garbage collection could occur any time an index location is updated. -- Remove any pruned forks from the index. Any remaining forks lower in number than the root are can be considered root. -- Scan the index, migrate any old roots into the new one. Any remaining forks lower than the new root can be deleted later. - -## Garbage collection - -As accounts get updated, they move to the end of the AppendVec. Once capacity has run out, a new AppendVec can be created and updates can be stored there. Eventually references to an older AppendVec will disappear because all the accounts have been updated, and the old AppendVec can be deleted. - -To speed up this process, it's possible to move Accounts that have not been recently updated to the front of a new AppendVec. This form of garbage collection can be done without requiring exclusive locks to any of the data structures except for the index update. - -The initial implementation for garbage collection is that once all the accounts in an AppendVec become stale versions, it gets reused. The accounts are not updated or moved around once appended. - -## Index Recovery - -Each bank thread has exclusive access to the accounts during append, since the accounts locks cannot be released until the data is committed. But there is no explicit order of writes between the separate AppendVec files. To create an ordering, the index maintains an atomic write version counter. Each append to the AppendVec records the index write version number for that append in the entry for the Account in the AppendVec. - -To recover the index, all the AppendVec files can be read in any order, and the latest write version for every fork should be stored in the index. - -## Snapshots - -To snapshot, the underlying memory-mapped files in the AppendVec need to be flushed to disk. The index can be written out to disk as well. - -## Performance - -- Append-only writes are fast. SSDs and NVMEs, as well as all the OS level kernel data structures, allow for appends to run as fast as PCI or NVMe bandwidth will allow \(2,700 MB/s\). -- Each replay and banking thread writes concurrently to its own AppendVec. -- Each AppendVec could potentially be hosted on a separate NVMe. -- Each replay and banking thread has concurrent read access to all the AppendVecs without blocking writes. -- Index requires an exclusive write lock for writes. Single-thread performance for HashMap updates is on the order of 10m per second. -- Banking and Replay stages should use 32 threads per NVMe. NVMes have optimal performance with 32 concurrent readers or writers. diff --git a/docs/src/implemented-proposals/readonly-accounts.md b/docs/src/implemented-proposals/readonly-accounts.md deleted file mode 100644 index 0900a5dec06..00000000000 --- a/docs/src/implemented-proposals/readonly-accounts.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Read-Only Accounts ---- - -This design covers the handling of readonly and writable accounts in the [runtime](../validator/runtime.md). Multiple transactions that modify the same account must be processed serially so that they are always replayed in the same order. Otherwise, this could introduce non-determinism to the ledger. Some transactions, however, only need to read, and not modify, the data in particular accounts. Multiple transactions that only read the same account can be processed in parallel, since replay order does not matter, providing a performance benefit. - -In order to identify readonly accounts, the transaction MessageHeader structure contains `num_readonly_signed_accounts` and `num_readonly_unsigned_accounts`. Instruction `program_ids` are included in the account vector as readonly, unsigned accounts, since executable accounts likewise cannot be modified during instruction processing. - -## Runtime handling - -Runtime transaction processing rules need to be updated slightly. Programs still can't write or spend accounts that they do not own. But new runtime rules ensure that readonly accounts cannot be modified, even by the programs that own them. - -Readonly accounts have the following property: - -- Read-only access to all account fields, including lamports (cannot be credited or debited), and account data - -Instructions that credit, debit, or modify the readonly account will fail. - -## Account Lock Optimizations - -The Accounts module keeps track of current locked accounts in the runtime, which separates readonly accounts from the writable accounts. The default account lock gives an account the "writable" designation, and can only be accessed by one processing thread at one time. Readonly accounts are locked by a separate mechanism, allowing for parallel reads. - -Although not yet implemented, readonly accounts could be cached in memory and shared between all the threads executing transactions. An ideal design would hold this cache while a readonly account is referenced by any transaction moving through the runtime, and release the cache when the last transaction exits the runtime. - -Readonly accounts could also be passed into the processor as references, saving an extra copy. diff --git a/docs/src/implemented-proposals/reliable-vote-transmission.md b/docs/src/implemented-proposals/reliable-vote-transmission.md deleted file mode 100644 index c4632be4a97..00000000000 --- a/docs/src/implemented-proposals/reliable-vote-transmission.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Reliable Vote Transmission ---- - -Validator votes are messages that have a critical function for consensus and continuous operation of the network. Therefore it is critical that they are reliably delivered and encoded into the ledger. - -## Challenges - -1. Leader rotation is triggered by PoH, which is clock with high drift. So many nodes are likely to have an incorrect view if the next leader is active in realtime or not. -2. The next leader may be easily be flooded. Thus a DDOS would not only prevent delivery of regular transactions, but also consensus messages. -3. UDP is unreliable, and our asynchronous protocol requires any message that is transmitted to be retransmitted until it is observed in the ledger. Retransmission could potentially cause an unintentional _thundering herd_ against the leader with a large number of validators. Worst case flood would be `(num_nodes * num_retransmits)`. -4. Tracking if the vote has been transmitted or not via the ledger does not guarantee it will appear in a confirmed block. The current observed block may be unrolled. Validators would need to maintain state for each vote and fork. - -## Design - -1. Send votes as a push message through gossip. This ensures delivery of the vote to all the next leaders, not just the next future one. -2. Leaders will read the Crds table for new votes and encode any new received votes into the blocks they propose. This allows for validator votes to be included in rollback forks by all the future leaders. -3. Validators that receive votes in the ledger will add them to their local crds table, not as a push request, but simply add them to the table. This shortcuts the push message protocol, so the validation messages do not need to be retransmitted twice around the network. -4. CrdsValue for vote should look like this `Votes(Vec)` - -Each vote transaction should maintain a `wallclock` in its data. The merge strategy for Votes will keep the last N set of votes as configured by the local client. For push/pull the vector is traversed recursively and each Transaction is treated as an individual CrdsValue with its own local wallclock and signature. - -Gossip is designed for efficient propagation of state. Messages that are sent through gossip-push are batched and propagated with a minimum spanning tree to the rest of the network. Any partial failures in the tree are actively repaired with the gossip-pull protocol while minimizing the amount of data transferred between any nodes. - -## How this design solves the Challenges - -1. Because there is no easy way for validators to be in sync with leaders on the leader's "active" state, gossip allows for eventual delivery regardless of that state. -2. Gossip will deliver the messages to all the subsequent leaders, so if the current leader is flooded the next leader would have already received these votes and is able to encode them. -3. Gossip minimizes the number of requests through the network by maintaining an efficient spanning tree, and using bloom filters to repair state. So retransmit back-off is not necessary and messages are batched. -4. Leaders that read the crds table for votes will encode all the new valid votes that appear in the table. Even if this leader's block is unrolled, the next leader will try to add the same votes without any additional work done by the validator. Thus ensuring not only eventual delivery, but eventual encoding into the ledger. - -## Performance - -1. Worst case propagation time to the next leader is Log\(N\) hops with a base depending on the fanout. With our current default fanout of 6, it is about 6 hops to 20k nodes. -2. The leader should receive 20k validation votes aggregated by gossip-push into MTU-sized shreds. Which would reduce the number of packets for 20k network to 80 shreds. -3. Each validators votes is replicated across the entire network. To maintain a queue of 5 previous votes the Crds table would grow by 25 megabytes. `(20,000 nodes * 256 bytes * 5)`. - -## Two step implementation rollout - -Initially the network can perform reliably with just 1 vote transmitted and maintained through the network with the current Vote implementation. For small networks a fanout of 6 is sufficient. With small network the memory and push overhead is minor. - -### Sub 1k validator network - -1. Crds just maintains the validators latest vote. -2. Votes are pushed and retransmitted regardless if they are appearing in the ledger. -3. Fanout of 6. -4. Worst case 256kb memory overhead per node. -5. Worst case 4 hops to propagate to every node. -6. Leader should receive the entire validator vote set in 4 push message shreds. - -### Sub 20k network - -Everything above plus the following: - -1. CRDS table maintains a vector of 5 latest validator votes. -2. Votes encode a wallclock. CrdsValue::Votes is a type that recurses into the transaction vector for all the gossip protocols. -3. Increase fanout to 20. -4. Worst case 25mb memory overhead per node. -5. Sub 4 hops worst case to deliver to the entire network. -6. 80 shreds received by the leader for all the validator messages. diff --git a/docs/src/implemented-proposals/rent.md b/docs/src/implemented-proposals/rent.md deleted file mode 100644 index 02adc0390ef..00000000000 --- a/docs/src/implemented-proposals/rent.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Rent ---- - -Accounts on Solana may have owner-controlled state \(`Account::data`\) that's separate from the account's balance \(`Account::lamports`\). Since validators on the network need to maintain a working copy of this state in memory, the network charges a time-and-space based fee for this resource consumption, also known as Rent. - -## Two-tiered rent regime - -Accounts which maintain a minimum balance equivalent to 2 years of rent payments are exempt. The _2 years_ is drawn from the fact hardware cost drops by 50% in price every 2 years and the resulting convergence due to being a geometric series. Accounts whose balance falls below this threshold are charged rent at a rate specified in genesis, in lamports per byte-year. The network charges rent on a per-epoch basis, in credit for the next epoch, and `Account::rent_epoch` keeps track of the next time rent should be collected from the account. - -Currently, the rent cost is fixed at the genesis. However, it's anticipated to be dynamic, reflecting the underlying hardware storage cost at the time. So the price is generally expected to decrease as the hardware cost declines as the technology advances. - -## Timings of collecting rent - -There are two timings of collecting rent from accounts: \(1\) when referenced by a transaction, \(2\) periodically once an epoch. \(1\) includes the transaction to create the new account itself, and it happens during the normal transaction processing by the bank as part of the load phase. \(2\) exists to ensure to collect rents from stale accounts, which aren't referenced in recent epochs at all. \(2\) requires the whole scan of accounts and is spread over an epoch based on account address prefix to avoid load spikes due to this rent collection. - -On the contrary, rent collection isn't applied to accounts that are directly manipulated by any of protocol-level bookkeeping processes including: - -- The distribution of rent collection itself (Otherwise, it may cause recursive rent collection handling) -- The distribution of staking rewards at the start of every epoch (To reduce as much as processing spike at the start of new epoch) -- The distribution of transaction fee at the end of every slot - -Even if those processes are out of scope of rent collection, all of manipulated accounts will eventually be handled by the \(2\) mechanism. - -## Actual processing of collecting rent - -Rent is due for one epoch's worth of time, and accounts have `Account::rent_epoch` of `current_epoch` or `current_epoch + 1` depending on the rent regime. - -If the account is in the exempt regime, `Account::rent_epoch` is simply updated to `current_epoch`. - -If the account is non-exempt, the difference between the next epoch and `Account::rent_epoch` is used to calculate the amount of rent owed by this account \(via `Rent::due()`\). Any fractional lamports of the calculation are truncated. Rent due is deducted from `Account::lamports` and `Account::rent_epoch` is updated to `current_epoch + 1` (= next epoch). If the amount of rent due is less than one lamport, no changes are made to the account. - -Accounts whose balance is insufficient to satisfy the rent that would be due simply fail to load. - -A percentage of the rent collected is destroyed. The rest is distributed to validator accounts by stake weight, a la transaction fees, at the end of every slot. - -Finally, rent collection happens according to the protocol-level account updates like the rent distribution to validators, meaning there is no corresponding transaction for rent deductions. So, rent collection is rather invisible, only implicitly observable by a recent transaction or predetermined timing given its account address prefix. - -## Design considerations - -### Current design rationale - -Under the preceding design, it is NOT possible to have accounts that linger, never get touched, and never have to pay rent. Accounts always pay rent exactly once for each epoch, except rent-exempt, sysvar and executable accounts. - -This is an intended design choice. Otherwise, it would be possible to trigger unauthorized rent collection with `Noop` instruction by anyone who may unfairly profit from the rent (a leader at the moment) or save the rent given anticipated fluctuating rent cost. - -As another side-effect of this choice, also note that this periodic rent collection effectively forces validators not to store stale accounts into a cold storage optimistically and save the storage cost, which is unfavorable for account owners and may cause transactions on them to stall longer than others. On the flip side, this prevents malicious users from creating significant numbers of garbage accounts, burdening validators. - -As the overall consequence of this design, all accounts are stored equally as a validator's working set with the same performance characteristics, reflecting the uniform rent pricing structure. - -### Ad-hoc collection - -Collecting rent on an as-needed basis \(i.e. whenever accounts were loaded/accessed\) was considered. The issues with such an approach are: - -- accounts loaded as "credit only" for a transaction could very reasonably be expected to have rent due, - - but would not be writable during any such transaction - -- a mechanism to "beat the bushes" \(i.e. go find accounts that need to pay rent\) is desirable, - - lest accounts that are loaded infrequently get a free ride - -### System instruction for collecting rent - -Collecting rent via a system instruction was considered, as it would naturally have distributed rent to active and stake-weighted nodes and could have been done incrementally. However: - -- it would have adversely affected network throughput -- it would require special-casing by the runtime, as accounts with non-SystemProgram owners may be debited by this instruction -- someone would have to issue the transactions diff --git a/docs/src/implemented-proposals/repair-service.md b/docs/src/implemented-proposals/repair-service.md deleted file mode 100644 index 49d1ff1c9f1..00000000000 --- a/docs/src/implemented-proposals/repair-service.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: Repair Service ---- - -## Repair Service - -The RepairService is in charge of retrieving missing shreds that failed to be -delivered by primary communication protocols like Turbine. It is in charge of -managing the protocols described below in the `Repair Protocols` section below. - -## Challenges: - -1\) Validators can fail to receive particular shreds due to network failures - -2\) Consider a scenario where blockstore contains the set of slots {1, 3, 5}. -Then Blockstore receives shreds for some slot 7, where for each of the shreds -b, b.parent == 6, so then the parent-child relation 6 -> 7 is stored in -blockstore. However, there is no way to chain these slots to any of the -existing banks in Blockstore, and thus the `Shred Repair` protocol will not -repair these slots. If these slots happen to be part of the main chain, this -will halt replay progress on this node. - -## Repair-related primitives - -Epoch Slots: -Each validator advertises separately on gossip the various parts of an -`Epoch Slots`: - -- The `stash`: An epoch-long compressed set of all completed slots. -- The `cache`: The Run-length Encoding (RLE) of the latest `N` completed - slots starting from some slot `M`, where `N` is the number of slots - that will fit in an MTU-sized packet. - -`Epoch Slots` in gossip are updated every time a validator receives a -complete slot within the epoch. Completed slots are detected by blockstore -and sent over a channel to RepairService. It is important to note that we -know that by the time a slot `X` is complete, the epoch schedule must exist -for the epoch that contains slot `X` because WindowService will reject -shreds for unconfirmed epochs. - -Every `N/2` completed slots, the oldest `N/2` slots are moved from the -`cache` into the `stash`. The base value `M` for the RLE should also -be updated. - -## Repair Request Protocols - -The repair protocol makes best attempts to progress the forking structure of -Blockstore. - -The different protocol strategies to address the above challenges: - -1. Shred Repair \(Addresses Challenge \#1\): This is the most basic repair - protocol, with the purpose of detecting and filling "holes" in the ledger. - Blockstore tracks the latest root slot. RepairService will then periodically - iterate every fork in blockstore starting from the root slot, sending repair - requests to validators for any missing shreds. It will send at most some `N` - repair requests per iteration. Shred repair should prioritize repairing - forks based on the leader's fork weight. Validators should only send repair - requests to validators who have marked that slot as completed in their - EpochSlots. Validators should prioritize repairing shreds in each slot - that they are responsible for retransmitting through turbine. Validators can - compute which shreds they are responsible for retransmitting because the - seed for turbine is based on leader id, slot, and shred index. - - Note: Validators will only accept shreds within the current verifiable - epoch \(epoch the validator has a leader schedule for\). - -2. Preemptive Slot Repair \(Addresses Challenge \#2\): The goal of this - protocol is to discover the chaining relationship of "orphan" slots that do not - currently chain to any known fork. Shred repair should prioritize repairing - orphan slots based on the leader's fork weight. - - - Blockstore will track the set of "orphan" slots in a separate column family. - - RepairService will periodically make `Orphan` requests for each of - the orphans in blockstore. - - `Orphan(orphan)` request - `orphan` is the orphan slot that the - requestor wants to know the parents of `Orphan(orphan)` response - - The highest shreds for each of the first `N` parents of the requested - `orphan` - - On receiving the responses `p`, where `p` is some shred in a parent slot, - validators will: - - - Insert an empty `SlotMeta` in blockstore for `p.slot` if it doesn't - already exist. - - If `p.slot` does exist, update the parent of `p` based on `parents` - - Note: that once these empty slots are added to blockstore, the - `Shred Repair` protocol should attempt to fill those slots. - - Note: Validators will only accept responses containing shreds within the - current verifiable epoch \(epoch the validator has a leader schedule - for\). - -Validators should try to send orphan requests to validators who have marked that -orphan as completed in their EpochSlots. If no such validators exist, then -randomly select a validator in a stake-weighted fashion. - -## Repair Response Protocol - -When a validator receives a request for a shred `S`, they respond with the -shred if they have it. - -When a validator receives a shred through a repair response, they check -`EpochSlots` to see if <= `1/3` of the network has marked this slot as -completed. If so, they resubmit this shred through its associated turbine -path, but only if this validator has not retransmitted this shred before. diff --git a/docs/src/implemented-proposals/rpc-transaction-history.md b/docs/src/implemented-proposals/rpc-transaction-history.md deleted file mode 100644 index 99e828551cd..00000000000 --- a/docs/src/implemented-proposals/rpc-transaction-history.md +++ /dev/null @@ -1,121 +0,0 @@ -# Long term RPC Transaction History - -There's a need for RPC to serve at least 6 months of transaction history. The -current history, on the order of days, is insufficient for downstream users. - -6 months of transaction data cannot be stored practically in a validator's -rocksdb ledger so an external data store is necessary. The validator's rocksdb -ledger will continue to serve as the primary data source, and then will fall -back to the external data store. - -The affected RPC endpoints are: - -- [getFirstAvailableBlock](https://solana.com/docs/rpc/http/getfirstavailableblock) -- [getConfirmedBlock](https://solana.com/docs/rpc/deprecated/getconfirmedblock) -- [getConfirmedBlocks](https://solana.com/docs/rpc/deprecated/getconfirmedblocks) -- [getConfirmedSignaturesForAddress](https://solana.com/docs/rpc/http/getconfirmedsignaturesforaddress) -- [getConfirmedTransaction](https://solana.com/docs/rpc/deprecated/getConfirmedTransaction) -- [getSignatureStatuses](https://solana.com/docs/rpc/http/getsignaturestatuses) -- [getBlockTime](https://solana.com/docs/rpc/http/getblocktime) - -Some system design constraints: - -- The volume of data to store and search can quickly jump into the terabytes, - and is immutable. -- The system should be as light as possible for SREs. For example an SQL - database cluster that requires an SRE to continually monitor and rebalance - nodes is undesirable. -- Data must be searchable in real time - batched queries that take minutes or - hours to run are unacceptable. -- Easy to replicate the data worldwide to co-locate it with the RPC endpoints - that will utilize it. -- Interfacing with the external data store should be easy and not require - depending on risky lightly-used community-supported code libraries - -Based on these constraints, Google's BigTable product is selected as the data -store. - -## Table Schema - -A BigTable instance is used to hold all transaction data, broken up into -different tables for quick searching. - -New data may be copied into the instance at anytime without affecting the -existing data, and all data is immutable. Generally the expectation is that new -data will be uploaded once a current epoch completes but there is no limitation -on the frequency of data dumps. - -Cleanup of old data is automatic by configuring the data retention policy of the -instance tables appropriately, it just disappears. Therefore the order of when -data is added becomes important. For example if data from epoch N-1 is added -after data from epoch N, the older epoch data will outlive the newer data. -However beyond producing _holes_ in query results, this kind of unordered -deletion will have no ill effect. Note that this method of cleanup effectively -allows for an unlimited amount of transaction data to be stored, restricted only -by the monetary costs of doing so. - -The table layout s supports the existing RPC endpoints only. New RPC endpoints -in the future may require additions to the schema and potentially iterating over -all transactions to build up the necessary metadata. - -## Accessing BigTable - -BigTable has a gRPC endpoint that can be accessed using the -[tonic](https://crates.io/crates/tonic) and the raw protobuf API, as currently -no higher-level Rust crate for BigTable exists. Practically this makes parsing -the results of BigTable queries more complicated but is not a significant issue. - -## Data Population - -The ongoing population of instance data will occur on an epoch cadence through -the use of a new `agave-ledger-tool` command that will convert rocksdb data for -a given slot range into the instance schema. - -The same process will be run once, manually, to backfill the existing ledger -data. - -### Block Table: `block` - -This table contains the compressed block data for a given slot. - -The row key is generated by taking the 16 digit lower case hexadecimal -representation of the slot, to ensure that the oldest slot with a confirmed -block will always be first when the rows are listed. eg, The row key for slot 42 -would be 000000000000002a. - -The row data is a compressed `StoredConfirmedBlock` struct. - -### Account Address Transaction Signature Lookup Table: `tx-by-addr` - -This table contains the transactions that affect a given address. - -The row key is -`/`. -The row data is a compressed `TransactionByAddrInfo` struct. - -Taking the one's compliment of the slot allows for listing of slots ensures that -the newest slot with transactions that affect an address will always be listed -first. - -Sysvar addresses are not indexed. However frequently used programs such as Vote -or System are, and will likely have a row for every confirmed slot. - -### Transaction Signature Lookup Table: `tx` - -This table maps a transaction signature to its confirmed block, and index within -that block. - -The row key is the base58-encoded transaction signature. -The row data is a compressed `TransactionInfo` struct. - -### Entries Table: `entries` - -> Support for the `entries` table was added in v1.18.0. - -This table contains data about the entries in a slot. - -The row key is the same as a `block` row key. - -The row data is a compressed `Entries` struct, which is a list of entry-summary -data, including hash, number of hashes since previous entry, number of -transactions, and starting transaction index. diff --git a/docs/src/implemented-proposals/snapshot-verification.md b/docs/src/implemented-proposals/snapshot-verification.md deleted file mode 100644 index 88fdc356d95..00000000000 --- a/docs/src/implemented-proposals/snapshot-verification.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Snapshot Verification ---- - -## Problem - -When a validator boots up from a snapshot, it needs a way to verify the account set matches what the rest of the network sees quickly. A potential -attacker could give the validator an incorrect state, and then try to convince it to accept a transaction that would otherwise be rejected. - -## Solution - -Currently the bank hash is derived from hashing the delta state of the accounts in a slot which is then combined with the previous bank hash value. -The problem with this is that the list of hashes will grow on the order of the number of slots processed by the chain and become a burden to both -transmit and verify successfully. - -Another naive method could be to create a merkle tree of the account state. This has the downside that with each account update, the merkle tree -would have to be recomputed from the entire account state of all live accounts in the system. - -To verify the snapshot, we do the following: - -On account store of non-zero lamport accounts, we hash the following data: - -- Account owner -- Account data -- Account pubkey -- Account lamports balance -- Fork the account is stored on - -Use this resulting hash value as input to an expansion function which expands the hash value into an image value. -The function will create a 440 byte block of data where the first 32 bytes are the hash value, and the next 440 - 32 bytes are -generated from a Chacha RNG with the hash as the seed. - -The account images are then combined with xor. The previous account value will be xored into the state and the new account value also xored into the state. - -Voting and sysvar hash values occur with the hash of the resulting full image value. - -On validator boot, when it loads from a snapshot, it would verify the hash value with the accounts set. It would then -use SPV to display the percentage of the network that voted for the hash value given. - -The resulting value can be verified by a validator to be the result of xoring all current account states together. - -A snapshot must be purged of zero lamport accounts before creation and during verify since the zero lamport accounts do not affect the hash value but may cause -a validator bank to read that an account is not present when it really should be. - -An attack on the xor state could be made to influence its value: - -Thus the 440 byte image size comes from this paper, avoiding xor collision with 0 \(or thus any other given bit pattern\): \[[https://link.springer.com/content/pdf/10.1007%2F3-540-45708-9_19.pdf](https://link.springer.com/content/pdf/10.1007%2F3-540-45708-9_19.pdf)\] - -The math provides 128 bit security in this case: - -```text -O(k * 2^(n/(1+lg(k))) -k=2^40 accounts -n=440 -2^(40) * 2^(448 * 8 / 41) ~= O(2^(128)) -``` diff --git a/docs/src/implemented-proposals/staking-rewards.md b/docs/src/implemented-proposals/staking-rewards.md deleted file mode 100644 index 4ab55b5c0ea..00000000000 --- a/docs/src/implemented-proposals/staking-rewards.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Staking Rewards ---- - -A Proof of Stake \(PoS\), \(i.e. using in-protocol asset, SOL, to provide secure consensus\) design is outlined here. Solana implements a proof of stake reward/security scheme for validator nodes in the cluster. The purpose is threefold: - -- Align validator incentives with that of the greater cluster through skin-in-the-game deposits at risk - -- Avoid 'nothing at stake' fork voting issues by implementing slashing rules aimed at promoting fork convergence - -- Provide an avenue for validator rewards provided as a function of validator participation in the cluster. - -While many of the details of the specific implementation are currently under consideration and are expected to come into focus through specific modeling studies and parameter exploration on the Solana testnet, we outline here our current thinking on the main components of the PoS system. Much of this thinking is based on the current status of Casper FFG, with optimizations and specific attributes to be modified as is allowed by Solana's Proof of History \(PoH\) blockchain data structure. - -## General Overview - -Solana's ledger validation design is based on a rotating, stake-weighted selected leader broadcasting transactions in a PoH data structure to validating nodes. These nodes, upon receiving the leader's broadcast, have the opportunity to vote on the current state and PoH height by signing a transaction into the PoH stream. - -To become a Solana validator, one must deposit/lock-up some amount of SOL in a contract. This SOL will not be accessible for a specific time period. The precise duration of the staking lockup period has not been determined. However we can consider three phases of this time for which specific parameters will be necessary: - -- _Warm-up period_: which SOL is deposited and inaccessible to the node, however PoH transaction validation has not begun. Most likely on the order of days to weeks - -- _Validation period_: a minimum duration for which the deposited SOL will be inaccessible, at risk of slashing \(see slashing rules below\) and earning rewards for the validator participation. Likely duration of months to a year. - -- _Cool-down period_: a duration of time following the submission of a 'withdrawal' transaction. During this period validation responsibilities have been removed and the funds continue to be inaccessible. Accumulated rewards should be delivered at the end of this period, along with the return of the initial deposit. - -Solana's trustless sense of time and ordering provided by its PoH data structure, along with its [turbine](https://docs.anza.xyz/consensus/turbine-block-propagation) data broadcast and transmission design, should provide sub-second transaction confirmation times that scale with the log of the number of nodes in the cluster. This means we shouldn't have to restrict the number of validating nodes with a prohibitive 'minimum deposits' and expect nodes to be able to become validators with nominal amounts of SOL staked. At the same time, Solana's focus on high-throughput should create incentive for validation clients to provide high-performant and reliable hardware. Combined with potentially a minimum network speed threshold to join as a validation-client, we expect a healthy validation delegation market to emerge. - -## Penalties - -As discussed in the [Economic Design](ed_overview/ed_overview.md) section, annual validator interest rates are to be specified as a function of total percentage of circulating supply that has been staked. The cluster rewards validators who are online and actively participating in the validation process throughout the entirety of their _validation period_. For validators that go offline/fail to validate transactions during this period, their annual reward is effectively reduced. - -Similarly, we may consider an algorithmic reduction in a validator's active staked amount in the case that they are offline. I.e. if a validator is inactive for some amount of time, either due to a partition or otherwise, the amount of their stake that is considered ‘active’ \(eligible to earn rewards\) may be reduced. This design would be structured to help long-lived partitions to eventually reach finality on their respective chains as the % of non-voting total stake is reduced over time until a supermajority can be achieved by the active validators in each partition. Similarly, upon re-engaging, the ‘active’ amount staked will come back online at some defined rate. Different rates of stake reduction may be considered depending on the size of the partition/active set. diff --git a/docs/src/implemented-proposals/testing-programs.md b/docs/src/implemented-proposals/testing-programs.md deleted file mode 100644 index 583eda7b7b4..00000000000 --- a/docs/src/implemented-proposals/testing-programs.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Testing Programs ---- - -Applications send transactions to a Solana cluster and query validators to confirm the transactions were processed and to check each transaction's result. When the cluster doesn't behave as anticipated, it could be for a number of reasons: - -- The program is buggy -- The BPF loader rejected an unsafe program instruction -- The transaction was too big -- The transaction was invalid -- The Runtime tried to execute the transaction when another one was accessing - - the same account - -- The network dropped the transaction -- The cluster rolled back the ledger -- A validator responded to queries maliciously - -## The AsyncClient and SyncClient Traits - -To troubleshoot, the application should retarget a lower-level component, where fewer errors are possible. Retargeting can be done with different implementations of the AsyncClient and SyncClient traits. - -Components implement the following primary methods: - -```text -trait AsyncClient { - fn async_send_transaction(&self, transaction: Transaction) -> io::Result; -} - -trait SyncClient { - fn get_signature_status(&self, signature: &Signature) -> Result>>; -} -``` - -Users send transactions and asynchronously and synchronously await results. - -### TpuClient for the TPU - -The next level is the TPU implementation, which is not yet implemented. At the TPU level, the application sends transactions over Rust channels, where there can be no surprises from network queues or dropped packets. The TPU implements all "normal" transaction errors. It does signature verification, may report account-in-use errors, and otherwise results in the ledger, complete with proof of history hashes. - -## Low-level testing - -### BankClient for the Bank - -Below the TPU level is the Bank. The Bank doesn't do signature verification or generate a ledger. The Bank is a convenient layer at which to test new on-chain programs. It allows developers to toggle between native program implementations and BPF-compiled variants. No need for the Transact trait here. The Bank's API is synchronous. - -## Unit-testing with the Runtime - -Below the Bank is the Runtime. The Runtime is the ideal test environment for unit-testing. By statically linking the Runtime into a native program implementation, the developer gains the shortest possible edit-compile-run loop. Without any dynamic linking, stack traces include debug symbols and program errors are straightforward to troubleshoot. diff --git a/docs/src/implemented-proposals/tower-bft.md b/docs/src/implemented-proposals/tower-bft.md deleted file mode 100644 index c8a7819bbfe..00000000000 --- a/docs/src/implemented-proposals/tower-bft.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -title: Tower BFT ---- - -This design describes Solana's _Tower BFT_ algorithm. It addresses the following problems: - -- Some forks may not end up accepted by the supermajority of the cluster, and voters need to recover from voting on such forks. -- Many forks may be votable by different voters, and each voter may see a different set of votable forks. The selected forks should eventually converge for the cluster. -- Reward based votes have an associated risk. Voters should have the ability to configure how much risk they take on. -- The [cost of rollback](tower-bft.md#cost-of-rollback) needs to be computable. It is important to clients that rely on some measurable form of Consistency. The costs to break consistency need to be computable, and increase super-linearly for older votes. -- ASIC speeds are different between nodes, and attackers could employ Proof of History ASICS that are much faster than the rest of the cluster. Consensus needs to be resistant to attacks that exploit the variability in Proof of History ASIC speed. - -For brevity this design assumes that a single voter with a stake is deployed as an individual validator in the cluster. - -## Time - -The Solana cluster generates a source of time via a Verifiable Delay Function we are calling [Proof of History](../consensus/synchronization.md). - -The unit of time is called a "slot". Each slot has a designated leader that can -produce a block `B`. The `slot` of block `B` is designated `slot(B)`. A leader -does not necessarily need to generate a block for its slot, in which case there -may not be blocks for some slots. - -For more details, see [fork generation](../consensus/fork-generation.md) and [leader rotation](../consensus/leader-rotation.md). - -## Votes - -Validators communicate which fork they think is the heaviest through votes. -Each vote `v` is signed by the validator that produces it, and is of the form `(i, B)`, where `i` is the public key of the validator producing the vote and `B` is a hash identifying the block being voted for. - -## Lockouts - -Making votes on a particular fork incurs a lockout on that particular fork. A lockout is a designated period of time, measured in slots, within which a validator cannot vote on another fork. The purpose of the lockout is to force a -validator to commit opportunity cost to a specific fork. Lockouts are measured -in slots, and therefore represent a real-time forced delay that a validator -needs to wait before breaking the commitment to a fork. - -Validators that violate the lockouts and vote for a diverging fork within the lockout should be punished. The proposed punishment is to slash the validator stake if a concurrent vote within a lockout for a non-descendant fork can be proven to the cluster. - -## Algorithm - -The basic idea to this approach is to stack consensus votes and double lockouts. Each vote in the stack is a confirmation of a fork. Each confirmed fork is an ancestor of the fork above it. Each vote has a `lockout` in units of slots before the validator can submit a vote that does not contain the confirmed fork as an ancestor. - -We call this stack the Vote Tower. - -When a vote is added to the tower, the lockouts of all the previous votes in the tower are doubled (more on this in [Vote Tower](#vote-tower)). With each new vote, a validator commits the previous votes to an ever-increasing lockout. At 32 votes we can consider the vote to be at `max lockout` any votes with a lockout equal to or above `1<<32` are dequeued \(FIFO\). Dequeuing a vote is the trigger for a reward. If the vote on the top of the tower expires before it is dequeued, it and subsequent expired votes are popped in a LIFO fashion from the vote tower. The validator needs to start rebuilding the tower from that point. - -### Vote Tower - -Before a vote is pushed to the tower, all the votes leading up to vote with a lower lock expiration slot than the new vote are popped. After rollback -lockouts are not doubled until the validator catches up to the rollback height of votes. - -For example, a vote tower with the following state: - -| vote | vote slot | lockout | lock expiration slot | -| ---: | --------: | ------: | -------------------: | -| 4 | 4 | 2 | 6 | -| 3 | 3 | 4 | 7 | -| 2 | 2 | 8 | 10 | -| 1 | 1 | 16 | 17 | - -_Vote 5_ is at slot 9, and the resulting state is - -| vote | vote slot | lockout | lock expiration slot | -| ---: | --------: | ------: | -------------------: | -| 5 | 9 | 2 | 11 | -| 2 | 2 | 8 | 10 | -| 1 | 1 | 16 | 17 | - -_Vote 6_ is at slot 10 - -| vote | vote slot | lockout | lock expiration slot | -| ---: | --------: | ------: | -------------------: | -| 6 | 10 | 2 | 12 | -| 5 | 9 | 4 | 13 | -| 2 | 2 | 8 | 10 | -| 1 | 1 | 16 | 17 | - -At slot 10 the new votes caught up to the previous votes. When _vote 7_ at slot 11 is applied we scan top down to pop expired votes. Although _vote 2_ has expired, since _vote 6_ has not expired, we do not continue scanning. Finally we have reached a new stack depth, lockouts are doubled - -| vote | vote slot | lockout | lock expiration slot | -| ---: | --------: | ------: | -------------------: | -| 7 | 11 | 2 | 13 | -| 6 | 10 | 4 | 14 | -| 5 | 9 | 8 | 17 | -| 2 | 2 | 16 | 18 | -| 1 | 1 | 32 | 33 | - -Finally we have _vote 8_ at slot 18, this leads to the expiry of _vote 7_, _vote 6_, and _vote 5_. - -| vote | vote slot | lockout | lock expiration slot | -| ---: | --------: | ------: | -------------------: | -| 8 | 18 | 2 | 20 | -| 2 | 2 | 16 | 18 | -| 1 | 1 | 32 | 33 | - -### Cost of Rollback - -Cost of rollback of _fork A_ is defined as the cost in terms of lockout time to the validator to confirm any other fork that does not include _fork A_ as an ancestor. - -The **Economic Finality** of _fork A_ can be calculated as the loss of all the rewards from rollback of _fork A_ and its descendants, plus the opportunity cost of reward due to the exponentially growing lockout of the votes that have confirmed _fork A_. - -### Threshold Check -In order to prevent a validator from locking itself out on the wrong fork -in the case of a partition, there also needs to be a check to ensure that the rest of the cluster is committing to the same fork. This check is called the -"threshold check", and is outlined as follows. - -In deciding whether to vote for a block `B`: - -1. Simulate a vote for `B` on your current tower -2. Simulate popping off all the votes that would be expired by `B` -3. Now index every vote in the tower from `[0, tower.length()]`, assuming that the most recent simulated vote `B` is index 0, the second most recent vote is index 1, etc. -4. Let `T` be the vote in the tower with index equal to `threshold_check_depth`, currently hardcoded to `8`. -5. Check all the blocks descended from `T`. Let `Votes` be the set of all votes in these blocks for `T` or any descendants `D_n` of `T`. Let `V` be the set of all validators that have made a vote in `V`. If the sum of the validators' stakes in `V` totals `>= 2/3` of the stake of the network, then we commit a vote to `T`. - -### Algorithm parameters - -The following parameters need to be tuned: - -- Number of votes in the stack before dequeue occurs \(32\). -- Rate of growth for lockouts in the stack \(2x\). -- Starting default lockout \(2\). -- Threshold check depth for minimum cluster commitment before committing to the fork \(8\). -- Minimum cluster commitment size at threshold depth \(50%+\). - -### Fork Choice - -Fork choice is how each validator determines which fork to vote on when multiple -concurrent forks exist. Forks are weighted based on the latest votes made by the validator set, and individual validators then vote on the "heaviest" -such fork. - -Given the view of a single validator `i`: - -Let `V` be the set of "most recent" valid votes received by `i`, i.e., `v = (j, B)` is in `V` and `i` has not also received a vote of the form `(j, B′) `such that `slot(B′) > slot(B)`. - -Now the algorithm proceeds as follows: - -1. For each vote `(j, B)` in `V`, add the stake of `j` to `B` and all of its -ancestors. -2. Now Set `B` to be the rooted block. Set `finish := 0`. -3. Perform the following loop: - -``` -*While* `finish == 0` -*Do*: - *If*: `i` has received no children of `B` then set `finish := 1` and return - `B`. - *Else*: Let `B′` be the child of `B` (amongst those received by `i`) with - most the most stake-weighted votes in `V`, breaking ties by the smallest - slot. Set `B` equal to `B'`. -``` - -### Voting Algorithm - -Each validator maintains a vote tower `T` which follows the rules described above in [Vote Tower](#vote-tower), which is a sequence of blocks it has voted for (initially empty). The variable `l` records the length of the stack. For each entry in the tower, denoted by `B = T(x)` for `x < l` where `B` is the `xth` entry in the tower, we record also a value `confcount(B)`. Define the lock expiration slot `lockexp(B) := slot(B) + 2 ^ confcount(B)`. - -The validator `i` runs a voting loop as follows. Let `B` be the heaviest -block returned by the fork choice rule above [Fork Choice](#fork-choice). If `i` has not voted for `B` before, then `i` votes for `B` so long as the following conditions are satisfied: - -1. Respecting lockouts: For any block `B′` in the tower that is not an ancestor of `B`, `lockexp(B′) ≤ slot(B)`. -2. Threshold check: Described above in [Threshold Check](#threshold-check) -3. Switching threshold: Have sufficiently many votes on other forks if switching forks. Let `Btop` denote the block at the top of the stack. If `Btop` is not an ancestor of `B`, then: - - Let `VBtop ⊆ V` be the set of votes on `Btop` or ancestors or descendents of `Btop`. - - We need `|V \ VBtop | > 38%`. More details on this can be found in [Optimistic Confirmation](../proposals/optimistic_confirmation.md) - -If all the conditions are satisfied and validator `i` votes for block `B` then it adjusts its tower as follows (same rules described above in [Vote Tower](#vote-tower)). -1. Remove expired blocks top down. Let `x := l - 1`. While `x >= 0 && lockexp(T(x)) < slot(B)`, remove `T(x)` from the tower, and set `l := l - 1` and `x := x - 1`. -2. Add block to tower. `T(l) := B`, `confcount(B) := 1`, and set `l := l + 1`. -3. Double lockouts. For each element `B = T(x)` if `l > x + confcount(B)`, then `confcount(B) := confcount(B) + 1`. - -## PoH ASIC Resistance - -Votes and lockouts grow exponentially while ASIC speed up is linear. There are possible attack vectors involving a faster ASIC outlined below. - -### ASIC Rollback - -An attacker generates a concurrent fork from an older block to try to rollback the cluster. In this attack the concurrent fork is competing with forks that have already been voted on. This attack is limited by the exponential growth of the lockouts. - -- 1 vote has a lockout of 2 slots. Concurrent fork must be at least 2 slots ahead, and be produced in 1 slot. Therefore requires an ASIC 2x faster. -- 2 votes have a lockout of 4 slots. Concurrent fork must be at least 4 slots ahead and produced in 2 slots. Therefore requires an ASIC 2x faster. -- 3 votes have a lockout of 8 slots. Concurrent fork must be at least 8 slots ahead and produced in 3 slots. Therefore requires an ASIC 2.6x faster. -- 10 votes have a lockout of 1024 slots. 1024/10, or 102.4x faster ASIC. -- 20 votes have a lockout of 2^20 slots. 2^20/20, or 52,428.8x faster ASIC. diff --git a/docs/src/implemented-proposals/transaction-fees.md b/docs/src/implemented-proposals/transaction-fees.md deleted file mode 100644 index 1163c3fcaed..00000000000 --- a/docs/src/implemented-proposals/transaction-fees.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Deterministic Transaction Fees ---- - -## Calculating fees - -Before sending a transaction to the cluster, a client may query the network to -determine what the transaction's fee will be via the rpc request -[getFeeForMessage](https://solana.com/docs/rpc/http/getfeeformessage). - -## Fee Parameters - -The fee is based on the number of signatures in the transaction, the more -signatures a transaction contains, the higher the fee. In addition, a -transaction can specify an additional fee that determines how the transaction is -relatively prioritized against others. A transaction's prioritization fee is -calculated by multiplying the number of compute units by the compute unit price -(measured in micro-lamports) set by the transaction via compute budget -instructions. diff --git a/docs/src/implemented-proposals/validator-timestamp-oracle.md b/docs/src/implemented-proposals/validator-timestamp-oracle.md deleted file mode 100644 index cc0bcef54ad..00000000000 --- a/docs/src/implemented-proposals/validator-timestamp-oracle.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: Validator Timestamp Oracle ---- - -Third-party users of Solana sometimes need to know the real-world time a block -was produced, generally to meet compliance requirements for external auditors or -law enforcement. This proposal describes a validator timestamp oracle that -would allow a Solana cluster to satisfy this need. - -The general outline of the proposed implementation is as follows: - -- At regular intervals, each validator records its observed time for a known slot - on-chain (via a Timestamp added to a slot Vote) -- A client can request a block time for a rooted block using the `getBlockTime` - RPC method. When a client requests a timestamp for block N: - - 1. A validator determines a "cluster" timestamp for a recent timestamped slot - before block N by observing all the timestamped Vote instructions recorded on - the ledger that reference that slot, and determining the stake-weighted mean - timestamp. - - 2. This recent mean timestamp is then used to calculate the timestamp of - block N using the cluster's established slot duration - -Requirements: - -- Any validator replaying the ledger in the future must come up with the same - time for every block since genesis -- Estimated block times should not drift more than an hour or so before resolving - to real-world (oracle) data -- The block times are not controlled by a single centralized oracle, but - ideally based on a function that uses inputs from all validators -- Each validator must maintain a timestamp oracle - -The same implementation can provide a timestamp estimate for a not-yet-rooted -block. However, because the most recent timestamped slot may or may not be -rooted yet, this timestamp would be unstable (potentially failing requirement -1). Initial implementation will target rooted blocks, but if there is a use case -for recent-block timestamping, it will be trivial to add the RPC apis in the -future. - -## Recording Time - -At regular intervals as it is voting on a particular slot, each validator -records its observed time by including a timestamp in its Vote instruction -submission. The corresponding slot for the timestamp is the newest Slot in the -Vote vector (`Vote::slots.iter().max()`). It is signed by the validator's -identity keypair as a usual Vote. In order to enable this reporting, the Vote -struct needs to be extended to include a timestamp field, `timestamp: Option`, which will be set to `None` in most Votes. - -As of https://github.com/solana-labs/solana/pull/10630, validators submit a -timestamp every vote. This enables implementation of a block time caching -service that allows nodes to calculate the estimated timestamp immediately after -the block is rooted, and cache that value in Blockstore. This provides -persistent data and quick queries, while still meeting requirement 1) above. - -### Vote Accounts - -A validator's vote account will hold its most recent slot-timestamp in VoteState. - -### Vote Program - -The on-chain Vote program needs to be extended to process a timestamp sent with -a Vote instruction from validators. In addition to its current process_vote -functionality (including loading the correct Vote account and verifying that the -transaction signer is the expected validator), this process needs to compare the -timestamp and corresponding slot to the currently stored values to verify that -they are both monotonically increasing, and store the new slot and timestamp in -the account. - -## Calculating Stake-Weighted Mean Timestamp - -In order to calculate the estimated timestamp for a particular block, a -validator first needs to identify the most recently timestamped slot: - -```text -let timestamp_slot = floor(current_slot / timestamp_interval); -``` - -Then the validator needs to gather all Vote WithTimestamp transactions from the -ledger that reference that slot, using `Blockstore::get_slot_entries()`. As these -transactions could have taken some time to reach and be processed by the leader, -the validator needs to scan several completed blocks after the timestamp_slot to -get a reasonable set of Timestamps. The exact number of slots will need to be -tuned: More slots will enable greater cluster participation and more timestamp -datapoints; fewer slots will speed how long timestamp filtering takes. - -From this collection of transactions, the validator calculates the -stake-weighted mean timestamp, cross-referencing the epoch stakes from -`staking_utils::staked_nodes_at_epoch()`. - -Any validator replaying the ledger should derive the same stake-weighted mean -timestamp by processing the Timestamp transactions from the same number of -slots. - -## Calculating Estimated Time for a Particular Block - -Once the mean timestamp for a known slot is calculated, it is trivial to -calculate the estimated timestamp for subsequent block N: - -```text -let block_n_timestamp = mean_timestamp + (block_n_slot_offset * slot_duration); -``` - -where `block_n_slot_offset` is the difference between the slot of block N and -the timestamp_slot, and `slot_duration` is derived from the cluster's -`slots_per_year` stored in each Bank diff --git a/docs/src/index.mdx b/docs/src/index.mdx deleted file mode 100644 index 6314ed65581..00000000000 --- a/docs/src/index.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -slug: / -id: home -title: Home -sidebar_label: Home -pagination_label: Agave Validator Documentation Home -description: "Agave is a validator for the Solana blockchain maintained by the Anza core engineering team \ - it is a fork of the original Solana validator software." ---- - -# Agave Validator Documentation - -Solana is a blockchain built for mass adoption. It's a high performance network -that is utilized for a range of use cases, including finance, NFTs, payments, -and gaming. Solana operates as a single global state machine, and is open, -interoperable and decentralized. Agave is a fork of the original Solana validator -previously maintained by the Solana Labs team. Agave is now under active development by the -core engineering team at Anza, one of several Solana validator clients. - -## Command Line Interface and Tool Suite - -To get started using the Solana Command Line (CLI) tools: - -- [Install the Solana CLI Tool Suite](./cli/install.md) - Quickly get setup - locally with the CLI, optionally build from source -- [Introduction to the CLI conventions](./cli/intro.md) - Understand the common - conventions used within the CLI tool suite -- [Choose a cluster](./cli/examples/choose-a-cluster.md) - Select a Solana - network cluster to connect (e.g. `devnet`, `testnet`, `mainnet-beta`) -- [Create a wallet](./cli/wallets/index.md) - Create a command line wallet for - use within the CLI and beyond - -## Understanding the Architecture - -Get to know the underlying architecture of how the proof-of-stake blockchain -works: - -- [Clusters](./clusters/index.md) - a collection of validators that work - together for consensus -- [Validators](./validator/anatomy.md) - the individual nodes that are the - backbone of the network -- [Runtime](./runtime/programs.md) - the native programs that are core to the - validator and the blockchain - -## Running a Validator - -Explore what it takes to operate an Agave validator and help secure the network. - -- [Validator vs RPC node](./operations/validator-or-rpc-node.md) - Understand - the important differences between voting and non-voting validators on the - network -- [System requirements](./operations/requirements.md) - Recommended hardware - requirements and expected SOL needed to operate a validator -- [Quick start guide](./operations/setup-a-validator.md) - Setup a validator and - get connected to a cluster for the first time - -## Learn more - -import HomeCtaLinks from "@site/components/HomeCtaLinks"; - - diff --git a/docs/src/operations/_category_.json b/docs/src/operations/_category_.json deleted file mode 100644 index 289f63ff950..00000000000 --- a/docs/src/operations/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "position": 4, - "label": "Operating a Validator", - "collapsible": true, - "collapsed": true -} diff --git a/docs/src/operations/best-practices/_category_.json b/docs/src/operations/best-practices/_category_.json deleted file mode 100644 index caf50117dc3..00000000000 --- a/docs/src/operations/best-practices/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "position": 7, - "label": "Best Practices", - "collapsible": true, - "collapsed": true, - "link": null -} diff --git a/docs/src/operations/best-practices/general.md b/docs/src/operations/best-practices/general.md deleted file mode 100644 index 18c8d73cceb..00000000000 --- a/docs/src/operations/best-practices/general.md +++ /dev/null @@ -1,197 +0,0 @@ ---- -title: Agave Validator Operations Best Practices -sidebar_label: General Operations -pagination_label: "Best Practices: Validator Operations" ---- - -After you have successfully setup and started a -[validator on testnet](../setup-a-validator.md) (or another cluster -of your choice), you will want to become familiar with how to operate your -validator on a day-to-day basis. During daily operations, you will be -[monitoring your server](./monitoring.md), updating software regularly (both the -Solana validator software and operating system packages), and managing your vote -account and identity account. - -All of these skills are critical to practice. Maximizing your validator uptime -is an important part of being a good operator. - -## Educational Workshops - -The Solana validator community holds regular educational workshops. You can -watch past workshops through the -[Solana validator educational workshops playlist](https://www.youtube.com/watch?v=86zySQ5vGW8&list=PLilwLeBwGuK6jKrmn7KOkxRxS9tvbRa5p). - -## Community Validator calls - -The Solana validator community holds regular calls. -There is the 'Solana Foundation Validator Discussion' which is hosted by the Solana Foundation and the 'Community Led Validator Call' -which is hosted by the community itself. - -### Solana Foundation Validator Discussion - -This is a monthly call that is hosted by the Solana Foundation. -- Schedule: every second Thursday of the month 18:00 CET -- Agenda: See [validator-announcements channel in Discord](https://discord.com/channels/428295358100013066/586252910506016798). -- This call **is recorded** and past calls can be watched back on the [Community Validator Discussions playlist](https://www.youtube.com/playlist?list=PLilwLeBwGuK78yjGBZwYhTf7rao0t13Zw) - -### Community Led Validator Call - -This is also a monthly call hosted by the Solana validator community itself. -- Schedule: every fourth Thursday of the month 18:00 CET -- Agenda: See [HackMD site](https://hackmd.io/1DFauFMWTZG37-U7CXhxMg?view#Solana-Community-Validator-Call-Agendas). -- This call is **not recorded** - -***Please note that the scheduling of these calls can be changed last minute due to any circumstances. For the most up-to-date information go to the [validator-announcements channel in Discord](https://discord.com/channels/428295358100013066/586252910506016798).*** - -## Help with the validator command line - -From within the Solana CLI, you can execute the `agave-validator` command with -the `--help` flag to get a better understanding of the flags and sub commands -available. - -``` -agave-validator --help -``` - -## Restarting your validator - -There are many operational reasons you may want to restart your validator. As a -best practice, you should avoid a restart during a leader slot. A -[leader slot](https://solana.com/docs/terminology#leader-schedule) is the time -when your validator is expected to produce blocks. For the health of the cluster -and also for your validator's ability to earn transaction fee rewards, you do -not want your validator to be offline during an opportunity to produce blocks. - -To see the full leader schedule for an epoch, use the following command: - -``` -solana leader-schedule -``` - -Based on the current slot and the leader schedule, you can calculate open time -windows where your validator is not expected to produce blocks. - -Assuming you are ready to restart, you may use the `agave-validator exit` -command. The command exits your validator process when an appropriate idle time -window is reached. Assuming that you have systemd implemented for your validator -process, the validator should restart automatically after the exit. See the -below help command for details: - -``` -agave-validator exit --help -``` - -## Upgrading - -There are many ways to upgrade the -[Solana CLI software](../../cli/install.md). As an operator, you -will need to upgrade often, so it is important to get comfortable with this -process. - -> **Note** validator nodes do not need to be offline while the newest version is -> being built from source. All methods below can be done before -> the validator process is restarted. - -### Building the newest version from source - -The easiest way to upgrade the Solana CLI software is to build the newest -version from source. See the -[build from source](../../cli/install.md#build-from-source) instructions for details. - -### Restart - -The validator process will need to be restarted before -the newly installed version is in use. Use `agave-validator exit` to restart -your validator process. - -### Verifying version - -The best way to verify that your validator process has changed to the desired -version is to grep the logs after a restart. The following grep command should -show you the version that your validator restarted with: - -``` -grep -B1 'Starting validator with' -``` - -## Snapshots - -Validators operators who have not experienced significant downtime (multiple -hours of downtime), should avoid downloading snapshots. It is important for the -health of the cluster as well as your validator history to maintain the local -ledger. Therefore, you should not download a new snapshot any time your -validator is offline or experiences an issue. Downloading a snapshot should only -be reserved for occasions when you do not have local state. Prolonged downtime -or the first install of a new validator are examples of times when you may not -have state locally. In other cases, such as restarts for upgrades, a snapshot -download should be avoided. - -To avoid downloading a snapshot on restart, add the following flag to the -`agave-validator` command: - -``` ---no-snapshot-fetch -``` - -If you use this flag with the `agave-validator` command, make sure that you run -`solana catchup ` after your validator starts to make sure that the -validator is catching up in a reasonable time. After some time (potentially a -few hours), if it appears that your validator continues to fall behind, then you -may have to download a new snapshot. - -### Downloading Snapshots - -If you are starting a validator for the first time, or your validator has fallen -too far behind after a restart, then you may have to download a snapshot. - -To download a snapshot, you must **_NOT_** use the `--no-snapshot-fetch` flag. -Without the flag, your validator will automatically download a snapshot from -your known validators that you specified with the `--known-validator` flag. - -If one of the known validators is downloading slowly, you can try adding the -`--minimal-snapshot-download-speed` flag to your validator. This flag will -switch to another known validator if the initial download speed is below the -threshold that you set. - -## Regularly Check Account Balances - -It is important that you do not accidentally run out of funds in your identity -account, as your node will stop voting. It is also important to note that this -account keypair is the most vulnerable of the three keypairs in a vote account -because the keypair for the identity account is stored on your validator when -running the `agave-validator` software. How much SOL you should store there is -up to you. As a best practice, make sure to check the account regularly and -refill or deduct from it as needed. To check the account balance do: - -``` -solana balance validator-keypair.json -``` - -> **Note** `agave-watchtower` can monitor for a minimum validator identity -> balance. See [monitoring best practices](./monitoring.md) for details. - -## Withdrawing From The Vote Account - -As a reminder, your withdrawer's keypair should **_NEVER_** be stored on your -server. It should be stored on a hardware wallet, paper wallet, or multisig -mitigates the risk of hacking and theft of funds. - -To withdraw your funds from your vote account, you will need to run -`solana withdraw-from-vote-account` on a trusted computer. For example, on a -trusted computer, you could withdraw all of the funds from your vote account -(excluding the rent exempt minimum). The below example assumes you have a -separate keypair to store your funds called `person-keypair.json` - -``` -solana withdraw-from-vote-account \ - vote-account-keypair.json \ - person-keypair.json ALL \ - --authorized-withdrawer authorized-withdrawer-keypair.json -``` - -To get more information on the command, use -`solana withdraw-from-vote-account --help`. - -For a more detailed explanation of the different keypairs and other related -operations refer to -[vote account management](../guides/vote-accounts.md). diff --git a/docs/src/operations/best-practices/monitoring.md b/docs/src/operations/best-practices/monitoring.md deleted file mode 100644 index f681b55db82..00000000000 --- a/docs/src/operations/best-practices/monitoring.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: Agave Validator Monitoring Best Practices -sidebar_label: Monitoring -pagination_label: "Best Practices: Validator Monitoring" ---- - -It is essential that you have monitoring in place on your validator. In the event that your validator is delinquent (behind the rest of the network) you want to respond immediately to fix the issue. One very useful tool to monitor your validator is [`agave-watchtower`](#agave-watchtower). - -## Agave Watchtower - -Agave Watchtower is an extremely useful monitoring tool that will regularly monitor the health of your validator. It can monitor your validator for delinquency then notify you on your application of choice: Slack, Discord, Telegram or Twilio. Additionally, `agave-watchtower` has the ability to monitor the health of the entire cluster so that you can be aware of any cluster wide problems. - -### Getting Started - -To get started with Agave Watchtower, run `agave-watchtower --help`. From the help menu, you can see the optional flags and an explanation of the command. - -Here is a sample command that will monitor a validator node with an identity public key of `2uTk98rqqwENevkPH2AHHzGHXgeGc1h6ku8hQUqWeXZp`: - -``` -agave-watchtower --monitor-active-stake --validator-identity \ - 2uTk98rqqwENevkPH2AHHzGHXgeGc1h6ku8hQUqWeXZp -``` - -The command will monitor your validator, but you will not get notifications unless you added the environment variables mentioned in `agave-watchtower --help`. Since getting each of these services setup for notifications is not straight forward, the next section will walk through [setting up watchtower notifications on Telegram](#setup-telegram-notifications). - -### Best Practices - -It is a best practice to run the `agave-watchtower` command on a separate server from your validator. - -In the case that you run `agave-watchtower` on the same computer as your `agave-validator` process, then during catastrophic events like a power outage, you will not be aware of the issue, because your `agave-watchtower` process will stop at the same time as your `agave-validator` process. - -Additionally, while running the `agave-watchtower` process manually with environment variables set in the terminal is a good way to test out the command, it is not operationally sound because the process will not be restarted when the terminal closes or during a system restart. - -Instead, you could run your `agave-watchtower` command as a system process similar to `agave-validator`. In the system process file, you can specify the environment variables for your bot. - -### Setup Telegram Notifications - -To send validator health notifications to your Telegram account, we are going to do a few things: - -1. Create a bot to send the messages. The bot will be created using BotFather on Telegram -2. Send a message to the bot -3. Create a Telegram group that will get the watchtower notifications -4. Add the environment variables to your command line environment -5. Restart the `agave-watchtower` command - -#### Create a Bot Using BotFather - -In Telegram, search for `@BotFather`. Send the following message to _@BotFather_: `/newbot`. - -Now you will have to come up with a name for the bot. The only requirement is that it cannot have dashes or spaces, and it **must** end in the word `bot`. Many names have already been taken, so you may have to try a few. Once you find an available name, you will get a response from _@BotFather_ that includes a link to chat with the bot as well as a token for the bot. Take note of the token. You will need it when you setup your environment variables. - -#### Send a Message to The Bot - -Find the bot in Telegram and send it the following message: `/start`. Messaging the bot will help you later when looking for the bot chatroom id. - -#### Create Telegram Group - -In Telegram, click on the new message icon and then select new group. Find your newly created bot and add the bot to the group. Next, name the group whatever you'd like. - -#### Set Environment Variables For Watchtower - -Now that you have a bot setup, you will need to set the environment variables for the bot so that watchtower can send notifications. - -First, recall the chat message that you got from _@BotFather_. In the message, there was an HTTP API token for your bot. The token will have this format: `389178471:MMTKMrnZB4ErUzJmuFIXTKE6DupLSgoa7h4o`. You will use that token to set the `TELEGRAM_BOT_TOKEN` environment variable. In the terminal where you plan to run `agave-watchtower`, run the following: - -``` -export TELEGRAM_BOT_TOKEN= -``` - -Next, you need the chat id for your group so that `solana-watcher` knows where to send the message. First, send a message to your bot in the chat group that you created. Something like `@newvalidatorbot hello`. - -Next, in your browser, go to `https://api.telegram.org/bot/getUpdates`. Make sure to replace `` with your API token that you got in the _@BotFather_ message. Also make sure that you include the word `bot` in the URL before the API token. Make the request in the browser. - -The response should be in JSON. Search for the string `"chat":` in the JSON. The `id` value of that chat is your `TELEGRAM_CHAT_ID`. It will be a negative number like: `-781559558`. Remember to include the negative sign! If you cannot find `"chat":` in the JSON, then you may have to remove the bot from your chat group and add it again. - -With your Telegram chat id in hand, export the environment variable where you plan to run `agave-watchtower`: - -``` -export TELEGRAM_CHAT_ID= -``` - -#### Restart agave-watchtower - -Once your environment variables are set, restart `agave-watchtower`. You should see output about your validator. - -To test that your Telegram configuration is working properly, you could stop your validator briefly until it is labeled as delinquent. Up to a minute after the validator is delinquent, you should receive a message in the Telegram group from your bot. Start the validator again and verify that you get another message in your Telegram group from the bot. The message should say `all clear`. - -## Collecting metrics - -It is important to collect metrics: it helps diagnose existing problems and allows to anticipate future ones. - -### metrics.solana.com - -There are several public dashboards available, one of them is hosted at [metrics.solana.com](https://metrics.solana.com). Reporting to the solana.com public dashboard is even required if you participate in the [Solana Foundation Delegation Program](https://solana.org/delegation-program). Using it is done by simply setting the `$SOLANA_METRICS_CONFIG` variable in your validator's environment (e.g. at the beginning of your `validator.sh` script). - -Refer to the [available Solana clusters documentation](../../clusters/available.md) to get the appropriate value of `$SOLANA_METRICS_CONFIG` for your validator. diff --git a/docs/src/operations/best-practices/security.md b/docs/src/operations/best-practices/security.md deleted file mode 100644 index bbaf120bc04..00000000000 --- a/docs/src/operations/best-practices/security.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Agave Validator Security Best Practices -sidebar_label: Security -pagination_label: "Best Practices: Validator Security" ---- - -Being a system administrator for an Ubuntu computer requires technical knowledge of the system and best security practices. The following list should help you get started and is considered the bare minimum for keeping your system safe. - -## Keep Your System Up To Date - -Make sure to regularly update packages in your Ubuntu system. Out of date packages may contain known security vulnerabilities that a hacker could exploit. A good practice would be to update weekly at least. To update your system, do the following: - -``` -sudo apt update -sudo apt upgrade -``` - -## DO NOT Store Your Withdrawer Key On Your Validator Machine - -Your withdrawer key gives the operator full control of the vote account. It is highly sensitive information and should not be stored on the validator itself. - -There are a number of options for withdrawer key management. Some operators choose to use hardware wallets or paper wallets for the withdrawer keypair. Another option is a multisig where each key of the multisig is a hardware wallet or paper wallet. Whichever option you choose, make sure the authorized withdrawer key is stored securely and that it has been generated on a trusted computer (other than your validator computer). - -To reiterate, the withdrawer keypair should never be stored on your validator at any time. - -## DO NOT Run The Solana Validator as a Root User - -It may be easier to get started by running your application as root, but it is a bad practice. - -If there is an exploit in your system, a hacker could have full access if your Solana application is running as the `root` user. Instead, see the [setup instructions](../setup-a-validator.md#sol-user) for creating a user called `sol` and running the application as the `sol` user. - -## Close Ports That Are Not In Use - -Your system should close all ports that do not need to be open to the outside world. A common firewall for closing ports is `ufw` (uncomplicated firewall). You can find a guide to using `ufw` from [Digital Ocean](https://www.digitalocean.com/community/tutorials/ufw-essentials-common-firewall-rules-and-commands). - -## Eliminate Brute Force Attacks With fail2ban - -[fail2ban](https://github.com/fail2ban/fail2ban) is a network security tool that checks your logs for suspicious login attempts and bans those IP addresses after repeated attempts. This will help mitigate brute force attacks on your server. - -The default setup should work out-of-the-box by simply installing `fail2ban`: - -``` -sudo apt install fail2ban -``` - -## DO NOT Use Password Authentication for SSH - -In addition to installing `fail2ban`, it is recommended to disable password based authentication for SSH access. SSH key based authentication is preferred. diff --git a/docs/src/operations/guides/_category_.json b/docs/src/operations/guides/_category_.json deleted file mode 100644 index 133867d4088..00000000000 --- a/docs/src/operations/guides/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "position": 8, - "label": "Validator Guides", - "collapsible": true, - "collapsed": true, - "link": null -} diff --git a/docs/src/operations/guides/restart-cluster.md b/docs/src/operations/guides/restart-cluster.md deleted file mode 100644 index 0d3178b36a3..00000000000 --- a/docs/src/operations/guides/restart-cluster.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -title: "Restarting a Solana Cluster" -# really high number to ensure it is listed last in the sidebar -sidebar_position: 999 -sidebar_label: Restart a Cluster -pagination_label: "Validator Guides: Restart a Cluster" ---- - -### Step 1. Identify the latest optimistically confirmed slot for the cluster - -In Agave 1.14 or greater, run the following command to output the latest -optimistically confirmed slot your validator observed: -```bash -agave-ledger-tool -l ledger latest-optimistic-slots -``` - -In Agave 1.13 or less, the latest optimistically confirmed can be found by looking for the more recent occurrence of -[this](https://github.com/solana-labs/solana/blob/0264147d42d506fb888f5c4c021a998e231a3e74/core/src/optimistic_confirmation_verifier.rs#L71) -metrics datapoint. - -Call this slot `SLOT_X` - -Note that it's possible that some validators observed an optimistically -confirmed slot that's greater than others before the outage. Survey the other -validators on the cluster to ensure that a greater optimistically confirmed slot -does not exist before proceeding. If a greater slot value is found use it -instead. - - -### Step 2. Stop the validator(s) - -### Step 3. Optionally install the new solana version - -### Step 4. Create a new snapshot for slot `SLOT_X` with a hard fork at slot `SLOT_X` - -```bash -$ agave-ledger-tool -l --snapshot-archive-path --incremental-snapshot-archive-path create-snapshot SLOT_X --hard-fork SLOT_X -``` - -The snapshots directory should now contain the new snapshot. -`agave-ledger-tool create-snapshot` will also output the new shred version, and bank hash value, -call this NEW_SHRED_VERSION and NEW_BANK_HASH respectively. - -Adjust your validator's arguments: - -```bash - --wait-for-supermajority SLOT_X - --expected-bank-hash NEW_BANK_HASH -``` - -Then restart the validator. - -Confirm with the log that the validator booted and is now in a holding pattern at `SLOT_X`, waiting for a super majority. - -Once NEW_SHRED_VERSION is determined, nudge foundation entrypoint operators to update entrypoints. - -### Step 5. Announce the restart on Discord: - -Post something like the following to #announcements (adjusting the text as appropriate): - -> Hi @Validators, -> -> We've released v1.1.12 and are ready to get testnet back up again. -> -> Steps: -> -> 1. Install the v1.1.12 release: https://github.com/solana-labs/solana/releases/tag/v1.1.12 -> 2. a. Preferred method, start from your local ledger with: -> -> ```bash -> agave-validator -> --wait-for-supermajority SLOT_X # <-- NEW! IMPORTANT! REMOVE AFTER THIS RESTART -> --expected-bank-hash NEW_BANK_HASH # <-- NEW! IMPORTANT! REMOVE AFTER THIS RESTART -> --hard-fork SLOT_X # <-- NEW! IMPORTANT! REMOVE AFTER THIS RESTART -> --no-snapshot-fetch # <-- NEW! IMPORTANT! REMOVE AFTER THIS RESTART -> --entrypoint entrypoint.testnet.solana.com:8001 -> --known-validator 5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on -> --expected-genesis-hash 4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY -> --only-known-rpc -> --limit-ledger-size -> ... # <-- your other --identity/--vote-account/etc arguments -> ``` -> -> b. If your validator doesn't have ledger up to slot SLOT_X or if you have deleted your ledger, have it instead download a snapshot with: -> -> ```bash -> agave-validator -> --wait-for-supermajority SLOT_X # <-- NEW! IMPORTANT! REMOVE AFTER THIS RESTART -> --expected-bank-hash NEW_BANK_HASH # <-- NEW! IMPORTANT! REMOVE AFTER THIS RESTART -> --entrypoint entrypoint.testnet.solana.com:8001 -> --known-validator 5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on -> --expected-genesis-hash 4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY -> --only-known-rpc -> --limit-ledger-size -> ... # <-- your other --identity/--vote-account/etc arguments -> ``` -> -> You can check for which slots your ledger has with: `agave-ledger-tool -l path/to/ledger bounds` -> -> 3. Wait until 80% of the stake comes online -> -> To confirm your restarted validator is correctly waiting for the 80%: -> a. Look for `N% of active stake visible in gossip` log messages -> b. Ask it over RPC what slot it's on: `solana --url http://127.0.0.1:8899 slot`. It should return `SLOT_X` until we get to 80% stake -> -> Thanks! - -### Step 7. Wait and listen - -Monitor the validators as they restart. Answer questions, help folks, - -## Troubleshooting - -### 80% of the stake didn't participate in the restart, now what? -If less than 80% of the stake join the restart after a reasonable amount of -time, it will be necessary to retry the restart attempt with the stake from the -non-responsive validators removed. - -The community should identify and come to social consensus on the set of -non-responsive validators. Then all participating validators return to Step 4 -and create a new snapshot with additional `--destake-vote-account ` -arguments for each of the non-responsive validator's vote account address - -```bash -$ agave-ledger-tool -l ledger create-snapshot SLOT_X ledger --hard-fork SLOT_X \ - --destake-vote-account \ - --destake-vote-account \ - . - . - --destake-vote-account \ -``` - -This will cause all stake associated with the non-responsive validators to be -immediately deactivated. All their stakers will need to re-delegate their stake -once the cluster restart is successful. diff --git a/docs/src/operations/guides/validator-failover.md b/docs/src/operations/guides/validator-failover.md deleted file mode 100644 index e9469ae61f7..00000000000 --- a/docs/src/operations/guides/validator-failover.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: "Validator Guide: Setup Node Failover" -sidebar_position: 9 -sidebar_label: Node Failover -pagination_label: "Validator Guides: Node Failover" ---- - -A simple two machine instance failover method is described here, which allows you to: -* Upgrade your validator software with virtually no downtime, and -* Failover to the secondary instance when your monitoring detects a problem with the primary instance -without any safety issues that would otherwise be associated with running two instances of your validator. - -You will need: -* Two non-delinquent validator nodes -* Identities that are not associated with a staked vote account on both validators to use when not actively voting -* Validator startup scripts both modified to use a symbolic link as the identity -* Validator startup scripts both modified to include staked identity as authorized voter - -## Setup - -### Generating an Unstaked Secondary Identity - -Both validators need to have secondary (unstaked) identities to assume when not actively voting. -You can generate these secondary identities on each of your validators like so: -``` -solana-keygen new -s --no-bip39-passphrase -o unstaked-identity.json -``` -### Validator Startup Script Modifications - -The identity flag and authorized voter flags should be modified on both validators. - -Note that `identity.json` is not a real file but a symbolic link we will create shortly. -However, the authorized voter flag does need to point to the staked identity file (your main identity). -In this guide, the main identity is renamed to `staked-identity.json` for clarity and simplicity. -You can certainly name your main identity file however you'd like; make sure it is specified as an authorized voter as shown below: - -``` -exec /home/sol/bin/agave-validator \ - --identity /home/sol/identity.json \ - --vote-account /home/sol/vote.json \ - --authorized-voter /home/sol/staked-identity.json \ -``` - -Summary: - -* Identity is a symbolic link we will create in the next section. It may point to your staked identity or your inactive unstaked identity. -* No changed to the vote account, just shown for context. -* Authorized voter points to your main, staked identity. - -### Creating Identity Symlinks -An important part of how this system functions is the identity.json symbolic link. -This link allows us to soft link the desired identity so that the validator can restart or stop/start with the same identity we last intended it to have. - -On your actively voting validator, link this to your staked identity -``` -ln -sf /home/sol/staked-identity.json /home/sol/identity.json -``` - -On your inactive, non-voting validator, link this to your unstaked identity -``` -ln -sf /home/sol/unstaked-identity.json /home/sol/identity.json -``` - -### Transition Preparation Checklist -At this point, you should have completed the following on each validator: -* Generated an unstaked identity -* Updated your validator startup script -* Created a symbolic link to point to the respective identity file - -If you have done this - great! You're ready to transition! - -### Transition Process -#### Active Validator -* Wait for a restart window -* Set identity to unstaked identity -* Correct symbolic link to reflect this change -* Copy the tower file to the inactive validator - -``` -#!/bin/bash - -# example script of the above steps - change specifics such as user / IP / ledger path -agave-validator -l /mnt/ledger wait-for-restart-window --min-idle-time 2 --skip-new-snapshot-check -agave-validator -l /mnt/ledger set-identity /home/sol/unstaked-identity.json -ln -sf /home/sol/unstaked-identity.json /home/sol/identity.json -scp /mnt/ledger/tower-1_9-$(solana-keygen pubkey /home/sol/staked-identity.json).bin @/mnt/ledger -``` - -(At this point your primary identity is no longer voting) - -#### Inactive Validator -* Set identity to your staked identity (requiring the tower) -* Rewrite the symbolic link to reflect this - -``` -#!/bin/bash - -# example script of the above steps -agave-validator -l /mnt/ledger set-identity --require-tower /home/sol/staked-identity.json -ln -sf /home/sol/staked-identity.json /home/sol/identity.json -``` - -### Verification -Verify identities transitioned successfully using either `agave-validator monitor` or `solana catchup --our-localhost 8899` diff --git a/docs/src/operations/guides/validator-info.md b/docs/src/operations/guides/validator-info.md deleted file mode 100644 index 56b74f732c4..00000000000 --- a/docs/src/operations/guides/validator-info.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: "Validator Guide: Publishing Validator Info" -sidebar_position: 1 -sidebar_label: Publishing Validator Info -pagination_label: "Validator Guides: Publishing Validator Info" ---- - -You can publish your validator information to the chain to be publicly visible to other users. - -## Run solana validator-info - -Run the solana CLI to populate a validator info account: - -```bash -solana validator-info publish --keypair ~/validator-keypair.json -``` - -For details about optional fields for VALIDATOR_INFO_ARGS: - -```bash -solana validator-info publish --help -``` - -The recommended dimensions for the validator icon are 360x360px and PNG format. - -## Example Commands - -Example publish command: - -```bash -solana validator-info publish "Elvis Validator" -w "https://elvis-validates.com" -i "https://elvis-validates.com/my-icon.png" -``` - -Example query command: - -```bash -solana validator-info get -``` - -which outputs - -```text -Validator info from 8WdJvDz6obhADdxpGCiJKZsDYwTLNEDFizayqziDc9ah - Validator pubkey: 6dMH3u76qZ7XG4bVboVRnBHR2FfrxEqTTTyj4xmyDMWo - Info: {"iconUrl":"elvis","name":"Elvis Validator","website":"https://elvis-validates.com"} -``` - -For older accounts instead of `iconUrl` you might see `keybaseUsername` as those accounts used Keybase for their validator icon (see next section for further information). - -## Keybase - -Previously Keybase was used by validators to provide their validator icon, however Keybase has sunset its service and thus is no longer supported. Some old validator info accounts will still contain keybase usernames this is reflected in the serialized data returned when querying these validator info accounts. diff --git a/docs/src/operations/guides/validator-monitor.md b/docs/src/operations/guides/validator-monitor.md deleted file mode 100644 index 5e314c52e82..00000000000 --- a/docs/src/operations/guides/validator-monitor.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: "Validator Guide: Monitoring a Validator" -sidebar_position: 2 -sidebar_label: Monitoring a Validator -pagination_label: "Validator Guides: Monitoring a Validator" ---- - -## Check Gossip - -Confirm the IP address and **identity pubkey** of your validator is visible in -the gossip network by running: - -```bash -solana gossip -``` - -## Check Your Balance - -Your account balance should decrease by the transaction fee amount as your -validator submits votes, and increase after serving as the leader. Pass the -`--lamports` are to observe in finer detail: - -```bash -solana balance --lamports -``` - -## Check Vote Activity - -The `solana vote-account` command displays the recent voting activity from -your validator: - -```bash -solana vote-account ~/vote-account-keypair.json -``` - -## Get Cluster Info - -There are several useful JSON-RPC endpoints for monitoring your validator on the -cluster, as well as the health of the cluster: - -```bash -# Similar to solana-gossip, you should see your validator in the list of cluster nodes -curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getClusterNodes"}' http://api.devnet.solana.com -# If your validator is properly voting, it should appear in the list of `current` vote accounts. If staked, `stake` should be > 0 -curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getVoteAccounts"}' http://api.devnet.solana.com -# Returns the current leader schedule -curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getLeaderSchedule"}' http://api.devnet.solana.com -# Returns info about the current epoch. slotIndex should progress on subsequent calls. -curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getEpochInfo"}' http://api.devnet.solana.com -``` diff --git a/docs/src/operations/guides/validator-stake.md b/docs/src/operations/guides/validator-stake.md deleted file mode 100644 index 4c550cd3e8a..00000000000 --- a/docs/src/operations/guides/validator-stake.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -title: "Validator Guide: Staking" -sidebar_position: 3 -sidebar_label: Staking -pagination_label: "Validator Guides: Staking" ---- - -**By default your validator will have no stake.** This means it will be -ineligible to become leader, and unable to land votes. - -## Monitoring Catch Up - -To delegate stake, first make sure your validator is running and has caught up -to the cluster. It may take some time to catch up after your validator boots. -Use the `catchup` command to monitor your validator through this process: - -```bash -solana catchup ~/validator-keypair.json -``` - -Until your validator has caught up, it will not be able to vote successfully and -stake cannot be delegated to it. - -Also if you find the cluster's slot advancing faster than yours, you will likely -never catch up. This typically implies some kind of networking issue between -your validator and the rest of the cluster. - -## Create Stake Keypair - -If you haven’t already done so, create a staking keypair. If you have completed -this step, you should see the "validator-stake-keypair.json" in your Solana -runtime directory. - -```bash -solana-keygen new -o ~/validator-stake-keypair.json -``` - -## Delegate Stake - -Now delegate 1 SOL to your validator by first creating your stake account: - -```bash -solana create-stake-account ~/validator-stake-keypair.json 1 -``` - -and then delegating that stake to your validator: - -```bash -solana delegate-stake ~/validator-stake-keypair.json ~/vote-account-keypair.json -``` - -> Don’t delegate your remaining SOL, as your validator will use those tokens to -> vote. - -Stakes can be re-delegated to another node at any time with the same command, -but only one re-delegation is permitted per epoch: - -```bash -solana delegate-stake ~/validator-stake-keypair.json ~/some-other-vote-account-keypair.json -``` - -## Validator Stake Warm-up - -To combat various attacks on consensus, new stake delegations are subject to a -[warm-up](https://solana.com/docs/references/staking/stake-accounts#delegation-warmup-and-cooldown) period. - -Monitor a validator's stake during warmup by: - -- View your vote account:`solana vote-account ~/vote-account-keypair.json` This - displays the current state of all the votes the validator has submitted to the - network. -- View your stake account, the delegation preference and details of your - stake:`solana stake-account ~/validator-stake-keypair.json` -- `solana validators` displays the current active stake of all validators, - including yours -- `solana stake-history` shows the history of stake warming up and cooling down - over recent epochs -- Look for log messages on your validator indicating your next leader slot: - `[2019-09-27T20:16:00.319721164Z INFO solana_core::replay_stage] voted and reset PoH at tick height ####. My next leader slot is ####` -- Once your stake is warmed up, you will see a stake balance listed for your - validator by running `solana validators` - -## Validator Rewards - -Once your stake is warmed up, and assuming the node is voting, you will now be -generating validator rewards. Rewards are paid automatically on epoch -boundaries. - -The rewards lamports earned are split between your stake account and the vote -account according to the commission rate set in the vote account. Rewards can -only be earned while the validator is up and running. Further, once staked, the -validator becomes an important part of the network. In order to safely remove a -validator from the network, first deactivate its stake. - -At the end of each slot, a validator is expected to send a vote transaction. -These vote transactions are paid for by lamports from a validator's identity -account. - -This is a normal transaction so the standard transaction fee will apply. The -transaction fee range is defined by the genesis block. The actual fee will -fluctuate based on transaction load. You can determine the current fee via the -[RPC API "getRecentBlockhash"](https://solana.com/docs/rpc/deprecated/getrecentblockhash) before submitting -a transaction. - -Learn more about -[transaction fees here](../../implemented-proposals/transaction-fees.md). - -## Monitor Your Staked Validator - -Confirm your validator becomes a -[leader](https://solana.com/docs/terminology#leader) - -- After your validator is caught up, use the `solana balance` command to monitor - the earnings as your validator is selected as leader and collects transaction - fees -- Solana nodes offer a number of useful JSON-RPC methods to return information - about the network and your validator's participation. Make a request by using - curl \(or another http client of your choosing\), specifying the desired - method in JSON-RPC-formatted data. For example: - -```bash - // Request - curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getEpochInfo"}' http://localhost:8899 - - // Result - {"jsonrpc":"2.0","result":{"epoch":3,"slotIndex":126,"slotsInEpoch":256},"id":1} -``` - -Helpful JSON-RPC methods: - -- `getEpochInfo`[An epoch](https://solana.com/docs/terminology#epoch) is the - time, i.e. number of [slots](https://solana.com/docs/terminology#slot), for - which a [leader schedule](https://solana.com/docs/terminology#leader-schedule) - is valid. This will tell you what the current epoch is and how far into it the - cluster is. -- `getVoteAccounts` This will tell you how much active stake your validator - currently has. A % of the validator's stake is activated on an epoch boundary. - You can learn more about staking on Solana - [here](../../consensus/stake-delegation-and-rewards.md). -- `getLeaderSchedule` At any given moment, the network expects only one - validator to produce ledger entries. The - [validator currently selected to produce ledger entries](../../consensus/leader-rotation.md#leader-rotation) - is called the "leader". This will return the complete leader schedule \(on a - slot-by-slot basis\) for currently activated stake, the identity pubkey will - show up 1 or more times here. - -## Deactivating Stake - -Before detaching your validator from the cluster, you should deactivate the -stake that was previously delegated by running: - -```bash -solana deactivate-stake ~/validator-stake-keypair.json -``` - -Stake is not deactivated immediately and instead cools down in a similar fashion -as stake warm up. Your validator should remain attached to the cluster while the -stake is cooling down. While cooling down, your stake will continue to earn -rewards. Only after stake cooldown is it safe to turn off your validator or -withdraw it from the network. Cooldown may take several epochs to complete, -depending on active stake and the size of your stake. - -Note that a stake account may only be used once, so after deactivation, use the -cli's `withdraw-stake` command to recover the previously staked lamports. diff --git a/docs/src/operations/guides/validator-start.md b/docs/src/operations/guides/validator-start.md deleted file mode 100644 index ea7e2e8fd1c..00000000000 --- a/docs/src/operations/guides/validator-start.md +++ /dev/null @@ -1,448 +0,0 @@ ---- -title: "Validator Guide: Starting a Validator" -sidebar_position: 0 -sidebar_label: Starting a Validator -pagination_label: "Validator Guides: Starting a Validator" ---- - -## Configure Solana CLI - -The solana cli includes `get` and `set` configuration commands to automatically -set the `--url` argument for cli commands. For example: - -```bash -solana config set --url http://api.devnet.solana.com -``` - -While this section demonstrates how to connect to the Devnet cluster, the steps -are similar for the other [Solana Clusters](../../clusters/available.md). - -## Confirm The Cluster Is Reachable - -Before attaching a validator node, sanity check that the cluster is accessible -to your machine by fetching the transaction count: - -```bash -solana transaction-count -``` - -View the [metrics dashboard](https://metrics.solana.com:3000/d/monitor-edge/cluster-telemetry) for more -detail on cluster activity. - -## System Tuning - -### Linux - -If you would prefer to manage system settings on your own, you may do so with -the following commands. - -#### **Optimize sysctl knobs** - -```bash -sudo bash -c "cat >/etc/sysctl.d/21-agave-validator.conf </etc/security/limits.d/90-solana-nofiles.conf < Note: The "validator-keypair.json" file is also your \(ed25519\) private key. - -### Paper Wallet identity - -You can create a paper wallet for your identity file instead of writing the -keypair file to disk with: - -```bash -solana-keygen new --no-outfile -``` - -The corresponding identity public key can now be viewed by running: - -```bash -solana-keygen pubkey ASK -``` - -and then entering your seed phrase. - -See [Paper Wallet Usage](../../cli/wallets/paper.md) for more info. - ---- - -### Vanity Keypair - -You can generate a custom vanity keypair using solana-keygen. For instance: - -```bash -solana-keygen grind --starts-with e1v1s:1 -``` - -You may request that the generated vanity keypair be expressed as a seed phrase -which allows recovery of the keypair from the seed phrase and an optionally -supplied passphrase (note that this is significantly slower than grinding without -a mnemonic): - -```bash -solana-keygen grind --use-mnemonic --starts-with e1v1s:1 -``` - -Depending on the string requested, it may take days to find a match... - ---- - -Your validator identity keypair uniquely identifies your validator within the -network. **It is crucial to back-up this information.** - -If you don’t back up this information, you WILL NOT BE ABLE TO RECOVER YOUR -VALIDATOR if you lose access to it. If this happens, YOU WILL LOSE YOUR -ALLOCATION OF SOL TOO. - -To back-up your validator identify keypair, **back-up your -"validator-keypair.json" file or your seed phrase to a secure location.** - -## More Solana CLI Configuration - -Now that you have a keypair, set the solana configuration to use your validator -keypair for all following commands: - -```bash -solana config set --keypair ~/validator-keypair.json -``` - -You should see the following output: - -```text -Config File: /home/solana/.config/solana/cli/config.yml -RPC URL: http://api.devnet.solana.com -WebSocket URL: ws://api.devnet.solana.com/ (computed) -Keypair Path: /home/solana/validator-keypair.json -Commitment: confirmed -``` - -## Airdrop & Check Validator Balance - -Airdrop yourself some SOL to get started: - -```bash -solana airdrop 1 -``` - -Note that airdrops are only available on Devnet and Testnet. Both are limited -to 1 SOL per request. - -To view your current balance: - -```text -solana balance -``` - -Or to see in finer detail: - -```text -solana balance --lamports -``` - -Read more about the difference between SOL and lamports here: [What is SOL?](https://solana.com/docs/references/terminology#sol), [What is a lamport?](https://solana.com/docs/references/terminology#lamport). - -## Create Authorized Withdrawer Account - -If you haven't already done so, create an authorized-withdrawer keypair to be used -as the ultimate authority over your validator. This keypair will have the -authority to withdraw from your vote account, and will have the additional -authority to change all other aspects of your vote account. Needless to say, -this is a very important keypair as anyone who possesses it can make any -changes to your vote account, including taking ownership of it permanently. -So it is very important to keep your authorized-withdrawer keypair in a safe -location. It does not need to be stored on your validator, and should not be -stored anywhere from where it could be accessed by unauthorized parties. To -create your authorized-withdrawer keypair: - -```bash -solana-keygen new -o ~/authorized-withdrawer-keypair.json -``` - -## Create Vote Account - -If you haven’t already done so, create a vote-account keypair and create the -vote account on the network. If you have completed this step, you should see the -"vote-account-keypair.json" in your Solana runtime directory: - -```bash -solana-keygen new -o ~/vote-account-keypair.json -``` - -The following command can be used to create your vote account on the blockchain -with all the default options: - -```bash -solana create-vote-account ~/vote-account-keypair.json ~/validator-keypair.json ~/authorized-withdrawer-keypair.json -``` - -Remember to move your authorized withdrawer keypair into a very secure location after running the above command. - -Read more about [creating and managing a vote account](./vote-accounts.md). - -## Known validators - -If you know and respect other validator operators, you can specify this on the -command line with the `--known-validator ` argument to -`agave-validator`. You can specify multiple ones by repeating the argument -`--known-validator --known-validator `. This has the effect -that when the validator is booting with `--only-known-rpc`, it will only ask -that set of known nodes for downloading genesis and snapshot data. - -It is highly recommended you use this option to prevent malicious snapshot -state download. - -## Connect Your Validator - -Connect to the cluster by running: - -```bash -agave-validator \ - --identity ~/validator-keypair.json \ - --vote-account ~/vote-account-keypair.json \ - --rpc-port 8899 \ - --entrypoint entrypoint.devnet.solana.com:8001 \ - --limit-ledger-size \ - --log ~/agave-validator.log -``` - -To force validator logging to the console add a `--log -` argument, otherwise -the validator will automatically log to a file. - -The ledger will be placed in the `ledger/` directory by default, use the -`--ledger` argument to specify a different location. - -> Note: You can use a -> [paper wallet seed phrase](../../cli/wallets/paper.md) -> for your `--identity` and/or -> `--authorized-voter` keypairs. To use these, pass the respective argument as -> `agave-validator --identity ASK ... --authorized-voter ASK ...` -> and you will be prompted to enter your seed phrases and optional passphrase. - -Confirm your validator is connected to the network by opening a new terminal and -running: - -```bash -solana gossip -``` - -If your validator is connected, its public key and IP address will appear in the list. - -### Controlling local network port allocation - -By default the validator will dynamically select available network ports in the -8000-10000 range, and may be overridden with `--dynamic-port-range`. For -example, `agave-validator --dynamic-port-range 11000-11020 ...` will restrict -the validator to ports 11000-11020. - -### Limiting ledger size to conserve disk space - -The `--limit-ledger-size` parameter allows you to specify how many ledger -[shreds](https://solana.com/docs/terminology#shred) your node retains on disk. -If you do not include this parameter, the validator will keep all received -ledger data until it runs out of disk space. Otherwise, the validator will -periodically purge the oldest data (FIFO) to remain under the specified -`--limit-ledger-size` value. - -The default value attempts to keep the blockstore (data within the rocksdb -directory) disk usage under 500 GB. More or less disk usage may be requested -by adding an argument to `--limit-ledger-size` if desired. More information -about selecting a custom limit value is [available -here](https://github.com/solana-labs/solana/blob/aa72aa87790277619d12c27f1ebc864d23739557/core/src/ledger_cleanup_service.rs#L26-L37). - -Note that the above target of 500 GB does not account for other items that -may reside in the `ledger` directory, depending on validator configuration. -These items may include (but are not limited to): -- Persistent accounts data -- Persistent accounts index -- Snapshots - -Additionally, specifying `--enable-rpc-transaction-history` will store extra -block and transaction metadata. The space required to store this data varies -with cluster activity, and is hard to account for. Thus, using this flag will -likely cause the 500 GB target to be exceeded. - -### Systemd Unit - -Running the validator as a systemd unit is one easy way to manage running in the -background. - -Assuming you have a user called `sol` on your machine, create the file `/etc/systemd/system/sol.service` with -the following: - -``` -[Unit] -Description=Solana Validator -After=network.target -StartLimitIntervalSec=0 - -[Service] -Type=simple -Restart=always -RestartSec=1 -User=sol -LimitNOFILE=1000000 -LimitMEMLOCK=2000000000 -LogRateLimitIntervalSec=0 -Environment="PATH=/bin:/usr/bin:/home/sol/.local/share/solana/install/active_release/bin" -ExecStart=/home/sol/bin/validator.sh - -[Install] -WantedBy=multi-user.target -``` - -Now create `/home/sol/bin/validator.sh` to include the desired -`agave-validator` command-line. Ensure that the 'exec' command is used to -start the validator process (i.e. "exec agave-validator ..."). This is -important because without it, logrotate will end up killing the validator -every time the logs are rotated. - -Ensure that running `/home/sol/bin/validator.sh` manually starts -the validator as expected. Don't forget to mark it executable with `chmod +x /home/sol/bin/validator.sh` - -Start the service with: - -```bash -sudo systemctl enable --now sol -``` - -### Logging - -#### Log output tuning - -The messages that a validator emits to the log can be controlled by the `RUST_LOG` -environment variable. Details can by found in the [documentation](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) -for the `env_logger` Rust crate. - -Note that if logging output is reduced, this may make it difficult to debug issues -encountered later. Should support be sought from the team, any changes will need -to be reverted and the issue reproduced before help can be provided. - -#### Log rotation - -The validator log file, as specified by `--log ~/agave-validator.log`, can get -very large over time and it's recommended that log rotation be configured. - -The validator will re-open its log file when it receives the `USR1` signal, which is the -basic primitive that enables log rotation. - -If the validator is being started by a wrapper shell script, it is important to -launch the process with `exec` (`exec agave-validator ...`) when using logrotate. -This will prevent the `USR1` signal from being sent to the script's process -instead of the validator's, which will kill them both. - -#### Using logrotate - -An example setup for the `logrotate`, which assumes that the validator is -running as a systemd service called `sol.service` and writes a log file at -/home/sol/agave-validator.log: - -```bash -# Setup log rotation - -cat > logrotate.sol </rocksdb/ list_column_families -``` - -**Warning**: Please seek guidance on discord before modifying the validator -blockstore. - -Drop a column family: -``` -ldb --db=/rocksdb drop_column_family -``` diff --git a/docs/src/operations/guides/vote-accounts.md b/docs/src/operations/guides/vote-accounts.md deleted file mode 100644 index 636a3806bfa..00000000000 --- a/docs/src/operations/guides/vote-accounts.md +++ /dev/null @@ -1,282 +0,0 @@ ---- -title: "Validator Guide: Vote Account Management" -sidebar_position: 5 -sidebar_label: Vote Account Management -pagination_label: "Validator Guides: Vote Account Management" ---- - -This page describes how to set up an on-chain _vote account_. Creating a vote -account is needed if you plan to run a validator node on Solana. - -## Create a Vote Account - -A vote account can be created with the -[create-vote-account](../../cli/usage.md#solana-create-vote-account) command. The -vote account can be configured when first created or after the validator is -running. All aspects of the vote account can be changed except for the -[vote account address](#vote-account-address), which is fixed for the lifetime -of the account. - -### Configure an Existing Vote Account - -- To change the [validator identity](#validator-identity), use - [vote-update-validator](../../cli/usage.md#solana-vote-update-validator). -- To change the [vote authority](#vote-authority), use - [vote-authorize-voter-checked](../../cli/usage.md#solana-vote-authorize-voter-checked). -- To change the [authorized withdrawer](#authorized-withdrawer), use - [vote-authorize-withdrawer-checked](../../cli/usage.md#solana-vote-authorize-withdrawer-checked). -- To change the [commission](#commission), use - [vote-update-commission](../../cli/usage.md#solana-vote-update-commission). - -## Vote Account Structure - -### Vote Account Address - -A vote account is created at an address that is either the public key of a -keypair file, or at a derived address based on a keypair file's public key and a -seed string. - -The address of a vote account is never needed to sign any transactions, but is -just used to look up the account information. - -When someone wants to -[delegate tokens in a stake account](https://solana.com/staking), -the delegation command is pointed at the vote account address of the validator -to whom the token-holder wants to delegate. - -### Validator Identity - -The _validator identity_ is a system account that is used to pay for all the -vote transaction fees submitted to the vote account. Because the validator is -expected to vote on most valid blocks it receives, the validator identity -account is frequently (potentially multiple times per second) signing -transactions and paying fees. For this reason the validator identity keypair -must be stored as a "hot wallet" in a keypair file on the same system the -validator process is running. - -Because a hot wallet is generally less secure than an offline or "cold" wallet, -the validator operator may choose to store only enough SOL on the identity -account to cover voting fees for a limited amount of time, such as a few weeks -or months. The validator identity account could be periodically topped off from -a more secure wallet. - -This practice can reduce the risk of loss of funds if the validator node's disk -or file system becomes compromised or corrupted. - -The validator identity is required to be provided when a vote account is -created. The validator identity can also be changed after an account is created -by using the -[vote-update-validator](../../cli/usage.md#solana-vote-update-validator) command. - -### Vote Authority - -The _vote authority_ keypair is used to sign each vote transaction the validator -node wants to submit to the cluster. This doesn't necessarily have to be unique -from the validator identity, as you will see later in this document. Because the -vote authority, like the validator identity, is signing transactions frequently, -this also must be a hot keypair on the same file system as the validator -process. - -The vote authority can be set to the same address as the validator identity. If -the validator identity is also the vote authority, only one signature per vote -transaction is needed in order to both sign the vote and pay the transaction -fee. Because transaction fees on Solana are assessed per-signature, having one -signer instead of two will result in half the transaction fee paid compared to -setting the vote authority and validator identity to two different accounts. - -The vote authority can be set when the vote account is created. If it is not -provided, the default behavior is to assign it the same as the validator -identity. The vote authority can be changed later with the -[vote-authorize-voter-checked](../../cli/usage.md#solana-vote-authorize-voter-checked) -command. - -The vote authority can be changed at most once per epoch. If the authority is -changed with -[vote-authorize-voter-checked](../../cli/usage.md#solana-vote-authorize-voter-checked), -this will not take effect until the beginning of the next epoch. To support a -smooth transition of the vote signing, `agave-validator` allows the -`--authorized-voter` argument to be specified multiple times. This allows the -validator process to keep voting successfully when the network reaches an epoch -boundary at which the validator's vote authority account changes. - -### Authorized Withdrawer - -The _authorized withdrawer_ keypair is used to withdraw funds from a vote -account using the -[withdraw-from-vote-account](../../cli/usage.md#solana-withdraw-from-vote-account) -command. Any network rewards a validator earns are deposited into the vote -account and are only retrievable by signing with the authorized withdrawer -keypair. - -The authorized withdrawer is also required to sign any transaction to change a -vote account's [commission](#commission), and to change the validator identity -on a vote account. - -Because theft of an authorized withdrawer keypair can give complete control over -the operation of a validator to an attacker, it is advised to keep the withdraw -authority keypair in an offline/cold wallet in a secure location. The withdraw -authority keypair is not needed during operation of a validator and should not -stored on the validator itself. - -The authorized withdrawer must be set when the vote account is created. It must -not be set to a keypair that is the same as either the validator identity -keypair or the vote authority keypair. - -The authorized withdrawer can be changed later with the -[vote-authorize-withdrawer-checked](../../cli/usage.md#solana-vote-authorize-withdrawer-checked) -command. - -### Commission - -_Commission_ is the percent of network rewards earned by a validator that are -deposited into the validator's vote account. The remainder of the rewards are -distributed to all of the stake accounts delegated to that vote account, -proportional to the active stake weight of each stake account. - -For example, if a vote account has a commission of 10%, for all rewards earned -by that validator in a given epoch, 10% of these rewards will be deposited into -the vote account in the first block of the following epoch. The remaining 90% -will be deposited into delegated stake accounts as immediately active stake. - -A validator may choose to set a low commission to try to attract more stake -delegations as a lower commission results in a larger percentage of rewards -passed along to the delegator. As there are costs associated with setting up and -operating a validator node, a validator would ideally set a high enough -commission to at least cover their expenses. - -Commission can be set upon vote account creation with the `--commission` option. -If it is not provided, it will default to 100%, which will result in all rewards -deposited in the vote account, and none passed on to any delegated stake -accounts. - -Commission can also be changed later with the -[vote-update-commission](../../cli/usage.md#solana-vote-update-commission) command. - -When setting the commission, only integer values in the set [0-100] are -accepted. The integer represents the number of percentage points for the -commission, so creating an account with `--commission 10` will set a 10% -commission. - -Note that validators can only update their commission during the first half of -any epoch. This prevents validators from stealing delegator rewards by setting a -low commission, increasing it right before the end of the epoch, and then -changing it back after reward distribution. - -## Key Rotation - -Rotating the vote account authority keys requires special handling when dealing -with a live validator. - -Note that vote account key rotation has no effect on the stake accounts that -have been delegated to the vote account. For example it is possible to use key -rotation to transfer all authority of a vote account from one entity to another -without any impact to staking rewards. - -### Vote Account Validator Identity - -You will need access to the _authorized withdrawer_ keypair for the vote account -to change the validator identity. The following steps assume that -`~/authorized_withdrawer.json` is that keypair. - -1. Create the new validator identity keypair, - `solana-keygen new -o ~/new-validator-keypair.json`. -2. Ensure that the new identity account has been funded, - `solana transfer ~/new-validator-keypair.json 500`. -3. Run - `solana vote-update-validator ~/vote-account-keypair.json ~/new-validator-keypair.json ~/authorized_withdrawer.json` - to modify the validator identity in your vote account -4. Restart your validator with the new identity keypair for the `--identity` - argument - -**Additional steps are required if your validator has stake.** The leader -schedule is computed two epochs in advance. Therefore if your old validator -identity was in the leader schedule, it will remain in the leader schedule for -up to two epochs after the validator identity change. If extra steps are not -taken your validator will produce no blocks until your new validator identity is -added to the leader schedule. - -After your validator is restarted with the new identity keypair, per step 4, -start a second non-voting validator on a different machine with the old identity -keypair without providing the `--vote-account` argument, as well as with the -`--no-wait-for-vote-to-start-leader` argument. - -This temporary validator should be run for two full epochs. During this time it -will: - -- Produce blocks for the remaining slots that are assigned to your old validator - identity -- Receive the transaction fees and rent rewards for your old validator identity - -It is safe to stop this temporary validator when your old validator identity is -no longer listed in the `solana leader-schedule` output. - -### Vote Account Authorized Voter - -The _vote authority_ keypair may only be changed at epoch boundaries and -requires some additional arguments to `agave-validator` for a seamless -migration. - -1. Run `solana epoch-info`. If there is not much time remaining time in the - current epoch, consider waiting for the next epoch to allow your validator - plenty of time to restart and catch up. -2. Create the new vote authority keypair, - `solana-keygen new -o ~/new-vote-authority.json`. -3. Determine the current _vote authority_ keypair by running - `solana vote-account ~/vote-account-keypair.json`. It may be validator's - identity account (the default) or some other keypair. The following steps - assume that `~/validator-keypair.json` is that keypair. -4. Run - `solana vote-authorize-voter-checked ~/vote-account-keypair.json ~/validator-keypair.json ~/new-vote-authority.json`. - The new vote authority is scheduled to become active starting at the next - epoch. -5. `agave-validator` now needs to be restarted with the old and new vote - authority keypairs, so that it can smoothly transition at the next epoch. Add - the two arguments on restart: - `--authorized-voter ~/validator-keypair.json --authorized-voter ~/new-vote-authority.json` -6. After the cluster reaches the next epoch, remove the - `--authorized-voter ~/validator-keypair.json` argument and restart - `agave-validator`, as the old vote authority keypair is no longer required. - -### Vote Account Authorized Withdrawer - -No special handling or timing considerations are required. Use the -`solana vote-authorize-withdrawer-checked` command as needed. - -### Consider Durable Nonces for a Trustless Transfer of the Authorized Voter or Withdrawer - -If the Authorized Voter or Withdrawer is to be transferred to another entity -then a two-stage signing process using a -[Durable Nonce](../../cli/examples/durable-nonce.md) is recommended. - -1. Entity B creates a durable nonce using `solana create-nonce-account` -2. Entity B then runs a `solana vote-authorize-voter-checked` or - `solana vote-authorize-withdrawer-checked` command, including: - -- the `--sign-only` argument -- the `--nonce`, `--nonce-authority`, and `--blockhash` arguments to specify the - nonce particulars -- the address of the Entity A's existing authority, and the keypair for Entity - B's new authority - -3. When the `solana vote-authorize-...-checked` command successfully executes, - it will output transaction signatures that Entity B must share with Entity A -4. Entity A then runs a similar `solana vote-authorize-voter-checked` or - `solana vote-authorize-withdrawer-checked` command with the following - changes: - -- the `--sign-only` argument is removed, and replaced with a `--signer` argument - for each of the signatures provided by Entity B -- the address of Entity A's existing authority is replaced with the - corresponding keypair, and the keypair for Entity B's new authority is - replaced with the corresponding address - -On success the authority is now changed without Entity A or B having to reveal -keypairs to the other even though both entities signed the transaction. - -## Close a Vote Account - -A vote account can be closed with the -[close-vote-account](../../cli/usage.md#solana-close-vote-account) command. Closing -a vote account withdraws all remaining SOL funds to a supplied recipient address -and renders it invalid as a vote account. It is not possible to close a vote -account with active stake. diff --git a/docs/src/operations/index.md b/docs/src/operations/index.md deleted file mode 100644 index 54312ae38d8..00000000000 --- a/docs/src/operations/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Operating a Validator -sidebar_position: 0 ---- - -This section describes how to run an Agave validator node. - -There are several clusters available to connect to; see [Choosing a Cluster](../cli/examples/choose-a-cluster.md) for an overview of each. diff --git a/docs/src/operations/prerequisites.md b/docs/src/operations/prerequisites.md deleted file mode 100644 index caa3e58cca6..00000000000 --- a/docs/src/operations/prerequisites.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Agave Validator Prerequisites -sidebar_position: 2 -sidebar_label: Prerequisites -pagination_label: Prerequisites to run a Validator ---- - -Operating an Agave validator is an interesting and rewarding task. Generally speaking, it requires someone with a technical background but also involves community engagement and marketing. - -## How to be a good Validator Operator - -Here is a list of some of the requirements for being a good operator: - -- Performant computer hardware and a fast internet connection - - You can find a list of [hardware requirements here](./requirements.md) -- Knowledge of the Linux terminal -- Linux system administration - - Accessing your machine via ssh and scp - - Installing software (installing from source is encouraged) - - Keeping your Linux distribution up to date - - Managing users and system access - - Understanding computer processes - - Understanding networking basics - - Formatting and mounting drives - - Managing firewall rules (UFW/iptables) -- Hardware performance monitoring -- Cluster and node monitoring -- Quick response times in case of a validator issue -- Marketing and communications to attract delegators -- Customer support - -Whether you decide to run a [validator](../what-is-a-validator.md) or an [RPC node](../what-is-an-rpc-node.md), you should consider all of these areas of expertise. A team of people is likely necessary for you to achieve your goals. - -## Can I use my computer at home? - -While anyone can join the network, you should make sure that your home computer and network meets the specifications in the [hardware requirements](./requirements.md) doc. Most home internet service providers do not provide consistent service that would allow your validator to perform well. If your home network or personal hardware is not performant enough to keep up with the Solana cluster, your validator will not be able to participate in consensus. - -In addition to performance considerations, you will want to make sure that your home computer is resistant to outages caused by loss of power, flooding, fire, theft, etc. If you are just getting started on the testnet cluster and learning about being an operator, a home setup may be sufficient, but you will want to consider all of these factors when you start operating your validator on the mainnet-beta cluster. diff --git a/docs/src/operations/requirements.md b/docs/src/operations/requirements.md deleted file mode 100644 index 6b828adf069..00000000000 --- a/docs/src/operations/requirements.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Agave Validator Requirements -sidebar_position: 3 -sidebar_label: Requirements -pagination_label: Requirements to Operate a Validator ---- - -## Minimum SOL requirements - -There is no strict minimum amount of SOL required to run an Agave validator on Solana. - -However in order to participate in consensus, a vote account is required which -has a rent-exempt reserve of 0.02685864 SOL. Voting also requires sending a vote -transaction for each block the validator agrees with, which can cost up to -1.1 SOL per day. - -## Hardware Recommendations - -The hardware recommendations below are provided as a guide. Operators are encouraged to do their own performance testing. - -| Component | Validator Requirements | Additional RPC Node Requirements | -|-----------|------------------------|----------------------------------| -| **CPU** | - 2.8GHz base clock speed, or faster
- SHA extensions instruction support
- AMD Gen 3 or newer
- Intel Ice Lake or newer
- Higher clock speed is preferable over more cores
- AVX2 instruction support (to use official release binaries, self-compile otherwise)
- Support for AVX512f is helpful
|| -| | 12 cores / 24 threads, or more | 16 cores / 32 threads, or more | -| **RAM** | Error Correction Code (ECC) memory is suggested
Motherboard with 512GB capacity suggested || -| | 256GB or more| 512 GB or more for **all [account indexes](https://docs.anza.xyz/operations/setup-an-rpc-node#account-indexing)** | -| **Disk** | PCIe Gen3 x4 NVME SSD, or better, on each of:
- **Accounts**: 1TB, or larger. High TBW (Total Bytes Written)
- **Ledger**: 1TB or larger. High TBW suggested
- **Snapshots**: 500GB or larger. High TBW suggested
- **OS**: (Optional) 500GB, or larger. SATA OK

The OS may be installed on the ledger disk, though testing has shown better performance with the ledger on its own disk

Accounts and ledger *can* be stored on the same disk, however due to high IOPS, this is not recommended

The Samsung 970 and 980 Pro series SSDs are popular with the validator community | Consider a larger ledger disk if longer transaction history is required

Accounts and ledger **should not** be stored on the same disk | - -A community maintained list of currently optimal hardware can be found here: [solanahcl.org](https://solanahcl.org/). It is updated automatically from the [solanahcl/solanahcl Github repo](https://github.com/solanahcl/solanahcl). - -## Virtual machines on Cloud Platforms - -Running an Agave node in the cloud requires significantly greater -operational expertise to achieve stability and performance. Do not -expect to find sympathetic voices should you chose this route and -find yourself in need of support. - -## Docker - -Running an Agave validator for live clusters (including mainnet-beta) inside Docker is -not recommended and generally not supported. This is due to general concerns of -Docker's containerization overhead and resultant performance degradation unless -specially configured. We use Docker only for development purposes. - -## Software - -- We build and run on Ubuntu 24.04. -- See [Installing Solana CLI](../cli/install.md) for the current Solana CLI software release. - -Prebuilt binaries are currently available targeting x86_64 with AVX2 support on Ubuntu 20.04. We -will stop publishing these in the near future (as of May 2025). -[Building from source](https://docs.anza.xyz/cli/install#build-from-source) is required for all -other supported target platforms (MacOS and Windows) today and suggested for linux as it will soon -be required there as well - -## Networking -A stable connection with a public IPv4 address is required. - -- For an unstaked node: 1 GBit/s symmetric connection is sufficient. -- For a staked node: at least 2 GBit/s symmetric connection is required, 10 GBit/s of available bandwidth is recommended for stable operation. - -### Firewall -It is not recommended to run a validator behind a NAT. Operators who choose to -do so should be comfortable configuring their networking equipment and debugging -any traversal issues on their own. Upstream agave will generally not accept -patches to support operation behind NAT. - -The following traffic needs to be allowed. Furthermore, there should not be any traffic filtering from your validator to internet. - -#### Required - -| Source | Destination | Protocol | Port(s) | Comment | -|--------|---------------------|-------------|------------|--------------------------------------------------------------------------------------------------------------------------| -| any | your validator's IP | TCP and UDP | 8000-8030 | P2P protocols (gossip, turbine, repair, etc). This can be limited to any free port range with `--dynamic-port-range` | - -#### Recommended -When you manage your validator via SSH it is recommended to limit the allowed SSH traffic to your validator management IP. - -| Source | Destination | Protocol | Port(s) | Comment | -|-----------------|--------------------------------|----------|---------|-----------------------------------------------------------------------------| -| your IP address | your validator's management IP | TCP | 22 | SSH management traffic. Port can be different depending on your SSH config. | - -***Please note that your source IP address should be a static public IP address. Please check with your service provider if you're not sure if your public IP address is static.*** - -#### Optional -For security purposes, it is not suggested that the following ports be open to -the internet on staked, mainnet-beta validators. - -| Source | Destination | Protocol | Port(s) | Comment | -|----------|---------------------|----------|---------|--------------------------------------------------------| -| RPC user | your validator's IP | TCP | 8899 | JSONRPC over HTTP. Change with `--rpc-port RPC_PORT` | -| RPC user | your validator's IP | TCP | 8900 | JSONRPC over Websockets. Derived. Uses `RPC_PORT + 1` | diff --git a/docs/src/operations/setup-a-validator.md b/docs/src/operations/setup-a-validator.md deleted file mode 100644 index 21a49df5075..00000000000 --- a/docs/src/operations/setup-a-validator.md +++ /dev/null @@ -1,618 +0,0 @@ ---- -title: Setup an Agave Validator -sidebar_label: Setup an Agave Validator -sidebar_position: 5 ---- - -This is a guide for getting your validator setup on the Solana testnet cluster -for the first time. Testnet is a Solana cluster used for performance -testing of the software before the software is used on mainnet. Since testnet is -stress tested daily, it is a good cluster to practice validator operations. - -Once you have a working validator on testnet, you will want to learn about -[operational best practices](./best-practices/general.md) in the next section. -Although the guide is specific to testnet, it can be adapted to mainnet or -devnet as well. - -> Refer to the [Available Clusters](../clusters/available.md) section of the -> documentation to see example commands for each cluster. - -Now let's get started. - -## Open The Terminal Program - -To start this guide, you will be running commands on your trusted computer, not -on the remote machine that you plan to use for validator operations. First, -locate the terminal program on your _trusted computer_. - -- on Mac, you can search for the word _terminal_ in spotlight. -- on Ubuntu, you can type `CTRL + Alt + T`. -- on Windows, you will have to open the command prompt as an Administrator. - -## Install The Solana CLI Locally - -Validator operators are required to install the tools included in the Solana CLI using the [installation instructions](../cli/install.md). - -Once the Solana CLI is installed, you can return to this document once you are -able to run two commands and get an answer on your terminal. - -First, run the following command to verify that the Solana CLI is installed: - -``` -solana --version -``` - -You should see an output that looks similar to this (note your version number -may be higher): - -``` -solana-cli 1.14.17 (src:b29a37cf; feat:3488713414) -``` - -Now, run the following command to verify that the agave-validator binary is -installed: - -``` -agave-validator --version -``` - -You should see an output that looks similar to this (note your version number -may be higher): - -``` -agave-validator 2.3.1 (src:e3eca4c1; feat:3640012085, client:Agave) -``` - -Once you have successfully installed the cli and validator binary, the next step is to change your -config so that it is making requests to the `testnet` cluster: - -``` -solana config set --url https://api.testnet.solana.com -``` - -To verify that your config has changed, run: - -``` -solana config get -``` - -You should see a line that says: `RPC URL: https://api.testnet.solana.com` - -## Create Keys - -On your local computer, create the 3 keypairs that you will need to run your -validator ([docs for reference](./guides/validator-start.md#generate-identity)): - -> **NOTE** Some operators choose to make vanity keypairs for their identity and -> vote account using the `grind` sub command -> ([docs for reference](./guides/validator-start.md#vanity-keypair)). - -``` -solana-keygen new -o validator-keypair.json -``` - -``` -solana-keygen new -o vote-account-keypair.json -``` - -``` -solana-keygen new -o authorized-withdrawer-keypair.json -``` - -> **IMPORTANT** the `authorized-withdrawer-keypair.json` should be considered -> very sensitive information. Many operators choose to use a multisig, hardware -> wallet, or paper wallet for the authorized withdrawer keypair. A keypair is -> created on disk in this example for simplicity. Additionally, the withdrawer -> keypair should always be stored safely. The authorized withdrawer keypair -> should **never** be stored on the remote machine that the validator software -> runs on. For more information, see -> [validator security best practices](./best-practices/security.md#do-not-store-your-withdrawer-key-on-your-validator) - -## Create a Vote Account - -Before you can create your vote account, you need to configure the Solana -command line tool a bit more. - -The below command sets the default keypair that the Solana CLI uses to the -`validator-keypair.json` file that you just created in the terminal: - -``` -solana config set --keypair ./validator-keypair.json -``` - -Now verify your account balance of `0`: - -``` -solana balance -``` - -Next, you need to deposit some SOL into that keypair account in order create a -transaction (in this case, making your vote account): - -``` -solana airdrop 1 -``` - -> **NOTE** The `airdrop` sub command does not work on mainnet, so you will have -> to acquire SOL and transfer it into this keypair's account if you are setting -> up a mainnet validator. - -Now, use the Solana cluster to create a vote account. - -As a reminder, all commands mentioned so far **should be done on your trusted -computer** and **NOT** on a server where you intend to run your validator. It is -especially important that the following command is done on a **trusted -computer**: - -``` -solana create-vote-account -ut \ - --fee-payer ./validator-keypair.json \ - ./vote-account-keypair.json \ - ./validator-keypair.json \ - ./authorized-withdrawer-keypair.json -``` - -> Note `-ut` tells the cli command that we would like to use the testnet -> cluster. `--fee-payer` specifies the keypair that will be used to pay the -> transaction fees. Both flags are not necessary if you configured the solana -> cli properly above but they are useful to ensure you're using the intended -> cluster and keypair. - -## Save the Withdrawer Keypair Securely - -Make sure your `authorized-withdrawer-keypair.json` is stored in a safe place. -If you have chosen to create a keypair on disk, you should first backup the -keypair and then delete it from your local machine. - -**IMPORTANT**: If you lose your withdrawer key pair, you will lose control of -your vote account. You will not be able to withdraw tokens from the vote account -or update the withdrawer. Make sure to store the -`authorized-withdrawer-keypair.json` securely before you move on. - -## SSH To Your Validator - -Connect to your remote server. This is specific to your server but will look -something like this: - -``` -ssh user@ -``` - -You will have to check with your server provider to get the correct user account -and hostname that you will ssh into. - -## Update Your Ubuntu Packages - -Make sure you have the latest and greatest package versions on your server - -``` -sudo apt update -sudo apt upgrade -``` - -## Sol User - -Create a new Ubuntu user, named `sol`, for running the validator: - -``` -sudo adduser sol -``` - -It is a best practice to always run your validator as a non-root user, like the -`sol` user we just created. - -## Hard Drive Setup - -On your Ubuntu computer make sure that you have at least `2TB` of disk space -mounted. You can check disk space using the `df` command: - -``` -df -h -``` - -> If you have a drive that is not mounted/formatted, you will have to set up the -> partition and mount the drive. - -To see the hard disk devices that you have available, use the list block devices -command: - -``` -lsblk -f -``` - -You may see some devices in the list that have a name but do not have a UUID. -Any device without a UUID is unformatted. - -### Drive Formatting: Ledger - -Assuming you have an nvme drive that is not formatted, you will have to format -the drive and then mount it. - -For example, if your computer has a device located at `/dev/nvme0n1`, then you -can format the drive with the command: - -``` -sudo mkfs -t ext4 /dev/nvme0n1 -``` - -For your computer, the device name and location may be different. - -Next, check that you now have a UUID for that device: - -``` -lsblk -f -``` - -In the fourth column, next to your device name, you should see a string of -letters and numbers that look like this: `6abd1aa5-8422-4b18-8058-11f821fd3967`. -That is the UUID for the device. - -### Mounting Your Drive: Ledger - -So far we have created a formatted drive, but you do not have access to it until -you mount it. Make a directory for mounting your drive: - -``` -sudo mkdir -p /mnt/ledger -``` - -Next, change the ownership of the directory to your `sol` user: - -``` -sudo chown -R sol:sol /mnt/ledger -``` - -Now you can mount the drive: - -``` -sudo mount /dev/nvme0n1 /mnt/ledger -``` - -### Formatting And Mounting Drive: AccountsDB - -You will also want to mount the accounts db on a separate hard drive. The -process will be similar to the ledger example above. - -Assuming you have a device at `/dev/nvme1n1`, format the device and verify it -exists: - -``` -sudo mkfs -t ext4 /dev/nvme1n1 -``` - -Then verify the UUID for the device exists: - -``` -lsblk -f -``` - -Create a directory for mounting: - -``` -sudo mkdir -p /mnt/accounts -``` - -Change the ownership of that directory: - -``` -sudo chown -R sol:sol /mnt/accounts -``` - -And lastly, mount the drive: - -``` -sudo mount /dev/nvme1n1 /mnt/accounts -``` - -## System Tuning - -### Linux - -Your system will need to be tuned to run properly. Your validator may -not start without the settings below. - -#### **Optimize sysctl knobs** - -```bash -sudo bash -c "cat >/etc/sysctl.d/21-agave-validator.conf </etc/security/limits.d/90-solana-nofiles.conf <: -scp vote-account-keypair.json sol@: -``` - -> **Note**: The `vote-account-keypair.json` does not have any function other -> than identifying the vote account to potential delegators. Only the public key -> of the vote account is important once the account is created. - -## Switch to the sol User - -On the validator server, switch to the `sol` user: - -``` -su - sol -``` - -## Install agave-validator on Remote Machine - -Your remote machine will need `agave-validator` installed to run the Agave validator -software. For simplicity, install the application with user `sol`. Refer again to -[build from source](../cli/install.md#build-from-source). - -## Create A Validator Startup Script - -In your sol home directory (e.g. `/home/sol/`), create a folder called `bin`. -Inside that folder create a file called `validator.sh` and make it executable: - -``` -mkdir -p /home/sol/bin -touch /home/sol/bin/validator.sh -chmod +x /home/sol/bin/validator.sh -``` - -Next, open the `validator.sh` file for editing: - -``` -nano /home/sol/bin/validator.sh -``` - -Copy and paste the following contents into `validator.sh` then save the file: - -``` -#!/bin/bash -exec agave-validator \ - --identity /home/sol/validator-keypair.json \ - --vote-account /home/sol/vote-account-keypair.json \ - --known-validator 5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on \ - --known-validator 7XSY3MrYnK8vq693Rju17bbPkCN3Z7KvvfvJx4kdrsSY \ - --known-validator Ft5fbkqNa76vnsjYNwjDZUXoTWpP7VYm3mtsaQckQADN \ - --known-validator 9QxCLckBiJc783jnMvXZubK4wH86Eqqvashtrwvcsgkv \ - --only-known-rpc \ - --log /home/sol/agave-validator.log \ - --ledger /mnt/ledger \ - --accounts /mnt/accounts \ - --rpc-port 8899 \ - --dynamic-port-range 8000-8020 \ - --entrypoint entrypoint.testnet.solana.com:8001 \ - --entrypoint entrypoint2.testnet.solana.com:8001 \ - --entrypoint entrypoint3.testnet.solana.com:8001 \ - --expected-genesis-hash 4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY \ - --wal-recovery-mode skip_any_corrupted_record \ - --limit-ledger-size -``` - -Refer to `agave-validator --help` for more information on what each flag is -doing in this script. Also refer to the section on -[best practices for operating a validator](./best-practices/general.md). - -This startup script is specifically intended for testnet. For more startup script examples intended for other clusters, refer to the -[clusters section.](./../clusters/available.md). -## Verifying Your Validator Is Working - -Test that your `validator.sh` file is running properly by executing the -`validator.sh` script: - -``` -/home/sol/bin/validator.sh -``` - -The script should execute the `agave-validator` process. In a new terminal -window, ssh into your server, then verify that the process is running: - -``` -ps aux | grep agave-validator -``` - -You should see a line in the output that includes `agave-validator` with all -the flags that were added to your `validator.sh` script. - -Next, we need to look at the logs to make sure everything is operating properly. - -### Tailing The Logs - -As a spot check, you will want to make sure your validator is producing -reasonable log output (**warning**, there will be a lot of log output). - -In a new terminal window, ssh into your validator machine, switch users to the -`sol` user and `tail` the logs: - -``` -su - sol -tail -f agave-validator.log -``` - -The `tail` command will continue to display the output of a file as the file -changes. You should see a continuous stream of log output as your validator -runs. Keep an eye out for any lines that say `_ERROR_`. - -Assuming you do not see any error messages, exit out of the command. - -### Gossip Protocol - -Gossip is a protocol used in the Solana clusters to communicate between -validator nodes. For more information on gossip, see -[Gossip Service](../validator/gossip.md). To verify that your validator is -running properly, make sure that the validator has registered itself with the -gossip network. - -In a new terminal window, connect to your server via ssh. Identify your -validator's pubkey: - -``` -solana-keygen pubkey ~/validator-keypair.json -``` - -The command `solana gossip` lists all validators that have registered with the -protocol. To check that the newly setup validator is in gossip, we will `grep` -for our pubkey in the output: - -``` -solana gossip | grep -``` - -After running the command, you should see a single line that looks like this: - -``` -139.178.68.207 | 5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on | 8001 | 8004 | 139.178.68.207:80 | 1.14.17 | 3488713414 -``` - -If you do not see any output after grep-ing the output of gossip, your validator -may be having startup problems. If that is the case, start debugging by looking -through the validator log output. - -### Solana Validators - -After you have verified that your validator is in gossip, you should stake some -SOL to your validator. Once the stake has activated (which happens at the start -of the next epoch), you can verify that your validator is ready to be a voting -participant of the network with the `solana validators` command. The command -lists all validators in the network, but like before, we can `grep` the output -for the validator we care about: - -``` -solana validators | grep -``` - -You should see a line of output that looks like this: - -``` -5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on FX6NNbS5GHc2kuzgTZetup6GZX6ReaWyki8Z8jC7rbNG 100% 197434166 ( 0) 197434133 ( 0) 2.11% 323614 1.14.17 2450110.588302720 SOL (1.74%) -``` - -### Solana Catchup - -The `solana catchup` command is a useful tool for seeing how quickly your -validator is processing blocks. The Solana network has the capability to produce -many transactions per second. Since your validator is new to the network, it has -to ask another validator (listed as a `--known-validator` in your startup -script) for a recent snapshot of the ledger. By the time you receive the -snapshot, you may already be behind the network. Many transactions may have been -processed and finalized in that time. In order for your validator to participate -in consensus, it must _catchup_ to the rest of the network by asking for the -more recent transactions that it does not have. - -The `solana catchup` command is a tool that tells you how far behind the network -your validator is and how quickly you are catching up: - -``` -solana catchup -``` - -If you see a message about trying to connect, your validator may not be part of -the network yet. Make sure to check the logs and double check `solana gossip` -and `solana validators` to make sure your validator is running properly. - -Once you are happy that the validator can start up without errors, the next step -is to create a system service to run the `validator.sh` file automatically. Stop -the currently running validator by pressing `CTRL+C` in the window where -`validator.sh` is running. - -## Create a System Service - -Follow these instructions for -[running the validator as a system service](./guides/validator-start.md#systemd-unit) - -Make sure to implement log rotate as well. Once you have the system service -configured, start your validator using the newly configured service: - -``` -sudo systemctl enable --now sol -``` - -Now verify that the validator is running properly by tailing the logs and using -the commands mentioned earlier to check gossip and Solana validators: - -``` -tail -f /home/sol/agave-validator*.log -``` - -## Monitoring - -`agave-watchtower` is a command you can run on a separate machine to monitor -your server. You can read more about handling -[automatic restarts and monitoring](./best-practices/monitoring.md#agave-watchtower) -using Agave Watchtower here in the docs. - -## Common issues - -### Out of disk space - -Make sure your ledger is on drive with at least `2TB` of space. - -### Validator not catching up - -This could be a networking/hardware issue, or you may need to get the latest -snapshot from another validator node. - -### PoH hashes/second rate is slower than the cluster target - -If you are using `agave-validator` built from source, ensure that you are using a `release` build and not a `debug` build - -Ensure that your machine's CPU base clock speed is 2.8GHz or faster. Use `lscpu` to check your clock speed. `CPU(s) scaling MHz` can cause your clock speed to be underclocked. Some additional tuning: - -Set performance governor -```bash -echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor -``` - -Force minimum frequency to maximum -```bash -# Example if your maximum GHz is 2.8 -echo 2850000 | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_min_freq diff --git a/docs/src/operations/setup-an-rpc-node.md b/docs/src/operations/setup-an-rpc-node.md deleted file mode 100644 index 3aa3ba6ab86..00000000000 --- a/docs/src/operations/setup-an-rpc-node.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: Setup an Agave RPC Node -sidebar_label: Setup an Agave RPC Node -sidebar_position: 6 ---- - -Since a Solana RPC server runs the same process as a consensus validator, first follow the instructions on -[how to setup a Solana validator](./setup-a-validator.md) to get started. -Note that you do not need to create a vote account if you are operating an RPC node. -An RPC node typically does not vote. - -After your validator is running, you can refer to this section for the RPC node specific setup instructions. - -_**WARNING:**_ The RPC service is _**NOT**_ intended to be directly exposed to the public internet. Operators who -intend to do so should be familiar with common strategies for protecting HTTP services from abuse. This topic -will not be covered in this document, nor will the team offer any guidance or technical support on the matter. - -## Sample RPC Node - -Below is an example `validator.sh` file for a `testnet` RPC server. - -You will want to be aware of the following flags: - -- `--full-rpc-api`: enables all RPC operations on this validator. -- `--no-voting`: runs the validator without participating in consensus. Typically, you do not want to run a validator as _both_ a consensus node and a full RPC node due to resource constraints. -- `--private-rpc`: does not publish the validator's open RPC port in the `solana gossip` command - -> For more explanation on the flags used in the command, refer to the `agave-validator --help` command - -``` -#!/bin/bash -exec agave-validator \ - --identity /home/sol/validator-keypair.json \ - --known-validator 5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on \ - --known-validator dDzy5SR3AXdYWVqbDEkVFdvSPCtS9ihF5kJkHCtXoFs \ - --known-validator eoKpUABi59aT4rR9HGS3LcMecfut9x7zJyodWWP43YQ \ - --known-validator 7XSY3MrYnK8vq693Rju17bbPkCN3Z7KvvfvJx4kdrsSY \ - --known-validator Ft5fbkqNa76vnsjYNwjDZUXoTWpP7VYm3mtsaQckQADN \ - --known-validator 9QxCLckBiJc783jnMvXZubK4wH86Eqqvashtrwvcsgkv \ - --only-known-rpc \ - --full-rpc-api \ - --no-voting \ - --ledger /mnt/ledger \ - --accounts /mnt/accounts \ - --log /home/sol/solana-rpc.log \ - --rpc-port 8899 \ - --private-rpc \ - --dynamic-port-range 8000-8020 \ - --entrypoint entrypoint.testnet.solana.com:8001 \ - --entrypoint entrypoint2.testnet.solana.com:8001 \ - --entrypoint entrypoint3.testnet.solana.com:8001 \ - --expected-genesis-hash 4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY \ - --wal-recovery-mode skip_any_corrupted_record \ - --limit-ledger-size -``` - -### Solana Bigtable - -The Solana blockchain is able to create many transactions per second. Because of the volume of transactions on the chain, it is not practical for an RPC node to store the entire blockchain on the machine. Instead, RPC operators use the `--limit-ledger-size` flag to specify how many blocks to store on the RPC node. If the user of the RPC node needs historical blockchain data, then the RPC server will have to access older blocks through a Solana bigtable instance. - -If you are interested in setting up your own bigtable instance, see these docs in the Solana GitHub repository: [solana-labs/solana-bigtable](https://github.com/solana-labs/solana-bigtable) - -### Example Known Validators - -The identities of the [known validators](./guides/validator-start.md#known-validators) supplied in these example snippets (via the `--known-validator` flag) are: - -- `5D1fNXzvv5NjV1ysLjirC4WY92RNsVH18vjmcszZd8on` - Anza -- `dDzy5SR3AXdYWVqbDEkVFdvSPCtS9ihF5kJkHCtXoFs` - MonkeDAO -- `Ft5fbkqNa76vnsjYNwjDZUXoTWpP7VYm3mtsaQckQADN` - Certus One -- `eoKpUABi59aT4rR9HGS3LcMecfut9x7zJyodWWP43YQ` - SerGo -- `9QxCLckBiJc783jnMvXZubK4wH86Eqqvashtrwvcsgkv` - Algo|Stake - -## Examples for other clusters - -Additional examples of other Solana cluster-specific validator commands can be found on the [Clusters](../clusters/available.md) page. - -Keep in mind, you will still need to customize these commands to operate as an RPC node, as well as other -operator-specific configuration settings. - -## Account indexing - -As the number of populated accounts on the cluster grows, account-data RPC -requests that scan the entire account set -- like -[`getProgramAccounts`](https://solana.com/docs/rpc/http/getprogramaccounts) and -[SPL-token-specific requests](https://solana.com/docs/rpc/http/gettokenaccountsbydelegate) -- -may perform poorly. If your validator needs to support any of these requests, -you can use the `--account-index` parameter to activate one or more in-memory -account indexes that significantly improve RPC performance by indexing accounts -by the key field. Currently, it supports the following parameter values: - -- `program-id`: each account indexed by its owning program; used by [getProgramAccounts](https://solana.com/docs/rpc/http/getprogramaccounts) -- `spl-token-mint`: each SPL token account indexed by its token Mint; used by [getTokenAccountsByDelegate](https://solana.com/docs/rpc/http/gettokenaccountsbydelegate), and [getTokenLargestAccounts](https://solana.com/docs/rpc/http/gettokenlargestaccounts) -- `spl-token-owner`: each SPL token account indexed by the token-owner address; used by [getTokenAccountsByOwner](https://solana.com/docs/rpc/http/gettokenaccountsbyowner), and [getProgramAccounts](https://solana.com/docs/rpc/http/getprogramaccounts) requests that include an spl-token-owner filter. diff --git a/docs/src/operations/validator-or-rpc-node.md b/docs/src/operations/validator-or-rpc-node.md deleted file mode 100644 index ca4ded55597..00000000000 --- a/docs/src/operations/validator-or-rpc-node.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Consensus Validator or RPC Node? -sidebar_position: 1 -sidebar_label: Validator vs RPC Node -pagination_label: Consensus Validator vs RPC Node ---- - -Operators who run a [consensus validator](../what-is-a-validator.md) have much -different incentives than operators who run an -[RPC node](../what-is-an-rpc-node.md). You will have to decide which choice is -best for you based on your interests, technical background, and goals. - -## Consensus Validators - -As a validator your primary focus is maintaining the network and making sure -that your node is performing optimally so that you can fully participate in the -cluster consensus. You will want to attract a delegation of SOL to your -validator which will allow your validator the opportunity to produce more blocks -and earn rewards. - -Each staked validator earns inflation rewards from -[vote credits](https://solana.com/docs/terminology#vote-credit). Vote credits -are assigned to validators that vote on -[blocks](https://solana.com/docs/terminology#block) produced by the -[leader](https://solana.com/docs/terminology#leader). The vote credits are given -to all validators that successfully vote on blocks that are added to the -blockchain. Additionally, when the validator is the leader, it can earn -transaction fees and storage -[rent fees](https://solana.com/docs/core/accounts#rent) for each block that it -produces that is added to the blockchain. - -Since all votes in Solana happen on the blockchain, a validator incurs a -transaction cost for each vote that it makes. These transaction fees amount to -approximately 1.0 SOL per day. - -> It is important to make sure your validator always has enough SOL in its -> identity account to pay for these transactions! - -### Economics of running a consensus validator - -As an operator, it is important to understand how a consensus validator spends -and earns sol through the protocol. - -All validators who vote (consensus validators) must pay vote transaction fees -for blocks that they agree with. The cost of voting can be up to 1.1 SOL per -day. - -A voting validator can earn SOL through 2 methods: - -1. Inflationary rewards paid at the end of an epoch. See - [staking rewards](../implemented-proposals/staking-rewards.md) -2. Earning 50% of transaction fees for the blocks produced by the validator. See - [transaction fee basic economic design](https://solana.com/docs/intro/transaction_fees#basic-economic-design) - -The following links are community provided resources that discuss the economics -of running a validator: - -- Michael Hubbard wrote an - [article](https://laine-sa.medium.com/solana-staking-rewards-validator-economics-how-does-it-work-6718e4cccc4e) - that explains the economics of Solana in more depth for stakers and for - validators. -- Congent Crypto has written a - [blog post](https://medium.com/@Cogent_Crypto/how-to-become-a-validator-on-solana-9dc4288107b7) - that discusses economics and getting started. -- Cogent Crypto also provides a - [validator profit calculator](https://cogentcrypto.io/ValidatorProfitCalculator) - -## RPC Nodes - -While RPC operators **do NOT** receive rewards (because the node is not -participating in voting), there are different motivations for running an RPC -node. - -An RPC operator is providing a service to users who want to interact with the -Solana blockchain. Because your primary user is often technical, you will have -to be able to answer technical questions about performance of RPC calls. This -option may require more understanding of the -[core Solana architecture](../clusters/index.md). - -If you are operating an RPC node as a business, your job will also involve -scaling your system to meet the demands of the users. For example, some RPC -providers create dedicated servers for projects that require a high volume of -requests to the node. Someone with a background in development operations or -software engineering will be a very important part of your team. You will need a -strong understanding of the Solana architecture and the -[JSON RPC API](https://solana.com/docs/rpc/http). - -Alternatively, you may be a development team that would like to run their own -infrastructure. In this case, the RPC infrastructure could be a part of your -production stack. A development team could use the -[Geyser plugin](../validator/geyser.md), for example, to get -real time access to information about accounts or blocks in the cluster. diff --git a/docs/src/pages/styles.module.css b/docs/src/pages/styles.module.css deleted file mode 100644 index 677dc9b0686..00000000000 --- a/docs/src/pages/styles.module.css +++ /dev/null @@ -1,45 +0,0 @@ -/* stylelint-disable docusaurus/copyright-header */ - -/** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. - */ - -.heroBanner { - padding: 4rem 0; - text-align: center; - position: relative; - overflow: hidden; -} - -@media screen and (max-width: 966px) { - .heroBanner { - padding: 2rem; - } -} - -.cardTitle { - color: green; -} - -.buttons { - display: flex; - align-items: center; - justify-content: center; -} - -.features { - display: flex; - align-items: top; - padding: 2rem 0; - width: 100%; -} - -.featureImage { - height: 200px; - width: 200px; -} - -.iconExternalIcon { - margin-left: 0.5em; -} diff --git a/docs/src/proposals.md b/docs/src/proposals.md deleted file mode 100644 index 0835a338524..00000000000 --- a/docs/src/proposals.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: System Design Proposals -sidebar_label: Overview ---- - -Changes to the Solana architecture are performed through a public proposal process (via pull requests) on the [Solana GitHub repository](https://github.com/solana-labs/solana). New proposals should be submitted with the "[Submit a Design Proposal](#submit-a-design-proposal)" guide below. - -There are currently two different states of these design proposals: - -1. [Accepted Proposals](./proposals/accepted-design-proposals.md) -2. [Implemented Proposals](./implemented-proposals/index.md) - -## Accepted Proposals - -These architectural proposals have been accepted by the Solana team, but are not yet fully implemented. - -Each proposal may be implemented as described, implemented differently as issues in the designs become evident, or not implemented at all. If implemented, the proposal will be moved to [Implemented Proposals](./implemented-proposals/index.md) and the details will be added to relevant sections of the docs. - -## Implemented Proposals - -These architectural proposals have been accepted and implemented by the Solana team. - -Any designs that may be subject to future change are noted in their specific proposal page. - -## Submit a Design Proposal - -To submit a new design proposal for Solana: - -1. Propose a design by creating a PR that adds a markdown document to the `docs/src/proposals` directory. -2. Add any relevant Solana maintainers to the PR review. -3. Publish the PR for community review and feedback. - -> **NOTE:** All people submitting PRs to the Solana repo should consult the [CONTRIBUTING](https://github.com/solana-labs/solana/blob/master/CONTRIBUTING.md) doc in the repo. - -### After Accepted - -Once a design proposal has been accepted, the PR will be merged into the `master` branch of the Solana repo. This also signifies the maintainers support your plan of attack. - -> **NOTE:** The merging of the PR will **automatically** create a link in the "Accepted Proposals" table of contents sidebar. -> Once approved, continue to submit PRs that implement the proposal. When the implementation reveals the need for tweaks to the proposal, be sure to update the "accepted proposal" document and have these changes reviewed by the same approving maintainers. - -### After Implemented - -After a proposal has been fully implemented into the Solana architecture, a PR should be created to perform the following: - -1. Move the newly implemented proposal file from `docs/src/proposals` to `docs/src/implemented-proposals` -2. Create a new redirect in the `publish-docs.sh` to redirect the old `accepted` proposal page to the new `implemented-proposal` page -3. Publish the PR - -> **NOTE:** Moving the proposal document into the `implemented-proposals` directory will **automatically** move the link in the "Accepted Proposals" table of contents sidebar to the "Implemented Proposals" sidebar. diff --git a/docs/src/proposals/accepted-design-proposals.md b/docs/src/proposals/accepted-design-proposals.md deleted file mode 100644 index 61d2d2ef4f6..00000000000 --- a/docs/src/proposals/accepted-design-proposals.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Accepted Design Proposals -sidebar_position: 1 -sidebar_label: Overview ---- - -These architectural proposals have been accepted by the Solana maintainers, but are not yet fully implemented. These proposals may be implemented as described, implemented differently as issues in the designs become evident, or not implemented at all. - -## After Implemented - -Once a proposal has been implemented, it will be moved to [Implemented Proposals](../implemented-proposals/index.md) and the details will be added to relevant sections of the docs. - -## Submit a New Proposal - -To submit a new design proposal, consult this guide on [how to submit a design proposal](../proposals.md#submit-a-design-proposal). diff --git a/docs/src/proposals/accounts-db-replication.md b/docs/src/proposals/accounts-db-replication.md deleted file mode 100644 index 5ff0fc4e8f9..00000000000 --- a/docs/src/proposals/accounts-db-replication.md +++ /dev/null @@ -1,185 +0,0 @@ ---- -title: AccountsDB Replication for RPC Services ---- - -## Problem - -Validators fall behind the network when bogged down by heavy RPC load. This -seems to be due to a combination of CPU load and lock contention caused by -serving RPC requests. The most expensive RPC requests involve account scans. - -## Solution Overview - -AccountsDB `replicas` that run separately from the main validator can be used to -offload account-scan requests. Replicas would only: request and pull account -updates from the validator, serve client account-state RPC requests, and manage -AccountsDb and AccountsBackgroundService clean + shrink. - -The replica communicates to the main validator via a new RPC mechanism to fetch -metadata information about the replication and the accounts update from the validator. -The main validator supports only one replica node. A replica node can relay the -information to 1 or more other replicas forming a replication tree. - -At the initial start of the replica node, it downloads the latest snapshot -from a validator and constructs the bank and AccountsDb from it. After that, it queries -its main validator for new slots and requests the validator to send the updated -accounts for that slot and update to its own AccountsDb. - -The same RPC replication mechanism can be used between a replica to another replica. -This requires the replica to serve both the client and server in the replication model. - -On the client RPC serving side, `JsonRpcAccountsService` is responsible for serving -the client RPC calls for accounts related information similar to the existing -`JsonRpcService`. - -The replica will also take snapshots periodically so that it can start quickly after -a restart if the snapshot is not too old. - -## Detailed Solution -The following sections provide more details of the design. - -### Consistency Model -The AccountsDb information is replicated asynchronously from the main validator to the replica. -When a query against the replica's AccountsDb is made, the replica may not have the latest -information of the latest slot. In this regard, it will eventually be consistent. However, for -a particular slot, the information provided is consistent with that of its peer validator -for commitment levels confirmed and finalized. For V1, we only support queries at these two -levels. - -### Solana Replica Node -A new node named solana-replica-node will be introduced whose main responsibility is to maintain -the AccountsDb replica. The RPC node or replica node is used interchangeably in this document. -It will be a separate executable from the validator. - -The replica consists of the following major components: - -The `ReplicaSlotConfirmationRequestor`: this service is responsible for periodically sending the -request `ReplicaSlotConfirmationRequest` to its peer validator or replica for the latest slots. -It specifies the latest slot (last_replicated_slot) for which the replica has already -fetched the accounts information for. This maintains the ReplWorkingSlotSet and manages -the lifecycle of BankForks, BlockCommitmentCache (for the highest confirmed slot) and -the optimistically confirmed bank. - -The `ReplicaSlotConfirmationServer`: this service is responsible for serving the -`ReplicaSlotConfirmationRequest` and sends the `ReplicaSlotConfirmationResponse` back to the requestor. -The response consists of a vector of new slots the validator knows of which is later than the -specified last_replicated_slot. This service also runs in the main validator. This service -gets the slots for replication from the BankForks, BlockCommitmentCache and OptimisticallyConfirmedBank. - -The `ReplicaAccountsRequestor`: this service is responsible for sending the request -`ReplicaAccountsRequest` to its peer validator or replica for the `ReplicaAccountInfo` for a -slot for which it has not completed accounts db replication. The `ReplicaAccountInfo` contains -the `ReplicaAccountMeta`, Hash and the AccountData. The `ReplicaAccountMeta` contains info about -the existing `AccountMeta` in addition to the account data length in bytes. - -The `ReplicaAccountsServer`: this service is responsible for serving the `ReplicaAccountsRequest` -and sends `ReplicaAccountsResponse` to the requestor. The response contains the count of the -ReplAccountInfo and the vector of ReplAccountInfo. This service runs both in the validator -and the replica relaying replication information. The server can stream the account information -from its AccountCache or from the storage if already flushed. This is similar to how a snapshot -package is created from the AccountsDb with the difference that the storage does not need to be -flushed to the disk before streaming to the client. If the account data is in the cache, it can -be directly streamed. Care must be taken to avoid account data for a slot being cleaned while -serving the streaming. When attempting a replication of a slot, if the slot is already cleaned -up with accounts data cleaned as result of update in later rooted slots, the replica should -forsake this slot and try the later uncleaned root slot. - -During replication we also need to replicate the information of accounts that have been cleaned -up due to zero lamports, i.e. we need to be able to tell the difference between an account in a -given slot which was not updated and hence has no storage entry in that slot, and one that -holds 0 lamports and has been cleaned up through the history. We may record this via some -"Tombstone" mechanism -- recording the dead accounts cleaned up for a slot. The tombstones -themselves can be removed after exceeding the retention period expressed as epochs. Any -attempt to replicate slots with tombstones removed will fail and the replica should skip -this slot and try later ones. - -The `JsonRpcAccountsService`: this is the RPC service serving client requests for account -information. The existing JsonRpcService serves other client calls than AccountsDb ones. -The replica node only serves the AccountsDb calls. - -The existing JsonRpcService requires `BankForks`, `OptimisticallyConfirmedBank` and -`BlockCommitmentCache` to load the Bank. The JsonRpcAccountsService will need to use -information obtained from ReplicaSlotConfirmationResponse to construct the AccountsDb. - -The `AccountsBackgroundService`: this service also runs in the replica which is responsible -for taking snapshots periodically and shrinking the AccountsDb and doing accounts cleaning. -The existing code also uses BankForks which we need to keep in the replica. - -### Compatibility Consideration - -For protocol compatibility considerations, all the requests have the replication version which -is initially set to 1. Alternatively, we can use the validator's version. The RPC server side -shall check the request version and fail if it is not supported. - -### Replication Setup -To limit adverse effects on the validator and the replica due to replication, they can be -configured with a list of replica nodes which can form a replication pair with it. And the -replica node is configured with the validator which can serve its requests. - - -### Fault Tolerance -The main responsibility of making sure the replication is tolerant of faults lies with the -replica. In case of request failures, the replica shall retry the requests. - - -### Interface - -Following are the client RPC APIs supported by the replica node in JsonRpcAccountsService. - -- getAccountInfo -- getBlockCommitment -- getMultipleAccounts -- getProgramAccounts -- getMinimumBalanceForRentExemption -- getInflationGovernor -- getInflationRate -- getEpochSchedule -- getRecentBlockhash -- getFees -- getFeeCalculatorForBlockhash -- getFeeRateGovernor -- getLargestAccounts -- getSupply -- getStakeActivation -- getTokenAccountBalance -- getTokenSupply -- getTokenLargestAccounts -- getTokenAccountsByOwner -- getTokenAccountsByDelegate - -Following APIs are not included: - -- getInflationReward -- getClusterNodes -- getRecentPerformanceSamples -- getGenesisHash -- getSignatureStatuses -- getMaxRetransmitSlot -- getMaxShredInsertSlot -- sendTransaction -- simulateTransaction -- getSlotLeader -- getSlotLeaders -- minimumLedgerSlot -- getBlock -- getBlockTime -- getBlocks -- getBlocksWithLimit -- getTransaction -- getSignaturesForAddress -- getFirstAvailableBlock -- getBlockProduction - - -Action Items - -1. Build the replica framework and executable -2. Integrate snapshot restore code for bootstrap the AccountsDb. -3. Develop the ReplicaSlotConfirmationRequestor and ReplicaSlotConfirmationServer interface code -4. Develop the ReplicaSlotConfirmationRequestor and ReplicaSlotConfirmationServer detailed implementations: managing the ReplEligibleSlotSet lifecycle: adding new roots and deleting root to it. And interfaces managing ReplWorkingSlotSet interface: adding and removing. Develop component synthesising information from BankForks, BlockCommitmentCache and OptimistcallyConfirmedBank on the server side and maintaining information on the client side. -5. Develop the interface code for ReplicaAccountsRequestor and ReplicaAccountsServer -6. Develop detailed implementation for ReplicaAccountsRequestor and ReplicaAccountsServer and develop the replication account storage serializer and deserializer. -7. Develop the interface code JsonRpcAccountsService -8. Detailed Implementation of JsonRpcAccountsService, refactor code to share with part of JsonRpcService. -9. Integrate with the AccountsBackgroundService in the replica for shrinking, cleaning, snapshotting. -10. Metrics and performance testing diff --git a/docs/src/proposals/bankless-leader.md b/docs/src/proposals/bankless-leader.md deleted file mode 100644 index 462b253a337..00000000000 --- a/docs/src/proposals/bankless-leader.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: Bankless Leader ---- - -A bankless leader does the minimum amount of work to produce a valid block. The -leader is tasked with ingress transactions, sorting and filtering valid -transactions, arranging them into entries, shredding the entries and -broadcasting the shreds. While a validator only needs to reassemble the block -and replay execution of well formed entries. The leader does 3x more memory -operations before any bank execution than the validator per processed -transaction. - -## Rationale - -Normal bank operation for a spend needs to do 2 loads and 2 stores. With this -design leader just does 1 load. so 4x less account_db work before generating the -block. The store operations are likely to be more expensive than reads. - -When replay stage starts processing the same transactions, it can assume that -PoH is valid, and that all the entries are safe for parallel execution. The fee -accounts that have been loaded to produce the block are likely to still be in -memory, so the additional load should be warm and the cost is likely to be -amortized. - -## Fee Account - -The [fee account](https://solana.com/docs/terminology#fee_account) pays for the -transaction to be included in the block. The leader only needs to validate that -the fee account has the balance to pay for the fee. - -## Balance Cache - -For the duration of the leaders consecutive blocks, the leader maintains a -temporary balance cache for all the processed fee accounts. The cache is a map -of pubkeys to lamports. - -At the start of the first block the balance cache is empty. At the end of the -last block the cache is destroyed. - -The balance cache lookups must reference the same base fork for the entire -duration of the cache. At the block boundary, the cache can be reset along with -the base fork after replay stage finishes verifying the previous block. - -## Balance Check - -Prior to the balance check, the leader validates all the signatures in the -transaction. - -1. Verify the accounts are not in use and BlockHash is valid. -2. Check if the fee account is present in the cache, or load the account from - accounts_db and store the lamport balance in the cache. -3. If the balance is less than the fee, drop the transaction. -4. Subtract the fee from the balance. -5. For all the keys in the transaction that are Credit-Debit and are referenced - by an instruction, reduce their balance to 0 in the cache. The account fee is - declared as Credit-Debit, but as long as it is not used in any instruction - its balance will not be reduced to 0. - -## Leader Replay - -Leaders will need to replay their blocks as part of the standard replay stage -operation. - -## Leader Replay With Consecutive Blocks - -A leader can be scheduled to produce multiple blocks in a row. In that scenario -the leader is likely to be producing the next block while the replay stage for -the first block is playing. - -When the leader finishes the replay stage it can reset the balance cache by -clearing it, and set a new fork as the base for the cache which can become -active on the next block. - -## Resetting the Balance Cache - -1. At the start of the block, if the balance cache is uninitialized, set the - base fork for the balance cache to be the parent of the block and create an - empty cache. -2. if the cache is initialized, check if block's parents has a new frozen bank - that is newer than the current base fork for the balance cache. -3. if a parent newer than the cache's base fork exist, reset the cache to the - parent. - -## Impact on Clients - -The same fee account can be reused many times in the same block until it is used -once as Credit-Debit by an instruction. - -Clients that transmit a large number of transactions per second should use a -dedicated fee account that is not used as Credit-Debit in any instruction. - -Once an account fee is used as Credit-Debit, it will fail the balance check -until the balance cache is reset. - -### Check out the [SIMD here to contribute](https://github.com/solana-foundation/solana-improvement-documents/pull/5) diff --git a/docs/src/proposals/block-confirmation.md b/docs/src/proposals/block-confirmation.md deleted file mode 100644 index c973950a50a..00000000000 --- a/docs/src/proposals/block-confirmation.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Block Confirmation ---- - -A validator votes on a PoH hash for two purposes. First, the vote indicates it -believes the ledger is valid up until that point in time. Second, since many -valid forks may exist at a given height, the vote also indicates exclusive -support for the fork. This document describes only the former. The latter is -described in [Tower BFT](../implemented-proposals/tower-bft.md). - -## Current Design - -To start voting, a validator first registers an account to which it will send -its votes. It then sends votes to that account. The vote contains the tick -height of the block it is voting on. The account stores the 32 highest heights. - -### Problems - -- Only the validator knows how to find its own votes directly. - - Other components, such as the one that calculates confirmation time, needs to - be baked into the validator code. The validator code queries the bank for all - accounts owned by the vote program. - -- Voting ballots do not contain a PoH hash. The validator is only voting that - it has observed an arbitrary block at some height. - -- Voting ballots do not contain a hash of the bank state. Without that hash, - there is no evidence that the validator executed the transactions and - verified there were no double spends. - -## Proposed Design - -### No Cross-block State Initially - -At the moment a block is produced, the leader shall add a NewBlock transaction -to the ledger with a number of tokens that represents the validation reward. -It is effectively an incremental multisig transaction that sends tokens from -the mining pool to the validators. The account should allocate just enough -space to collect the votes required to achieve a supermajority. When a -validator observes the NewBlock transaction, it has the option to submit a vote -that includes a hash of its ledger state (the bank state). Once the account has -sufficient votes, the vote program should disperse the tokens to the -validators, which causes the account to be deleted. - -#### Logging Confirmation Time - -The bank will need to be aware of the vote program. After each transaction, it -should check if it is a vote transaction and if so, check the state of that -account. If the transaction caused the supermajority to be achieved, it should -log the time since the NewBlock transaction was submitted. - -### Finality and Payouts - -[Tower BFT](../implemented-proposals/tower-bft.md) is the proposed fork selection algorithm. It proposes -that payment to miners be postponed until the _stack_ of validator votes reaches -a certain depth, at which point rollback is not economically feasible. The vote -program may therefore implement Tower BFT. Vote instructions would need to -reference a global Tower account so that it can track cross-block state. - -## Challenges - -### On-chain voting - -Using programs and accounts to implement this is a bit tedious. The hardest -part is figuring out how much space to allocate in NewBlock. The two variables -are the _active set_ and the stakes of those validators. If we calculate the -active set at the time NewBlock is submitted, the number of validators to -allocate space for is known upfront. If, however, we allow new validators to -vote on old blocks, then we'd need a way to allocate space dynamically. - -Similar in spirit, if the leader caches stakes at the time of NewBlock, the -vote program doesn't need to interact with the bank when it processes votes. If -we don't, then we have the option to allow stakes to float until a vote is -submitted. A validator could conceivably reference its own staking account, but -that'd be the current account value instead of the account value of the most -recently finalized bank state. The bank currently doesn't offer a means to -reference accounts from particular points in time. - -### Voting Implications on Previous Blocks - -Does a vote on one height imply a vote on all blocks of lower heights of -that fork? If it does, we'll need a way to lookup the accounts of all -blocks that haven't yet reached supermajority. If not, the validator could -send votes to all blocks explicitly to get the block rewards. diff --git a/docs/src/proposals/cluster-test-framework.md b/docs/src/proposals/cluster-test-framework.md deleted file mode 100644 index 30c6b5bc6eb..00000000000 --- a/docs/src/proposals/cluster-test-framework.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: Cluster Test Framework ---- - -This document proposes the Cluster Test Framework \(CTF\). CTF is a test harness that allows tests to execute against a local, in-process cluster or a deployed cluster. - -## Motivation - -The goal of CTF is to provide a framework for writing tests independent of where and how the cluster is deployed. Regressions can be captured in these tests and the tests can be run against deployed clusters to verify the deployment. The focus of these tests should be on cluster stability, consensus, fault tolerance, API stability. - -Tests should verify a single bug or scenario, and should be written with the least amount of internal plumbing exposed to the test. - -## Design Overview - -Tests are provided an entry point, which is a `contact_info::ContactInfo` structure, and a keypair that has already been funded. - -Each node in the cluster is configured with a `validator::ValidatorConfig` at boot time. At boot time this configuration specifies any extra cluster configuration required for the test. The cluster should boot with the configuration when it is run in-process or in a data center. - -Once booted, the test will discover the cluster through a gossip entry point and configure any runtime behaviors via validator RPC. - -## Test Interface - -Each CTF test starts with an opaque entry point and a funded keypair. The test should not depend on how the cluster is deployed, and should be able to exercise all the cluster functionality through the publicly available interfaces. - -```text -use crate::contact_info::ContactInfo; -use solana_keypair::Keypair; -use solana_signer::Signer; -pub fn test_this_behavior( - entry_point_info: &ContactInfo, - funding_keypair: &Keypair, - num_nodes: usize, -) -``` - -## Cluster Discovery - -At test start, the cluster has already been established and is fully connected. The test can discover most of the available nodes over a few second. - -```text -use crate::gossip_service::discover_nodes; - -// Discover the cluster over a few seconds. -let cluster_nodes = discover_nodes(&entry_point_info, num_nodes); -``` - -## Cluster Configuration - -To enable specific scenarios, the cluster needs to be booted with special configurations. These configurations can be captured in `validator::ValidatorConfig`. - -For example: - -```text -let mut validator_config = ValidatorConfig::default_for_test(); -let local = LocalCluster::new_with_config( - num_nodes, - 10_000, - 100, - &validator_config - ); -``` - -## How to design a new test - -For example, there is a bug that shows that the cluster fails when it is flooded with invalid advertised gossip nodes. Our gossip library and protocol may change, but the cluster still needs to stay resilient to floods of invalid advertised gossip nodes. - -Configure the RPC service: - -```text -let mut validator_config = ValidatorConfig::default_for_test(); -validator_config.rpc_config.enable_rpc_gossip_push = true; -validator_config.rpc_config.enable_rpc_gossip_refresh_active_set = true; -``` - -Wire the RPCs and write a new test: - -```text -pub fn test_large_invalid_gossip_nodes( - entry_point_info: &ContactInfo, - funding_keypair: &Keypair, - num_nodes: usize, -) { - let cluster = discover_nodes(&entry_point_info, num_nodes); - - // Poison the cluster. - let client = create_client(entry_point_info.client_facing_addr(), VALIDATOR_PORT_RANGE); - for _ in 0..(num_nodes * 100) { - client.gossip_push( - cluster_info::invalid_contact_info() - ); - } - sleep(Durration::from_millis(1000)); - - // Force refresh of the active set. - for node in &cluster { - let client = create_client(node.client_facing_addr(), VALIDATOR_PORT_RANGE); - client.gossip_refresh_active_set(); - } - - // Verify that spends still work. - verify_spends(&cluster); -} -``` diff --git a/docs/src/proposals/comprehensive-compute-fees.md b/docs/src/proposals/comprehensive-compute-fees.md deleted file mode 100644 index 786b553721a..00000000000 --- a/docs/src/proposals/comprehensive-compute-fees.md +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: Comprehensive Compute Fees ---- - -## Motivation - -The current fee structure lacks a comprehensive account of the work required by -a validator to process a transaction. The fee structure is only based on the -number of signatures in a transaction but is meant to account for the work that -the validator must perform to validate each transaction. The validator performs -a lot more user-defined work than just signature verification. Processing a -transaction typically includes signature verification, account locking, account -loading, and instruction processing. - -## Proposed Solution - -The following solution does not specify what native token costs are to be -associated with the new fee structure. Instead, it sets the criteria and -provides the knobs that a cost model can use to determine those costs. - -### Fee - -The goal of the fees is to cover the computation cost of processing a -transaction. Each of the fee categories below will be represented as a compute -unit cost that, when added together, encompasses the entire cost of processing -the transaction. By calculating the total cost of the transaction, the runtime -can charge a more representative fee and make better transaction scheduling -decisions. - -A fee could be calculated based on: - -1. Number of signatures - - Fixed rate per signature -2. Number of write locks - - Fixed rate per writable account -3. Data byte cost - - Fixed rate per byte of the sum of the length all a transactions instruction - data -4. Account sizes - - Account sizes can't be known up-front but can account for a considerable - amount of the load the transaction incurs on the network. The payer will - be charged for a maximum account size (10m) upfront and refunded the - difference after the actual account sizes are known. -5. Compute budget - - Each transaction will be given a default transaction-wide compute budget of - 200k units with the option of requesting a larger budget via a compute - budget instruction up to a maximum of 1m units. This budget is used to - limit the time it takes to process a transaction. The compute budget - portion of the fee will be charged up-front based on the default or - requested amount. After processing, the actual number of units consumed - will be known, and the payer will be refunded the difference, so the payer - only pays for what they used. Builtin programs will have a fixed cost - while SBF program's cost will be measured at runtime. -6. Precompiled programs - - Precompiled programs are performing compute-intensive operations. The work - incurred by a precompiled program is predictable based on the instruction's - data array. Therefore a cost will be assigned per precompiled program - based on the parsing of instruction data. Because precompiled programs are - processed outside of the bank, their compute cost will not be reflected in - the compute budget and will not be used in transaction scheduling - decisions. The methods used to determine the fixed cost of the components - above are described in - [#19627](https://github.com/solana-labs/solana/issues/19627) - -### Cost model - -The cost model is used to assess what load a transaction will incur during -in-slot processing and then make decisions on how to best schedule transaction -into batches. - -The cost model's criteria are identical to the fee's criteria except for -signatures and precompiled programs. These two costs are incurred before a -transaction is scheduled and therefore do not affect how long a transaction -takes within a slot to process. - -### Cache account sizes and use them instead of the max - -https://github.com/solana-labs/solana/issues/20511 - -### Requestable compute budget caps and heap sizes - -The precompiled -[ComputeBudget](https://github.com/solana-labs/solana/blob/00929f836348d76cb3503d0ba5f76f0d275bcc66/sdk/src/compute_budget.rs#L34) -program can be used to request higher transaction-wide compute budget caps and -program heap sizes. The requested increases will be reflected in the -transaction's fee. - -### Fees for precompiled program failures - -https://github.com/solana-labs/solana/issues/20481 - -### Rate governing - -Current rate governing needs to be re-assessed. Fees are being rate -governed down to their minimums because the number of signatures in each slot is -far lower than the "target" signatures per slot. - -Instead of using the number of signatures to rate govern, the cost model will -feed back information based on the batch/queue load it is seeing. The fees will -sit at a target rate and only increase if the load goes above a specified but to -be determined threshold. The governing will be applied across all the fee -criteria. - -### Deterministic fees - -Solana's fees are currently deterministic based on a given blockhash. This -determinism is a nice feature that simplifies client interactions. An example -is when draining an account that is also the payer, the transaction issuer can -pre-compute the fee and then set the entire remaining balance to be transferred -out without worrying that the fee will change leaving a very small amount -remaining in the account. Another example is for offline signing, the payer -signer can guarantee what fee that will be charged for the transaction based on -the nonce's blockhash. - -Determinism is achieved in two ways: -- blockhash queue contains a list of recent (<=~2min) blockhashes and a - `lamports_per_signature` value. The blockhash queue is one of the snapshot's - serialized members and thus bank hash depends on it. -- Nonce accounts used for offline signing contain a `lamports_per_signature` - value in its account data - -In both cases, when a transaction is assessed a fee, the -`lamports_per_signature` to use is looked up (either in the queue or in the -nonce account's data) using the transaction's blockhash. - -This currently comes with the following challenges: -- Exposing the `FeeCalculator` object to the clients (holds the - `lamports_per_signature`) makes it hard to evolve the fee criteria due to - backward-compatibility. This issue is being solved by deprecating the - `FeeCalculator` object and instead the new apis take a message and return a - fee. -- Blockhash queue entries contain the fee criteria specifics and are part of the - bankhash so evolving the fees over time involves more work/risk -- Nonce accounts store the fee criteria directly in their account data so - evolving the fees over time requires changes to nonce account data and data - size. - -Two solutions to the latter two challenges -- Get rid of the concept of deterministic fees. Clients ask via RPC to - calculate the current fee estimate and the actual fee is assessed when the - transaction is processed. Fee changes will be governed and change slowly - based on network load so the fee differences will be small within the 2min - window. Nonce accounts no longer store the fee criteria but instead a fee - cap. If the assessed fee at the time of processing exceeds the cap then the - transaction fails. This solution removes fee criteria entirely from the - blockhash queue and nonce accounts and removes the need for either of those to - evolve if there is a need for fee criteria to evolve. -- Retain the concept of deterministic fees. Clients ask via RPC to calculate - the current fee and pass in a blockhash that fee will be associated with. - Blockhash queue and nonce accounts switch to a versioned but internal "Fee" - object (similar to "FeeCalculator"). Each time there is a need for fees to - evolve the fee object will add a new version and new blockhash queue entries - and new nonce accounts will use the new version. diff --git a/docs/src/proposals/embedding-move.md b/docs/src/proposals/embedding-move.md deleted file mode 100644 index e45730267fd..00000000000 --- a/docs/src/proposals/embedding-move.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Embedding the Move Language ---- - -### This document is outdated and a new approach to support 'move' is in the works. - -## Problem - -Solana enables developers to write on-chain programs in general purpose programming languages such as C or Rust, but those programs contain Solana-specific mechanisms. For example, there isn't another chain that asks developers to create a Rust module with a `process_instruction(KeyedAccounts)` function. Whenever practical, Solana should offer application developers more portable options. - -Until just recently, no popular blockchain offered a language that could expose the value of Solana's massively parallel [runtime](../validator/runtime.md). Solidity contracts, for example, do not separate references to shared data from contract code, and therefore need to be executed serially to ensure deterministic behavior. In practice we see that the most aggressively optimized EVM-based blockchains all seem to peak out around 1,200 TPS - a small fraction of what Solana can do. The Libra project, on the other hand, designed an on-chain programming language called Move that is more suitable for parallel execution. Like Solana's runtime, Move programs depend on accounts for all shared state. - -The biggest design difference between Solana's runtime and Libra's Move VM is how they manage safe invocations between modules. Solana took an operating systems approach and Libra took the domain-specific language approach. In the runtime, a module must trap back into the runtime to ensure the caller's module did not write to data owned by the callee. Likewise, when the callee completes, it must again trap back to the runtime to ensure the callee did not write to data owned by the caller. Move, on the other hand, includes an advanced type system that allows these checks to be run by its bytecode verifier. Because Move bytecode can be verified, the cost of verification is paid just once, at the time the module is loaded on-chain. In the runtime, the cost is paid each time a transaction crosses between modules. The difference is similar in spirit to the difference between a dynamically-typed language like Python versus a statically-typed language like Java. Solana's runtime allows applications to be written in general purpose programming languages, but that comes with the cost of runtime checks when jumping between programs. - -This proposal attempts to define a way to embed the Move VM such that: - -- cross-module invocations within Move do not require the runtime's - - cross-program runtime checks - -- Move programs can leverage functionality in other Solana programs and vice - - versa - -- Solana's runtime parallelism is exposed to batches of Move and non-Move - - transactions - -## Proposed Solution - -### Move VM as a Solana loader - -The Move VM shall be embedded as a Solana loader under the identifier `MOVE_PROGRAM_ID`, so that Move modules can be marked as `executable` with the VM as its `owner`. This will allow modules to load module dependencies, as well as allow for parallel execution of Move scripts. - -All data accounts owned by Move modules must set their owners to the loader, `MOVE_PROGRAM_ID`. Since Move modules encapsulate their account data in the same way Solana programs encapsulate theirs, the Move module owner should be embedded in the account data. The runtime will grant write access to the Move VM, and Move grants access to the module accounts. - -### Interacting with Solana programs - -To invoke instructions in non-Move programs, Solana would need to extend the Move VM with a `process_instruction()` system call. It would work the same as `process_instruction()` Rust SBF programs. diff --git a/docs/src/proposals/fee_transaction_priority.md b/docs/src/proposals/fee_transaction_priority.md deleted file mode 100644 index 1b6b06de365..00000000000 --- a/docs/src/proposals/fee_transaction_priority.md +++ /dev/null @@ -1,210 +0,0 @@ ---- -title: Fee Transaction Priority ---- - -Additional fees were introduced to transactions as a method to allow users to bid for priority for -their transactions in the leader's queue. - -The fee priority of a transaction `T` can then be defined as `F(T)`, where `F(T)` is the "fee-per -compute-unit", calculated by: - -`(additional_fee + base_fee) / requested_compute_units` - -To ensure users get fair priority based on their fee, the proposed scheduler for the leader must -guarantee that given `T1` and `T2` in the pending queue, and `F(T1) > F(T2)`: - -1. `T1` should be considered for processing before `T2` -2. If `T1` cannot be processed before `T2` because there's already a transaction currently being -processed that contends on an account `A`, then `T2` should not be scheduled if it would grab -any account locks needed by `T1`. This prevents lower fee transactions like `T2` from starving -higher paying transactions like `T1`. - - -### Transaction Pipeline - -Pipeline: -1. Sigverify -2. Scheduler -3. BankingStage threads - -Transactions from sigverify are connected via a channel to the scheduler. The scheduler maintains -`N` bi-directional channels with the `N` BankingStage threads, implemented as a pair of two -unidirectional channels. - -The scheduler's job is to run an algorithm in which it determines how to schedule transactions -received from sigverify to the `N` BankingStage threads. A transaction is scheduled to be executed -on a particular BankingStage thread by sending that transaction to the thread via its associated -channel. - -Once a BankingStage thread finishes processing a transaction `T` , it sends the `T` back -to the scheduler via the same channel to signal of completion. - -### Scheduler Implementation - -The scheduler is the most complex piece of the above pipeline, its implementation is made up of a -few pieces. Note for now, all these pieces are maintained by the single scheduler thread to avoid -locking complexity. - -#### Components of the `Scheduler`: - -1. `default_transaction_queue` - A max-heap `BinaryHeap` that tracks all pending transactions. -The priority in the heap is the additional fee of the transaction. Transactions are added to this queue -from sigverify before the leader slot begins. -2. `all_transaction_queues` - A `VecDeque>` that tracks all pending queues of work. -Some pending queues have higher priority than others (as will be explained later in the `Handling Completion Signals from BankingStage Threads` section below). The list is ordered in priority from highest to lowest priority. On -initialization, `all_transaction_queues[0] = default_transaction_queue`. -3. `locked_accounts` - A `HashMap` that tracks the set of accounts needed to execute the -current set of transactions scheduled/sent to banking threads. Accounts are added to this set -*before* being sent to BankingStage threads. The `usize` represents a refcount, because multiple read -accounts could be grabbed.`LockedPubkey` is defined as: -``` -enum LockedPubkey { - Read(Pubkey), - Write(Pubkey), -} -``` -4. `blocked_transactions` - A `HashMap>` keyed by -transaction signature, and maps to a `BlockedTransactionsQueue` defined as: -``` -/// Represents a heap of transactions that cannot be scheduled because they -/// would take locks on accounts needed by a higher paying transaction -struct BlockedTransactionsQueue { - // The higher priority transaction blocking all the other transactions in - // `blocked_transactions` below - highest_priority_blocked_transaction: Transaction, - other_blocked_transactions: BinaryHeap -} -``` -5. `blocked_transaction_queues_by_accounts` - A `HashMap>` keyed by -account keys. - -#### Algorithm (Main Loop): - -Assume `N` BankingStage threads: - -The scheduler will run for each banking thread a function `find_next_highest_transaction()`: - -1. Pop off the highest priority transaction `next_highest_transaction` from `self.all_transaction_queues[0]`. -If ``self.all_transaction_queues[0]` is empty, pop off the first deque item and continue. - -2. Let `transaction_accounts` be the set of `LockedPubkey` keys needed by -`next_highest_transaction`. We run the following: - -``` - for account_key in transaction_accounts { - // Check if the `LockedPubkey` conflicts with any key in the `locked_accounts` set, which - // would indicate a transaction using this account with a conflicting lock is already - // running - if self.locked_accounts.is_conflicting(account_key) { - return Conflict; - } - - // Check if any higher fee transaction has already reserved this account. This prevents - // lower fee transactions from starving higher fee transactions. - if self.blocked_transaction_queues_by_accounts.contains_key(account_key) { - return Conflict; - } - return NoConflict; - } -``` - -3. In the case of a `NoConflict` we run: - -``` - for account_key in transaction_accounts { - self.locked_accounts.insert_reference(account_key.key()); - } - - banking_thread_channel.send(next_highest_transaction); -``` - -4. In the case of a `Conflict` we run: - -``` -for locked_account_key in transaction_accounts { - let account_key = locked_account_key.key() - let blocked_transaction_entry = self.blocked_transaction_queues_by_accounts.entry(account_key); - match blocked_transaction_entry { - Occupied(existing_blocked_transaction) => { - // If there is already a set of transactions blocked on this account, add - // this transaction to the priority queue. - existing_blocked_transaction.insert_transaction(next_highest_transaction); - } - - Vacant(vacant_entry) => { - // Create a new queue blocked on this transaction - let new_blocked_transaction_queue = - Rc::new(BlockedTransactionsQueue { - highest_priority_blocked_transaction: next_highest_transaction, - other_blocked_transactions: BinaryHeap::new(), - }); - // Insert into the hashmap for this `account_key` - vacant_entry.insert(new_blocked_transaction_queue.clone()); - // Insert into the `blocked_transactions` hashmap to indicate this set of transactions - // is blocked by `next_highest_transaction` - self.blocked_transactions.insert( - next_highest_transaction.signature(), - new_blocked_transaction_queue - ); - } - } -} -``` - -5. Run until all `N` BankingStage threads have been sent `processing_batch` transactions (i.e. hit step 3 above). - -#### Banking Threads -1. Banking threads maintain a queue of transactions sent to them by the scheduler, sorted by priority. -2. Because the scheduler has guaranteed that there are no locking conflicts, the banking thread can process -some `M` of these transactions at a time and pack them into entries - -#### Handling Completion Signals from BankingStage Threads - -Outside of the main loop above, we rely on BankingThreads threads signaling us they've finished their -task to schedule the next transactions. - -1. Once a BankingStage thread finishes processing a batch of transactions `completed_transactions_batch` , -it sends the `completed_transactions_batch` back to the scheduler via the same channel to signal of completion. - -2. Upon receiving this signal, the BankingStage thread processes the locked accounts -`transaction_accounts` for each `completed_transaction` in `completed_transactions_batch`: -``` -let mut unlocked_accounts = vec![]; -// First remove all the locks from the tracking list -for locked_account in transaction_accounts { - if self.locked_accounts.remove_reference(locked_account) { - unlocked_accounts.push(locked_account.key()); - } -} - -// Check if freeing up these accounts has now allowed any new -// blocked transactions to run -for account_key in unlocked_accounts { - if let Some(blocked_transaction_queue) = self.blocked_transaction_queues_by_accounts.get(account_key) { - // Check if the transaction blocking this queue can be run now, thereby unblocking this queue - if blocked_transaction_queue.highest_priority_blocked_transaction.can_get_locks() { - // Schedule the transaction to the banking thread - banking_thread_channel.send(blocked_transaction_queue.highest_priority_blocked_transaction); - - return; - } - } - - // If no higher priority transactions were unblocked, continue scheduling from the main queue, - // described in the main loop section above - find_next_highest_transaction(); -} -``` - -3. Check if the finished transaction was the blocking transaction for any queue: - -``` -if let Some(blocked_transaction_queue) = self.blocked_transactions.get(completed_transaction.signature) { - // Now push the rest of the queue to the head of `all_transaction_queues`, since we know - // everything in this blocked queue must be of higher priority, (since they were - // added to this queue earlier, this means they must have been popped off the main - // `transaction_accounts` queue earlier, hence higher priority) - self.all_transaction_queues.push_front(blocked_transaction_queue.other_blocked_transactions); - self.blocked_transactions.remove(completed_transaction.signature); -} -``` \ No newline at end of file diff --git a/docs/src/proposals/handle-duplicate-block.md b/docs/src/proposals/handle-duplicate-block.md deleted file mode 100644 index 2941f17d270..00000000000 --- a/docs/src/proposals/handle-duplicate-block.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Handle Duplicate Block ---- - -# Leader Duplicate Block Slashing - -This design describes how the cluster slashes leaders that produce duplicate -blocks. - -Leaders that produce multiple blocks for the same slot increase the number of -potential forks that the cluster has to resolve. - -## Primitives -1. gossip_root: Nodes now gossip their current root -2. gossip_duplicate_slots: Nodes can gossip up to `N` duplicate slot proofs. -3. `DUPLICATE_THRESHOLD`: The minimum percentage of stake that needs to vote on a fork with version `X` of a duplicate slot, in order for that fork to become votable. - -## Protocol -1. When WindowStage detects a duplicate slot proof `P`, it checks the new `gossip_root` to see if `<= 1/3` of the nodes have rooted a slot `S >= P`. If so, it pushes a proof to `gossip_duplicate_slots` to gossip. WindowStage then signals ReplayStage about this duplicate slot `S`. These proofs can be purged from gossip once the validator sees > 2/3 of people gossiping roots `R > S`. - -2. When ReplayStage receives the signal for a duplicate slot `S` from `1)` above, the validator monitors gossip and replay waiting for`>= DUPLICATE_THRESHOLD` votes for the same hash which implies the same version of the slot. If this condition is met for some version with hash `H` of slot `S`, this is then known as the `duplicate_confirmed` version of the slot. - -Before a duplicate slot `S` is `duplicate_confirmed`, it's first excluded from the vote candidate set in the fork choice rules. In addition, ReplayStage also resets PoH to the *latest* ancestor of the *earliest* `non-duplicate/confirmed_duplicate_slot`, so that block generation can start happening on the earliest known *safe* block. - -Some notes about the `DUPLICATE_THRESHOLD`. In the cases below, assume `DUPLICATE_THRESHOLD = 52`: - -a) If less than `2 * DUPLICATE_THRESHOLD - 1` percentage of the network is malicious, then there can only be one such `duplicate_confirmed` version of the slot. With `DUPLICATE_THRESHOLD = 52`, this is -a malicious tolerance of `4%` - -b) The liveness of the network is at most `1 - DUPLICATE_THRESHOLD - SWITCH_THRESHOLD`. This is because if you need at least `SWITCH_THRESHOLD` percentage of the stake voting on a different fork in order to switch off of a duplicate fork that has `< DUPLICATE_THRESHOLD` stake voting on it, and is *not* `duplicate_confirmed`. For `DUPLICATE_THRESHOLD = 52` and `DUPLICATE_THRESHOLD = 38`, this implies a liveness tolerance of `10%`. - -For example in the situation below, validators that voted on `2` can't vote any further on fork `2` because it's been removed from fork choice. Now slot 6 better have enough stake for a switching proof, or the network halts. - -```text - |-------- 2 (51% voted, then detected this slot was a duplicate and removed this slot from fork choice) -0---| - |---------- 6 (39%) - -``` - -3. Switching proofs need to be extended to allow including vote hashes from different versions of the same slot (detected through 1). Right now this is not supported since switching proofs can -only be built using votes from banks in BankForks, and two different versions of the same slot cannot -simultaneously exist in BankForks. For instance: - -```text - |-------- 2 - | -0------------- 1 ------ 2' - | - |---------- 6 - -``` - -Imagine each version of slot 2 and 2' have `DUPLICATE_THRESHOLD / 2` of the votes on them, so neither duplicate can be confirmed. At most slot 6 has `1 - DUPLICATE_THRESHOLD / 2` of the votes -on it, which is less than the switching threshold. Thus, in order for validators voting on `2` or `2'` to switch to slot 6, and make progress, they need to incorporate votes from the other version of the slot into their switching proofs. - - -### The repair problem. -Now what happens if one of the following occurs: - -1) Due to network blips/latencies, some validators fail to observe the gossip votes before they are overwritten by newer votes? Then some validators may conclude a slot `S` is `duplicate_confirmed` while others don't. - -2) Due to lockouts, no version of duplicate slot `S` reaches `duplicate_confirmed` status, but one of its descendants may reach `duplicate_confirmed` after those lockouts expire, which by definition, means `S` is also `duplicate_confirmed`. - -3) People who are catching up and don't see the votes in gossip encounter a dup block and can't make progress. - -We assume that given a network is eventually stable, if at least one correct validator observed `S` is `duplicate_confirmed`, then if `S` is part of the heaviest fork, then eventually all validators will observe some descendant of `S` is duplicate confirmed. - -This problem we need to solve is modeled simply by the below scenario: - -```text -1 -> 2 (duplicate) -> 3 -> 4 (duplicate) -``` -Assume the following: - -1. Due to gossiping duplicate proofs, we assume everyone will eventually see duplicate proofs for 2 and 4, so everyone agrees to remove them from fork choice until they are `duplicate_confirmed`. - -2. Due to lockouts, `> DUPLICATE_THRESHOLD` of the stake votes on 4, but not 2. This means at least `DUPLICATE_THRESHOLD` of people have the "correct" version of both slots 2 and 4. - -3. However, the remaining `1-DUPLICATE_THRESHOLD` of people have wrong version of 2. This means in replay, their slot 3 will be marked dead, *even though the faulty slot is 2*. The goal is to get these people on the right fork again. - -Possible solution: - -1. Change `EpochSlots` to signal when a bank is frozen, not when a slot is complete. If we see > `DUPLICATE_THRESHOLD` have frozen the dead slot 3, then we attempt recovery. Note this does not mean that all `DUPLICATE_THRESHOLD` have frozen the same version of the bank, it's just a signal to us that something may be wrong with our version of the bank. - -2. Recovery takes the form of a special repair request, `RepairDuplicateConfirmed(dead_slot, Vec<(Slot, Hash)>)`, which specifies a dead slot, and then a vector of `(slot, hash)` of `N` of its latest parents. - -3. The repairer sees this request and responds with the correct hash only if any element of the `(slot, hash)` vector is both `duplicate_confirmed` and the hash doesn't match the requester's hash in the vector. - -4. Once the requester sees the "correct" hash is different than their frozen hash, they dump the block so that they can accept a new block, and ask the network for the block with the correct hash. - -Of course the repairer might lie to you, and you'll get the wrong version of the block, in which case you'll end up with another dead block and repeat the procedure. diff --git a/docs/src/proposals/interchain-transaction-verification.md b/docs/src/proposals/interchain-transaction-verification.md deleted file mode 100644 index 7a50042227f..00000000000 --- a/docs/src/proposals/interchain-transaction-verification.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: Inter-chain Transaction Verification ---- - -## Problem - -Inter-chain applications are not new to the digital asset ecosystem; in fact, even the smaller centralized exchanges still categorically dwarf all single chain applications put together in terms of users and volume. They command massive valuations and have spent years effectively optimizing their core products for a broad range of end users. However, their basic operations center around mechanisms that require their users to unilaterally trust them, typically with little to no recourse or protection from accidental loss. This has led to the broader digital asset ecosystem being fractured along network lines because interoperability solutions typically: - -- Are technically complex to fully implement -- Create unstable network scale incentive structures -- Require consistent and high level cooperation between stakeholders - -## Proposed Solution - -Simple Payment Verification \(SPV\) is a generic term for a range of different methodologies used by light clients on most major blockchain networks to verify aspects of the network state without the burden of fully storing and maintaining the chain itself. In most cases, this means relying on a form of hash tree to supply a proof of the presence of a given transaction in a certain block by comparing against a root hash in that block’s header or equivalent. This allows a light client or wallet to reach a probabilistic level of certainty about on-chain events by itself with a minimum of trust required with regard to network nodes. - -Traditionally the process of assembling and validating these proofs is carried out off chain by nodes, wallets, or other clients, but it also offers a potential mechanism for inter-chain state verification. However, by moving the capability to validate SPV proofs on-chain as a smart contract while leveraging the archival properties inherent to the blockchain, it is possible to construct a system for programmatically detecting and verifying transactions on other networks without the involvement of any type of trusted oracle or complex multi-stage consensus mechanism. This concept is broadly generalisable to any network with an SPV mechanism and can even be operated bilaterally on other smart contract platforms, opening up the possibility of cheap, fast, inter-chain transfer of value without relying on collateral, hashlocks, or trusted intermediaries. - -Opting to take advantage of well established and developmentally stable mechanisms already common to all major blockchains allows SPV based interoperability solutions to be dramatically simpler than orchestrated multi-stage approaches. As part of this, they dispense with the need for widely agreed upon cross chain communication standards and the large multi-party organizations that write them in favor of a set of discrete contract-based services that can be easily utilized by caller contracts through a common abstraction format. This will set the groundwork for a broad range of applications and contracts able to interoperate across the variegated and every growing platform ecosystem. - -## Terminology - -SPV Program - Client-facing interface for the inter-chain SPV system, manages participant roles. SPV Engine - Validates transaction proofs, subset of the SPV Program. Client - The caller to the SPV Program, typically another solana contract. Prover - Party who generates proofs for transactions and submits them to the SPV Program. Transaction Proof - Created by Provers, contains a merkle proof, transaction, and blockheader reference. Merkle Proof - Basic SPV proof that validates the presence of a transaction in a certain block. Block Header - Represents the basic parameters and relative position of a given block. Proof Request - An order placed by a client for verification of transaction\(s\) by provers. Header Store - A data structure for storing and referencing ranges of block headers in proofs. Client Request - Transaction from the client to the SPV Program to trigger creation of a Proof Request. Sub-account - A Solana account owned by another contract account, without its own private key. - -## Service - -SPV Programs run as contracts deployed on the Solana network and maintain a type of public marketplace for SPV proofs that allows any party to submit both requests for proofs as well as proofs themselves for verification in response to requests. There will be multiple SPV Program instances active at any given time, at least one for each connected external network and potentially multiple instances per network. SPV program instances will be relatively consistent in their high level API and feature sets with some variation between currency platforms \(Bitcoin, Litecoin\) and smart contract platforms owing to the potential for verification of network state changes beyond simply transactions. In every case regardless of network, the SPV Program relies on an internal component called an SPV engine to provide stateless verification of the actual SPV proofs upon which the higher level client facing features and api are built. The SPV engine requires a network specific implementation, but allows easy extension of the larger inter-chain ecosystem by any team who chooses to carry out that implementation and drop it into the standard SPV program for deployment. - -For purposes of Proof Requests, the requester is referred to as the program client, which in most if not all cases will be another Solana Contract. The client can choose to submit a request pertaining to a specific transaction or to include a broader filter that can apply to any of a range of parameters of a transaction including its inputs, outputs, and amount. For example, A client could submit a request for any transaction sent from a given address A to address B with the amount X after a certain time. This structure can be used in a range of applications, such as verifying a specific intended payment in the case of an atomic swap or detecting the movement of collateral assets for a loan. - -Following submission of a Client Request, assuming that it is successfully validated, a proof request account is created by the SPV program to track the progress of the request. Provers use the account to specify the request they intend to fill in the proofs they submit for validation, at which point the SPV program validates those proofs and if successful, saves them to the account data of the request account. Clients can monitor the status of their requests and see any applicable transactions alongside their proofs by querying the account data of the request account. In future iterations when supported by Solana, this process will be simplified by contracts publishing events rather than requiring a polling style process as described. - -## Implementation - -The Solana Inter-chain SPV mechanism consists of the following components and participants: - -### SPV engine - -A contract deployed on Solana which statelessly verifies SPV proofs for the caller. It takes as arguments for validation: - -- An SPV proof in the correct format of the blockchain associated with the program -- Reference\(s\) to the relevant block headers to compare that proof against -- The necessary parameters of the transaction to verify - - If the proof in question is successfully validated, the SPV program saves proof - - of that verification to the request account, which can be saved by the caller to - - its account data or otherwise handled as necessary. SPV programs also expose - - utilities and structs used for representation and validation of headers, - - transactions, hashes, etc. on a chain by chain basis. - -### SPV program - -A contract deployed on Solana which coordinates and intermediates the interaction between Clients and Provers and manages the validation of requests, headers, proofs, etc. It is the primary point of access for Client contracts to access the inter-chain. SPV mechanism. It offers the following core features: - -- Submit Proof Request - allows client to place a request for a specific proof or set of proofs -- Cancel Proof Request - allows client to invalidate a pending request -- Fill Proof Request - used by Provers to submit for validation a proof corresponding to a given Proof Request - - The SPV program maintains a publicly available listing of valid pending Proof - - Requests in its account data for the benefit of the Provers, who monitor it and - - enclose references to target requests with their submitted proofs. - -### Proof Request - -A message sent by the Client to the SPV engine denoting a request for a proof of a specific transaction or set of transactions. Proof Requests can either manually specify a certain transaction by its hash or can elect to submit a filter that matches multiple transactions or classes of transactions. For example, a filter matching "any transaction from address xxx to address yyy" could be used to detect payment of a debt or settlement of an inter-chain swap. Likewise, a filter matching "any transaction from address xxx" could be used by a lending or synthetic token minting contract to monitor and react to changes in collateralization. Proof Requests are sent with a fee, which is disbursed by the SPV engine contract to the appropriate Prover once a proof matching that request is validated. - -### Request Book - -The public listing of valid, open Proof Requests available to provers to fill or for clients to cancel. Roughly analogous to an orderbook in an exchange, but with a single type of listing rather than two separate sides. It is stored in the account data of the SPV program. - -### Proof - -A proof of the presence of a given transaction in the blockchain in question. Proofs encompass both the actual merkle proof and reference\(s\) to a chain of valid sequential block headers. They are constructed and submitted by Provers in accordance with the specifications of the publicly available Proof Requests hosted on the request book by the SPV program. Upon Validation, they are saved to the account data of the relevant Proof Request, which can be used by the Client to monitor the state of the request. - -### Client - -The originator of a request for a transaction proof. Clients will most often be other contracts as parts of applications or specific financial products like loans, swaps, escrow, etc. The client in any given verification process cycle initially submits a ClientRequest which communicates the parameters and fee and if successfully validated, results in the creation of a Proof Request account by the SPV program. The Client may also submit a CancelRequest referencing an active Proof Request in order to denote it as invalid for purposes of proof submission. - -### Prover - -The submitter of a proof that fills a Proof Request. Provers monitor the request book of the SPV program for outstanding Proof Requests and generate matching proofs, which they submit to the SPV program for validation. If the proof is accepted, the fee associated with the Proof Request in question is disbursed to the Prover. Provers typically operate as Solana Blockstreamer nodes that also have access to a Bitcoin node, which they use for purposes of constructing proofs and accessing block headers. - -### Header Store - -An account-based data structure used to maintain block headers for the purpose of inclusion in submitted proofs by reference to the header store account. header stores can be maintained by independent entities, since header chain validation is a component of the SPV program proof validation mechanism. Fees that are paid out by Proof Requests to Provers are split between the submitter of the merkle proof itself and the header store that is referenced in the submitted proof. Due to the current inability to grow already allocated account data capacity, the use case necessitates a data structure that can grow indefinitely without rebalancing. Sub-accounts are accounts owned by the SPV program without their own private keys that are used for storage by allocating blockheaders to their account data. Multiple potential approaches to the implementation of the header store system are feasible: - -Store Headers in program sub-accounts indexed by Public address: - -- Each sub-account holds one header and has a public key matching the blockhash -- Requires same number of account data lookups as confirmations per verification -- Limit on number of confirmations \(15-20\) via max transaction data ceiling -- No network-wide duplication of individual headers - -Linked List of multiple sub-accounts storing headers: - -- Maintain sequential index of storage accounts, many headers per storage account -- Max 2 account data lookups for >99.9% of verification \(1 for most\) -- Compact sequential data address format allows any number of confirmations and fast lookups -- Facilitates network-wide header duplication inefficiencies diff --git a/docs/src/proposals/ledger-replication-to-implement.md b/docs/src/proposals/ledger-replication-to-implement.md deleted file mode 100644 index 1d68fdf1c25..00000000000 --- a/docs/src/proposals/ledger-replication-to-implement.md +++ /dev/null @@ -1,415 +0,0 @@ ---- -title: Ledger Replication ---- - -Note: this ledger replication solution was partially implemented, but not -completed. The partial implementation was removed by -https://github.com/solana-labs/solana/pull/9992 in order to prevent the security -risk of unused code. The first part of this design document reflects the -once-implemented parts of ledger replication. The -[second part of this document](#ledger-replication-not-implemented) describes the -parts of the solution never implemented. - -## Proof of Replication - -At full capacity on a 1gbps network solana will generate 4 petabytes of data per year. To prevent the network from centralizing around validators that have to store the full data set this protocol proposes a way for mining nodes to provide storage capacity for pieces of the data. - -The basic idea to Proof of Replication is encrypting a dataset with a public symmetric key using CBC encryption, then hash the encrypted dataset. The main problem with the naive approach is that a dishonest storage node can stream the encryption and delete the data as it's hashed. The simple solution is to periodically regenerate the hash based on a signed PoH value. This ensures that all the data is present during the generation of the proof and it also requires validators to have the entirety of the encrypted data present for verification of every proof of every identity. So the space required to validate is `number_of_proofs * data_size` - -## Optimization with PoH - -Our improvement on this approach is to randomly sample the encrypted segments faster than it takes to encrypt, and record the hash of those samples into the PoH ledger. Thus the segments stay in the exact same order for every PoRep and verification can stream the data and verify all the proofs in a single batch. This way we can verify multiple proofs concurrently. The total space required for verification is `1_ledger_segment + 2_cbc_blocks * number_of_identities` with core count equal to `number_of_identities`. We use a 64-byte chacha CBC block size. - -## Network - -Validators for PoRep are the same validators that are verifying transactions. If an archiver can prove that a validator verified a fake PoRep, then the validator will not receive a reward for that storage epoch. - -Archivers are specialized _light clients_. They download a part of the ledger \(a.k.a Segment\) and store it, and provide PoReps of storing the ledger. For each verified PoRep archivers earn a reward of sol from the mining pool. - -## Constraints - -We have the following constraints: - -- Verification requires generating the CBC blocks. That requires space of 2 - - blocks per identity. So as many identities at once should be batched with as - - many proofs for those identities verified concurrently for the same dataset. - -- Validators will randomly sample the set of storage proofs to the set that - - they can handle, and only the creators of those chosen proofs will be - - rewarded. The validator can run a benchmark whenever its hardware configuration - - changes to determine what rate it can validate storage proofs. - -## Validation and Replication Protocol - -### Constants - -1. SLOTS_PER_SEGMENT: Number of slots in a segment of ledger data. The - - unit of storage for an archiver. - -2. NUM_KEY_ROTATION_SEGMENTS: Number of segments after which archivers - - regenerate their encryption keys and select a new dataset to store. - -3. NUM_STORAGE_PROOFS: Number of storage proofs required for a storage proof - - claim to be successfully rewarded. - -4. RATIO_OF_FAKE_PROOFS: Ratio of fake proofs to real proofs that a storage - - mining proof claim has to contain to be valid for a reward. - -5. NUM_STORAGE_SAMPLES: Number of samples required for a storage mining - - proof. - -6. NUM_CHACHA_ROUNDS: Number of encryption rounds performed to generate - - encrypted state. - -7. NUM_SLOTS_PER_TURN: Number of slots that define a single storage epoch or - - a "turn" of the PoRep game. - -### Validator behavior - -1. Validators join the network and begin looking for archiver accounts at each - - storage epoch/turn boundary. - -2. Every turn, Validators sign the PoH value at the boundary and use that signature - - to randomly pick proofs to verify from each storage account found in the turn boundary. - - This signed value is also submitted to the validator's storage account and will be used by - - archivers at a later stage to cross-verify. - -3. Every `NUM_SLOTS_PER_TURN` slots the validator advertises the PoH value. This is value - - is also served to Archivers via RPC interfaces. - -4. For a given turn N, all validations get locked out until turn N+3 \(a gap of 2 turn/epoch\). - - At which point all validations during that turn are available for reward collection. - -5. Any incorrect validations will be marked during the turn in between. - -### Archiver behavior - -1. Since an archiver is somewhat of a light client and not downloading all the - - ledger data, they have to rely on other validators and archivers for information. - - Any given validator may or may not be malicious and give incorrect information, although - - there are not any obvious attack vectors that this could accomplish besides having the - - archiver do extra wasted work. For many of the operations there are a number of options - - depending on how paranoid an archiver is: - - - \(a\) archiver can ask a validator - - \(b\) archiver can ask multiple validators - - \(c\) archiver can ask other archivers - - \(d\) archiver can subscribe to the full transaction stream and generate - - the information itself \(assuming the slot is recent enough\) - - - \(e\) archiver can subscribe to an abbreviated transaction stream to - - generate the information itself \(assuming the slot is recent enough\) - -2. An archiver obtains the PoH hash corresponding to the last turn with its slot. -3. The archiver signs the PoH hash with its keypair. That signature is the - - seed used to pick the segment to replicate and also the encryption key. The - - archiver mods the signature with the slot to get which segment to - - replicate. - -4. The archiver retrieves the ledger by asking peer validators and - - archivers. See 6.5. - -5. The archiver then encrypts that segment with the key with chacha algorithm - - in CBC mode with `NUM_CHACHA_ROUNDS` of encryption. - -6. The archiver initializes a chacha rng with the signed recent PoH value as - - the seed. - -7. The archiver generates `NUM_STORAGE_SAMPLES` samples in the range of the - - entry size and samples the encrypted segment with sha256 for 32-bytes at each - - offset value. Sampling the state should be faster than generating the encrypted - - segment. - -8. The archiver sends a PoRep proof transaction which contains its sha state - - at the end of the sampling operation, its seed and the samples it used to the - - current leader and it is put onto the ledger. - -9. During a given turn the archiver should submit many proofs for the same segment - - and based on the `RATIO_OF_FAKE_PROOFS` some of those proofs must be fake. - -10. As the PoRep game enters the next turn, the archiver must submit a - - transaction with the mask of which proofs were fake during the last turn. This - - transaction will define the rewards for both archivers and validators. - -11. Finally for a turn N, as the PoRep game enters turn N + 3, archiver's proofs for - - turn N will be counted towards their rewards. - -### The PoRep Game - -The Proof of Replication game has 4 primary stages. For each "turn" multiple PoRep games can be in progress but each in a different stage. - -The 4 stages of the PoRep Game are as follows: - -1. Proof submission stage - - Archivers: submit as many proofs as possible during this stage - - Validators: No-op -2. Proof verification stage - - Archivers: No-op - - Validators: Select archivers and verify their proofs from the previous turn -3. Proof challenge stage - - Archivers: Submit the proof mask with justifications \(for fake proofs submitted 2 turns ago\) - - Validators: No-op -4. Reward collection stage - - Archivers: Collect rewards for 3 turns ago - - Validators: Collect rewards for 3 turns ago - -For each turn of the PoRep game, both Validators and Archivers evaluate each stage. The stages are run as separate transactions on the storage program. - -### Finding who has a given block of ledger - -1. Validators monitor the turns in the PoRep game and look at the rooted bank - - at turn boundaries for any proofs. - -2. Validators maintain a map of ledger segments and corresponding archiver public keys. - - The map is updated when a Validator processes an archiver's proofs for a segment. - - The validator provides an RPC interface to access this map. Using this API, clients - - can map a segment to an archiver's network address \(correlating it via cluster_info table\). - - The clients can then send repair requests to the archiver to retrieve segments. - -3. Validators would need to invalidate this list every N turns. - -## Sybil attacks - -For any random seed, we force everyone to use a signature that is derived from a PoH hash at the turn boundary. Everyone uses the same count, so the same PoH hash is signed by every participant. The signatures are then each cryptographically tied to the keypair, which prevents a leader from grinding on the resulting value for more than 1 identity. - -Since there are many more client identities than encryption identities, we need to split the reward for multiple clients, and prevent Sybil attacks from generating many clients to acquire the same block of data. To remain BFT we want to avoid a single human entity from storing all the replications of a single chunk of the ledger. - -Our solution to this is to force the clients to continue using the same identity. If the first round is used to acquire the same block for many client identities, the second round for the same client identities will force a redistribution of the signatures, and therefore PoRep identities and blocks. Thus to get a reward for archivers need to store the first block for free and the network can reward long lived client identities more than new ones. - -## Validator attacks - -- If a validator approves fake proofs, archiver can easily out them by - - showing the initial state for the hash. - -- If a validator marks real proofs as fake, no on-chain computation can be done - - to distinguish who is correct. Rewards would have to rely on the results from - - multiple validators to catch bad actors and archivers from being denied rewards. - -- Validator stealing mining proof results for itself. The proofs are derived - - from a signature from an archiver, since the validator does not know the - - private key used to generate the encryption key, it cannot be the generator of - - the proof. - -## Reward incentives - -Fake proofs are easy to generate but difficult to verify. For this reason, PoRep proof transactions generated by archivers may require a higher fee than a normal transaction to represent the computational cost required by validators. - -Some percentage of fake proofs are also necessary to receive a reward from storage mining. - -## Notes - -- We can reduce the costs of verification of PoRep by using PoH, and actually - - make it feasible to verify a large number of proofs for a global dataset. - -- We can eliminate grinding by forcing everyone to sign the same PoH hash and - - use the signatures as the seed - -- The game between validators and archivers is over random blocks and random - - encryption identities and random data samples. The goal of randomization is - - to prevent colluding groups from having overlap on data or validation. - -- Archiver clients fish for lazy validators by submitting fake proofs that - - they can prove are fake. - -- To defend against Sybil client identities that try to store the same block we - - force the clients to store for multiple rounds before receiving a reward. - -- Validators should also get rewarded for validating submitted storage proofs - - as incentive for storing the ledger. They can only validate proofs if they - - are storing that slice of the ledger. - -# Ledger Replication Not Implemented - -Replication behavior yet to be implemented. - -## Storage epoch - -The storage epoch should be the number of slots which results in around 100GB-1TB of ledger to be generated for archivers to store. Archivers will start storing ledger when a given fork has a high probability of not being rolled back. - -## Validator behavior - -1. Every NUM_KEY_ROTATION_TICKS it also validates samples received from - - archivers. It signs the PoH hash at that point and uses the following - - algorithm with the signature as the input: - - - The low 5 bits of the first byte of the signature creates an index into - - another starting byte of the signature. - - - The validator then looks at the set of storage proofs where the byte of - - the proof's sha state vector starting from the low byte matches exactly - - with the chosen byte\(s\) of the signature. - - - If the set of proofs is larger than the validator can handle, then it - - increases to matching 2 bytes in the signature. - - - Validator continues to increase the number of matching bytes until a - - workable set is found. - - - It then creates a mask of valid proofs and fake proofs and sends it to - - the leader. This is a storage proof confirmation transaction. - -2. After a lockout period of NUM_SECONDS_STORAGE_LOCKOUT seconds, the - - validator then submits a storage proof claim transaction which then causes the - - distribution of the storage reward if no challenges were seen for the proof to - - the validators and archivers party to the proofs. - -## Archiver behavior - -1. The archiver then generates another set of offsets which it submits a fake - - proof with an incorrect sha state. It can be proven to be fake by providing the - - seed for the hash result. - - - A fake proof should consist of an archiver hash of a signature of a PoH - - value. That way when the archiver reveals the fake proof, it can be - - verified on chain. - -2. The archiver monitors the ledger, if it sees a fake proof integrated, it - - creates a challenge transaction and submits it to the current leader. The - - transaction proves the validator incorrectly validated a fake storage proof. - - The archiver is rewarded and the validator's staking balance is slashed or - - frozen. - -## Storage proof contract logic - -Each archiver and validator will have their own storage account. The validator's account would be separate from their gossip id similar to their vote account. These should be implemented as two programs one which handles the validator as the keysigner and one for the archiver. In that way when the programs reference other accounts, they can check the program id to ensure it is a validator or archiver account they are referencing. - -### SubmitMiningProof - -```text -SubmitMiningProof { - slot: u64, - sha_state: Hash, - signature: Signature, -}; -keys = [archiver_keypair] -``` - -Archivers create these after mining their stored ledger data for a certain hash value. The slot is the end slot of the segment of ledger they are storing, the sha_state the result of the archiver using the hash function to sample their encrypted ledger segment. The signature is the signature that was created when they signed a PoH value for the current storage epoch. The list of proofs from the current storage epoch should be saved in the account state, and then transferred to a list of proofs for the previous epoch when the epoch passes. In a given storage epoch a given archiver should only submit proofs for one segment. - -The program should have a list of slots which are valid storage mining slots. This list should be maintained by keeping track of slots which are rooted slots in which a significant portion of the network has voted on with a high lockout value, maybe 32-votes old. Every SLOTS_PER_SEGMENT number of slots would be added to this set. The program should check that the slot is in this set. The set can be maintained by receiving a AdvertiseStorageRecentBlockHash and checking with its bank/Tower BFT state. - -The program should do a signature verify check on the signature, public key from the transaction submitter and the message of the previous storage epoch PoH value. - -### ProofValidation - -```text -ProofValidation { - proof_mask: Vec, -} -keys = [validator_keypair, archiver_keypair(s) (unsigned)] -``` - -A validator will submit this transaction to indicate that a set of proofs for a given segment are valid/not-valid or skipped where the validator did not look at it. The keypairs for the archivers that it looked at should be referenced in the keys so the program logic can go to those accounts and see that the proofs are generated in the previous epoch. The sampling of the storage proofs should be verified ensuring that the correct proofs are skipped by the validator according to the logic outlined in the validator behavior of sampling. - -The included archiver keys will indicate the storage samples which are being referenced; the length of the proof_mask should be verified against the set of storage proofs in the referenced archiver account\(s\), and should match with the number of proofs submitted in the previous storage epoch in the state of said archiver account. - -### ClaimStorageReward - -```text -ClaimStorageReward { -} -keys = [validator_keypair or archiver_keypair, validator/archiver_keypairs (unsigned)] -``` - -Archivers and validators will use this transaction to get paid tokens from a program state where SubmitStorageProof, ProofValidation and ChallengeProofValidations are in a state where proofs have been submitted and validated and there are no ChallengeProofValidations referencing those proofs. For a validator, it should reference the archiver keypairs to which it has validated proofs in the relevant epoch. And for an archiver it should reference validator keypairs for which it has validated and wants to be rewarded. - -### ChallengeProofValidation - -```text -ChallengeProofValidation { - proof_index: u64, - hash_seed_value: Vec, -} -keys = [archiver_keypair, validator_keypair] -``` - -This transaction is for catching lazy validators who are not doing the work to validate proofs. An archiver will submit this transaction when it sees a validator has approved a fake SubmitMiningProof transaction. Since the archiver is a light client not looking at the full chain, it will have to ask a validator or some set of validators for this information maybe via RPC call to obtain all ProofValidations for a certain segment in the previous storage epoch. The program will look in the validator account state see that a ProofValidation is submitted in the previous storage epoch and hash the hash_seed_value and see that the hash matches the SubmitMiningProof transaction and that the validator marked it as valid. If so, then it will save the challenge to the list of challenges that it has in its state. - -### AdvertiseStorageRecentBlockhash - -```text -AdvertiseStorageRecentBlockhash { - hash: Hash, - slot: u64, -} -``` - -Validators and archivers will submit this to indicate that a new storage epoch has passed and that the storage proofs which are current proofs should now be for the previous epoch. Other transactions should check to see that the epoch that they are referencing is accurate according to current chain state. diff --git a/docs/src/proposals/log_data.md b/docs/src/proposals/log_data.md deleted file mode 100644 index 2744a6c5673..00000000000 --- a/docs/src/proposals/log_data.md +++ /dev/null @@ -1,146 +0,0 @@ -# Program log binary data - -## Problem - -There is no support for logging binary data in Solidity. - -### Events in Solidity - -In Solidity, events can be reported. These look like structures with zero or -more fields, and can be emitted with specific values. For example: - -``` -event PaymentReceived { - address sender; - uint amount; -} - -contract c { - function pay() public payable { - emit PaymentReceived(msg.sender, msg.value); - } -} -``` - -Events are write-only from a Solidity/VM perspective and are written to -the blocks in the tx records. - -Some of these fields can be marked `indexed`, which affects how the data is -encoded. All non-indexed fields are eth abi encoded into a variable length -byte array. All indexed fields go into so-called topics. - -Topics are fixed length fields of 32 bytes. There are a maximum of 4 topics; -if a type does not always fit into 32 bytes (e.g. string types), then the topic -is keccak256 hashed. - -The first topic is a keccak256 hash of the event signature, in this case -`keccak256('PaymentReceived(address,uint)')`. The four remaining are available -for `indexed` fields. The event may be declared `anonymous`, in which case -the first field is not a hash of the signature, and it is permitted to have -4 indexed fields. - -### Listening to events in a client - -The reason for the distinction between topics/indexed and regular fields is -that it easier to filter on topics. - -``` -const Web3 = require('web3'); -const url = 'ws://127.0.0.1:8546'; -const web3 = new Web3(url); - -var options = { - address: '0xfbBE8f06FAda977Ea1E177da391C370EFbEE3D25', - topics: [ - '0xdf50c7bb3b25f812aedef81bc334454040e7b27e27de95a79451d663013b7e17', - //'0x0000000000000000000000000d8a3f5e71560982fb0eb5959ecf84412be6ae3e' - ] -}; - -var subscription = web3.eth.subscribe('logs', options, function(error, result){ - if (!error) console.log('got result'); - else console.log(error); -}).on("data", function(log){ - console.log('got data', log); -}).on("changed", function(log){ - console.log('changed'); -}); -``` - -In order to decode the non-indexed fields (the data), the abi of the contract -is needed. So, the topic is first used to discover what event was used, and -then the data can be decoded. - -### Ethereum Tx in block - -The transaction calls event logs. Here is a tx with a single event, with 3 -topics and some data. - -``` -{ - "tx": { - "nonce": "0x2", - "gasPrice": "0xf224d4a00", - "gas": "0xc350", - "to": "0x6B175474E89094C44Da98b954EedeAC495271d0F", - "value": "0x0", - "input": "0xa9059cbb000000000000000000000000a12431d0b9db640034b0cdfceef9cce161e62be40000000000000000000000000000000000000000000000a030dcebbd2f4c0000", - "hash": "0x98a67f0a35ebc0ac068acf0885d38419c632ffa4354e96641d6d5103a7681910", - "blockNumber": "0xc96431", - "from": "0x82f890D638478d211eF2208f3c1466B5Abf83551", - "transactionIndex": "0xe1" - }, - "receipt": { - "gasUsed": "0x74d2", - "status": "0x1", - "logs": [ - { - "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", - "topics": [ - "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - "0x00000000000000000000000082f890d638478d211ef2208f3c1466b5abf83551", - "0x000000000000000000000000a12431d0b9db640034b0cdfceef9cce161e62be4" - ], - "data": "0x0000000000000000000000000000000000000000000000a030dcebbd2f4c0000" - } - ] - } -} -``` - -### Further considerations - -In Ethereum, events are stored in blocks. Events mark certain state changes in -smart contracts. This serves two purposes: - - - Listen to events (i.e. state changes) as they happen by reading new blocks - as they are published - - Re-read historical events by reading old blocks - -So for example, smart contracts may emit changes as they happen but never the -complete state, so the only way to recover the entire state of the contract -is by re-reading all events from the chain. So an application will read events -from block 1 or whatever block the application was deployed at and then use -that state for local processing. This is a local cache and may re-populated -from the chain at any point. - -## Proposed Solution - -Binary logging should be added to the program log. The program log should include the base64 encoded data (zero or more one permitted). - -So if we encoding the topics first, followed by the data then the event in the -tx above would look like: -``` -program data: 3fJSrRviyJtpwrBo/DeNqpUrpFjxKEWKPVaTfUjs8AAAAAAAAAAAAAAACC+JDWOEeNIR7yII88FGa1q/g1UQAAAAAAAAAAAAAAAKEkMdC522QANLDN/O75zOFh5ivk AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgMNzrvS9MAAA= -``` - -This requires a new system call: - -``` -void sol_log_data(SolBytes *fields, uint64_t length); -``` - -### Considerations - -- Should there be text field in the program log so we can have a little bit of - metadata on the binary data, to make it more human readable diff --git a/docs/src/proposals/off-chain-message-signing.md b/docs/src/proposals/off-chain-message-signing.md deleted file mode 100644 index 7d5d25b10f7..00000000000 --- a/docs/src/proposals/off-chain-message-signing.md +++ /dev/null @@ -1,138 +0,0 @@ -# Off-chain message signing -## Motivation - -There is ecosystem demand for a method of signing non-transaction messages with -a Solana wallet. Typically this is some kind of "proof of wallet ownership" for -entry into a whitelisted system. - -Some inspiration can be gleaned from relevant portions of Ethereum's -[EIP-712](https://eips.ethereum.org/EIPS/eip-712) - -## Considerations - -* Security - * Off-chain message signatures should not be valid transaction message signatures -* Future-proofing - * Versioning - * Co-exist with versioned transaction messages and extended length transactions -* Compatibility - * Hardware wallet signing - * Localization - -## Message Preamble - -| Field | Start offset | Length (bytes) | Description -| :---- | :----------: | :------------: | :---------- -| Signing Domain | 0x00 | 16 | \[[1](#signing-domain-specifier)\] -| Header version | 0x10 | 1 | \[[2](#header-version)\] -| Application domain | 0x11 | 32 | \[[3](#application-domain)\] -| Message format | 0x31 | 1 | \[[4](#message-format)\] -| Signer count | 0x32 | 1 | Number of signers committing to the message. An 8-bit, unsigned integer. **MUST NOT** be zero. -| Signers | 0x33 | `SIGNER_COUNT` * 32 | `SIGNER_COUNT` ed25519 public keys of signers -| Message length | 0x33 + `SIGNER_CNT` * 32 | 2 | Length of message in bytes. A 16-bit, unsigned, little-endian integer. **MUST NOT** be zero. - -### Signing Domain Specifier - -The signing domain specifier is a prefix byte string used to give similar structure -to all off-chain message signatures. We assign its value to be: -``` -b"\xffsolana offchain" -``` -The first byte, `\xff`, was chosen for the following reasons -1. It corresponds to a value that is illegal as the first byte in a transaction -`MessageHeader`. -1. It avoids unintentional misuse in languages with C-like, null-terminated strings. - -The remaining bytes, `b"solana offchain"`, were chosen to be descriptive and -reasonably long, but are otherwise arbitrary. - -This field **SHOULD NOT** be displayed to users - -### Header - -#### Header version - -The header version is represented as an 8-bit unsigned integer. Only the version -0 header format is specified in this document - -This field **SHOULD NOT** be displayed to users - -#### Application domain - -A 32-byte array identifying the application requesting off-chain message signing. -This may be any arbitrary bytes. For instance the on-chain address of a program, -DAO instance, Candy Machine, etc. - -This field **SHOULD** be displayed to users as a base58-encoded ASCII string rather -than interpreted otherwise. - -#### Message Format - -Version `0` headers specify three message formats allowing for trade-offs between -compatibility and composition of messages. - -| ID | Encoding | Maximum Length \* | Hardware Wallet Support | -| :-: | :-------------------: | :---------------: | :---------------------: | -| 0 | Restricted ASCII \*\* | 1232 | Yes | -| 1 | UTF-8 | 1232 | Blind sign only | -| 2 | UTF-8 | 65535 | No | - -\* Combined length of the [message preamble](#message-preamble) and message body
-\*\* Those characters for which [`isprint(3)`](https://linux.die.net/man/3/isprint) -returns true. That is, `0x20..=0x7e`. - -Both the message encoding and maximum message length **MUST** be enforced by -signer and verifier. - -Formats `0` and `1` are motivated by hardware wallet support where both RAM -to store the payload and font character support are limited. - -This field **SHOULD NOT** be displayed to users - -## Signing - -Solana off-chain messages **MUST** only be signed using the ed25519 digital -signature scheme. Before signing, the message **MUST** be strictly checked to -conform to the associated preamble. The message body is then appended to the -[message preamble](#message-preamble). Finally the result is ed25519 signed. - -## Verification -Upon successful ed25519 verification of _all_ attached signatures, the message -**MUST** be strictly checked to conform to the [message preamble](#message-preamble). -A message that does not conform to its preamble is invalid, regardless of the -validity of any signatures. - -## Envelope - -When passing around signed off-chain messages a common format is helpful. The -recommended binary representation is as follows: - -| Field | Start offset | Length (bytes) | Description -| :---- | :----------: | :------------: | :---------- -| Signature Count \* | 0x00 | 1 | Number of signatures. An 8-bit, unsigned integer -| Signatures | 0x01 | `SIG_COUNT` * 64 | `SIG_COUNT` ed25519 signatures -| Message Preamble | 0x01 + `SIG_COUNT` * 64 | `PREAMBLE_LEN` | The [message preamble](#message-preamble) -| Message Body | 0x01 + `SIG_COUNT * 64 + `PREAMBLE_LEN` | `MESSAGE_LEN` | The message content - -The signature count **MUST** match the value of signers count from the message -preamble. - -Signatures **MUST** be ordered to match their corresponding public keys as -specified in the message preamble. - -## Runtime Considerations - -To prevent social attacks by which the signer is tricked into signing a transaction, -the runtime **MUST NOT** accept signed off-chain messages as transactions under any -circumstances. The first byte of the [signing domain specifier](#signing-domain-specifier) -is chosen such that it corresponds to a value (`0xff`) which is implicitly illegal -as the first byte in a transaction `MessageHeader` today. The property is implicit -because the top bit in the first byte of a `MessageHeader` being set signals a -versioned transaction, but only a value of -[zero is supported](https://github.com/solana-labs/solana/blob/b6ae6c1fe17e4b64c5051c651ca2585e4f55468c/sdk/program/src/message/versions/mod.rs#L269-L281) -at this time. The runtime will need to be modified to reserve 127 as an illegal -version number, making this property explicit. - -### Implementation - -The runtime changes described above have been implemented in PR [#29807](https://github.com/solana-labs/solana/pull/29807) diff --git a/docs/src/proposals/optimistic-confirmation-and-slashing.md b/docs/src/proposals/optimistic-confirmation-and-slashing.md deleted file mode 100644 index 0c6de6eb57b..00000000000 --- a/docs/src/proposals/optimistic-confirmation-and-slashing.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: Optimistic Confirmation and Slashing ---- - -Progress on optimistic confirmation can be tracked here - -https://github.com/solana-labs/solana/projects/52 - -At the end of May, the mainnet-beta is moving to 1.1, and testnet is -moving to 1.2. With 1.2, testnet will behave as if it has optimistic -finality as long as at least no more than 4.66% of the validators are -acting maliciously. Applications can assume that 2/3+ votes observed in -gossip confirm a block or that at least 4.66% of the network is violating -the protocol. - -## How does it work? - -The general idea is that validators must continue voting following their -last fork, unless the validator can construct a proof that their current -fork may not reach finality. The way validators construct this proof is -by collecting votes for all the forks excluding their own. If the set -of valid votes represents over 1/3+X of the epoch stake weight, there -may not be a way for the validators current fork to reach 2/3+ finality. -The validator hashes the proof (creates a witness) and submits it with -their vote for the alternative fork. But if 2/3+ votes for the same -block, it is impossible for any of the validators to construct this proof, -and therefore no validator is able to switch forks and this block will -be eventually finalized. - -## Tradeoffs - -The safety margin is 1/3+X, where X represents the minimum amount of stake -that will be slashed in case the protocol is violated. The tradeoff is -that liveness is now reduced by 2X in the worst case. If more than 1/3 - -2X of the network is unavailable, the network may stall and will only -resume finalizing blocks after the network recovers below 1/3 - 2X of -failing nodes. So far, we haven’t observed a large unavailability hit -on our mainnet, cosmos, or tezos. For our network, which is primarily -composed of high availability systems, this seems unlikely. Currently, -we have set the threshold percentage to 4.66%, which means that if 23.68% -have failed the network may stop finalizing blocks. For our network, -which is primarily composed of high availability systems, a 23.68% drop -in availability seems unlikely. 1:10^12 odds, assuming five 4.7% staked -nodes with 0.995 uptime. - -## Security - -Long term average votes per slot has been 670,000,000 votes / 12,000,000 -slots, or 55 out of 64 voting validators. This includes missed blocks due -to block producer failures. When a client sees 55/64, or ~86% confirming -a block, it can expect that ~24% or `(86 - 66.666.. + 4.666..)%` of -the network must be slashed for this block to fail full finalization. - -## Why Solana? - -This approach can be built on other networks, but the implementation -complexity is significantly reduced on Solana because our votes -have provable VDF-based timeouts. It’s not clear if switching proofs -can be easily constructed in networks with weak assumptions about -time. - -## Slashing roadmap - -Slashing is a hard problem, and it becomes harder when the goal of -the network is to have the lowest possible latency. The tradeoffs are -especially apparent when optimizing for latency. For example, ideally -validators should cast and propagate their votes before the -memory has been synced to disk, which means that the risk of local state -corruption is much higher. - -Fundamentally, our goal for slashing is to slash 100% in cases where -the node is maliciously trying to violate safety rules and 0% during -routine operation. How we aim to achieve that is to first implement -slashing proofs without any automatic slashing whatsoever. - -Right now, for regular consensus, after a safety violation, the -network will halt. We can analyze the data and figure out who was -responsible and propose that the stake should be slashed after -restart. A similar approach will be used with a optimistic conf. -An optimistic conf safety violation is easily observable, but under -normal circumstances, an optimistic confirmation safety violation -may not halt the network. Once the violation has been observed, the -validators will freeze the affected stake in the next epoch and -will decide on the next upgrade if the violation requires slashing. - -In the long term, transactions should be able to recover a portion -of the slashing collateral if the optimistic safety violation is -proven. In that scenario, each block is effectively insured by the -network. diff --git a/docs/src/proposals/optimistic-transaction-propagation-signal.md b/docs/src/proposals/optimistic-transaction-propagation-signal.md deleted file mode 100644 index b6c18d2d0ce..00000000000 --- a/docs/src/proposals/optimistic-transaction-propagation-signal.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: Optimistic Transaction Propagation Signal ---- - -## Current Retransmit behavior - -The retransmission tree currently considers: -1. epoch staked nodes -2. tvu peers (filtered by contact info and shred version) -3. current validator - -concatenating (1), (2), and (3) -deduplicating this list of entries by pubkey favoring entries with contact info -filtering this list by entries with contact info - -This list is then randomly shuffled by stake weight. - -Shreds are then retransmitted to up to FANOUT neighbors and up to FANOUT -children. - -## Deterministic retransmission tree - -`weighted_shuffle` will use a deterministic seed when -`enable_deterministic_seed` has been enabled based on the triple (shred slot, -shred index, leader pubkey): - -``` -if enable_deterministic_seed(self.slot(), root_bank) { - hashv(&[ - &self.slot().to_le_bytes(), - &self.index().to_le_bytes(), - &leader_pubkey.to_bytes(), - ]) -``` - -First, only epoch staked nodes will be considered regardless of presence of -contact info (and possibly including the validator node itself). - -A deterministic ordering of the epoch staked nodes will be created based on the -deterministic shred seed using weighted_shuffle. - -Let `neighbor_set` be selected from up to FANOUT neighbors of the current node. -Let `child_set` be selected from up to FANOUT children of the current node. - -Filter `neighbor_set` by contact info. -Filter `child_set` by contact info. - -Let `epoch_set` be the union of `neighbor_set` and `child_set`. - -Let `remaining_set` be all other nodes with contact info not contained in -`epoch_set`. - -If `epoch_set.len < 2*FANOUT` then we may randomly select up to -`2*FANOUT - epoch_set.len` nodes to retransmit to from `remaining_set`. - -## Receiving retransmitted shred - -If the current validator node is not in the set of epoch staked nodes for the -shred epoch then no early retransmission information can be obtained. - -Compute the deterministic shred seed. - -Run the deterministic epoch_stakes shuffle. - -Find position of self in the neighbor or child sets. - -Calculate the sum of the stakes of all nodes in the current and prior -distribution levels. - -### Stake summation considerations: - -- Stake sum could include stakes of nodes which had been skipped in prior -distribution levels because of lack of contact info. -- Current node was part of original epoch staked shuffle from retransmitter -but was filtered out because of missing contact info. Current node subsequently -receives retransmission of shred and assumes that the retransmit was a result -of the deterministic tree calculation and not from subsequent random selection. -This should be benign because the current node will underestimate prior stake -weight in the retransmission tree. - -### General considerations: - -attack by leader (level 0): -- transmits shred for distribution through the tree as normal -- additionally transmits shred (or fake shred) directly to node(s) at level >=2 -leading the node(s) to believe a greater percentage of the tree retransmission -tree had been processed - -attack by node at level n: -- retransmits shred to node(s) at level >=n+2 leading the node(s) to believe a -greater percentage of the tree retransmission tree had been processed - -### Questions - -- Should receiving nodes attempt to verify that the origin of the shred was -retransmitted from the expected node? If so, consideration of spoofing? -- How is this information consumed? - -## Notes - -Practically, signals should fall into the following buckets: -1. current leader (can signal layer 1 when broadcast is sent) -2. layer 1 -1.1. can signal layer 1 when shred is received -1.2. can signal layer 1 + subset of layer 2 when retransmit is sent -3. layer 2 -3.1. can signal layer 2 when shred is received -3.2. can signal layer 2 + subset of layer 3 when retransmit is sent -4. current node not a member of epoch staked nodes, no signal can be sent diff --git a/docs/src/proposals/optimistic_confirmation.md b/docs/src/proposals/optimistic_confirmation.md deleted file mode 100644 index 3cb553cdcb9..00000000000 --- a/docs/src/proposals/optimistic_confirmation.md +++ /dev/null @@ -1,408 +0,0 @@ ---- -title: Optimistic Confirmation ---- - -## Primitives - -`vote(X, S)` - Votes will be augmented with a "reference" slot, `X` -which is the **latest** ancestor of this fork that this validator voted on -with a proof of switching. As long as the validator makes consecutive votes -that are all descended from each other, the same `X` should be used for all -those votes. When the validator makes a vote for a slot `s` that is not -descended from the previous, `X` will be set to the new slot `s`. All votes -will then be of the form `vote(X, S)`, where `S` is the sorted list of slots -`(s, s.lockout)` being voted for. - -Given a vote `vote(X, S)`, let `S.last == vote.last` be the last slot in `S`. - -Now we define some "Optimistic Slashing" slashing conditions. The intuition -for these is described below: - -- `Intuition`: If a validator submits `vote(X, S)`, the same validator - should not have voted on a different fork that "overlaps" this fork. - More concretely, this validator should not have cast another vote - `vote(X', S')` where the range `[X, S.last]` overlaps the range - `[X', S'.last]`, `X != X'`, as shown below: - -```text - +-------+ - | | - +---------+ +--------+ - | | | | - | +-------+ | - | | - | | - | | - +---+---+ | - | | | - X | | | - | | | - +---+---+ | - | | - | +---+---+ - | | | - | | | X' - | | | - | +---+---+ - | | - | | - | | - | | - | +---+---+ - | | | - | | | S'.last - | | | - | +-------+ - | - +---+---+ - | | - s | | - | | - +---+---+ - | - | - | - | - +---+---+ - | | - S.last | | - | | - +-------+ -``` - -(Example of slashable votes vote(X', S') and vote(X, S)) - -In the diagram above, note that the vote for `S.last` must have been sent after -the vote for `S'.last` (due to lockouts, the higher vote must have been sent -later). Thus, the sequence of votes must have been: `X ... S'.last ... S.last`. -This means after the vote on `S'.last`, the validator must have switched back -to the other fork at some slot `s > S'.last > X`. Thus, the vote for `S.last` -should have used `s` as the "reference" point, not `X`, because that was the -last "switch" on the fork. - -To enforce this, we define the "Optimistic Slashing" slashing conditions. Given -any two distinct votes `vote(X, S)`and `vote(X', S')` by the same validator, -the votes must satisfy: - -- `X <= S.last`, `X' <= S'.last` -- All `s` in `S` are ancestors/descendants of one another, - all `s'` in `S'` are ancestors/descendants of one another, -- -- `X == X'` implies `S` is parent of `S'` or `S'` is a parent of `S` -- `X' > X` implies `X' > S.last` and `S'.last > S.last` - and for all `s` in `S`, `s + lockout(s) < X'` -- `X > X'` implies `X > S'.last` and `S.last > S'.last` - and for all `s` in `S'`, `s + lockout(s) < X` - -(The last two rules imply the ranges cannot overlap): -Otherwise the validator is slashed. - -`Range(vote)` - Given a vote `v = vote(X, S)`, define `Range(v)` to be the range -of slots `[X, S.last]`. - -`SP(old_vote, new_vote)` - This is the "Switching Proof" for `old_vote`, the -validator's latest vote. Such a proof is necessary anytime a validator switches -their "reference" slot (see vote section above). The switching proof includes -a reference to `old_vote`, so that there's a record of what the "range" of that -`old_vote` was (to make other conflicting switches in this range slashable). -Such a switch must still respect lockouts. - -A switching proof shows that `> 1/3` of the network is locked out at slot -`old_vote.last`. - -The proof is a list of elements `(validator_id, validator_vote(X, S))`, where: - -1. The sum of the stakes of all the validator id's `> 1/3` - -2. For each `(validator_id, validator_vote(X, S))`, there exists some slot `s` - in `S` where: - _ a.`s` is not a common ancestor of both `validator_vote.last` and - `old_vote.last` and `new_vote.last`. - _ b. `s` is not a descendant of `validator_vote.last`. \* c. `s + s.lockout() >= old_vote.last` (implies validator is still locked - out on slot `s` at slot `old_vote.last`). - -Switching forks without a valid switching proof is slashable. - -## Definitions: - -Optimistic Confirmation - A block `B` is then said to have achieved -"optimistic confirmation" if `>2/3` of stake have voted with votes `v` -where `Range(v)` for each such `v` includes `B.slot`. - -Finalized - A block `B` is said to be finalized if at least one -correct validator has rooted `B` or a descendant of `B`. - -Reverted - A block `B` is said to be reverted if another block `B'` that -is not a parent or descendant of `B` was finalized. - -## Guarantees: - -A block `B` that has reached optimistic confirmation will not be reverted -unless at least one validator is slashed. - -## Proof: - -Assume for the sake of contradiction, a block `B` has achieved -`optimistic confirmation` at some slot `B + n` for some `n`, and: - -- Another block `B'` that is not a parent or descendant of `B` - was finalized. -- No validators violated any slashing conditions. - -By the definition of `optimistic confirmation`, this means `> 2/3` of validators -have each shown some vote `v` of the form `Vote(X, S)` where `X <= B <= v.last`. -Call this set of validators the `Optimistic Validators`. - -Now given a validator `v` in `Optimistic Validators`, given two votes made by -`v`, `Vote(X, S)` and `Vote(X', S')` where `X <= B <= S.last`, and -`X' <= B <= S'.last`, then `X == X'` otherwise an "Optimistic Slashing" condition -is violated (the "ranges" of each vote would overlap at `B`). - -Thus define the `Optimistic Votes` to be the set of votes made by -`Optimistic Validators`, where for each optimistic validator `v`, the vote made -by `v` included in the set is the `maximal` vote `Vote(X, S)` with the -greatest `S.last` out of any votes made by `v` that satisfy `X <= B <= S.last`. -Because we know from above `X` for all such votes made by `v` is unique, we know -there is such a unique `maximal` vote. - -### Lemma 1: - -`Claim:` Given a vote `Vote(X, S)` made by a validator `V` in the -`Optimistic Validators` set, and `S` contains a vote for a slot `s` -for which: - -- `s + s.lockout > B`, -- `s` is not an ancestor or descendant of `B`, - -then `X > B`. - -```text - +-------+ - | | - +---------+ +--------+ - | | | | - | +-------+ | - | | - | | - | | - | +---+---+ - | | | - | | | X' - | | | - | +---+---+ - | | - | | - | +---+---+ - | | | - | | | B (Optimistically Confirmed) - | | | - | +---+---+ - | | - | | - | | - | +---+---+ - | | | - | | | S'.last - | | | - | +-------+ - | - +---+---+ - | | - X | | - | | - +---+---+ - | - | - | - | - | - | - +---+---+ - | | - S.last | | - | | - +---+---+ - | - | - | - | - +---+---+ - | | - s + s.lockout | | - +-------+ -``` - -`Proof`: Assume for the sake of contradiction a validator `V` from the -"Optimistic Validators" set made such a vote `Vote(X, S)` where `S` contains -a vote for a slot `s` not an ancestor or descendant of `B`, where -`s + s.lockout > B`, but `X <= B`. - -Let `Vote(X', S')` be the vote in `Optimistic Votes` set made by validator `V`. -By definition of that set (all votes optimistically confirmed `B`), -`X' <= B <= S'.last` (see diagram above). - -This implies that because it's assumed above `X <= B`, then `X <= S'.last`, -so by the slashing rules, either `X == X'` or `X < X'` (otherwise would -overlap the range `(X', S'.last)`). - -`Case X == X'`: - -Consider `s`. We know `s != X` because it is assumed `s` is not an ancestor -or descendant of `B`, and `X` is an ancestor of `B`. Because `S'.last` is a -descendant of `B`, this means `s` is also not an ancestor or descendant of -`S'.last`. Then because `S.last` is descended from `s`, then `S'.last` cannot -be an ancestor or descendant of `S.last` either. This implies `X != X'` by the -"Optimistic Slashing" rules. - -`Case X < X'`: - -Intuitively, this implies that `Vote(X, S)` was made "before" `Vote(X', S')`. - -From the assumption above, `s + s.lockout > B > X'`. Because `s` is not an -ancestor of `X'`, lockouts would have been violated when this validator -first attempted to submit a switching vote to `X'` with some vote of the -form `Vote(X', S'')`. - -Since none of these cases are valid, the assumption must have been invalid, -and the claim is proven. - -### Lemma 2: - -Recall `B'` was the block finalized on a different fork than -"optimistically" confirmed" block `B`. - -`Claim`: For any vote `Vote(X, S)` in the `Optimistic Votes` set, it must be -true that `B' > X` - -```text - +-------+ - | | - +--------+ +---------+ - | | | | - | +-------+ | - | | - | | - | | - | +---+---+ - | | | - | | | X - | | | - | +---+---+ - | | - | | - | +---+---+ - | | | - | | | B (Optimistically Confirmed) - | | | - | +---+---+ - | | - | | - | | - | +---+---+ - | | | - | | | S.last - | | | - | +-------+ - | - +---+---+ - | | - B'(Finalized) | | - | | - +-------+ -``` - -`Proof`: Let `Vote(X, S)` be a vote in the `Optimistic Votes` set. Then by -definition, given the "optimistically confirmed" block `B`, `X <= B <= S.last`. - -Because `X` is a parent of `B`, and `B'` is not a parent or ancestor of `B`, -then: - -- `B' != X` -- `B'` is not a parent of `X` - -Now consider if `B'` < `X`: - -`Case B' < X`: We will show this is a violation of lockouts. -From above, we know `B'` is not a parent of `X`. Then because `B'` was rooted, -and `B'` is not a parent of `X`, then the validator should not have been able -to vote on the higher slot `X` that does not descend from `B'`. - -### Proof of Safety: - -We now aim to show at least one of the validators in the -`Optimistic Validators` set violated a slashing rule. - -First note that in order for `B'` to have been rooted, there must have been -`> 2/3` stake that voted on `B'` or a descendant of `B'`. Given that the -`Optimistic Validator` set also contains `> 2/3` of the staked validators, -it follows that `> 1/3` of the staked validators: - -- Rooted `B'` or a descendant of `B'` -- Also submitted a vote `v` of the form `Vote(X, S)` where `X <= B <= v.last`. - -Let the `Delinquent` set be the set of validators that meet the above -criteria. - -By definition, in order to root `B'`, each validator `V` in `Delinquent` -must have each made some "switching vote" of the form `Vote(X_v, S_v)` where: - -- `S_v.last > B'` -- `S_v.last` is a descendant of `B'`, so it can't be a descendant of `B` -- Because `S_v.last` is not a descendant of `B`, then `X_v` cannot be a - descendant or ancestor of `B`. - -By definition, this delinquent validator `V` also made some vote `Vote(X, S)` -in the `Optimistic Votes` where by definition of that set (optimistically -confirmed `B`), we know `S.last >= B >= X`. - -By `Lemma 2` we know `B' > X`, and from above `S_v.last > B'`, so then -`S_v.last > X`. Because `X_v != X` (cannot be a descendant or ancestor of -`B` from above), then by the slashing rules then, we know `X_v > S.last`. -From above, `S.last >= B >= X` so for all such "switching votes", `X_v > B`. - -Now ordering all these "switching votes" in time, let `V` to be the validator -in `Optimistic Validators` that first submitted such a "switching vote" -`Vote(X', S')`, where `X' > B`. We know that such a validator exists because -we know from above that all delinquent validators must have submitted such -a vote, and the delinquent validators are a subset of the -`Optimistic Validators`. - -Let `Vote(X, S)` be the unique vote in `Optimistic Votes` made by -validator `V` (maximizing `S.last`). - -Given `Vote(X, S)` because `X' > B >= X`, then `X' > X`, so -by the "Optimistic Slashing" rules, `X' > S.last`. - -In order to perform such a "switching vote" to `X'`, a switching proof -`SP(Vote(X, S), Vote(X', S'))` must show `> 1/3` of stake being locked -out at this validator's latest vote, `S.last`. Combine this `>1/3` with the -fact that the set of validators in the `Optimistic Voters` set consists of -`> 2/3` of the stake, implies at least one optimistic validator `W` from the -`Optimistic Voters` set must have submitted a vote (recall the definition of -a switching proof),`Vote(X_w, S_w)` that was included in validator `V`'s -switching proof for slot `X'`, where `S_w` contains a slot `s` such that: - -- `s` is not a common ancestor of `S.last` and `X'` -- `s` is not a descendant of `S.last`. -- `s' + s'.lockout > S.last` - -Because `B` is an ancestor of `S.last`, it is also true then: - -- `s` is not a common ancestor of `B` and `X'` -- `s' + s'.lockout > B` - -which was included in `V`'s switching proof. - -Now because `W` is also a member of `Optimistic Voters`, then by the `Lemma 1` -above, given a vote by `W`, `Vote(X_w, S_w)`, where `S_w` contains a vote for -a slot `s` where `s + s.lockout > B`, and `s` is not an ancestor of `B`, then -`X_w > B`. - -Because validator `V` included vote `Vote(X_w, S_w)` in its proof of switching -for slot `X'`, then his implies validator `V'` submitted vote `Vote(X_w, S_w)` -**before** validator `V` submitted its switching vote for slot `X'`, -`Vote(X', S')`. - -But this is a contradiction because we chose `Vote(X', S')` to be the first vote -made by any validator in the `Optimistic Voters` set where `X' > B` and `X'` is -not a descendant of `B`. diff --git a/docs/src/proposals/partitioned-inflationary-rewards-distribution.md b/docs/src/proposals/partitioned-inflationary-rewards-distribution.md deleted file mode 100644 index e5694e7b70e..00000000000 --- a/docs/src/proposals/partitioned-inflationary-rewards-distribution.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Partitioned Inflationary Rewards Distribution ---- - -## Problem - -With the increase of number of stake accounts, computing and redeeming the stake -rewards at the start block of the epoch boundary becomes very expensive. -Currently, with 550K stake accounts, the stake reward time has already taken -more than 10 seconds. This prolonged computation slows down the network, and can -cause large number of forks at the epoch boundary, which makes the matter even -worse. - -## Proposed Solutions - -Instead of computing and reward stake accounts at epoch boundary, we will -decouple reward computation and reward credit into two phases. - -A separate service, "EpochRewardCalculationService" will be created. The service -will listen to a channel for any incoming rewards calculation requests, and -perform the calculation for the rewards. For each block that cross the epoch -boundary, the bank will send a request to the `EpochRewardCalculationService`. -This marks the start of the reward computation phase. - -``` -N-1 -- N -- N+1 - \ - \ - N+2 -``` - -In the above example, N is the start of the new epoch. Two rewards calculation -requests will be sent out at slot N and slot N+2 because they both cross the -epoch boundary and are on different forks. To avoid repeated computation with -the same input, the signature of the computation requests, `hash(epoch_number, -hash(stake_accounts_data), hash(vote_accounts), hash(delegation_map))`, are -calculated. Duplicated computation requests will be discard. For the above -example, if there are no stake/vote accounts changes between slot N and slot -N+2, the 2nd computation request will be discarded. - -When reaching block height `N` after the start of the `reward computation -phase`, the bank starts the second phase - reward credit, in which, the bank -first query the `epoch calc service` with the request signature to get the -rewards result, which will be resented as a map from accounts_pubkey->rewards, -then credit the rewards to the stake accounts for the next `M` blocks. If the -rewards result is not available, the bank will wait until the results are -available. - -We call them:
-(a) calculating interval: `[epoch_start, epoch_start+N]`
-(b) credit interval: `[epoch_start+N+1, epoch_start+N+M]`, respectively.
-And the combined interval `[epoch_start, epoch_start+N+M]` is called -`rewarding interval`. - -For `calculating interval`, `N` is chosen to be sufficiently large so that the -background computation should have completed and the result of the reward -computation is available at the end of `calculating interval`. `N` can be fixed -such as 100 (roughly equivalent to 50 seconds), or chosen as a function of the -number of stake accounts, `f(num_stake_accounts)`. - -In `credit interval`, the bank will fetch the reward computation results from -the background thread and start credit the rewards during the next `M` blocks. -The idea is partition the accounts into `M` partitions. And each block, the bank -credit `1/M` accounts. The partition is required to be deterministic for the -current epoch, but must also be random across different epochs. One way to -achieve these properties is to hash the account's pubkey with some epoch -dependent values, sort the results, and divide them into `M` bins. The epoch -dependent value can be the epoch number, total rewards for the epoch, the leader -pubkey for the epoch block, etc. `M` can be choses based on 50K account per -block, which equal to `ceil(num_stake_accounts/50,000)`. - -`num_stake_account` is extracted from `leader_schedule_epoch` block, so we don't -run into discrepancy where new transactions right before an epoch boundary -creates one fork with `X` stake accounts and another fork with `Y` stake accounts. - -In order to avoid putting extra burden of computing and credit the stake reward -for blocks produced during the `rewarding interval`, we can reduce the compute -budget limits on those blocks in `rewarding interval`, and reserve some computing -and read/write capacity to perform stake rewarding. - -### Challenges - -1. stake accounts reads/writes during the `rewarding interval` - -`epoch_start..epoch_start+N+M` Because of the delayed credit of the rewards, -Reads to those stake accounts will not return the value that the user are -expecting (viz. not include the recent epoch stake rewards). Writes to those -stake accounts will be lost once the reward are credited on block -`epoch_start+N+M`. We will need to modify the runtime to restrict read/writes to -stake accounts during the `rewarding interval`. Any transactions, which involves -stake accounts, will result in a new execution error, i.e. "stake rewards -pending, account access is restricted". However, normal rpc queries, such as -'getBalance', will return the current lamport of the account. The user can -expect the rewards to be credit as some time point during the 'rewarding -interval'. - -2. voting during `reward interval` - -During reward interval, vote transactions must be processed normally for -achieving consensus and making progress for rooted blocks. However, those vote -transactions may potentially change the vote accounts balance (i.e. pay for the -voting transaction fee if vote_account and block reward recipient accounts -are the same), before the epoch rewards are paid. When the epoch rewards are -paid, those block rewards will be wiped out by the stale cached value. To -prevent this, we will enforce that the vote_account and authorized_voter -authority must be different. - -3. snapshot taken during the `rewarding interval` - -If a snapshot is taken during the `rewarding interval`, it would miss the -rewards for the stake accounts. Any plain restart from those snapshots will be -wrong, unless we reconstruct the rewards from the recent epoch boundary. This -will add some complexity to validator restart. In the first implementation, we -will force *not* taking any snapshot and *not* performing accounts hash -calculation during the `rewarding interval`. Incremental snapshot request will -be skipped. Full snapshot request will be re-queued be picked up later at the -end of the `reward interval`. - -In future, if needed, we can -revisit to enable taking snapshots and perform hash calculation during reward -interval. - -4. account-db related action during the `rewarding interval` - -Account-db related action such as flush, clean, squash, shrink etc. may touch -and evict the stake accounts from account db's cache during the `rewarding -interval`. This will slow down the credit in the future at bank `epoch_start+N`. -We may need to exclude such accounts_db actions for stake_accounts during -`rewarding interval`. This is going to be a performance tuning problem. In the -first implementation, for simplicity, we will keep the account-db action as it -is, and make the `credit interval` larger to accommodate the performance hit -when writing back those accounts. In future, we can continue tuning account db -actions during 'rewarding interval'. - -5. view of total epoch capitalization change - -The view of total epoch capitalization, instead of being available at every -epoch boundary, is only available after the `rewarding interval`. Any third -party application logic, which depends on total epoch capitalization, need to -wait after `rewarding interval`. - -6. `getInflationReward` JSONRPC API method call - -Today, the `getInflationReward` JSONRPC API method call can simply grab the -first block in the target epoch and lookup the target stake account's rewards -entry. With these changes, the call will need updated to derive the target -stake account's credit block, grab _that_ block, then lookup rewards. -Additionally we'll need to return more informative errors for queries made -during the lockout period, so users can know that their rewards are pending for -the target epoch. A new rpc API, i.e. `getRewardInterval`, will be added for -querying the `rewarding interval` for the current epoch. diff --git a/docs/src/proposals/program-instruction-macro.md b/docs/src/proposals/program-instruction-macro.md deleted file mode 100644 index e87b0930f50..00000000000 --- a/docs/src/proposals/program-instruction-macro.md +++ /dev/null @@ -1,226 +0,0 @@ -# Program Instruction Macro - -## Problem - -Currently, inspecting an on-chain transaction requires depending on a -client-side, language-specific decoding library to parse the instruction. If -rpc methods could return decoded instruction details, these custom solutions -would be unnecessary. - -We can deserialize instruction data using a program's Instruction enum, but -decoding the account-key list into human-readable identifiers requires manual -parsing. Our current Instruction enums have that account information, but only -in variant docs. - -Similarly, we have instruction constructor functions that duplicate nearly all -the information in the enum, but we can't generate that constructor from the -enum definition because the list of account references is in code comments. - -Also, Instruction docs can vary between implementations, as there is no -mechanism to ensure consistency. - -## Proposed Solution - -Move the data from code comments to attributes, such that the constructors -can be generated, and include all the documentation from the enum definition. - -Here is an example of an Instruction enum using the new accounts format: - -```rust,ignore -#[instructions(test_program::id())] -pub enum TestInstruction { - /// Transfer lamports - #[accounts( - from_account(SIGNER, WRITABLE, desc = "Funding account"), - to_account(WRITABLE, desc = "Recipient account"), - )] - Transfer { - lamports: u64, - }, - - /// Provide M of N required signatures - #[accounts( - data_account(WRITABLE, desc = "Data account"), - signers(SIGNER, multiple, desc = "Signer"), - )] - Multisig, - - /// Consumes a stored nonce, replacing it with a successor - #[accounts( - nonce_account(SIGNER, WRITABLE, desc = "Nonce account"), - recent_blockhashes_sysvar(desc = "RecentBlockhashes sysvar"), - nonce_authority(SIGNER, optional, desc = "Nonce authority"), - )] - AdvanceNonceAccount, -} -``` - -An example of the generated TestInstruction with docs: - -```rust,ignore -pub enum TestInstruction { - /// Transfer lamports - /// - /// * Accounts expected by this instruction: - /// 0. `[WRITABLE, SIGNER]` Funding account - /// 1. `[WRITABLE]` Recipient account - Transfer { - lamports: u64, - }, - - /// Provide M of N required signatures - /// - /// * Accounts expected by this instruction: - /// 0. `[WRITABLE]` Data account - /// * (Multiple) `[SIGNER]` Signers - Multisig, - - /// Consumes a stored nonce, replacing it with a successor - /// - /// * Accounts expected by this instruction: - /// 0. `[WRITABLE, SIGNER]` Nonce account - /// 1. `[]` RecentBlockhashes sysvar - /// 2. (Optional) `[SIGNER]` Nonce authority - AdvanceNonceAccount, -} -``` - -Generated constructors: - -```rust,ignore -/// Transfer lamports -/// -/// * `from_account` - `[WRITABLE, SIGNER]` Funding account -/// * `to_account` - `[WRITABLE]` Recipient account -pub fn transfer(from_account: Pubkey, to_account: Pubkey, lamports: u64) -> Instruction { - let account_metas = vec![ - AccountMeta::new(from_pubkey, true), - AccountMeta::new(to_pubkey, false), - ]; - Instruction::new_with_bincode( - test_program::id(), - &SystemInstruction::Transfer { lamports }, - account_metas, - ) -} - -/// Provide M of N required signatures -/// -/// * `data_account` - `[WRITABLE]` Data account -/// * `signers` - (Multiple) `[SIGNER]` Signers -pub fn multisig(data_account: Pubkey, signers: &[Pubkey]) -> Instruction { - let mut account_metas = vec![ - AccountMeta::new(nonce_pubkey, false), - ]; - for pubkey in signers.iter() { - account_metas.push(AccountMeta::new_readonly(pubkey, true)); - } - - Instruction::new_with_bincode( - test_program::id(), - &TestInstruction::Multisig, - account_metas, - ) -} - -/// Consumes a stored nonce, replacing it with a successor -/// -/// * nonce_account - `[WRITABLE, SIGNER]` Nonce account -/// * recent_blockhashes_sysvar - `[]` RecentBlockhashes sysvar -/// * nonce_authority - (Optional) `[SIGNER]` Nonce authority -pub fn advance_nonce_account( - nonce_account: Pubkey, - recent_blockhashes_sysvar: Pubkey, - nonce_authority: Option, -) -> Instruction { - let mut account_metas = vec![ - AccountMeta::new(nonce_account, false), - AccountMeta::new_readonly(recent_blockhashes_sysvar, false), - ]; - if let Some(pubkey) = authorized_pubkey { - account_metas.push(AccountMeta::new_readonly*nonce_authority, true)); - } - Instruction::new_with_bincode( - test_program::id(), - &TestInstruction::AdvanceNonceAccount, - account_metas, - ) -} - -``` - -Generated TestInstructionVerbose enum: - -```rust,ignore -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub enum TestInstruction { - /// Transfer lamports - Transfer { - /// Funding account - funding_account: u8 - - /// Recipient account - recipient_account: u8 - - lamports: u64, - }, - - /// Provide M of N required signatures - Multisig { - data_account: u8, - signers: Vec, - }, - - /// Consumes a stored nonce, replacing it with a successor - AdvanceNonceAccount { - nonce_account: u8, - recent_blockhashes_sysvar: u8, - nonce_authority: Option, - } -} - -impl TestInstructionVerbose { - pub fn from_instruction(instruction: TestInstruction, account_keys: Vec) -> Self { - match instruction { - TestInstruction::Transfer { lamports } => TestInstructionVerbose::Transfer { - funding_account: account_keys[0], - recipient_account: account_keys[1], - lamports, - } - TestInstruction::Multisig => TestInstructionVerbose::Multisig { - data_account: account_keys[0], - signers: account_keys[1..], - } - TestInstruction::AdvanceNonceAccount => TestInstructionVerbose::AdvanceNonceAccount { - nonce_account: account_keys[0], - recent_blockhashes_sysvar: account_keys[1], - nonce_authority: &account_keys.get(2), - } - } - } -} - -``` - -## Considerations - -1. **Named fields** - Since the resulting Verbose enum constructs variants with - named fields, any unnamed fields in the original Instruction variant will need - to have names generated. As such, it would be considerably more straightforward - if all Instruction enum fields are converted to named types, instead of unnamed - tuples. This seems worth doing anyway, adding more precision to the variants and - enabling real documentation (so developers don't have to do - [this](https://github.com/solana-labs/solana/blob/3aab13a1679ba2b7846d9ba39b04a52f2017d3e0/sdk/src/system_instruction.rs#L140) - This will cause a little churn in our current code base, but not a lot. -2. **Variable account lists** - This approach offers a couple options for - variable account lists. First, optional accounts may be added and tagged with - the `optional` keyword. However, currently only one optional account is - supported per instruction. Additional data will need to be added to the - instruction to support multiples, enabling identification of which accounts are - present when some but not all are included. Second, accounts that share the same - features may be added as a set, tagged with the `multiple` keyword. Like - optional accounts, only one multiple account set is supported per instruction - (and optional and multiple may not coexist). More complex instructions that - cannot be accommodated by `optional` or `multiple`, requiring logic to figure - out account order/representation, should probably be made into separate - instructions. diff --git a/docs/src/proposals/return-data.md b/docs/src/proposals/return-data.md deleted file mode 100644 index 3e70e87195e..00000000000 --- a/docs/src/proposals/return-data.md +++ /dev/null @@ -1,144 +0,0 @@ -# Return data from SBF programs - -## Problem - -In the Solidity language it is permitted to return any number of values from a function, -for example a variable length string can be returned: - -``` -function foo1() public returns (string) { - return "Hello, world!\n"; -} -``` - -Multiple values, arrays and structs are permitted too. - -``` -struct S { - int f1; - bool f2 -}; - -function foo2() public returns (string, int[], S) { - return (a, b, c); -} -``` - -All the return values are eth abi encoded to a variable-length byte array. - -On ethereum errors can be returned too: - -``` -function withdraw() public { - require(msg.sender == owner, "Permission denied"); -} - -function failure() public { - revert("I afraid I can't do that dave"); -} -``` -These errors help the developer debug any issue they are having, and can -also be caught in a Solidity `try` .. `catch` block. Outside of a `try` .. `catch` -block, any of these would cause the transaction or rpc to fail. - -## Existing solution - -The existing solution that Solang uses, writes the return data to the callee account data. -The caller's account cannot be used, since the callee may not be the same SBF program, so -it will not have permission to write to the callee's account data. - -Another solution would be to have a single return data account which is passed -around through CPI. Again this does not work for CPI as the callee may not have -permission to write to it. - -The problem with this solution is: - -- It does not work for RPC calls -- It is very racey; a client has to submit the Tx and then retrieve the account - data. This is not atomic so the return data can be overwritten by another transaction. - -## Requirements for Solution - -It must work for: - -- RPC: An RPC should be able to return any number of values without writing to account data -- Transaction: An transaction should be able to return any number of values without needing to write them account data -- CPI: The callee must "set" return value, and the caller must be able to retrieve it. - -## Review of other chains - -### Ethereum (EVM) - -The `RETURN` opcode allows a contract to set a buffer as a returndata. This opcode takes a pointer to memory and a size. The `REVERT` opcode works similarly but signals that the call failed, and all account data changes must be reverted. - -For CPI, the caller can retrieve the returned data of the callee using the `RETURNDATASIZE` opcode which returns the length, and the `RETURNDATACOPY` opcode, which takes a memory destination pointer, offset into the returndata, and a length argument. - -Ethereum stores the returndata in blocks. - -### Parity Substrate - -The return data can be set using the `seal_return(u32 flags, u32 pointer, u32 size)` syscall. -- Flags can be 1 for revert, 0 for success (nothing else defined) -- Function does not return - -CPI: The `seal_call()` syscall takes pointer to buffer and pointer to buffer size where return data goes - - There is a 32KB limit for return data. - -Parity Substrate does not write the return data to blocks. - -## Rejected Solution - -The concept of ephemeral accounts has been proposed a solution for this. This would -certainly work for the CPI case, but this would not work RPC or Transaction case. - -## Proposed Solution - -The callee can set the return data using a new system call `sol_set_return_data(buf: *const u8, length: u64)`. -There is a limit of 1024 bytes for the returndata. This function can be called multiple times, and -will simply overwrite what was written in the last call. - -The return data can be retrieved with `sol_get_return_data(buf: *mut u8, length: u64, program_id: *mut Pubkey) -> u64`. -This function copies the return buffer, and the program_id that set the return data, and -returns the length of the return data, or `0` if no return data is set. In this case, program_id is not set. - -When an instruction calls `sol_invoke()`, the return data of the callee is copied into the return data -of the current instruction. This means that any return data is automatically passed up the call stack, -to the callee of the current instruction (or the RPC call). - -Note that `sol_invoke()` clears the returns data before invoking the callee, so that any return data from -a previous invoke is not reused if the invoked fails to set a return data. For example: - - - A invokes B - - Before entry to B, return data is cleared.0 - - B sets some return data and returns - - A invokes C - - Before entry to C, return data is cleared. - - C does not set return data and returns - - A checks return data and finds it empty - -Another scenario to consider: - - - A invokes B - - B invokes C - - C sets return data and returns - - B does not touch return data and returns - - A gets return data from C - - A does not touch return data - - Return data from transaction is what C set. - -The compute costs are calculated for getting and setting the return data using -the syscalls. - -For a normal RPC or Transaction, the returndata is base64-encoded and stored along side the sol_log -strings in the [stable log](https://github.com/solana-labs/solana/blob/95292841947763bdd47ef116b40fc34d0585bca8/sdk/src/process_instruction.rs#L275-L281). - -## Note on returning errors - -Solidity on Ethereum allows the contract to return an error in the return data. In this case, all -the account data changes for the account should be reverted. On Solana, any non-zero exit code -for a SBF program means the entire transaction fails. We do not wish to support an error return -by returning success and then returning an error in the return data. This would mean we would have -to support reverting the account data changes; this too expensive both on the VM side and the SBF -contract side. - -Errors will be reported via sol_log. diff --git a/docs/src/proposals/rip-curl.md b/docs/src/proposals/rip-curl.md deleted file mode 100644 index c562f01bd13..00000000000 --- a/docs/src/proposals/rip-curl.md +++ /dev/null @@ -1,56 +0,0 @@ -# RiP Curl: low-latency, transaction-oriented RPC - -## Problem - -Solana's initial RPC implementation was created for the purpose of allowing -users to confirm transactions that had just recently been sent to the cluster. -It was designed with memory usage in mind such that any validator should be -able to support the API without concern of DoS attacks. - -Later down the line, it became desirable to use that same API to support the -Solana explorer. The original design only supported minutes of history, so we -changed it to instead store transaction statuses in a local RocksDB instance -and offer days of history. We then extended that to 6 months via BigTable. - -With each modification, the API became more suitable for applications serving -static content and less appealing for transaction processing. The clients poll -for transaction status instead of being notified, giving the false impression -of higher confirmation times. Furthermore, what clients can poll for is -limited, preventing them from making reasonable real-time decisions, such as -recognizing a transaction is confirmed as soon as particular, known -validators vote on it. - -## Proposed Solution - -A web-friendly, transaction-oriented, streaming API built around the -validator's ReplayStage. - -Improved client experience: - -- Support connections directly from WebAssembly apps. -- Clients can be notified of confirmation progress in real-time, including votes - and voter stake weight. -- Clients can be notified when the heaviest fork changes, if it affects the - transactions confirmation count. - -Easier for validators to support: - -- Each validator supports some number of concurrent connections and otherwise - has no significant resource constraints. -- Transaction status is never stored in memory and cannot be polled for. -- Signatures are only stored in memory until the desired commitment level or - until the blockhash expires, whichever is later. - -How it works: - -1. The client connects to a validator using a reliable communication channel, - such as a web socket. -2. The validator registers the signature with ReplayStage. -3. The validator sends the transaction into the Gulf Stream and retries all - known forks until the blockhash expires (not until the transaction is - accepted on only the heaviest fork). If the blockhash expires, the - signature is unregistered, the client is notified, and connection is closed. -4. As ReplayStage detects events affecting the transaction's status, it - notifies the client in real-time. -5. After confirmation that the transaction is rooted (`CommitmentLevel::Max`), - the signature is unregistered and the server closes the upstream channel. diff --git a/docs/src/proposals/simple-payment-and-state-verification.md b/docs/src/proposals/simple-payment-and-state-verification.md deleted file mode 100644 index 6a561cd96a1..00000000000 --- a/docs/src/proposals/simple-payment-and-state-verification.md +++ /dev/null @@ -1,258 +0,0 @@ ---- -title: Simple Payment and State Verification ---- - -It is often useful to allow low resourced clients to participate in a Solana -cluster. Be this participation economic or contract execution, verification -that a client's activity has been accepted by the network is typically -expensive. This proposal lays out a mechanism for such clients to confirm that -their actions have been committed to the ledger state with minimal resource -expenditure and third-party trust. - -## A Naive Approach - -Validators store the signatures of recently confirmed transactions for a short -period of time to ensure that they are not processed more than once. Validators -provide a JSON RPC endpoint, which clients can use to query the cluster if a -transaction has been recently processed. Validators also provide a PubSub -notification, whereby a client registers to be notified when a given signature -is observed by the validator. While these two mechanisms allow a client to -verify a payment, they are not a proof and rely on completely trusting a -validator. - -We will describe a way to minimize this trust using Merkle Proofs to anchor the -validator's response in the ledger, allowing the client to confirm on their own -that a sufficient number of their preferred validators have confirmed a -transaction. Requiring multiple validator attestations further reduces trust in -the validator, as it increases both the technical and economic difficulty of -compromising several other network participants. - -## Light Clients - -A 'light client' is a cluster participant that does not itself run a validator. -This light client would provide a level of security greater than trusting a -remote validator, without requiring the light client to spend a lot of resources -verifying the ledger. - -Rather than providing transaction signatures directly to a light client, the -validator instead generates a Merkle Proof from the transaction of interest to -the root of a Merkle Tree of all transactions in the including block. This -Merkle Root is stored in a ledger entry which is voted on by validators, -providing it consensus legitimacy. The additional level of security for a light -client depends on an initial canonical set of validators the light client -considers to be the stakeholders of the cluster. As that set is changed, the -client can update its internal set of known validators with -[receipts](simple-payment-and-state-verification.md#receipts). This may become -challenging with a large number of delegated stakes. - -Validators themselves may want to use light client APIs for performance reasons. -For example, during the initial launch of a validator, the validator may use a -cluster provided checkpoint of the state and verify it with a receipt. - -## Receipts - -A receipt is a minimal proof that; a transaction has been included in a block, -that the block has been voted on by the client's preferred set of validators -and that the votes have reached the desired confirmation depth. - -### Transaction Inclusion Proof - -A transaction inclusion proof is a data structure that contains a Merkle Path -from a transaction, through an Entry-Merkle to a Block-Merkle, which is included -in a Bank-Hash with the required set of validator votes. A chain of PoH Entries -containing subsequent validator votes, deriving from the Bank-Hash, is the proof -of confirmation. - -#### Transaction Merkle - -An Entry-Merkle is a Merkle Root including all transactions in a given entry, -sorted by signature. Each transaction in an entry is already merkled here: -https://github.com/solana-labs/solana/blob/b6bfed64cb159ee67bb6bdbaefc7f833bbed3563/ledger/src/entry.rs#L205. -This means we can show a transaction `T` was included in an entry `E`. - -A Block-Merkle is the Merkle Root of all the Entry-Merkles sequenced in the -block. - -![Block Merkle Diagram](/img/spv-block-merkle.svg) - -Together the two merkle proofs show a transaction `T` was included in a block -with bank hash `B`. - -An Accounts-Hash is the hash of the concatenation of the state hashes of -each account modified during the current slot. - -Transaction status is necessary for the receipt because the state receipt is -constructed for the block. Two transactions over the same state can appear in -the block, and therefore, there is no way to infer from just the state whether -a transaction that is committed to the ledger has succeeded or failed in -modifying the intended state. It may not be necessary to encode the full status -code, but a single status bit to indicate the transaction's success. - -Currently, the Block-Merkle is not implemented, so to verify `E` was an entry -in the block with bank hash `B`, we would need to provide all the entry hashes -in the block. Ideally this Block-Merkle would be implemented, as the alternative -is very inefficient. - -#### Block Headers - -In order to verify transaction inclusion proofs, light clients need to be able -to infer the topology of the forks in the network - -More specifically, the light client will need to track incoming block headers -such that given two bank hashes for blocks `A` and `B`, they can determine -whether `A` is an ancestor of `B` (Below section on -`Optimistic Confirmation Proof` explains why!). Contents of header are the -fields necessary to compute the bank hash. - -A Bank-Hash is the hash of the concatenation of the Block-Merkle and -Accounts-Hash described in the `Transaction Merkle` section above. - -![Bank Hash Diagram](/img/spv-bank-hash.svg) - -In the code: - -https://github.com/solana-labs/solana/blob/b6bfed64cb159ee67bb6bdbaefc7f833bbed3563/runtime/src/bank.rs#L3468-L3473 - -``` - let mut hash = hashv(&[ - // bank hash of the parent block - self.parent_hash.as_ref(), - // hash of all the modified accounts - accounts_delta_hash.hash.as_ref(), - // Number of signatures processed in this block - &signature_count_buf, - // Last PoH hash in this block - self.latest_blockhash().as_ref(), - ]); -``` - -A good place to implement this logic along existing streaming logic in the -validator's replay logic: https://github.com/solana-labs/solana/blob/b6bfed64cb159ee67bb6bdbaefc7f833bbed3563/core/src/replay_stage.rs#L1092-L1096 - -#### Optimistic Confirmation Proof - -Currently optimistic confirmation is detected via a listener that monitors -gossip and the replay pipeline for votes: -https://github.com/solana-labs/solana/blob/b6bfed64cb159ee67bb6bdbaefc7f833bbed3563/core/src/cluster_info_vote_listener.rs#L604-L614. - -Each vote is a signed transaction that includes the bank hash of the block the -validator voted for, i.e. the `B` from the `Transaction Merkle` section above. -Once a certain threshold `T` of the network has voted on a block, the block is -considered optimistically confirmed. The votes made by this group of `T` -validators is needed to show the block with bank hash `B` was optimistically -confirmed. - -However other than some metadata, the signed votes themselves are not -currently stored anywhere, so they can't be retrieved on demand. These votes -probably need to be persisted in Rocksdb database, indexed by a key -`(Slot, Hash, Pubkey)` which represents the slot of the vote, bank hash of the -vote, and vote account pubkey responsible for the vote. - -Together, the transaction merkle and optimistic confirmation proofs can be -provided over RPC to subscribers by extending the existing signature -subscription logic. Clients who subscribe to the "Confirmed" confirmation -level are already notified when optimistic confirmation is detected, a flag -can be provided to signal the two proofs above should also be returned. - -It is important to note that optimistically confirming `B` also implies that all -ancestor blocks of `B` are also optimistically confirmed, and also that not -all blocks will be optimistically confirmed. - -``` - -B -> B' - -``` - -So in the example above if a block `B'` is optimistically confirmed, then so is -`B`. Thus if a transaction was in block `B`, the transaction merkle in the -proof will be for block `B`, but the votes presented in the proof will be for -block `B'`. This is why the headers in the `Block headers` section above are -important, the client will need to verify that `B` is indeed an ancestor of -`B'`. - -#### Proof of Stake Distribution - -Once presented with the transaction merkle and optimistic confirmation proofs -above, a client can verify a transaction `T` was optimistically confirmed in a -block with bank hash `B`. The last missing piece is how to verify that the -votes in the optimistic proofs above actually constitute the valid `T` -percentage of the stake necessary to uphold the safety guarantees of -"optimistic confirmation". - -One way to approach this might be for every epoch, when the stake set changes, -to write all the stakes to a system account, and then have validators subscribe -to that system account. Full nodes can then provide a merkle proving that the -system account state was updated in some block `B`, and then show that the -block `B` was optimistically confirmed/rooted. - -### Account State Verification - -An account's state (balance or other data) can be verified by submitting a -transaction with a **_TBD_** Instruction to the cluster. The client can then -use a [Transaction Inclusion Proof](#transaction-inclusion-proof) to verify -whether the cluster agrees that the account has reached the expected state. - -### Validator Votes - -Leaders should coalesce the validator votes by stake weight into a single entry. -This will reduce the number of entries necessary to create a receipt. - -### Chain of Entries - -A receipt has a PoH link from the payment or state Merkle Path root to a list -of consecutive validation votes. - -It contains the following: - -- Transaction -> Entry-Merkle -> Block-Merkle -> Bank-Hash - -And a vector of PoH entries: - -- Validator vote entries -- Ticks -- Light entries - -```text -/// This Entry definition skips over the transactions and only contains the -/// hash of the transactions used to modify PoH. -LightEntry { - /// The number of hashes since the previous Entry ID. - pub num_hashes: u64, - /// The SHA-256 hash `num_hashes` after the previous Entry ID. - hash: Hash, - /// The Merkle Root of the transactions encoded into the Entry. - entry_hash: Hash, -} -``` - -The light entries are reconstructed from Entries and simply show the entry -Merkle Root that was mixed in to the PoH hash, instead of the full transaction -set. - -Clients do not need the starting vote state. The -[fork selection](../implemented-proposals/tower-bft.md) algorithm is defined -such that only votes that appear after the transaction provide finality for the -transaction, and finality is independent of the starting state. - -### Verification - -A light client that is aware of the supermajority set validators can verify a -receipt by following the Merkle Path to the PoH chain. The Block-Merkle is the -Merkle Root and will appear in votes included in an Entry. The light client can -simulate [fork selection](../implemented-proposals/tower-bft.md) for the -consecutive votes and verify that the receipt is confirmed at the desired -lockout threshold. - -### Synthetic State - -Synthetic state should be computed into the Bank-Hash along with the bank -generated state. - -For example: - -- Epoch validator accounts and their stakes and weights. -- Computed fee rates - -These values should have an entry in the Bank-Hash. They should live under known -accounts, and therefore have an index into the hash concatenation. diff --git a/docs/src/proposals/slashing.md b/docs/src/proposals/slashing.md deleted file mode 100644 index 10d4fe988f7..00000000000 --- a/docs/src/proposals/slashing.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Slashing rules ---- - -Unlike Proof of Work \(PoW\) where off-chain capital expenses are already -deployed at the time of block construction/voting, PoS systems require -capital-at-risk to prevent a logical/optimal strategy of multiple chain voting. -We intend to implement slashing rules which, if broken, result some amount of -the offending validator's deposited stake to be removed from circulation. Given -the ordering properties of the PoH data structure, we believe we can simplify -our slashing rules to the level of a voting lockout time assigned per vote. - -I.e. Each vote has an associated lockout time \(PoH duration\) that represents -a duration by any additional vote from that validator must be in a PoH that -contains the original vote, or a portion of that validator's stake is -slashable. This duration time is a function of the initial vote PoH count and -all additional vote PoH counts. It will likely take the form: - -```text -Lockouti\(PoHi, PoHj\) = PoHj + K \* exp\(\(PoHj - PoHi\) / K\) -``` - -Where PoHi is the height of the vote that the lockout is to be applied to and -PoHj is the height of the current vote on the same fork. If the validator -submits a vote on a different PoH fork on any PoHk where k > j > i and -PoHk < Lockout\(PoHi, PoHj\), then a portion of that validator's stake is at -risk of being slashed. - -In addition to the functional form lockout described above, early -implementation may be a numerical approximation based on a First In, First Out -\(FIFO\) data structure and the following logic: - -- FIFO queue holding 32 votes per active validator -- new votes are pushed on top of queue \(`push_front`\) -- expired votes are popped off top \(`pop_front`\) -- as votes are pushed into the queue, the lockout of each queued vote doubles -- votes are removed from back of queue if `queue.len() > 32` -- the earliest and latest height that has been removed from the back of the - queue should be stored - -It is likely that a reward will be offered as a % of the slashed amount to any -node that submits proof of this slashing condition being violated to the PoH. - -### Partial Slashing - -In the schema described so far, when a validator votes on a given PoH stream, -they are committing themselves to that fork for a time determined by the vote -lockout. An open question is whether validators will be hesitant to begin -voting on an available fork if the penalties are perceived too harsh for an -honest mistake or flipped bit. - -One way to address this concern would be a partial slashing design that results -in a slashable amount as a function of either: - -1. the fraction of validators, out of the total validator pool, that were also - slashed during the same time period \(ala Casper\) -2. the amount of time since the vote was cast \(e.g. a linearly increasing % of - total deposited as slashable amount over time\), or both. - -This is an area currently under exploration. diff --git a/docs/src/proposals/snapshot-verification.md b/docs/src/proposals/snapshot-verification.md deleted file mode 100644 index 8955fb51ed2..00000000000 --- a/docs/src/proposals/snapshot-verification.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Snapshot Verification ---- - -## Problem - -Snapshot verification of the account states is implemented, but the bank hash of the snapshot which is used to verify is falsifiable. - -## Solution - -While a validator is processing transactions to catch up to the cluster from the snapshot, use incoming vote transactions and the commitment calculator to confirm that the cluster is indeed building on the snapshotted bank hash. Once a threshold commitment level is reached, accept the snapshot as valid and start voting. diff --git a/docs/src/proposals/tick-verification.md b/docs/src/proposals/tick-verification.md deleted file mode 100644 index a84c42c985d..00000000000 --- a/docs/src/proposals/tick-verification.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Tick Verification ---- - -This design the criteria and validation of ticks in a slot. It also describes -error handling and slashing conditions encompassing how the system handles -transmissions that do not meet these requirements. - -# Slot structure - -Each slot must contain an expected `ticks_per_slot` number of ticks. The last -shred in a slot must contain only the entirety of the last tick, and nothing -else. The leader must also mark this shred containing the last tick with the -`LAST_SHRED_IN_SLOT` flag. Between ticks, there must be `hashes_per_tick` -number of hashes. - -# Handling bad transmissions - -Malicious transmissions `T` are handled in two ways: - -1. If a leader can generate some erroneous transmission `T` and also some - alternate transmission `T'` for the same slot without violating any slashing - rules for duplicate transmissions (for instance if `T'` is a subset of `T`), - then the cluster must handle the possibility of both transmissions being live. - -Thus this means we cannot mark the erroneous transmission `T` as dead because -the cluster may have reached consensus on `T'`. These cases necessitate a -slashing proof to punish this bad behavior. - -2. Otherwise, we can simply mark the slot as dead and not playable. A slashing - proof may or may not be necessary depending on feasibility. - -# Blockstore receiving shreds - -When blockstore receives a new shred `s`, there are two cases: - -1. `s` is marked as `LAST_SHRED_IN_SLOT`, then check if there exists a shred - `s'` in blockstore for that slot where `s'.index > s.index` If so, together `s` - and `s'` constitute a slashing proof. - -2. Blockstore has already received a shred `s'` marked as `LAST_SHRED_IN_SLOT` - with index `i`. If `s.index > i`, then together `s` and `s'`constitute a - slashing proof. In this case, blockstore will also not insert `s`. - -3. Duplicate shreds for the same index are ignored. Non-duplicate shreds for - the same index are a slashable condition. Details for this case are covered - in the `Leader Duplicate Block Slashing` section. - -# Replaying and validating ticks - -1. Replay stage replays entries from blockstore, keeping track of the number of - ticks it has seen per slot, and verifying there are `hashes_per_tick` number of - hashes between ticks. After the tick from this last shred has been played, - replay stage then checks the total number of ticks. - -Failure scenario 1: If ever there are two consecutive ticks between which the -number of hashes is `!= hashes_per_tick`, mark this slot as dead. - -Failure scenario 2: If the number of ticks != `ticks_per_slot`, mark slot as -dead. - -Failure scenario 3: If the number of ticks reaches `ticks_per_slot`, but we still -haven't seen the `LAST_SHRED_IN_SLOT`, mark this slot as dead. - -2. When ReplayStage reaches a shred marked as the last shred, it checks if this - last shred is a tick. - -Failure scenario: If the signed shred with the `LAST_SHRED_IN_SLOT` flag cannot -be deserialized into a tick (either fails to deserialize or deserializes into -an entry), mark this slot as dead. diff --git a/docs/src/proposals/timely-vote-credits.md b/docs/src/proposals/timely-vote-credits.md deleted file mode 100644 index 39c496ff9df..00000000000 --- a/docs/src/proposals/timely-vote-credits.md +++ /dev/null @@ -1,190 +0,0 @@ ---- -title: Timely Vote Credits ---- - -## Timely Vote Credits - -This design describes a modification to the method that is used to calculate -vote credits earned by validator votes. - -Vote credits are the accounting method used to determine what percentage of -inflation rewards a validator earns on behalf of its stakers. Currently, when -a slot that a validator has previously voted on is "rooted", it earns 1 vote -credit. A "rooted" slot is one which has received full commitment by the -validator (i.e. has been finalized). - -One problem with this simple accounting method is that it awards one credit -regardless of how "old" the slot that was voted on at the time that it was -voted on. This means that a validator can delay its voting for many slots in -order to survey forks and wait to make votes that are less likely to be -expired, and without incurring any penalty for doing so. This is not just a -theoretical concern: there are known and documented instances of validators -using this technique to significantly delay their voting while earning more -credits as a result. - - -### Proposed Change - -The proposal is to award a variable number of vote credits per voted on slot, -with more credits being given for votes that have "less latency" than votes -that have "more latency". - -In this context, "latency" is the number of slots in between the slot that is -being voted on and the slot in which the vote has landed. Because a slot -cannot be voted on until after it has been completed, the minimum possible -latency is 1, which would occur when a validator voted as quickly as possible, -transmitting its vote on that slot in time for it to be included in the very -next slot. - -Credits awarded would become a function of this latency, with lower latencies -awarding more credits. This will discourage intentional "lagging", because -delaying a vote for any slots decreases the number of credits that vote will -earn, because it will necessarily land in a later slot if it is delayed, and -then earn a lower number of credits than it would have earned had it been -transmitted immediately and landed in an earlier slot. - -### Grace Period - -If landing a vote with 1 slot latency awarded more credit than landing that -same vote in 2 slots latency, then validators who could land votes -consistently within 1 slot would have a credits earning advantage over those -who could not. Part of the latency when transmitting votes is unavoidable as -it's a function of geographical distance between the sender and receiver of -the vote. The Solana network is spread around the world but it is not evenly -distributed over the whole planet; there are some locations which are, on -average, more distant from the network than others are. - -It would likely be harmful to the network to encourage tight geographical -concentration - if, for example, the only way to achieve 1 slot latency was to -be within a specific country - then a very strict credit rewards schedule -would encourage all validators to move to the same country in order to -maximize their credit earnings. - -For this reason, the credits reward schedule should have a built-in "grace -period" that gives all validators a "reasonable" amount of time to land their -votes. This will reduce the credits earning disadvantage that comes from -being more distant from the network. A balance needs to be struck between the -strictest rewards schedule, which most strongly discourages intentional -lagging, and more lenient rewards schedules, which improves credit earnings -for distant validators who are not artificially lagging. - -Historical voting data has been analyzed over many epochs and the data -suggests that the smallest grace period that allows for very minimal impact on -well behaved distant validators is 3 slots, which means that all slots voted -on within 3 slots will award maximum vote credits to the voting validator. -This gives validators nearly 2 seconds to land their votes without penalty. -The maximum latency between two points on Earth is about 100 ms, so allowing a -full 1,500 ms to 2,000 ms latency without penalty should not have adverse -impact on distant validators. - -### Maximum Vote Credits - -Another factor to consider is what the maximum vote credits to award for a -vote should be. Assuming linear reduction in vote credits awarded (where 1 -slot of additional lag reduces earned vote credits by 1), the maximum vote -credits value determines how much "penalty" there is for each additional slot -of latency. For example, a value of 10 would mean that after the grace period -slots, every additional slot of latency would result in a 10% reduction in -vote credits earned as each subsequent slot earns 1 credit less out of a -maximum possible 10 credits. - -Again, historical voting data was analyzed over many epochs and the conclusion -drawn was that a maximum credits of 10 is the largest value that can be used -and still have a noticeable effect on known laggers. Values higher than that -result in such a small penalty for each slot of lagging that intentional -lagging is still too profitable. Lower values are even more punishing to -intentional lagging; but an attempt has been made to conservatively choose the -highest value that produces noticeable results. - -The selection of these values is partially documented here: - -https://www.shinobi-systems.com/timely_voting_proposal - -The above document is somewhat out of date with more recent analysis, which -occurred in this github issue: - -https://github.com/solana-labs/solana/issues/19002 - -To summarize the findings of these documents: analysis over many epochs showed -that almost all validators from all regions have an average vote latency of 1 -slot or less. The validators with higher average latency are either known -laggers, or are not representative of their region since many other validators -in the same region achieve low latency voting. With a maximum vote credit of -10, there is almost no change in vote credits earned relative to the highest vote -earner by the majority of validators, aside from a general uplift of about 0.4%. -Additionally, data centers were analyzed to ensure that there aren't regions of -the world that would be adversely affected, and none were found. - - -### Method of Implementation - -When a Vote or VoteStateUpdate instruction is received by a validator, it will -use the Clock sysvar to identify the slot in which that instruction has -landed. For any newly voted on slot within that Vote or VoteStateUpdate -transaction, the validator will record the vote latency of that slot as -(voted_in_slot - voted_on_slot). - -These vote latencies will be stored a new vector of u8 latency values appended -to the end of the VoteState. VoteState currently has ~200 bytes of free space -at the end that is unused, so this new vector of u8 values should easily fit -within this available space. Because VoteState is an ABI frozen structure, -utilizing the mechanisms for updating frozen ABI will be required, which will -complicate the change. Furthermore, because VoteState is embedded in the -Tower data structure and it is frozen ABI as well, updates to the frozen ABI -mechanisms for Tower will be needed also. These are almost entirely -mechanical changes though, that involve ensuring that older versions of these -data structures can be updated to the new version as they are read in, and the -new version written out when the data structure is next persisted. - -The credits to award for a rooted slot will be calculated using the latency -value stored in latency vector for the slot, and a formula that awards -latencies of 1 - 3 slots ten credits, with a 1 credit reduction for each vote -latency after 3. Rooted slots will always be awarded a minimum credit of 1 -(never 0) so that very old votes, possibly necessary in times of network -stress, are not discouraged. - -To summarize the above: latency is recorded in a new Vector at the end of -VoteState when a vote first lands, but the credits for that slot are not -awarded until the slot becomes rooted, at which point the latency that was -recorded is used to compute the credits to award for that newly rooted slot. - -When a Vote instruction is processed, the changes are fairly easy to implement -as Vote can only add new slots to the end of Lockouts and pop existing slots -off of the back (which become rooted), so the logic merely has to compute -rewards for the new roots, and new latencies for the newly added slots, both -of which can be processed in the fairly simple existing logic for Vote -processing. - -When a VoteStateUpdate instruction is processed: - -1. For each slot that was in the previous VoteState but are not in the new -VoteState because they have been rooted in the transition from the old -VoteState to the new VoteState, credits to award are calculated based on the -latency that was recorded for them and still available in the old VoteState. - -2. For each slot that was in both the previous VoteState and the new -VoteState, the latency that was previously recorded for that slot is copied -from the old VoteState to the new VoteState. - -3. For each slot that is in the new VoteState but wasn't in the old VoteState, -the latency value is calculated for this new slot according to what slot the -vote is for and what slot is in the Clock (i.e. the slot this VoteStateUpdate -tx landed in) and this latency is stored in VoteState for that slot. - -The code to handle this is more complex, because VoteStateUpdate may include -removal of slots that expired as performed by the voting validator, in -addition to slots that have been rooted and new slots added. However, the -assumptions that are needed to handle VoteStateUpdate with timely vote credits -are already guaranteed by existing VoteStateUpdate correctness checking code: - -The existing VoteStateUpdate processing code already ensures that (1) only -roots slots that could actually have been rooted in the transition from the -old VoteState to the new VoteState, so there is no danger of over-counting -credits (i.e. imagine that a 'cheating' validator "pretended" that slots were -rooted by dropping them off of the back of the new VoteState before they have -actually achieved 32 confirmations; the existing logic prevents this). - -The existing VoteStateUpdate processing code already ensures that (2) new -slots included in the new VoteState are only slots after slots that have -already been voted on in the old VoteState (i.e. can't inject new slots in the -middle of slots already voted on). diff --git a/docs/src/proposals/validator-proposal.md b/docs/src/proposals/validator-proposal.md deleted file mode 100644 index 74bdfe3a066..00000000000 --- a/docs/src/proposals/validator-proposal.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Validator ---- - -## History - -When we first started Solana, the goal was to de-risk our TPS claims. We knew -that between optimistic concurrency control and sufficiently long leader slots, -that PoS consensus was not the biggest risk to TPS. It was GPU-based signature -verification, software pipelining and concurrent banking. Thus, the TPU was -born. After topping 100k TPS, we split the team into one group working toward -710k TPS and another to flesh out the validator pipeline. Hence, the TVU was -born. The current architecture is a consequence of incremental development with -that ordering and project priorities. It is not a reflection of what we ever -believed was the most technically elegant cross-section of those technologies. -In the context of leader rotation, the strong distinction between leading and -validating is blurred. - -## Difference between validating and leading - -The fundamental difference between the pipelines is when the PoH is present. In -a leader, we process transactions, removing bad ones, and then tag the result -with a PoH hash. In the validator, we verify that hash, peel it off, and -process the transactions in exactly the same way. The only difference is that -if a validator sees a bad transaction, it can't simply remove it like the -leader does, because that would cause the PoH hash to change. Instead, it -rejects the whole block. The other difference between the pipelines is what -happens _after_ banking. The leader broadcasts entries to downstream validators -whereas the validator will have already done that in RetransmitStage, which is -a confirmation time optimization. The validation pipeline, on the other hand, -has one last step. Any time it finishes processing a block, it needs to weigh -any forks it's observing, possibly cast a vote, and if so, reset its PoH hash -to the block hash it just voted on. - -## Proposed Design - -We unwrap the many abstraction layers and build a single pipeline that can -toggle leader mode on whenever the validator's ID shows up in the leader -schedule. - -![Validator block diagram](/img/validator-proposal.svg) - -## Notable changes - -- Hoist FetchStage and BroadcastStage out of TPU -- BankForks renamed to Banktree -- TPU moves to new socket-free crate called solana-tpu. -- TPU's BankingStage absorbs ReplayStage -- TVU goes away -- New RepairStage absorbs Shred Fetch Stage and repair requests -- JSON RPC Service is optional - used for debugging. It should instead be part - of a separate `solana-blockstreamer` executable. -- New MulticastStage absorbs retransmit part of RetransmitStage -- MulticastStage downstream of Blockstore diff --git a/docs/src/proposals/versioned-transactions.md b/docs/src/proposals/versioned-transactions.md deleted file mode 100644 index b51c772afbc..00000000000 --- a/docs/src/proposals/versioned-transactions.md +++ /dev/null @@ -1,338 +0,0 @@ -# Versioned Transactions - v0: Address Lookup Tables - -## Problem - -Messages transmitted to Solana validators must not exceed the IPv6 MTU size to -ensure fast and reliable network transmission of cluster info over UDP. -Solana's networking stack uses a conservative MTU size of 1280 bytes which, -after accounting for headers, leaves 1232 bytes for packet data like serialized -transactions. - -Developers building applications on Solana must design their on-chain program -interfaces within the above transaction size limit constraint. One common -work-around is to store state temporarily on-chain and consume that state in -later transactions. This is the approach used by the BPF loader program for -deploying Solana programs. - -However, this workaround doesn't work well when developers compose many on-chain -programs in a single atomic transaction. With more composition comes more -account inputs, each of which takes up 32 bytes. There is currently no available -workaround for increasing the number of accounts used in a single transaction -since each transaction must list all accounts that it needs to properly lock -accounts for parallel execution. Therefore the current cap is about 35 accounts -after accounting for signatures and other transaction metadata. - -## Proposed Solution - -1. Introduce a new program which manages on-chain address lookup tables -2. Add a new transaction format which can make use of on-chain - address lookup tables to efficiently load more accounts in a single transaction. - -### Address Lookup Table Program - -Here we describe a program-based solution to the problem, whereby a protocol -developer or end-user can create collections of related addresses on-chain for -concise use in a transaction's account inputs. - -After addresses are stored on-chain in an address lookup table account, they may be -succinctly referenced in a transaction using a 1-byte u8 index rather than a -full 32-byte address. This will require a new transaction format to make use of -these succinct references as well as runtime handling for looking up and loading -addresses from the on-chain lookup tables. - -#### State - -Address lookup tables must be rent-exempt when initialized and after -each time new addresses are appended. Lookup tables can either be extended -from an on-chain buffered list of addresses or directly by appending -addresses through instruction data. Newly appended addresses require -one slot to warmup before being available to transactions for lookups. - -Since transactions use a `u8` index to look up addresses, address tables can -store up to 256 addresses each. In addition to stored addresses, address table -accounts also tracks various metadata explained below. - -```rust -/// The maximum number of addresses that a lookup table can hold -pub const LOOKUP_TABLE_MAX_ADDRESSES: usize = 256; - -/// The serialized size of lookup table metadata -pub const LOOKUP_TABLE_META_SIZE: usize = 56; - -pub struct LookupTableMeta { - /// Lookup tables cannot be closed until the deactivation slot is - /// no longer "recent" (not accessible in the `SlotHashes` sysvar). - pub deactivation_slot: Slot, - /// The slot that the table was last extended. Address tables may - /// only be used to lookup addresses that were extended before - /// the current bank's slot. - pub last_extended_slot: Slot, - /// The start index where the table was last extended from during - /// the `last_extended_slot`. - pub last_extended_slot_start_index: u8, - /// Authority address which must sign for each modification. - pub authority: Option, - // Raw list of addresses follows this serialized structure in - // the account's data, starting from `LOOKUP_TABLE_META_SIZE`. -} -``` - -#### Cleanup - -Once an address lookup table is no longer needed, it can be deactivated and closed -to have its rent balance reclaimed. Address lookup tables may not be recreated -at the same address because each new lookup table must be initialized at an address -derived from a recent slot. - -Address lookup tables can be deactivated at any time but can continue to be used -by transactions until the deactivation slot is no longer present in the slot hashes -sysvar. This cool-down period ensures that in-flight transactions cannot be -censored and that address lookup tables cannot be closed and recreated for the same -slot. - -### Versioned Transactions - -In order to support address table lookups, the structure of serialized -transactions must be modified. The new transaction format should not -affect transaction processing in the Solana program runtime beyond the -increased capacity for accounts and program invocations. Invoked -programs will be unaware of which transaction format was used. - -The new transaction format must be distinguished from the current transaction -format. Current transactions can fit at most 19 signatures (64-bytes each) but -the message header encodes `num_required_signatures` as a `u8`. Since the upper -bit of the `u8` will never be set for a valid transaction, we can enable it to -denote whether a transaction should be decoded with the versioned format or not. - -The original transaction format will be referred to as the legacy transaction -version and the first versioned transaction format will start at version 0. - -#### New Transaction Format - -```rust -#[derive(Serialize, Deserialize)] -pub struct VersionedTransaction { - /// List of signatures - #[serde(with = "short_vec")] - pub signatures: Vec, - /// Message to sign. - pub message: VersionedMessage, -} - -// Uses custom serialization. If the first bit is set, the remaining bits -// in the first byte will encode a version number. If the first bit is not -// set, the first byte will be treated as the first byte of an encoded -// legacy message. -pub enum VersionedMessage { - Legacy(LegacyMessage), - V0(v0::Message), -} - -// The structure of the new v0 Message -#[derive(Serialize, Deserialize)] -pub struct Message { - // unchanged - pub header: MessageHeader, - - // unchanged - #[serde(with = "short_vec")] - pub account_keys: Vec, - - // unchanged - pub recent_blockhash: Hash, - - // unchanged - // - // # Notes - // - // Account and program indexes will index into the list of addresses - // constructed from the concatenation of three key lists: - // 1) message `account_keys` - // 2) ordered list of keys loaded from address table `writable_indexes` - // 3) ordered list of keys loaded from address table `readonly_indexes` - #[serde(with = "short_vec")] - pub instructions: Vec, - - /// List of address table lookups used to load additional accounts - /// for this transaction. - #[serde(with = "short_vec")] - pub address_table_lookups: Vec, -} - -/// Address table lookups describe an on-chain address lookup table to use -/// for loading more readonly and writable accounts in a single tx. -#[derive(Serialize, Deserialize)] -pub struct MessageAddressTableLookup { - /// Address lookup table account key - pub account_key: Pubkey, - /// List of indexes used to load writable account addresses - #[serde(with = "short_vec")] - pub writable_indexes: Vec, - /// List of indexes used to load readonly account addresses - #[serde(with = "short_vec")] - pub readonly_indexes: Vec, -} -``` - -#### Size changes - -- 1 extra byte for the `version` field -- 1 extra byte for the `address_table_lookups` length -- 34 extra bytes for each address table lookup which includes 32 bytes for the - address of the table and a byte each for the lengths of the `writable_indexes` - and `readonly_indexes` fields -- 1 extra byte for each lookup table index - -#### Cost - -Address lookups require extra computational overhead during transaction -processing, but also reduce network bandwidth due to smaller transactions and -therefore smaller blocks. - -#### Metadata changes - -Each resolved address from an address lookup table should be stored in -the transaction metadata for quick reference. This will avoid the need for -clients to make multiple RPC round trips to fetch all accounts loaded by a -v0 transaction. It will also make it easier to use the ledger tool to -analyze account access patterns. - -#### RPC changes - -Fetched transaction responses will likely require a new version field to -indicate to clients which transaction structure to use for deserialization. -Clients using pre-existing RPC methods will receive error responses when -attempting to fetch a versioned transaction which will indicate that they -must upgrade. - -The RPC API should also support an option for returning fully expanded -transactions to abstract away the address lookup table details from -downstream clients. - -### Limitations - -- Max of 256 unique accounts may be loaded by a transaction because `u8` - is used by compiled instructions to index into transaction message `account_keys`. -- Address lookup tables can hold up to 256 entries because lookup table indexes are also `u8`. -- Transaction signers may not be loaded through an address lookup table, the full - address of each signer must be serialized in the transaction. This ensures that - the performance of transaction signature checks is not affected. -- Hardware wallets will not be able to display details about accounts referenced - through address lookup tables due to inability to verify on-chain data. -- Only single level address lookup tables can be used. Recursive lookups will not be supported. - -## Security Concerns - -### Lookup table re-initialization - -If an address lookup table can be closed and re-initialized with new addresses, -any client which is unaware of the change could inadvertently lookup unexpected -addresses. To avoid this, all address lookup tables must be initialized at an -address derived from a recent slot and they cannot be closed until the slot -used for deactivation is no longer in the slot hashes sysvar. - -### Resource consumption - -Enabling more account inputs in a transaction allows for more program -invocations, write-locks, and data reads / writes. Before address tables are -enabled, transaction-wide compute limits and increased costs for write locks and -data reads are required. - -### Front running - -If the addresses listed within an address lookup table are mutable, front -running attacks could modify which addresses are resolved for a later -transaction. For this reason, address lookup tables are append-only and may -only be closed if it's no longer possible to create a new lookup table at the -same derived address. - -Additionally, a malicious actor could try to fork the chain immediately after a -new address lookup table account is added to a block. If successful, they could -add a different unexpected table entry in the fork. In order to deter this attack, -clients should wait for address lookup tables to be finalized before using them in a -transaction. Clients may also append integrity check instructions to the -transaction which verify that the correct accounts are looked up. - -### Denial of service - -Address lookup table accounts may be read very frequently and will therefore -be a more high profile target for denial of service attacks through write locks -similar to sysvar accounts. - -For this reason, special handling should be given to address lookup tables. -When an address lookup table is used to lookup addresses for a transaction, -it can be loaded without waiting for a read lock. To avoid race conditions, -only the addresses appended in previous blocks can be used for lookups and -deactivation requires a cool-down period. - -### Duplicate accounts - -Transactions may not load an account more than once whether directly through -`account_keys` or indirectly through `address_table_lookups`. - -## Other Proposals - -1. Account prefixes - -Needing to pre-register accounts in an on-chain address lookup table is cumbersome -because it adds an extra step for transaction processing. Instead, Solana -transactions could use variable length address prefixes to specify accounts. -These prefix shortcuts can save on data usage without needing to setup on-chain -state. - -However, this model requires nodes to keep a mapping of prefixes to active account -addresses. Attackers can create accounts with the same prefix as a popular account -to disrupt transactions. - -2. Transaction builder program - -Solana can provide a new on-chain program which allows "Big" transactions to be -constructed on-chain by normal transactions. Once the transaction is -constructed, a final "Execute" transaction can trigger a node to process the big -transaction as a normal transaction without needing to fit it into an MTU sized -packet. - -The UX of this approach is tricky. A user could in theory sign a big transaction -but it wouldn't be great if they had to use their wallet to sign multiple -transactions to build that transaction that they already signed and approved. This -could be a use-case for transaction relay services, though. A user could pay a -relayer to construct the large pre-signed transaction on-chain for them. - -In order to prevent the large transaction from being reconstructed and replayed, -its message hash will need to be added to the status cache when executed. - -3. Epoch account indexes - -Similarly to leader schedule calculation, validators could create a global index -of the most accessed accounts in the previous epoch and make that index -available to transactions in the following epoch. - -This approach has a downside of only updating the index at epoch boundaries -which means there would be a few day delay before popular new accounts could be -referenced. It also needs to be consistently generated by all validators by -using some criteria like adding accounts in order by access count. - -4. Address lists - -Extend the transaction structure to support addresses that, when loaded, expand -to a list of addresses. After expansion, all account inputs are concatenated to -form a single list of account keys which can be indexed into by instructions. -Address lists would likely need to be immutable to prevent attacks. They would -also need to be limited in length to limit resource consumption. - -This proposal can be thought of a special case of the proposed index account -approach. Since the full account list would be expanded, there's no need to add -additional offsets that use up the limited space in a serialized transaction. -However, the expected size of an address list may need to be encoded into the -transaction to aid the sanitization of account indexes. We would also need to -encode how many addresses in the list should be loaded as readonly vs -read-write. Lastly, special attention must be given to watch out for addresses -that exist in multiple account lists. - -5. Increase transaction size - -Significantly larger serialized transactions have an increased likelihood of being -dropped over the wire but this might not be a big issue since clients can retry -transactions anyways. The only time validators need to send individual transactions -over the network is when a leader forwards unprocessed transactions to the next -leader. diff --git a/docs/src/proposals/vote-signing-to-implement.md b/docs/src/proposals/vote-signing-to-implement.md deleted file mode 100644 index 52f32d6ab20..00000000000 --- a/docs/src/proposals/vote-signing-to-implement.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: Secure Vote Signing ---- - -## Secure Vote Signing - -This design describes additional vote signing behavior that will make the process more secure. - -Currently, Solana implements a vote-signing service that evaluates each vote to ensure it does not violate a slashing condition. The service could potentially have different variations, depending on the hardware platform capabilities. In particular, it could be used in conjunction with a secure enclave \(such as SGX\). The enclave could generate an asymmetric key, exposing an API for user \(untrusted\) code to sign the vote transactions, while keeping the vote-signing private key in its protected memory. - -The following sections outline how this architecture would work: - -### Message Flow - -1. The node initializes the enclave at startup - - - The enclave generates an asymmetric key and returns the public key to the - - node - - - The keypair is ephemeral. A new keypair is generated on node bootup. A - - new keypair might also be generated at runtime based on some to be determined - - criteria. - - - The enclave returns its attestation report to the node - -2. The node performs attestation of the enclave \(e.g using Intel's IAS APIs\) - - - The node ensures that the Secure Enclave is running on a TPM and is - - signed by a trusted party - -3. The stakeholder of the node grants ephemeral key permission to use its stake. - - This process is to be determined. - -4. The node's untrusted, non-enclave software calls trusted enclave software - - using its interface to sign transactions and other data. - - - In case of vote signing, the node needs to verify the PoH. The PoH - - verification is an integral part of signing. The enclave would be - - presented with some verifiable data to check before signing the vote. - - - The process of generating the verifiable data in untrusted space is to be determined - -### PoH Verification - -1. When the node votes on an en entry `X`, there's a lockout period `N`, for - - which it cannot vote on a fork that does not contain `X` in its history. - -2. Every time the node votes on the derivative of `X`, say `X+y`, the lockout - - period for `X` increases by a factor `F` \(i.e. the duration node cannot vote on - - a fork that does not contain `X` increases\). - - - The lockout period for `X+y` is still `N` until the node votes again. - -3. The lockout period increment is capped \(e.g. factor `F` applies maximum 32 - - times\). - -4. The signing enclave must not sign a vote that violates this policy. This - - means - - - Enclave is initialized with `N`, `F` and `Factor cap` - - Enclave stores `Factor cap` number of entry IDs on which the node had - - previously voted - - - The sign request contains the entry ID for the new vote - - Enclave verifies that new vote's entry ID is on the correct fork - - \(following the rules \#1 and \#2 above\) - -### Ancestor Verification - -This is alternate, albeit, less certain approach to verifying voting fork. 1. The validator maintains an active set of nodes in the cluster 2. It observes the votes from the active set in the last voting period 3. It stores the ancestor/last_tick at which each node voted 4. It sends new vote request to vote-signing service - -- It includes previous votes from nodes in the active set, and their - - corresponding ancestors - - 1. The signer checks if the previous votes contains a vote from the validator, - - and the vote ancestor matches with majority of the nodes - -- It signs the new vote if the check is successful -- It asserts \(raises an alarm of some sort\) if the check is unsuccessful - -The premise is that the validator can be spoofed at most once to vote on incorrect data. If someone hijacks the validator and submits a vote request for bogus data, that vote will not be included in the PoH \(as it'll be rejected by the cluster\). The next time the validator sends a request to sign the vote, the signing service will detect that validator's last vote is missing \(as part of - -## 5 above\). - -### Fork determination - -Due to the fact that the enclave cannot process PoH, it has no direct knowledge of fork history of a submitted validator vote. Each enclave should be initiated with the current _active set_ of public keys. A validator should submit its current vote along with the votes of the active set \(including itself\) that it observed in the slot of its previous vote. In this way, the enclave can surmise the votes accompanying the validator's previous vote and thus the fork being voted on. This is not possible for the validator's initial submitted vote, as it will not have a 'previous' slot to reference. To account for this, a short voting freeze should apply until the second vote is submitted containing the votes within the active set, along with it's own vote, at the height of the initial vote. - -### Enclave configuration - -A staking client should be configurable to prevent voting on inactive forks. This mechanism should use the client's known active set `N_active` along with a threshold vote `N_vote` and a threshold depth `N_depth` to determine whether or not to continue voting on a submitted fork. This configuration should take the form of a rule such that the client will only vote on a fork if it observes more than `N_vote` at `N_depth`. Practically, this represents the client from confirming that it has observed some probability of economic finality of the submitted fork at a depth where an additional vote would create a lockout for an undesirable amount of time if that fork turns out not to be live. - -### Challenges - -1. Generation of verifiable data in untrusted space for PoH verification in the - - enclave. - -2. Need infrastructure for granting stake to an ephemeral key. diff --git a/docs/src/runtime/programs.md b/docs/src/runtime/programs.md deleted file mode 100644 index f1a1280b9a8..00000000000 --- a/docs/src/runtime/programs.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: "Native Programs in the Solana Runtime" -pagination_label: Runtime Native Programs -sidebar_label: Native Programs ---- - -[Redirect](https://solana.com/docs/core/accounts) diff --git a/docs/src/runtime/sysvars.md b/docs/src/runtime/sysvars.md deleted file mode 100644 index 9f21203e3d8..00000000000 --- a/docs/src/runtime/sysvars.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: Solana Sysvar Cluster Data -pagination_label: Runtime Sysvar Cluster Data -sidebar_label: Sysvar Cluster Data ---- - -Solana exposes a variety of cluster state data to programs via -[`sysvar`](https://solana.com/docs/terminology#sysvar) accounts. These accounts -are populated at known addresses published along with the account layouts in the -[`solana-program` crate](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/sysvar/index.html), -and outlined below. - -There are two ways for a program to access a sysvar. - -The first is to query the sysvar at runtime via the sysvar's `get()` function: - -``` -let clock = Clock::get() -``` - -The following sysvars support `get`: - -- Clock -- EpochSchedule -- Fees -- Rent -- EpochRewards - -The second is to pass the sysvar to the program as an account by including its -address as one of the accounts in the `Instruction` and then deserializing the -data during execution. Access to sysvars accounts is always _readonly_. - -``` -let clock_sysvar_info = next_account_info(account_info_iter)?; -let clock = Clock::from_account_info(&clock_sysvar_info)?; -``` - -The first method incurs a base cost of 100 CU plus the size of the sysvar in -bytes (e.g., ~140 CU for Clock), but does not require passing the sysvar account -in the instruction. The second method avoids this syscall overhead and can be -more efficient for programs that read the same sysvar multiple times, since the -data is already in program memory — however, it requires including the sysvar -account in the transaction. - -## Clock - -The Clock sysvar contains data on cluster time, including the current slot, -epoch, and estimated wall-clock Unix timestamp. It is updated every slot. - -- Address: `SysvarC1ock11111111111111111111111111111111` -- Layout: - [Clock](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/clock/struct.Clock.html) -- Fields: - - - `slot`: the current slot - - `epoch_start_timestamp`: the Unix timestamp of the first slot in this epoch. - In the first slot of an epoch, this timestamp is identical to the - `unix_timestamp` (below). - - `epoch`: the current epoch - - `leader_schedule_epoch`: the most recent epoch for which the leader schedule - has already been generated - - `unix_timestamp`: the Unix timestamp of this slot. - - Each slot has an estimated duration based on Proof of History. But in reality, - slots may elapse faster and slower than this estimate. As a result, the Unix - timestamp of a slot is generated based on oracle input from voting validators. - This timestamp is calculated as the stake-weighted median of timestamp - estimates provided by votes, bounded by the expected time elapsed since the - start of the epoch. - - More explicitly: for each slot, the most recent vote timestamp provided by - each validator is used to generate a timestamp estimate for the current slot - (the elapsed slots since the vote timestamp are assumed to be - Bank::ns_per_slot). Each timestamp estimate is associated with the stake - delegated to that vote account to create a distribution of timestamps by - stake. The median timestamp is used as the `unix_timestamp`, unless the - elapsed time since the `epoch_start_timestamp` has deviated from the expected - elapsed time by more than 25%. - -## EpochSchedule - -The EpochSchedule sysvar contains epoch scheduling constants that are set in -genesis, and enables calculating the number of slots in a given epoch, the epoch -for a given slot, etc. (Note: the epoch schedule is distinct from the -[`leader schedule`](https://solana.com/docs/terminology#leader-schedule)) - -- Address: `SysvarEpochSchedu1e111111111111111111111111` -- Layout: - [EpochSchedule](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/epoch_schedule/struct.EpochSchedule.html) - -## Fees - -The Fees sysvar contains the fee calculator for the current slot. It is updated -every slot, based on the fee-rate governor. - -- Address: `SysvarFees111111111111111111111111111111111` -- Layout: - [Fees](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/sysvar/fees/struct.Fees.html) - -## Instructions - -The Instructions sysvar contains the serialized instructions in a Message while -that Message is being processed. This allows program instructions to reference -other instructions in the same transaction. Read more information on -[instruction introspection](implemented-proposals/instruction_introspection.md). - -- Address: `Sysvar1nstructions1111111111111111111111111` -- Layout: - [Instructions](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/sysvar/instructions/struct.Instructions.html) - -## RecentBlockhashes - -The RecentBlockhashes sysvar contains the active recent blockhashes as well as -their associated fee calculators. It is updated every slot. Entries are ordered -by descending block height, so the first entry holds the most recent block hash, -and the last entry holds an old block hash. - -- Address: `SysvarRecentB1ockHashes11111111111111111111` -- Layout: - [RecentBlockhashes](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/sysvar/recent_blockhashes/struct.RecentBlockhashes.html) - -## Rent - -The Rent sysvar contains the rental rate. Currently, the rate is static and set -in genesis. The Rent burn percentage is modified by manual feature activation. - -- Address: `SysvarRent111111111111111111111111111111111` -- Layout: - [Rent](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/rent/struct.Rent.html) - -## SlotHashes - -The SlotHashes sysvar contains the most recent hashes of the slot's parent -banks. It is updated every slot. - -- Address: `SysvarS1otHashes111111111111111111111111111` -- Layout: - [SlotHashes](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/slot_hashes/struct.SlotHashes.html) - -## SlotHistory - -The SlotHistory sysvar contains a bitvector of slots present over the last -epoch. It is updated every slot. - -- Address: `SysvarS1otHistory11111111111111111111111111` -- Layout: - [SlotHistory](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/slot_history/struct.SlotHistory.html) - -## StakeHistory - -The StakeHistory sysvar contains the history of cluster-wide stake activations -and de-activations per epoch. It is updated at the start of every epoch. - -- Address: `SysvarStakeHistory1111111111111111111111111` -- Layout: - [StakeHistory](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/stake_history/struct.StakeHistory.html) - -## EpochRewards - -The EpochRewards sysvar tracks whether the rewards period (including calculation -and distribution) is in progress, as well as the details needed to resume -distribution when starting from a snapshot during the rewards period. The sysvar -is repopulated at the start of the first block of each epoch. - - -- Address: `SysvarEpochRewards1111111111111111111111111` -- Layout: - [EpochRewards](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/epoch_rewards/struct.EpochRewards.html) -- Fields: - - - `distribution_starting_block_height` - starting block height for distribution for the current epoch - - `num_partitions` - the number of partitions in the distribution - - `parent_blockhash` - the blockhash seed used to generate the partition hasher, ie. the blockhash of the parent of the first block in the epoch - - `total_points` - the total rewards points calculated for the epoch - - `total_rewards` - total rewards for epoch, in lamports - - `distributed_rewards` - rewards for the epoch distributed so far, in lamports - - `active` - whether the rewards period is currently active - -## LastRestartSlot - -The LastRestartSlot sysvar contains the slot number of the last restart or _0_ -(zero) if none ever happened. - -- Address: `SysvarLastRestartS1ot1111111111111111111111` -- Layout: - [LastRestartSlot](https://docs.rs/solana-program/VERSION_FOR_DOCS_RS/solana_program/last_restart_slot/struct.LastRestartSlot.html) diff --git a/docs/src/runtime/zk-docs/ciphertext_ciphertext_equality.pdf b/docs/src/runtime/zk-docs/ciphertext_ciphertext_equality.pdf deleted file mode 100644 index 458be4b634f..00000000000 Binary files a/docs/src/runtime/zk-docs/ciphertext_ciphertext_equality.pdf and /dev/null differ diff --git a/docs/src/runtime/zk-docs/ciphertext_commitment_equality.pdf b/docs/src/runtime/zk-docs/ciphertext_commitment_equality.pdf deleted file mode 100644 index ac6036a3cff..00000000000 Binary files a/docs/src/runtime/zk-docs/ciphertext_commitment_equality.pdf and /dev/null differ diff --git a/docs/src/runtime/zk-docs/ciphertext_validity.pdf b/docs/src/runtime/zk-docs/ciphertext_validity.pdf deleted file mode 100644 index 9256f3bc438..00000000000 Binary files a/docs/src/runtime/zk-docs/ciphertext_validity.pdf and /dev/null differ diff --git a/docs/src/runtime/zk-docs/percentage_with_cap.pdf b/docs/src/runtime/zk-docs/percentage_with_cap.pdf deleted file mode 100644 index 78e2f6f54a1..00000000000 Binary files a/docs/src/runtime/zk-docs/percentage_with_cap.pdf and /dev/null differ diff --git a/docs/src/runtime/zk-docs/pubkey_proof.pdf b/docs/src/runtime/zk-docs/pubkey_proof.pdf deleted file mode 100644 index c60f5f69331..00000000000 Binary files a/docs/src/runtime/zk-docs/pubkey_proof.pdf and /dev/null differ diff --git a/docs/src/runtime/zk-docs/twisted_elgamal.pdf b/docs/src/runtime/zk-docs/twisted_elgamal.pdf deleted file mode 100644 index 6f577224cb6..00000000000 Binary files a/docs/src/runtime/zk-docs/twisted_elgamal.pdf and /dev/null differ diff --git a/docs/src/runtime/zk-docs/zero_proof.pdf b/docs/src/runtime/zk-docs/zero_proof.pdf deleted file mode 100644 index 2b414d777b0..00000000000 Binary files a/docs/src/runtime/zk-docs/zero_proof.pdf and /dev/null differ diff --git a/docs/src/runtime/zk-elgamal-proof.md b/docs/src/runtime/zk-elgamal-proof.md deleted file mode 100644 index c496779a8a9..00000000000 --- a/docs/src/runtime/zk-elgamal-proof.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -title: Solana ZK ElGamal Proof Program -pagination_label: Native ZK ElGamal Proof Program -sidebar_label: ZK ElGamal Proof Program ---- -The native Solana ZK ElGamal Proof program verifies a number of zero-knowledge -proofs that are tailored to work with Pedersen commitments and ElGamal -encryption over the elliptic curve -[curve25519](https://www.rfc-editor.org/rfc/rfc7748#section-4.1). The proof -verification instructions in the ZK ElGamal Proof program are flexibly designed -so that they can be combined to enable a number different applications. - -- Program id: see `solana_sdk_ids::zk_elgamal_proof_program::ID` -- Instructions: - [ProofInstruction](https://github.com/solana-program/zk-elgamal-proof/blob/main/zk-sdk/src/zk_elgamal_proof_program/instruction.rs) - -### Pedersen commitments and ElGamal encryption - -The ZK ElGamal Proof program verifies zero-knowledge proofs for Pedersen -commitments and ElGamal encryption, which are common cryptographic primitives -that are incorporated in many existing cryptographic protocols. - -ElGamal encryption is a popular instantiation of a public-key encryption scheme. -An ElGamal keypair consists of an ElGamal public key and an ElGamal secret key. -Messages can be encrypted under a public key to produce a ciphertext. A -ciphertext can then be decrypted using a corresponding ElGamal secret key. The -variant that is used in the proof program is the -[twisted ElGamal encryption](https://eprint.iacr.org/2019/319) over the elliptic -curve [curve25519](https://www.rfc-editor.org/rfc/rfc7748#section-4.1). - -The Pedersen commitment scheme is a popular instantiation of a cryptographic -commitment scheme. A commitment scheme allows a user to wrap a message into a -commitment with a purpose of revealing the committed message later on. Like a -ciphertext, the resulting commitment does not reveal any information about the -containing message. At the same time, the commitment is binding in that the user -cannot change the original value that is contained in a commitment. - -Interested readers can refer to the following resources for a more in-depth -treatment of Pedersen commitment and the (twisted) ElGamal encryption schemes. - -- [Notes](https://github.com/solana-labs/solana/blob/master/docs/src/runtime/zk-docs/twisted_elgamal.pdf) - on the twisted ElGamal encryption -- A technical - [overview](https://github.com/solana-program/token-2022/blob/main/zk-token-protocol-paper/part1.pdf) - of the SPL Token 2022 confidential extension -- Pretty Good Confidentiality [research paper](https://eprint.iacr.org/2019/319) - -The ZK ElGamal Proof program contains proof verification instructions on various -zero-knowledge proofs for working with the Pedersen commitment and ElGamal -encryption schemes. For example, the `VerifyBatchedRangeProofU64` instruction -verifies a zero-knowledge proof certifying that a Pedersen commitment contains -an unsigned 64-bit number as the message. The `VerifyPubkeyValidity` instruction -verifies a zero-knowledge proof certifying that an ElGamal public key is a -properly formed public key. - -### Context Data - -The proof data associated with each of the ZK ElGamal Proof instructions are -logically divided into two parts: - -- The context component contains the data that a zero-knowledge proof - is certifying. For example, context component for a - `VerifyBatchedRangeProofU64` instruction data is the Pedersen commitment that - holds an unsigned 64-bit number. The context component for a - `VerifyPubkeyValidity` instruction data is the ElGamal public key that is - properly formed. -- The proof component contains the actual mathematical pieces that - certify different properties of the context data. - -The ZK ElGamal Proof program processes a proof instruction in two steps: - -1. Verify the zero-knowledge proof data associated with the proof instruction. -2. If specified in the instruction, the program stores the context data in a - dedicated context state account. - -The simplest way to use a proof instruction is to execute it without producing a -context state account. In this case, the proof instruction can be included as -part of a larger Solana transaction that contains instructions of other Solana -programs. Programs should directly access the context data from the proof -instruction data and use it in its program logic. - -Alternatively, a proof instruction can be executed to produce a context state -account. In this case, the context data associated with a proof instruction -persists even after the transaction containing the proof instruction is finished -with its execution. The creation of context state accounts can be useful in -settings where ZK proofs are required from PDAs or when proof data is too large -to fit inside a single transaction. - -## Proof Instructions - -The ZK ElGamal Proof program supports the following list of zero-knowledge -proofs. - -#### Proofs on ElGamal encryption - -- `VerifyPubkeyValidity`: - - - The ElGamal public-key validity proof instruction certifies that an ElGamal - public-key is a properly formed public key. - - Mathematical description and proof of security: - [[Notes]](https://github.com/anza-xyz/agave/blob/master/docs/src/runtime/zk-docs/pubkey_proof.pdf) - -- `VerifyZeroCiphertext`: - - - The zero-ciphertext proof certifies that an ElGamal ciphertext encrypts the - number zero. - - Mathematical description and proof of security: - [[Notes]](https://github.com/anza-xyz/agave/blob/master/docs/src/runtime/zk-docs/zero_proof.pdf) - -#### Equality proofs - -- `VerifyCiphertextCommitmentEquality`: - - - The ciphertext-commitment equality proof certifies that an ElGamal - ciphertext and a Pedersen commitment encode the same message. - - Mathematical description and proof of security: - [[Notes]](https://github.com/anza-xyz/agave/blob/master/docs/src/runtime/zk-docs/ciphertext_commitment_equality.pdf) - -- `VerifyCiphertextCiphertextEquality`: - - - The ciphertext-ciphertext equality proof certifies that two ElGamal - ciphertexts encrypt the same message. - - Mathematical description and proof of security: - [[Notes]](https://github.com/anza-xyz/agave/blob/master/docs/src/runtime/zk-docs/ciphertext_ciphertext_equality.pdf) - -#### Range proofs - -- `VerifyBatchedRangeProofU64`: - - - Verifies that a Pedersen commitment contains an unsigned 64-bit value. - -- `VerifyBatchedRangeProofU128`: - - - Verifies that a Pedersen commitment contains an unsigned 128-bit value. - -- `VerifyBatchedRangeProofU256`: - - - Verifies that a Pedersen commitment contains an unsigned 256-bit value. - -#### Ciphertext Validity proofs - -- `VerifyGroupedCiphertext2HandlesValidity`: - - - Verifies that a grouped ElGamal ciphertext with 2 handles is well-formed. - - Mathematical description and proof of security: - [[Notes]](https://github.com/anza-xyz/agave/blob/master/docs/src/runtime/zk-docs/ciphertext_validity.pdf) - -- `VerifyBatchedGroupedCiphertext2HandlesValidity`: - - - Verifies, in batch, that grouped ElGamal ciphertexts with 2 handles are well-formed. - -- `VerifyGroupedCiphertext3HandlesValidity`: - - - Verifies that a grouped ElGamal ciphertext with 3 handles is well-formed. - - Mathematical description and proof of security: - [[Notes]](https://github.com/anza-xyz/agave/blob/master/docs/src/runtime/zk-docs/ciphertext_validity.pdf) - -- `VerifyBatchedGroupedCiphertext3HandlesValidity`: - - - Verifies, in batch, that grouped ElGamal ciphertexts with 3 handles are well-formed. - -#### VerifyPercentageWithCap - -- `VerifyPercentageWithCap`: - - - Verifies the percentage-with-cap relation used for fee calculations. - - Mathematical description and proof of security: - [[Notes]](https://github.com/anza-xyz/agave/blob/master/docs/src/runtime/zk-docs/percentage_with_cap.pdf) - -#### Administrative - -- `CloseContextState`: - - - Closes a proof context state account created by a previous verification and returns lamports to the destination account. diff --git a/docs/src/theme/Footer/index.js b/docs/src/theme/Footer/index.js deleted file mode 100644 index 73a4a8ae8d6..00000000000 --- a/docs/src/theme/Footer/index.js +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React from "react"; -import clsx from "clsx"; - -import Link from "@docusaurus/Link"; -import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import styles from "./styles.module.css"; - -function FooterLink({ to, href, label, prependBaseUrlToHref, ...props }) { - const toUrl = useBaseUrl(to); - const normalizedHref = useBaseUrl(href, { forcePrependBaseUrl: true }); - - return ( - - {label} - - ); -} - -const FooterLogo = ({ url, alt }) => ( - {alt} -); - -function Footer() { - const context = useDocusaurusContext(); - const { siteConfig = {} } = context; - const { themeConfig = {} } = siteConfig; - const { footer } = themeConfig; - - const { copyright, links = [], logo = {} } = footer || {}; - const logoUrl = useBaseUrl(logo.src); - - if (!footer) { - return null; - } - - return ( -
-
- {links && links.length > 0 && ( -
- {links.map((linkItem, i) => ( -
- {linkItem.title != null ? ( -

{linkItem.title}

- ) : null} - {linkItem.items != null && - Array.isArray(linkItem.items) && - linkItem.items.length > 0 ? ( -
    - {linkItem.items.map((item, key) => - item.html ? ( -
  • - ) : ( -
  • - -
  • - ) - )} -
- ) : null} -
- ))} -
- )} - {(logo || copyright) && ( -
- {logo && logo.src && ( -
- {logo.href ? ( - - - - ) : ( - - )} -
- )} - -
-
- )} -
-
- ); -} - -export default Footer; diff --git a/docs/src/theme/Footer/styles.module.css b/docs/src/theme/Footer/styles.module.css deleted file mode 100644 index 58c5842c88c..00000000000 --- a/docs/src/theme/Footer/styles.module.css +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -.footerLogoLink { - opacity: 0.5; - transition: opacity 0.15s ease-in-out; -} - -.footerLogoLink:hover { - opacity: 1; -} diff --git a/docs/src/validator/anatomy.md b/docs/src/validator/anatomy.md deleted file mode 100644 index 465b08d3fa9..00000000000 --- a/docs/src/validator/anatomy.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Anatomy of a Validator -sidebar_position: 1 -sidebar_label: Anatomy -pagination_label: Anatomy of a Validator ---- - -![Validator block diagrams](/img/validator.svg) - -## Pipelining - -The validators make extensive use of an optimization common in CPU design, called _pipelining_. Pipelining is the right tool for the job when there's a stream of input data that needs to be processed by a sequence of steps, and there's different hardware responsible for each. The quintessential example is using a washer and dryer to wash/dry/fold several loads of laundry. Washing must occur before drying and drying before folding, but each of the three operations is performed by a separate unit. To maximize efficiency, one creates a pipeline of _stages_. We'll call the washer one stage, the dryer another, and the folding process a third. To run the pipeline, one adds a second load of laundry to the washer just after the first load is added to the dryer. Likewise, the third load is added to the washer after the second is in the dryer and the first is being folded. In this way, one can make progress on three loads of laundry simultaneously. Given infinite loads, the pipeline will consistently complete a load at the rate of the slowest stage in the pipeline. - -## Pipelining in the Validator - -The validator contains two pipelined processes, one used in leader mode called the TPU and one used in validator mode called the TVU. In both cases, the hardware being pipelined is the same, the network input, the GPU cards, the CPU cores, writes to disk, and the network output. What it does with that hardware is different. The TPU exists to create ledger entries whereas the TVU exists to validate them. diff --git a/docs/src/validator/blockstore.md b/docs/src/validator/blockstore.md deleted file mode 100644 index 71d41d8a392..00000000000 --- a/docs/src/validator/blockstore.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: Blockstore in a Solana Validator -sidebar_position: 3 -sidebar_label: Blockstore -pagination_label: Validator Blockstore ---- - -After a block reaches finality, all blocks from that one on down to the genesis block form a linear chain with the familiar name blockchain. Until that point, however, the validator must maintain all potentially valid chains, called _forks_. The process by which forks naturally form as a result of leader rotation is described in [fork generation](../consensus/fork-generation.md). The _blockstore_ data structure described here is how a validator copes with those forks until blocks are finalized. - -The blockstore allows a validator to record every shred it observes on the network, in any order, as long as the shred is signed by the expected leader for a given slot. - -Shreds are moved to a fork-able key space the tuple of `leader slot` + `shred index` \(within the slot\). This permits the skip-list structure of the Solana protocol to be stored in its entirety, without a-priori choosing which fork to follow, which Entries to persist or when to persist them. - -Repair requests for recent shreds are served out of RAM or recent files and out of deeper storage for less recent shreds, as implemented by the store backing Blockstore. - -## Functionalities of Blockstore - -1. Persistence: the Blockstore lives in the front of the nodes verification - - pipeline, right behind network receive and signature verification. If the - - shred received is consistent with the leader schedule \(i.e. was signed by the - - leader for the indicated slot\), it is immediately stored. - -2. Repair: repair is the same as window repair above, but able to serve any - - shred that's been received. Blockstore stores shreds with signatures, - - preserving the chain of origination. - -3. Forks: Blockstore supports random access of shreds, so can support a - - validator's need to rollback and replay from a Bank checkpoint. - -4. Restart: with proper pruning/culling, the Blockstore can be replayed by - - ordered enumeration of entries from slot 0. The logic of the replay stage - - \(i.e. dealing with forks\) will have to be used for the most recent entries in - - the Blockstore. - -## Blockstore Design - -1. Entries in the Blockstore are stored as key-value pairs, where the key is the concatenated slot index and shred index for an entry, and the value is the entry data. Note shred indexes are zero-based for each slot \(i.e. they're slot-relative\). -2. The Blockstore maintains metadata for each slot, in the `SlotMeta` struct containing: - - - `slot_index` - The index of this slot - - `num_blocks` - The number of blocks in the slot \(used for chaining to a previous slot\) - - `consumed` - The highest shred index `n`, such that for all `m < n`, there exists a shred in this slot with shred index equal to `n` \(i.e. the highest consecutive shred index\). - - `received` - The highest received shred index for the slot - - `next_slots` - A list of future slots this slot could chain to. Used when rebuilding - - the ledger to find possible fork points. - - - `last_index` - The index of the shred that is flagged as the last shred for this slot. This flag on a shred will be set by the leader for a slot when they are transmitting the last shred for a slot. - - `is_connected` - True iff every block from 0...slot forms a full sequence without any holes. We can derive is_connected for each slot with the following rules. Let slot\(n\) be the slot with index `n`, and slot\(n\).is_full\(\) is true if the slot with index `n` has all the ticks expected for that slot. Let is_connected\(n\) be the statement that "the slot\(n\).is_connected is true". Then: - - is_connected\(0\) is_connected\(n+1\) iff \(is_connected\(n\) and slot\(n\).is_full\(\) - -3. Chaining - When a shred for a new slot `x` arrives, we check the number of blocks \(`num_blocks`\) for that new slot \(this information is encoded in the shred\). We then know that this new slot chains to slot `x - num_blocks`. -4. Subscriptions - The Blockstore records a set of slots that have been "subscribed" to. This means entries that chain to these slots will be sent on the Blockstore channel for consumption by the ReplayStage. See the `Blockstore APIs` for details. -5. Update notifications - The Blockstore notifies listeners when slot\(n\).is_connected is flipped from false to true for any `n`. - -## Blockstore APIs - -The Blockstore offers a subscription based API that ReplayStage uses to ask for entries it's interested in. These subscription API's are as follows: - -1. `fn get_slots_since(slots: &[u64]) -> Result>>`: Returns slots that are connected to any of the elements of `slots`. This method enables the discovery of new children slots. - -2. `fn get_slot_entries(slot: Slot, shred_start_index: u64) -> Result>`: For the specified `slot`, return a vector of the available, contiguous entries starting from `shred_start_index`. Shreds are fragments of serialized entries so the conversion from entry index to shred index is not one-to-one. However, there is a similar function `get_slot_entries_with_shred_info()` that returns the number of shreds that comprise the returned entry vector. This allows a caller to track progress through the slot. - -Note: Cumulatively, this means that the replay stage will now have to know when a slot is finished, and subscribe to the next slot it's interested in to get the next set of entries. Previously, the burden of chaining slots fell on the Blockstore. - -## Interfacing with Bank - -The bank exposes to replay stage: - -1. `prev_hash`: which PoH chain it's working on as indicated by the hash of the last entry it processed - -2. `tick_height`: the ticks in the PoH chain currently being verified by this bank - -3. `votes`: a stack of records that contains: - * `prev_hashes`: what anything after this vote must chain to in PoH - * `tick_height`: the tick height at which this vote was cast - * `lockout period`: how long a chain must be observed to be in the ledger to be able to be chained below this vote - -Replay stage uses Blockstore APIs to find the longest chain of entries it can hang off a previous vote. If that chain of entries does not hang off the latest vote, the replay stage rolls back the bank to that vote and replays the chain from there. - -## Pruning Blockstore - -Once Blockstore entries are old enough, representing all the possible forks becomes less useful, perhaps even problematic for replay upon restart. Once a validator's votes have reached max lockout, however, any Blockstore contents that are not on the PoH chain for that vote for can be pruned, expunged. diff --git a/docs/src/validator/geyser.md b/docs/src/validator/geyser.md deleted file mode 100644 index 2abe44e7e72..00000000000 --- a/docs/src/validator/geyser.md +++ /dev/null @@ -1,537 +0,0 @@ ---- -title: Solana Validator Geyser Plugins -sidebar_label: Geyser Plugins -pagination_label: Validator Geyser Plugins ---- - -## Overview - -Validators under heavy RPC loads, such as when serving getProgramAccounts calls, -can fall behind the network. To solve this problem, the validator has been -enhanced to support a plugin mechanism, called a "Geyser" plugin, through which -the information about accounts, slots, blocks, and transactions can be -transmitted to external data stores such as relational databases, NoSQL -databases or Kafka. RPC services then can be developed to consume data from -these external data stores with the possibility of more flexible and targeted -optimizations such as caching and indexing. This allows the validator to focus -on processing transactions without being slowed down by busy RPC requests. - -This document describes the interfaces of the plugin and the referential plugin -implementation for the PostgreSQL database. - -[crates.io]: https://crates.io/search?q=solana- -[docs.rs]: https://docs.rs/releases/search?query=solana- - -### Important Crates: - -- [`agave-geyser-plugin-interface`] — This crate defines the plugin -interfaces. - -- [`solana-accountsdb-plugin-postgres`] — The crate for the referential -plugin implementation for the PostgreSQL database. - -[`agave-geyser-plugin-interface`]: https://docs.rs/agave-geyser-plugin-interface -[`solana-accountsdb-plugin-postgres`]: https://docs.rs/solana-accountsdb-plugin-postgres -[`solana-sdk`]: https://docs.rs/solana-sdk -[`solana-transaction-status`]: https://docs.rs/solana-transaction-status - -## The Plugin Interface - -The Plugin interface is declared in [`agave-geyser-plugin-interface`]. It -is defined by the trait `GeyserPlugin`. The plugin should implement the -trait and expose a "C" function `_create_plugin` to return the pointer to this -trait. For example, in the referential implementation, the following code -instantiates the PostgreSQL plugin `GeyserPluginPostgres ` and returns its -pointer. - -``` -#[no_mangle] -#[allow(improper_ctypes_definitions)] -/// # Safety -/// -/// This function returns the GeyserPluginPostgres pointer as trait GeyserPlugin. -pub unsafe extern "C" fn _create_plugin() -> *mut dyn GeyserPlugin { - let plugin = GeyserPluginPostgres::new(); - let plugin: Box = Box::new(plugin); - Box::into_raw(plugin) -} -``` - -A plugin implementation can implement the `on_load` method to initialize itself. -This function is invoked after a plugin is dynamically loaded into the validator -when it starts. The configuration of the plugin is controlled by a configuration -file in JSON5 format. The JSON5 file must have a field `libpath` that points -to the full path name of the shared library implementing the plugin, and may -have other configuration information, like connection parameters for the external -database. The plugin configuration file is specified by the validator's CLI -parameter `--geyser-plugin-config` and the file must be readable to the -validator process. - -Please see the [config file](#config) for the referential -PostgreSQL plugin below for an example. - -The plugin can implement the `on_unload` method to do any cleanup before the -plugin is unloaded when the validator is gracefully shutdown. - -The plugin framework supports streaming either accounts, transactions or both. -A plugin uses the following function to indicate if it is interested in receiving -account data: - -``` -fn account_data_notifications_enabled(&self) -> bool -``` - -And it uses the following function to indicate if it is interested in receiving -transaction data: - -``` - fn transaction_notifications_enabled(&self) -> bool -``` - -The following method is used for notifying on an account update: - -``` - fn update_account( - &self, - account: ReplicaAccountInfoVersions, - slot: Slot, - is_startup: bool, - ) -> Result<()> -``` - -The `ReplicaAccountInfoVersions` struct contains the metadata and data of the account -streamed. The `slot` points to the slot the account is being updated at. When -`is_startup` is true, it indicates the account is loaded from snapshots when -the validator starts up. When `is_startup` is false, the account is updated -when processing a transaction. - - -The following method is called when all accounts have been notified when the -validator restores the AccountsDb from snapshots at startup. - -``` -fn notify_end_of_startup(&self) -> Result<()> -``` - -When `update_account` is called during processing transactions, the plugin -should process the notification as fast as possible because any delay may -cause the validator to fall behind the network. Persistence to external data -store is best to be done asynchronously. - -The following method is used for notifying slot status changes: - -``` - fn update_slot_status( - &self, - slot: Slot, - parent: Option, - status: &SlotStatus, - ) -> Result<()> -``` - -To ensure data consistency, the plugin implementation can choose to abort -the validator in case of error persisting to external stores. When the -validator restarts the account data will be re-transmitted. - -The following method is used for notifying transactions: - -``` - fn notify_transaction( - &self, - transaction: ReplicaTransactionInfoVersions, - slot: Slot, - ) -> Result<()> -``` - -The `ReplicaTransactionInfoVersions` struct -contains the information about a streamed transaction. It wraps `ReplicaTransactionInfo` - -``` -pub struct ReplicaTransactionInfo<'a> { - /// The first signature of the transaction, used for identifying the transaction. - pub signature: &'a Signature, - - /// Indicates if the transaction is a simple vote transaction. - pub is_vote: bool, - - /// The sanitized transaction. - pub transaction: &'a SanitizedTransaction, - - /// Metadata of the transaction status. - pub transaction_status_meta: &'a TransactionStatusMeta, -} -``` -For details of `SanitizedTransaction` and `TransactionStatusMeta `, -please refer to [`solana-sdk`] and [`solana-transaction-status`] - -The `slot` points to the slot the transaction is executed at. -For more details, please refer to the Rust documentation in -[`agave-geyser-plugin-interface`]. - -# Timing Relationships of Various Plugin Callbacks. - -Account update via update_account: As mentioned previously when is_startup is -false, the account is updated during transaction processing. The account update -has information about the transaction causing the update in the `txn` field. -Note, when account update is sent during start up, the txn field is None as -there is no transaction. - -``` -pub struct ReplicaAccountInfoV3<'a> { - /// The Pubkey for the account - pub pubkey: &'a [u8], - - /// The lamports for the account - pub lamports: u64, - - /// The Pubkey of the owner program account - pub owner: &'a [u8], - - /// This account's data contains a loaded program (and is now read-only) - pub executable: bool, - - /// The epoch at which this account will next owe rent - pub rent_epoch: u64, - - /// The data held in this account. - pub data: &'a [u8], - - /// A global monotonically increasing atomic number, which can be used - /// to tell the order of the account update. For example, when an - /// account is updated in the same slot multiple times, the update - /// with higher write_version should supersede the one with lower - /// write_version. - pub write_version: u64, - - /// Reference to transaction causing this account modification - pub txn: Option<&'a SanitizedTransaction>, -} -``` - -The updates are sent serially for different accounts via update_slot_status -in the transaction for a slot. After the accounts notifications are sent, the -SlotStatus::Processed event is sent. - -Starting with Agave 3.0, transaction notifications are sent before -SlotStatus::Processed. In prior Agave version, even though SlotStatus::Processed -is sent logically after the transaction events, because there are intermediate -threads emitting the notitications to the plugin, the plugin can see the -transaction notifications and the SlotStatus::Processed for a slot in either -order. - -Within a block, transactions are ordered with transaction index. Transactions -within a block are processed and notified in parallel. A plugin should use the -transaction index to determine their relative order. - -A plugin can use the notify_block_metadata to know the -executed_transaction_count for a given slot in the following structure: - -``` -/// Extending ReplicaBlockInfo by sending RewardsAndNumPartitions. -#[derive(Clone, Debug)] -#[repr(C)] -pub struct ReplicaBlockInfoV4<'a> { - pub parent_slot: Slot, - pub parent_blockhash: &'a str, - pub slot: Slot, - pub blockhash: &'a str, - pub rewards: &'a RewardsAndNumPartitions, - pub block_time: Option, - pub block_height: Option, - pub executed_transaction_count: u64, - pub entry_count: u64, -} -``` - -The plugin can associate accounts with transactions via the txn field in the -ReplicaAccountInfoV3 structure. It can also use ReplicaTransactionInfoV3 (via the -V0_0_3 variant of ReplicaTransactionInfoVersions) in the notify_transaction -callback to get the account addresses. - -The SlotStatus::Confirmed and SlotStatus::Processed events can reach the plugin -in any order as they are sent asynchronous to each other. A plugin should wait -for both events to confirm they are processed and confirmed. - -The SlotStatus::Rooted is sent after SlotStatus::Processed. - -## Example PostgreSQL Plugin - -The [`solana-accountsdb-plugin-postgres`] repository implements a plugin storing -account data to a PostgreSQL database to illustrate how a plugin can be -developed. - - -### Configuration File Format - - -The plugin is configured using the input configuration file. An example -configuration file looks like the following: - - -``` -{ - "libpath": "/solana/target/release/libsolana_geyser_plugin_postgres.so", - "host": "postgres-server", - "user": "solana", - "port": 5433, - "threads": 20, - "batch_size": 20, - "panic_on_db_errors": true, - "accounts_selector" : { - "accounts" : ["*"] - } -} -``` - -The `host`, `user`, and `port` control the PostgreSQL configuration -information. For more advanced connection options, please use the -`connection_str` field. Please see [Rust postgres configuration] -(https://docs.rs/postgres/0.19.2/postgres/config/struct.Config.html). - -To improve the throughput to the database, the plugin supports connection pooling -using multiple threads, each maintaining a connection to the PostgreSQL database. -The count of the threads is controlled by the `threads` field. A higher thread -count usually offers better performance. - -To further improve performance when saving large numbers of accounts at -startup, the plugin uses bulk inserts. The batch size is controlled by the -`batch_size` parameter. This can help reduce the round trips to the database. - -The `panic_on_db_errors` can be used to panic the validator in case of database -errors to ensure data consistency. - -### Account Selection - -The `accounts_selector` can be used to filter the accounts that should be persisted. - -For example, one can use the following to persist only the accounts with particular -Base58-encoded Pubkeys, - -``` - "accounts_selector" : { - "accounts" : ["pubkey-1", "pubkey-2", ..., "pubkey-n"], - } -``` - -Or use the following to select accounts with certain program owners: - -``` - "accounts_selector" : { - "owners" : ["pubkey-owner-1", "pubkey-owner-2", ..., "pubkey-owner-m"], - } -``` - -To select all accounts, use the wildcard character (*): - -``` - "accounts_selector" : { - "accounts" : ["*"], - } -``` - -### Transaction Selection - -`transaction_selector`, controls if and what transactions to store. -If this field is missing, none of the transactions are stored. - -For example, one can use the following to select only the transactions -referencing accounts with particular Base58-encoded Pubkeys, - -``` -"transaction_selector" : { - "mentions" : \["pubkey-1", "pubkey-2", ..., "pubkey-n"\], -} -``` - -The `mentions` field supports wildcards to select all transaction or -all 'vote' transactions. For example, to select all transactions: - -``` -"transaction_selector" : { - "mentions" : \["*"\], -} -``` - -To select all vote transactions: - -``` -"transaction_selector" : { - "mentions" : \["all_votes"\], -} -``` - -### Database Setup - -#### Install PostgreSQL Server - -Please follow [PostgreSQL Ubuntu Installation](https://www.postgresql.org/download/linux/ubuntu/) -on instructions to install the PostgreSQL database server. For example, to -install postgresql-14, - -``` -sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' -wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - -sudo apt-get update -sudo apt-get -y install postgresql-14 -``` -#### Control the Database Access - -Modify the pg_hba.conf as necessary to grant the plugin to access the database. -For example, in /etc/postgresql/14/main/pg_hba.conf, the following entry allows -nodes with IPs in the CIDR 10.138.0.0/24 to access all databases. The validator -runs in a node with an ip in the specified range. - -``` -host all all 10.138.0.0/24 trust -``` - -It is recommended to run the database server on a separate node from the validator for -better performance. - -#### Configure the Database Performance Parameters - -Please refer to the [PostgreSQL Server Configuration](https://www.postgresql.org/docs/14/runtime-config.html) -for configuration details. The referential implementation uses the following -configurations for better database performance in the /etc/postgresql/14/main/postgresql.conf -which are different from the default postgresql-14 installation. - -``` -max_connections = 200 # (change requires restart) -shared_buffers = 1GB # min 128kB -effective_io_concurrency = 1000 # 1-1000; 0 disables prefetching -wal_level = minimal # minimal, replica, or logical -fsync = off # flush data to disk for crash safety -synchronous_commit = off # synchronization level; -full_page_writes = off # recover from partial page writes -max_wal_senders = 0 # max number of walsender processes -``` - -The sample [postgresql.conf](https://github.com/solana-labs/solana/blob/7ac43b16d2c766df61ae0a06d7aaf14ba61996ac/accountsdb-plugin-postgres/scripts/postgresql.conf) -can be used for reference. - -#### Create the Database Instance and the Role - -Start the server: - -``` -sudo systemctl start postgresql@14-main -``` - -Create the database. For example, the following creates a database named 'solana': - -``` -sudo -u postgres createdb solana -p 5433 -``` - -Create the database user. For example, the following creates a regular user named 'solana': - -``` -sudo -u postgres createuser -p 5433 solana -``` - -Verify the database is working using psql. For example, assuming the node running -PostgreSQL has the ip 10.138.0.9, the following command will land in a shell where -SQL commands can be entered: - -``` -psql -U solana -p 5433 -h 10.138.0.9 -w -d solana -``` - -#### Create the Schema Objects - -Use the [create_schema.sql](https://github.com/solana-labs/solana/blob/a70eb098f4ae9cd359c1e40bbb7752b3dd61de8d/accountsdb-plugin-postgres/scripts/create_schema.sql) -to create the objects for storing accounts and slots. - -Download the script from github: - -``` -wget https://raw.githubusercontent.com/solana-labs/solana/a70eb098f4ae9cd359c1e40bbb7752b3dd61de8d/accountsdb-plugin-postgres/scripts/create_schema.sql -``` - -Then run the script: - -``` -psql -U solana -p 5433 -h 10.138.0.9 -w -d solana -f create_schema.sql -``` - -After this, start the validator with the plugin by using the `--geyser-plugin-config` -argument mentioned above. - -#### Destroy the Schema Objects - -To destroy the database objects, created by `create_schema.sql`, use -[drop_schema.sql](https://github.com/solana-labs/solana/blob/a70eb098f4ae9cd359c1e40bbb7752b3dd61de8d/accountsdb-plugin-postgres/scripts/drop_schema.sql). -For example, - -``` -psql -U solana -p 5433 -h 10.138.0.9 -w -d solana -f drop_schema.sql -``` - -### Capture Historical Account Data - -To capture account historical data, in the configuration file, turn -`store_account_historical_data` to true. - -And ensure the database trigger is created to save data in the `audit_table` when -records in `account` are updated, as shown in `create_schema.sql`, - -``` -CREATE FUNCTION audit_account_update() RETURNS trigger AS $audit_account_update$ - BEGIN - INSERT INTO account_audit (pubkey, owner, lamports, slot, executable, rent_epoch, data, write_version, updated_on) - VALUES (OLD.pubkey, OLD.owner, OLD.lamports, OLD.slot, - OLD.executable, OLD.rent_epoch, OLD.data, OLD.write_version, OLD.updated_on); - RETURN NEW; - END; - -$audit_account_update$ LANGUAGE plpgsql; - -CREATE TRIGGER account_update_trigger AFTER UPDATE OR DELETE ON account - FOR EACH ROW EXECUTE PROCEDURE audit_account_update(); -``` - -The trigger can be dropped to disable this feature, for example, - -``` -DROP TRIGGER account_update_trigger ON account; -``` - -Over time, the account_audit can accumulate large amount of data. You may choose to -limit that by deleting older historical data. - -For example, the following SQL statement can be used to keep up to 1000 of the most -recent records for an account: - -``` -delete from account_audit a2 where (pubkey, write_version) in - (select pubkey, write_version from - (select a.pubkey, a.updated_on, a.slot, a.write_version, a.lamports, - rank() OVER ( partition by pubkey order by write_version desc) as rnk - from account_audit a) ranked - where ranked.rnk > 1000) -``` - -### Main Tables - -The following are the tables in the Postgres database - -| Table | Description | -|:--------------|:------------------------| -| account | Account data | -| slot | Slot metadata | -| transaction | Transaction data | -| account_audit | Account historical data | - - -### Performance Considerations - -When a validator lacks sufficient compute power, the overhead of saving the -account data can cause it to fall behind the network especially when all -accounts or a large number of accounts are selected. The node hosting the -PostgreSQL database needs to be powerful enough to handle the database loads -as well. It has been found using GCP n2-standard-64 machine type for the -validator and n2-highmem-32 for the PostgreSQL node is adequate for handling -transmitting all accounts while keeping up with the network. In addition, it is -best to keep the validator and the PostgreSQL in the same local network to -reduce latency. You may need to size the validator and database nodes -differently if serving other loads. diff --git a/docs/src/validator/gossip.md b/docs/src/validator/gossip.md deleted file mode 100644 index f0a2e43f511..00000000000 --- a/docs/src/validator/gossip.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Gossip Service in a Solana Validator -sidebar_position: 5 -sidebar_label: Gossip Service -pagination_label: Validator Gossip Service ---- - -The Gossip Service acts as a gateway to nodes in the -[control plane](https://solana.com/docs/terminology#control-plane). Validators -use the service to ensure information is available to all other nodes in a -cluster. The service broadcasts information using a -[gossip protocol](https://en.wikipedia.org/wiki/Gossip_protocol). - -## Gossip Overview - -Nodes continuously share signed data objects among themselves in order to manage -a cluster. For example, they share their contact information, ledger height, and -votes. - -Every tenth of a second, each node sends a "push" message and/or a "pull" -message. Push and pull messages may elicit responses, and push messages may be -forwarded on to others in the cluster. - -Gossip runs on a well-known UDP/IP port or a port in a well-known range. Once a -cluster is bootstrapped, nodes advertise to each other where to find their -gossip endpoint (a socket address). - -## Gossip Records - -Records shared over gossip are arbitrary, but signed and versioned (with a -timestamp) as needed to make sense to the node receiving them. If a node -receives two records from the same source, it updates its own copy with the -record with the most recent timestamp. - -## Gossip Service Interface - -### Push Message - -A node sends a push message to tell the cluster it has information to share. -Nodes send push messages to `PUSH_FANOUT` push peers. - -Upon receiving a push message, a node examines the message for: - -1. Duplication: if the message has been seen before, the node drops the message - and may respond with `PushMessagePrune` if forwarded from a low staked node - -2. New data: if the message is new to the node - - - Stores the new information with an updated version in its cluster info and - purges any previous older value - - - Stores the message in `pushed_once` (used for detecting duplicates, purged - after `PUSH_MSG_TIMEOUT * 5` ms) - - - Retransmits the messages to its own push peers - -3. Expiration: nodes drop push messages that are older than `PUSH_MSG_TIMEOUT` - -### Push Peers, Prune Message - -A node selects its push peers at random from the active set of known peers. The -node keeps this selection for a relatively long time. When a prune message is -received, the node drops the push peer that sent the prune. Prune is an -indication that there is another, higher stake weighted path to that node than -direct push. - -The set of push peers is kept fresh by rotating a new node into the set every -`PUSH_MSG_TIMEOUT/2` milliseconds. - -### Pull Message - -A node sends a pull message to ask the cluster if there is any new information. -A pull message is sent to a single peer at random and comprises a Bloom filter -that represents things it already has. A node receiving a pull message iterates -over its values and constructs a pull response of things that miss the filter -and would fit in a message. - -A node constructs the pull Bloom filter by iterating over current values and -recently purged values. - -A node handles items in a pull response the same way it handles new data in a -push message. - -## Purging - -Nodes retain prior versions of values (those updated by a pull or push) and -expired values (those older than `GOSSIP_PULL_CRDS_TIMEOUT_MS`) in -`purged_values` (things I recently had). Nodes purge `purged_values` that are -older than `5 * GOSSIP_PULL_CRDS_TIMEOUT_MS`. - -## Eclipse Attacks - -An eclipse attack is an attempt to take over the set of node connections with -adversarial endpoints. - -This is relevant to our implementation in the following ways. - -- Pull messages select a random node from the network. An eclipse attack on - _pull_ would require an attacker to influence the random selection in such a - way that only adversarial nodes are selected for pull. -- Push messages maintain an active set of nodes and select a random fanout for - every push message. An eclipse attack on _push_ would influence the active set - selection, or the random fanout selection. - -### Time and Stake based weights - -Weights are calculated based on `time since last picked` and the `natural log` -of the `stake weight`. - -Taking the `ln` of the stake weight allows giving all nodes a fairer chance of -network coverage in a reasonable amount of time. It helps normalize the large -possible `stake weight` differences between nodes. This way a node with low -`stake weight`, compared to a node with large `stake weight` will only have to -wait a few multiples of ln(`stake`) seconds before it gets picked. - -There is no way for an adversary to influence these parameters. - -### Pull Message - -A node is selected as a pull target based on the weights described above. - -### Push Message - -A prune message can only remove an adversary from a potential connection. - -Just like _pull message_, nodes are selected into the active set based on -weights. - -## Notable differences from PlumTree - -The active push protocol described here is based on -[Plum Tree](https://haslab.uminho.pt/sites/default/files/jop/files/lpr07a.pdf). -The main differences are: - -- Push messages have a wallclock that is signed by the originator. Once the - wallclock expires the message is dropped. A hop limit is difficult to - implement in an adversarial setting. -- Lazy Push is not implemented because it's not obvious how to prevent an - adversary from forging the message fingerprint. A naive approach would allow - an adversary to be prioritized for pull based on their input. diff --git a/docs/src/validator/runtime.md b/docs/src/validator/runtime.md deleted file mode 100644 index dcb49425591..00000000000 --- a/docs/src/validator/runtime.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: Solana Runtime on a Solana Validator -sidebar_position: 6 -sidebar_label: Runtime -pagination_label: Validator Runtime ---- - -The runtime is a concurrent transaction processor. Transactions specify their data dependencies upfront and dynamic memory allocation is explicit. By separating program code from the state it operates on, the runtime is able to choreograph concurrent access. Transactions accessing only read-only accounts are executed in parallel whereas transactions accessing writable accounts are serialized. The runtime interacts with the program through an entrypoint with a well-defined interface. The data stored in an account is an opaque type, an array of bytes. The program has full control over its contents. - -The transaction structure specifies a list of public keys and signatures for those keys and a sequential list of instructions that will operate over the states associated with the account keys. For the transaction to be committed all the instructions must execute successfully; if any abort the whole transaction fails to commit. - -#### Account Structure - -Accounts maintain a lamport balance and program-specific memory. - -## Transaction Engine - -The engine maps public keys to accounts and routes them to the program's entrypoint. - -### Execution - -Transactions are batched and processed in a pipeline. The TPU and TVU follow a slightly different path. The TPU runtime ensures that PoH record occurs before memory is committed. - -The TVU runtime ensures that PoH verification occurs before the runtime processes any transactions. - -![Runtime pipeline](/img/runtime.svg) - -At the _execute_ stage, the loaded accounts have no data dependencies, so all the programs can be executed in parallel. - -The runtime enforces the following rules: - -1. Only the _owner_ program may modify the contents of an account. This means that upon assignment data vector is guaranteed to be zero. -2. Total balances on all the accounts are equal before and after execution of a transaction. -3. After the transaction is executed, balances of read-only accounts must be equal to the balances before the transaction. -4. All instructions in the transaction are executed atomically. If one fails, all account modifications are discarded. - -Execution of the program involves mapping the program's public key to an entrypoint which takes a pointer to the transaction, and an array of loaded accounts. - -### SystemProgram Interface - -The interface is best described by the `Instruction::data` that the user encodes. - -- `CreateAccount` - This allows the user to create an account with an allocated data array and assign it to a Program. -- `CreateAccountAllowPrefund` - Same as `CreateAccount`, but does not check (and fail) if the account has lamports. -- `CreateAccountWithSeed` - Same as `CreateAccount`, but the new account's address is derived from - - the funding account's pubkey, - - a mnemonic string (seed), and - - the pubkey of the Program -- `Assign` - Allows the user to assign an existing account to a program. -- `Transfer` - Transfers lamports between accounts. - -### Program State Security - -For blockchain to function correctly, the program code must be resilient to user inputs. That is why in this design the program specific code is the only code that can change the state of the data byte array in the Accounts that are assigned to it. It is also the reason why `Assign` or `CreateAccount` must zero out the data. Otherwise there would be no possible way for the program to distinguish the recently assigned account data from a natively generated state transition without some additional metadata from the runtime to indicate that this memory is assigned instead of natively generated. - -To pass messages between programs, the receiving program must accept the message and copy the state over. But in practice a copy isn't needed and is undesirable. The receiving program can read the state belonging to other Accounts without copying it, and during the read it has a guarantee of the sender program's state. - -### Notes - -- There is no dynamic memory allocation. Client's need to use `CreateAccount` instructions to create memory before passing it to another program. This instruction can be composed into a single transaction with the call to the program itself. -- `CreateAccount` and `Assign` guarantee that when account is assigned to the program, the Account's data is zero initialized. -- Transactions that assign an account to a program or allocate space must be signed by the Account address' private key unless the Account is being created by `CreateAccountWithSeed`, in which case there is no corresponding private key for the account's address/pubkey. -- Once assigned to program an Account cannot be reassigned. -- Runtime guarantees that a program's code is the only code that can modify Account data that the Account is assigned to. -- Runtime guarantees that the program can only spend lamports that are in accounts that are assigned to it. -- Runtime guarantees the balances belonging to accounts are balanced before and after the transaction. -- Runtime guarantees that instructions all executed successfully when a transaction is committed. - -## Future Work - -- [Continuations and Signals for long running Transactions](https://github.com/solana-labs/solana/issues/1485) diff --git a/docs/src/validator/tpu.md b/docs/src/validator/tpu.md deleted file mode 100644 index 0498894dd97..00000000000 --- a/docs/src/validator/tpu.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Transaction Processing Unit in a Solana Validator -sidebar_position: 2 -sidebar_label: TPU -pagination_label: Validator's Transaction Processing Unit (TPU) ---- - -TPU (Transaction Processing Unit) is the logic of the validator -responsible for block production. - -![TPU Block Diagram](/img/tpu.svg) - -Transactions are encoded and sent in QUIC streams into the validator -from clients (other validators/users of the network) as follows: - -* The quic streamer: allocates packet memory and reads the packet data from -the QUIC endpoint and applies some coalescing of packets received at -the same time. Each stream is used to transmit a packet. And there is limit on the -maximum of QUIC connections can be concurrently established between a client -identified by (IP Address, Node Pubkey) and the server. And there is a limit on the -maximum streams can be concurrently opened per connection based on the sender's -stake. Clients with higher stakes will be allowed to open more streams within -a maximum limit. The system also does rate limiting on the packets per -second(PPS) and applied the limit to the connection based on the stake. -Higher stakes offers better bandwidth. If the transfer rate is exceeded, -the server can drop the stream with the error code (15 -- STREAM_STOP_CODE_THROTTLING). -The client is expected to do some sort of exponential back off in retrying the -transactions when running into this situation. - -* sigverify stage: deduplicates packets and applies some load-shedding -to remove excessive packets before then filtering packets with invalid -signatures by setting the packet's discard flag. - -* banking stage: receives and buffers packet when the node is close to -becoming the leader. Once it detects the node is the block producer it -processes held packets and newly received packets with a Bank at the tip slot. - -* forwarding stage: forwards received packets to a node that is or will soon -be leader. Sorts packets by priority and forwards them. Non-vote transactions -are only forwarded if the node has the option enabled (stake overrides) but -will always forward tpu votes. - -* broadcast stage: receives the valid transactions formed into Entry's from -banking stage and packages them into shreds to send to network peers through -the turbine tree structure. Serializes, signs, and generates erasure codes -before sending the packets to the appropriate network peer. diff --git a/docs/src/validator/tvu.md b/docs/src/validator/tvu.md deleted file mode 100644 index 41def356096..00000000000 --- a/docs/src/validator/tvu.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: Transaction Validation Unit in a Solana Validator -sidebar_position: 3 -sidebar_label: TVU -pagination_label: Validator's Transaction Validation Unit (TVU) ---- - -TVU (Transaction Validation Unit) is the logic of the validator -responsible for propagating blocks between validators and ensuring that -those blocks' transactions reach the replay stage. Its principal external -interface is the turbine protocol. - -![TVU Block Diagram](/img/tvu.svg) - -## Retransmit Stage - -![Retransmit Block Diagram](/img/retransmit_stage.svg) - -## TVU sockets - -Externally, TVU UDP receiver appears to bind to one port, typically 8002 UDP. -Internally, TVU is actually bound with multiple sockets to improve kernel's handling of the packet queues. - -> **NOTE:** TPU sockets use similar logic - -A node advertises one external ip/port for TVU while binding multiple sockets to that same port: - -```rust -let (tvu_port, tvu_sockets) = multi_bind_in_range_with_config( - bind_ip_addr, - port_range, - socket_config, - num_tvu_sockets.get(), -) -.expect("tvu multi_bind"); -``` - - `multi_bind_in_range_with_config` sets `SO_REUSEPORT`. This means that other nodes only need to know about the one ip/port pair for TVU (similar principle applies in the case of TPU UDP sockets). The kernel distributes the incoming packets to all sockets bound to that port, and each socket can be serviced by a different thread. - -The TVU socket information is published via Gossip and is available in the `ContactInfo` struct. -To set a TVU socket, the node calls `set_tvu(...)`. The `set_tvu()` method is created by the macro `set_socket!`. For example: - -```rust -info.set_tvu(UDP, (addr, tvu_udp_port)).unwrap(); -``` - -Under the hood, `set_tvu()` calls `set_socket()`. - -In the `ContactInfo` struct, all sockets are identified by a tag/key, e.g.: - -```rust -const SOCKET_TAG_TVU: u8 = 10; // For UDP -``` - - * `set_socket()` creates a `SocketEntry` and stores that into `ContactInfo::sockets` - * `set_socket()` updates `ContactInfo::cache` - -```rust -cache: [SocketAddr; SOCKET_CACHE_SIZE] -``` - -`cache` is purely for quick lookups and optimization; it is not serialized and sent to peer nodes. -`SocketEntry` is serialized and sent to peer nodes within the gossip message type `CrdsData::ContactInfo`. Upon receiving the `ContactInfo`, the peer node calls the `get_socket!` macro to retrieve the TVU port associated with the node. -For example, to retrieve the TVU ports of the remote node, the peer node calls: -```rust -get_socket!(tvu, SOCKET_TAG_TVU, SOCKET_TAG_TVU_UDP); -``` diff --git a/docs/src/what-is-a-validator.md b/docs/src/what-is-a-validator.md deleted file mode 100644 index a6e673430d7..00000000000 --- a/docs/src/what-is-a-validator.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: What is a Validator? ---- - -A validator is a computer that helps to run the Solana network. Each validator executes a program that keeps track of all accounts on the Solana cluster and validates transactions being added to the network. Without validators, Solana would not be able to function. Agave is one of several possible validator clients operators can use to help run the Solana network. - -The more independent entities that run validators, the less vulnerable the cluster is to an attack or catastrophe that affects the cluster. - -> For a more in depth look at the health of the Solana network, see the [Solana Foundation Validator Health Report](https://solana.com/news/validator-health-report-march-2023). - -By becoming a validator, you are helping to grow the network. You are also learning first hand how the Solana cluster functions at the lowest level. You will become part of an active community of operators that are passionate about the Solana ecosystem. - -## Consensus vs RPC - -Before we discuss validators in more detail, it's useful to make some distinctions. Using the same validator software, you have the option of running a voting/consensus node or choosing to instead run an RPC node. An RPC node helps Solana devs and others interact with the blockchain but for performance reasons should not vote. We go into more detail on RPC nodes in the next section, [what is an rpc node](./what-is-an-rpc-node.md). - -For this document, when a validator is mentioned, we are talking about a voting/consensus node. Now, to better understand what your validator is doing, it would help to understand how the Solana network functions in more depth. This documentation specifically focuses on the Agave client. - -## Proof Of Stake - -Proof of stake is the blockchain architecture that is used in Solana. It is called proof of stake because token holders can stake their tokens to a validator of their choice. When a person stakes their tokens, that person still owns the tokens and can remove the stake at any time. The staked tokens represent their trust in that validator. When a person stakes their tokens to a validator, they are given a return of some amount of tokens as a reward for helping to run and secure the network. The more tokens you have staked to a validator the more rewards you receive. A validator that has a large amount of tokens staked to it has a larger vote share in consensus. A validator, therefore, is given more opportunities to produce blocks in the network proportional to the size of the stake in the validator. The validator that is currently producing blocks in the network is known as the leader. - -## Proof Of Work: For Contrast - -Solana is not a proof of work system. Proof of work is a different blockchain architecture in which a computer (often called a miner), works to solve a cryptographic problem before anyone else on the network is able to solve it. The more often the computer solves these problems, the more rewards the miner receives. Because of the incentive to solve a hard computational problem first, miners often use many computers at the same time. The number of computers used to solve these problems leads to large energy consumption and resulting environmental challenges. - -Solana, in contrast, does not incentivize validators to use many computers to solve a computational problem. Because a validator would like to have a larger amount staked to it, there is no real advantage for an independent validator to using many different computers. Here, you can see a comparison of [Solana's environmental impact](https://solana.com/news/solana-energy-usage-report-november-2021). - -## Proof Of History - -Proof of history, PoH, is one of the key innovations in Solana that allows transactions to be finalized very quickly. At a high level, PoH allows validators in the cluster to agree on a cryptographically repeatable clock. Both proof of stake and proof of work architectures mentioned above are architectures that bring the cluster to consensus. In other words, these algorithms decide which blocks should be added to the blockchain. Proof of history is not a consensus architecture, but rather a feature in Solana that makes block finalization faster in the proof of stake system. - -Understanding how PoH works is not necessary to run a good validator, but a very approachable discussion can be found [in this Medium article](https://medium.com/solana-labs/proof-of-history-explained-by-a-water-clock-e682183417b8). Also, the [Solana whitepaper](https://solana.com/solana-whitepaper.pdf) does a good job of explaining the algorithm in an approachable way for the more technically minded. - -## Your Role As A Validator - -As a validator, you are helping to secure the network by producing and voting on blocks and to improve decentralization by running an independent node. You have the right to participate in discussions of changes on the network. You are also assuming a responsibility to keep your system running properly, to make sure your system is secure, and to keep it up to date with the latest software. As more individuals stake their tokens to your validator, you can reward their trust by running a high performing and reliable validator. Hopefully, your validator is performing well a majority of the time, but you should also have systems in place to respond to an outage at any time of the day. If your validator is not responding late at night, someone (either you or other team members) need to be available to investigate and fix the issues. - -Running a validator is a [technical and important task](./operations/prerequisites.md), but it can also be very rewarding. Good luck and welcome to the community. diff --git a/docs/src/what-is-an-rpc-node.md b/docs/src/what-is-an-rpc-node.md deleted file mode 100644 index 6bf75915a88..00000000000 --- a/docs/src/what-is-an-rpc-node.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: What is an RPC Node? ---- - -An RPC (Remote Procedure Call) node runs the same software as a [validator](./what-is-a-validator.md), but it does not participate in the consensus process. Technically you could run the RPC software and also allow your node to vote as a consensus node, but it is strongly discouraged because your node will not be performant enough to do either task well. - -A node that runs RPC has a much different purpose in the cluster. An RPC node responds to requests about the blockchain and also allows users of the RPC node to submit new transactions to be included in blocks. - -For example, a website might request to transfer tokens from wallet A to wallet B (given wallet A's permission). That website would have to use wallet A to sign a transaction and then send it to an RPC node to be submitted to the leader. So you could think of running an RPC node as a similar engineering task to providing an api for others to use. - -The users of the RPC node are often developers, so this option may require a more technical understanding of Solana. To better understand RPC node operations, you'll want to become familiar with the different RPC calls. -You can find the [RPC API here](https://solana.com/docs/rpc/http). \ No newline at end of file diff --git a/docs/static/.nojekyll b/docs/static/.nojekyll deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/docs/static/img/android-chrome-192x192.png b/docs/static/img/android-chrome-192x192.png deleted file mode 100644 index b8222a9b6b4..00000000000 Binary files a/docs/static/img/android-chrome-192x192.png and /dev/null differ diff --git a/docs/static/img/android-chrome-512x512.png b/docs/static/img/android-chrome-512x512.png deleted file mode 100644 index 598b8a1722b..00000000000 Binary files a/docs/static/img/android-chrome-512x512.png and /dev/null differ diff --git a/docs/static/img/apple-touch-icon.png b/docs/static/img/apple-touch-icon.png deleted file mode 100644 index 282b79dee8a..00000000000 Binary files a/docs/static/img/apple-touch-icon.png and /dev/null differ diff --git a/docs/static/img/dark-mark-white.inline.svg b/docs/static/img/dark-mark-white.inline.svg deleted file mode 100644 index fac7a4fd110..00000000000 --- a/docs/static/img/dark-mark-white.inline.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/docs/static/img/data-plane-propagation.png b/docs/static/img/data-plane-propagation.png deleted file mode 100644 index ca390960d97..00000000000 Binary files a/docs/static/img/data-plane-propagation.png and /dev/null differ diff --git a/docs/static/img/economic_design_infl_230719.png b/docs/static/img/economic_design_infl_230719.png deleted file mode 100644 index 2ce0958cc42..00000000000 Binary files a/docs/static/img/economic_design_infl_230719.png and /dev/null differ diff --git a/docs/static/img/favicon-16x16.png b/docs/static/img/favicon-16x16.png deleted file mode 100644 index b1cb5f95482..00000000000 Binary files a/docs/static/img/favicon-16x16.png and /dev/null differ diff --git a/docs/static/img/favicon-32x32.png b/docs/static/img/favicon-32x32.png deleted file mode 100644 index 7580b1776ee..00000000000 Binary files a/docs/static/img/favicon-32x32.png and /dev/null differ diff --git a/docs/static/img/favicon.ico b/docs/static/img/favicon.ico deleted file mode 100644 index cf96c74ac09..00000000000 Binary files a/docs/static/img/favicon.ico and /dev/null differ diff --git a/docs/static/img/ledger-live-install-solana-app.png b/docs/static/img/ledger-live-install-solana-app.png deleted file mode 100644 index 46986a69e74..00000000000 Binary files a/docs/static/img/ledger-live-install-solana-app.png and /dev/null differ diff --git a/docs/static/img/ledger-live-latest-version-installed.png b/docs/static/img/ledger-live-latest-version-installed.png deleted file mode 100644 index a2d94caa9a8..00000000000 Binary files a/docs/static/img/ledger-live-latest-version-installed.png and /dev/null differ diff --git a/docs/static/img/ledger-live-update-available-v0.2.2.png b/docs/static/img/ledger-live-update-available-v0.2.2.png deleted file mode 100644 index 5756dff8784..00000000000 Binary files a/docs/static/img/ledger-live-update-available-v0.2.2.png and /dev/null differ diff --git a/docs/static/img/logo-horizontal-dark.svg b/docs/static/img/logo-horizontal-dark.svg deleted file mode 100644 index ab9afe0762a..00000000000 --- a/docs/static/img/logo-horizontal-dark.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/docs/static/img/logo-horizontal.svg b/docs/static/img/logo-horizontal.svg deleted file mode 100644 index f4b43eb0804..00000000000 --- a/docs/static/img/logo-horizontal.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/docs/static/img/logo.svg b/docs/static/img/logo.svg deleted file mode 100644 index 4e78f61097b..00000000000 --- a/docs/static/img/logo.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/docs/static/img/p_ex_adjusted_staked_yields.png b/docs/static/img/p_ex_adjusted_staked_yields.png deleted file mode 100644 index db68e4b0a92..00000000000 Binary files a/docs/static/img/p_ex_adjusted_staked_yields.png and /dev/null differ diff --git a/docs/static/img/p_ex_interest.png b/docs/static/img/p_ex_interest.png deleted file mode 100755 index 3329fe04e58..00000000000 Binary files a/docs/static/img/p_ex_interest.png and /dev/null differ diff --git a/docs/static/img/p_ex_relative_dilution.png b/docs/static/img/p_ex_relative_dilution.png deleted file mode 100644 index ba59a78bde7..00000000000 Binary files a/docs/static/img/p_ex_relative_dilution.png and /dev/null differ diff --git a/docs/static/img/p_ex_schedule.png b/docs/static/img/p_ex_schedule.png deleted file mode 100644 index beb4c0cc5c3..00000000000 Binary files a/docs/static/img/p_ex_schedule.png and /dev/null differ diff --git a/docs/static/img/p_ex_staked_dilution.png b/docs/static/img/p_ex_staked_dilution.png deleted file mode 100644 index 89de1ed947c..00000000000 Binary files a/docs/static/img/p_ex_staked_dilution.png and /dev/null differ diff --git a/docs/static/img/p_ex_staked_supply_w_range_initial_stake.png b/docs/static/img/p_ex_staked_supply_w_range_initial_stake.png deleted file mode 100644 index 89b893d0e83..00000000000 Binary files a/docs/static/img/p_ex_staked_supply_w_range_initial_stake.png and /dev/null differ diff --git a/docs/static/img/p_ex_staked_yields.png b/docs/static/img/p_ex_staked_yields.png deleted file mode 100644 index 32311c3966b..00000000000 Binary files a/docs/static/img/p_ex_staked_yields.png and /dev/null differ diff --git a/docs/static/img/p_ex_supply.png b/docs/static/img/p_ex_supply.png deleted file mode 100644 index 37798493687..00000000000 Binary files a/docs/static/img/p_ex_supply.png and /dev/null differ diff --git a/docs/static/img/p_ex_unstaked_dilution.png b/docs/static/img/p_ex_unstaked_dilution.png deleted file mode 100644 index a283011ac53..00000000000 Binary files a/docs/static/img/p_ex_unstaked_dilution.png and /dev/null differ diff --git a/docs/static/img/p_inflation_schedule.png b/docs/static/img/p_inflation_schedule.png deleted file mode 100644 index f515efa12cd..00000000000 Binary files a/docs/static/img/p_inflation_schedule.png and /dev/null differ diff --git a/docs/static/img/p_inflation_schedule_ranges_w_comments.png b/docs/static/img/p_inflation_schedule_ranges_w_comments.png deleted file mode 100644 index 498405095ee..00000000000 Binary files a/docs/static/img/p_inflation_schedule_ranges_w_comments.png and /dev/null differ diff --git a/docs/static/img/p_total_supply.png b/docs/static/img/p_total_supply.png deleted file mode 100644 index e2562ffde0c..00000000000 Binary files a/docs/static/img/p_total_supply.png and /dev/null differ diff --git a/docs/static/img/p_total_supply_ranges.png b/docs/static/img/p_total_supply_ranges.png deleted file mode 100644 index 319df2b5e59..00000000000 Binary files a/docs/static/img/p_total_supply_ranges.png and /dev/null differ diff --git a/docs/static/img/porep_reward.png b/docs/static/img/porep_reward.png deleted file mode 100644 index 501cf29e1d7..00000000000 Binary files a/docs/static/img/porep_reward.png and /dev/null differ diff --git a/docs/static/img/ramp-tps-rounds.png b/docs/static/img/ramp-tps-rounds.png deleted file mode 100644 index 6da4b959ed3..00000000000 Binary files a/docs/static/img/ramp-tps-rounds.png and /dev/null differ diff --git a/docs/static/img/rt-dropped-minority-fork-post-process.png b/docs/static/img/rt-dropped-minority-fork-post-process.png deleted file mode 100644 index ba56d1ccc83..00000000000 Binary files a/docs/static/img/rt-dropped-minority-fork-post-process.png and /dev/null differ diff --git a/docs/static/img/rt-dropped-minority-fork-pre-process.png b/docs/static/img/rt-dropped-minority-fork-pre-process.png deleted file mode 100644 index 10f08dfedb2..00000000000 Binary files a/docs/static/img/rt-dropped-minority-fork-pre-process.png and /dev/null differ diff --git a/docs/static/img/rt-dropped-via-rpc-pool.png b/docs/static/img/rt-dropped-via-rpc-pool.png deleted file mode 100644 index 8020ebd8738..00000000000 Binary files a/docs/static/img/rt-dropped-via-rpc-pool.png and /dev/null differ diff --git a/docs/static/img/rt-tpu-jito-labs.png b/docs/static/img/rt-tpu-jito-labs.png deleted file mode 100644 index 934e1dcdc8d..00000000000 Binary files a/docs/static/img/rt-tpu-jito-labs.png and /dev/null differ diff --git a/docs/static/img/rt-tx-journey.png b/docs/static/img/rt-tx-journey.png deleted file mode 100644 index faa8b6e22c9..00000000000 Binary files a/docs/static/img/rt-tx-journey.png and /dev/null differ diff --git a/docs/static/img/spiral.svg b/docs/static/img/spiral.svg deleted file mode 100644 index 03a96552c5b..00000000000 --- a/docs/static/img/spiral.svg +++ /dev/null @@ -1,206 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/static/katex/README.md b/docs/static/katex/README.md deleted file mode 100644 index 1dbd79f3495..00000000000 --- a/docs/static/katex/README.md +++ /dev/null @@ -1,91 +0,0 @@ -# [KaTeX](https://katex.org/) -[![npm](https://img.shields.io/npm/v/katex.svg)](https://www.npmjs.com/package/katex) -[![CircleCI](https://circleci.com/gh/KaTeX/KaTeX.svg?style=shield)](https://circleci.com/gh/KaTeX/KaTeX) -[![codecov](https://codecov.io/gh/KaTeX/KaTeX/branch/master/graph/badge.svg)](https://codecov.io/gh/KaTeX/KaTeX) -[![Join the chat at https://gitter.im/KaTeX/KaTeX](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KaTeX/KaTeX?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=KaTeX/KaTeX)](https://dependabot.com) -[![jsDelivr](https://data.jsdelivr.com/v1/package/npm/katex/badge?style=rounded)](https://www.jsdelivr.com/package/npm/katex) -![](https://img.badgesize.io/KaTeX/KaTeX/v0.12.0/dist/katex.min.js?compression=gzip) - -KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web. - - * **Fast:** KaTeX renders its math synchronously and doesn't need to reflow the page. See how it compares to a competitor in [this speed test](http://www.intmath.com/cg5/katex-mathjax-comparison.php). - * **Print quality:** KaTeX's layout is based on Donald Knuth's TeX, the gold standard for math typesetting. - * **Self contained:** KaTeX has no dependencies and can easily be bundled with your website resources. - * **Server side rendering:** KaTeX produces the same output regardless of browser or environment, so you can pre-render expressions using Node.js and send them as plain HTML. - -KaTeX is compatible with all major browsers, including Chrome, Safari, Firefox, Opera, Edge, and IE 11. - -KaTeX supports much (but not all) of LaTeX and many LaTeX packages. See the [list of supported functions](https://katex.org/docs/supported.html). - -Try out KaTeX [on the demo page](https://katex.org/#demo)! - -## Getting started - -### Starter template - -```html - - - - - - - - - - - - - ... - -``` - -You can also [download KaTeX](https://github.com/KaTeX/KaTeX/releases) and host it yourself. - -For details on how to configure auto-render extension, refer to [the documentation](https://katex.org/docs/autorender.html). - -### API - -Call `katex.render` to render a TeX expression directly into a DOM element. -For example: - -```js -katex.render("c = \\pm\\sqrt{a^2 + b^2}", element, { - throwOnError: false -}); -``` - -Call `katex.renderToString` to generate an HTML string of the rendered math, -e.g., for server-side rendering. For example: - -```js -var html = katex.renderToString("c = \\pm\\sqrt{a^2 + b^2}", { - throwOnError: false -}); -// '...' -``` - -Make sure to include the CSS and font files in both cases. -If you are doing all rendering on the server, there is no need to include the -JavaScript on the client. - -The examples above use the `throwOnError: false` option, which renders invalid -inputs as the TeX source code in red (by default), with the error message as -hover text. For other available options, see the -[API documentation](https://katex.org/docs/api.html), -[options documentation](https://katex.org/docs/options.html), and -[handling errors documentation](https://katex.org/docs/error.html). - -## Demo and Documentation - -Learn more about using KaTeX [on the website](https://katex.org)! - -## Contributing - -See [CONTRIBUTING.md](https://github.com/KaTeX/KaTeX/blob/main/CONTRIBUTING.md) - -## License - -KaTeX is licensed under the [MIT License](http://opensource.org/licenses/MIT). diff --git a/docs/static/katex/contrib/auto-render.js b/docs/static/katex/contrib/auto-render.js deleted file mode 100644 index d5944348342..00000000000 --- a/docs/static/katex/contrib/auto-render.js +++ /dev/null @@ -1,350 +0,0 @@ -(function webpackUniversalModuleDefinition(root, factory) { - if(typeof exports === 'object' && typeof module === 'object') - module.exports = factory(require("katex")); - else if(typeof define === 'function' && define.amd) - define(["katex"], factory); - else if(typeof exports === 'object') - exports["renderMathInElement"] = factory(require("katex")); - else - root["renderMathInElement"] = factory(root["katex"]); -})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__0__) { -return /******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); -/******/ } -/******/ }; -/******/ -/******/ // define __esModule on exports -/******/ __webpack_require__.r = function(exports) { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ -/******/ // create a fake namespace object -/******/ // mode & 1: value is a module id, require it -/******/ // mode & 2: merge all properties of value into the ns -/******/ // mode & 4: return value when already ns object -/******/ // mode & 8|1: behave like require -/******/ __webpack_require__.t = function(value, mode) { -/******/ if(mode & 1) value = __webpack_require__(value); -/******/ if(mode & 8) return value; -/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; -/******/ var ns = Object.create(null); -/******/ __webpack_require__.r(ns); -/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); -/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); -/******/ return ns; -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; -/******/ -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = 1); -/******/ }) -/************************************************************************/ -/******/ ([ -/* 0 */ -/***/ (function(module, exports) { - -module.exports = __WEBPACK_EXTERNAL_MODULE__0__; - -/***/ }), -/* 1 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); - -// EXTERNAL MODULE: external "katex" -var external_katex_ = __webpack_require__(0); -var external_katex_default = /*#__PURE__*/__webpack_require__.n(external_katex_); - -// CONCATENATED MODULE: ./contrib/auto-render/splitAtDelimiters.js -/* eslint no-constant-condition:0 */ -var findEndOfMath = function findEndOfMath(delimiter, text, startIndex) { - // Adapted from - // https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx - var index = startIndex; - var braceLevel = 0; - var delimLength = delimiter.length; - - while (index < text.length) { - var character = text[index]; - - if (braceLevel <= 0 && text.slice(index, index + delimLength) === delimiter) { - return index; - } else if (character === "\\") { - index++; - } else if (character === "{") { - braceLevel++; - } else if (character === "}") { - braceLevel--; - } - - index++; - } - - return -1; -}; - -var splitAtDelimiters = function splitAtDelimiters(startData, leftDelim, rightDelim, display) { - var finalData = []; - - for (var i = 0; i < startData.length; i++) { - if (startData[i].type === "text") { - var text = startData[i].data; - var lookingForLeft = true; - var currIndex = 0; - var nextIndex = void 0; - nextIndex = text.indexOf(leftDelim); - - if (nextIndex !== -1) { - currIndex = nextIndex; - finalData.push({ - type: "text", - data: text.slice(0, currIndex) - }); - lookingForLeft = false; - } - - while (true) { - if (lookingForLeft) { - nextIndex = text.indexOf(leftDelim, currIndex); - - if (nextIndex === -1) { - break; - } - - finalData.push({ - type: "text", - data: text.slice(currIndex, nextIndex) - }); - currIndex = nextIndex; - } else { - nextIndex = findEndOfMath(rightDelim, text, currIndex + leftDelim.length); - - if (nextIndex === -1) { - break; - } - - finalData.push({ - type: "math", - data: text.slice(currIndex + leftDelim.length, nextIndex), - rawData: text.slice(currIndex, nextIndex + rightDelim.length), - display: display - }); - currIndex = nextIndex + rightDelim.length; - } - - lookingForLeft = !lookingForLeft; - } - - finalData.push({ - type: "text", - data: text.slice(currIndex) - }); - } else { - finalData.push(startData[i]); - } - } - - return finalData; -}; - -/* harmony default export */ var auto_render_splitAtDelimiters = (splitAtDelimiters); -// CONCATENATED MODULE: ./contrib/auto-render/auto-render.js -/* eslint no-console:0 */ - - - -var auto_render_splitWithDelimiters = function splitWithDelimiters(text, delimiters) { - var data = [{ - type: "text", - data: text - }]; - - for (var i = 0; i < delimiters.length; i++) { - var delimiter = delimiters[i]; - data = auto_render_splitAtDelimiters(data, delimiter.left, delimiter.right, delimiter.display || false); - } - - return data; -}; -/* Note: optionsCopy is mutated by this method. If it is ever exposed in the - * API, we should copy it before mutating. - */ - - -var auto_render_renderMathInText = function renderMathInText(text, optionsCopy) { - var data = auto_render_splitWithDelimiters(text, optionsCopy.delimiters); - - if (data.length === 1 && data[0].type === 'text') { - // There is no formula in the text. - // Let's return null which means there is no need to replace - // the current text node with a new one. - return null; - } - - var fragment = document.createDocumentFragment(); - - for (var i = 0; i < data.length; i++) { - if (data[i].type === "text") { - fragment.appendChild(document.createTextNode(data[i].data)); - } else { - var span = document.createElement("span"); - var math = data[i].data; // Override any display mode defined in the settings with that - // defined by the text itself - - optionsCopy.displayMode = data[i].display; - - try { - if (optionsCopy.preProcess) { - math = optionsCopy.preProcess(math); - } - - external_katex_default.a.render(math, span, optionsCopy); - } catch (e) { - if (!(e instanceof external_katex_default.a.ParseError)) { - throw e; - } - - optionsCopy.errorCallback("KaTeX auto-render: Failed to parse `" + data[i].data + "` with ", e); - fragment.appendChild(document.createTextNode(data[i].rawData)); - continue; - } - - fragment.appendChild(span); - } - } - - return fragment; -}; - -var renderElem = function renderElem(elem, optionsCopy) { - for (var i = 0; i < elem.childNodes.length; i++) { - var childNode = elem.childNodes[i]; - - if (childNode.nodeType === 3) { - // Text node - var frag = auto_render_renderMathInText(childNode.textContent, optionsCopy); - - if (frag) { - i += frag.childNodes.length - 1; - elem.replaceChild(frag, childNode); - } - } else if (childNode.nodeType === 1) { - (function () { - // Element node - var className = ' ' + childNode.className + ' '; - var shouldRender = optionsCopy.ignoredTags.indexOf(childNode.nodeName.toLowerCase()) === -1 && optionsCopy.ignoredClasses.every(function (x) { - return className.indexOf(' ' + x + ' ') === -1; - }); - - if (shouldRender) { - renderElem(childNode, optionsCopy); - } - })(); - } // Otherwise, it's something else, and ignore it. - - } -}; - -var renderMathInElement = function renderMathInElement(elem, options) { - if (!elem) { - throw new Error("No element provided to render"); - } - - var optionsCopy = {}; // Object.assign(optionsCopy, option) - - for (var option in options) { - if (options.hasOwnProperty(option)) { - optionsCopy[option] = options[option]; - } - } // default options - - - optionsCopy.delimiters = optionsCopy.delimiters || [{ - left: "$$", - right: "$$", - display: true - }, { - left: "\\(", - right: "\\)", - display: false - }, // LaTeX uses $…$, but it ruins the display of normal `$` in text: - // {left: "$", right: "$", display: false}, - // \[…\] must come last in this array. Otherwise, renderMathInElement - // will search for \[ before it searches for $$ or \( - // That makes it susceptible to finding a \\[0.3em] row delimiter and - // treating it as if it were the start of a KaTeX math zone. - { - left: "\\[", - right: "\\]", - display: true - }]; - optionsCopy.ignoredTags = optionsCopy.ignoredTags || ["script", "noscript", "style", "textarea", "pre", "code", "option"]; - optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || []; - optionsCopy.errorCallback = optionsCopy.errorCallback || console.error; // Enable sharing of global macros defined via `\gdef` between different - // math elements within a single call to `renderMathInElement`. - - optionsCopy.macros = optionsCopy.macros || {}; - renderElem(elem, optionsCopy); -}; - -/* harmony default export */ var auto_render = __webpack_exports__["default"] = (renderMathInElement); - -/***/ }) -/******/ ])["default"]; -}); \ No newline at end of file diff --git a/docs/static/katex/contrib/auto-render.min.js b/docs/static/katex/contrib/auto-render.min.js deleted file mode 100644 index 840e74bf117..00000000000 --- a/docs/static/katex/contrib/auto-render.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,function(e){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=1)}([function(t,r){t.exports=e},function(e,t,r){"use strict";r.r(t);var n=r(0),o=r.n(n),a=function(e,t,r){for(var n=r,o=0,a=e.length;n className.indexOf(' ' + x + ' ') === -1); - - if (shouldRender) { - renderElem(childNode, optionsCopy); - } - } // Otherwise, it's something else, and ignore it. - - } -}; - -const renderMathInElement = function renderMathInElement(elem, options) { - if (!elem) { - throw new Error("No element provided to render"); - } - - const optionsCopy = {}; // Object.assign(optionsCopy, option) - - for (const option in options) { - if (options.hasOwnProperty(option)) { - optionsCopy[option] = options[option]; - } - } // default options - - - optionsCopy.delimiters = optionsCopy.delimiters || [{ - left: "$$", - right: "$$", - display: true - }, { - left: "\\(", - right: "\\)", - display: false - }, // LaTeX uses $…$, but it ruins the display of normal `$` in text: - // {left: "$", right: "$", display: false}, - // \[…\] must come last in this array. Otherwise, renderMathInElement - // will search for \[ before it searches for $$ or \( - // That makes it susceptible to finding a \\[0.3em] row delimiter and - // treating it as if it were the start of a KaTeX math zone. - { - left: "\\[", - right: "\\]", - display: true - }]; - optionsCopy.ignoredTags = optionsCopy.ignoredTags || ["script", "noscript", "style", "textarea", "pre", "code", "option"]; - optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || []; - optionsCopy.errorCallback = optionsCopy.errorCallback || console.error; // Enable sharing of global macros defined via `\gdef` between different - // math elements within a single call to `renderMathInElement`. - - optionsCopy.macros = optionsCopy.macros || {}; - renderElem(elem, optionsCopy); -}; - -export default renderMathInElement; diff --git a/docs/static/katex/contrib/copy-tex.css b/docs/static/katex/contrib/copy-tex.css deleted file mode 100644 index 0a337cc1985..00000000000 --- a/docs/static/katex/contrib/copy-tex.css +++ /dev/null @@ -1,13 +0,0 @@ -/* Force selection of entire .katex/.katex-display blocks, so that we can - * copy/paste the entire source code. If you omit this CSS, partial - * selections of a formula will work, but will copy the ugly HTML - * representation instead of the LaTeX source code. (Full selections will - * still produce the LaTeX source code.) - */ -.katex, -.katex-display { - user-select: all; - -moz-user-select: all; - -webkit-user-select: all; - -ms-user-select: all; -} diff --git a/docs/static/katex/contrib/copy-tex.js b/docs/static/katex/contrib/copy-tex.js deleted file mode 100644 index 4649f8f7e92..00000000000 --- a/docs/static/katex/contrib/copy-tex.js +++ /dev/null @@ -1,213 +0,0 @@ -(function webpackUniversalModuleDefinition(root, factory) { - if(typeof exports === 'object' && typeof module === 'object') - module.exports = factory(); - else if(typeof define === 'function' && define.amd) - define([], factory); - else { - var a = factory(); - for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; - } -})((typeof self !== 'undefined' ? self : this), function() { -return /******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); -/******/ } -/******/ }; -/******/ -/******/ // define __esModule on exports -/******/ __webpack_require__.r = function(exports) { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ -/******/ // create a fake namespace object -/******/ // mode & 1: value is a module id, require it -/******/ // mode & 2: merge all properties of value into the ns -/******/ // mode & 4: return value when already ns object -/******/ // mode & 8|1: behave like require -/******/ __webpack_require__.t = function(value, mode) { -/******/ if(mode & 1) value = __webpack_require__(value); -/******/ if(mode & 8) return value; -/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; -/******/ var ns = Object.create(null); -/******/ __webpack_require__.r(ns); -/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); -/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); -/******/ return ns; -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; -/******/ -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = 1); -/******/ }) -/************************************************************************/ -/******/ ([ -/* 0 */ -/***/ (function(module, exports, __webpack_require__) { - -// extracted by mini-css-extract-plugin - -/***/ }), -/* 1 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); - -// EXTERNAL MODULE: ./contrib/copy-tex/copy-tex.css -var copy_tex = __webpack_require__(0); - -// CONCATENATED MODULE: ./contrib/copy-tex/katex2tex.js -// Set these to how you want inline and display math to be delimited. -var defaultCopyDelimiters = { - inline: ['$', '$'], - // alternative: ['\(', '\)'] - display: ['$$', '$$'] // alternative: ['\[', '\]'] - -}; // Replace .katex elements with their TeX source ( element). -// Modifies fragment in-place. Useful for writing your own 'copy' handler, -// as in copy-tex.js. - -var katexReplaceWithTex = function katexReplaceWithTex(fragment, copyDelimiters) { - if (copyDelimiters === void 0) { - copyDelimiters = defaultCopyDelimiters; - } - - // Remove .katex-html blocks that are preceded by .katex-mathml blocks - // (which will get replaced below). - var katexHtml = fragment.querySelectorAll('.katex-mathml + .katex-html'); - - for (var i = 0; i < katexHtml.length; i++) { - var element = katexHtml[i]; - - if (element.remove) { - element.remove(null); - } else { - element.parentNode.removeChild(element); - } - } // Replace .katex-mathml elements with their annotation (TeX source) - // descendant, with inline delimiters. - - - var katexMathml = fragment.querySelectorAll('.katex-mathml'); - - for (var _i = 0; _i < katexMathml.length; _i++) { - var _element = katexMathml[_i]; - - var texSource = _element.querySelector('annotation'); - - if (texSource) { - if (_element.replaceWith) { - _element.replaceWith(texSource); - } else { - _element.parentNode.replaceChild(texSource, _element); - } - - texSource.innerHTML = copyDelimiters.inline[0] + texSource.innerHTML + copyDelimiters.inline[1]; - } - } // Switch display math to display delimiters. - - - var displays = fragment.querySelectorAll('.katex-display annotation'); - - for (var _i2 = 0; _i2 < displays.length; _i2++) { - var _element2 = displays[_i2]; - _element2.innerHTML = copyDelimiters.display[0] + _element2.innerHTML.substr(copyDelimiters.inline[0].length, _element2.innerHTML.length - copyDelimiters.inline[0].length - copyDelimiters.inline[1].length) + copyDelimiters.display[1]; - } - - return fragment; -}; -/* harmony default export */ var katex2tex = (katexReplaceWithTex); -// CONCATENATED MODULE: ./contrib/copy-tex/copy-tex.js - // Global copy handler to modify behavior on .katex elements. - -document.addEventListener('copy', function (event) { - var selection = window.getSelection(); - - if (selection.isCollapsed) { - return; // default action OK if selection is empty - } - - var fragment = selection.getRangeAt(0).cloneContents(); - - if (!fragment.querySelector('.katex-mathml')) { - return; // default action OK if no .katex-mathml elements - } // Preserve usual HTML copy/paste behavior. - - - var html = []; - - for (var i = 0; i < fragment.childNodes.length; i++) { - html.push(fragment.childNodes[i].outerHTML); - } - - event.clipboardData.setData('text/html', html.join('')); // Rewrite plain-text version. - - event.clipboardData.setData('text/plain', katex2tex(fragment).textContent); // Prevent normal copy handling. - - event.preventDefault(); -}); -// CONCATENATED MODULE: ./contrib/copy-tex/copy-tex.webpack.js -/** - * This is the webpack entry point for KaTeX. As ECMAScript doesn't support - * CSS modules natively, a separate entry point is used. - */ - - - -/***/ }) -/******/ ])["default"]; -}); \ No newline at end of file diff --git a/docs/static/katex/contrib/copy-tex.min.css b/docs/static/katex/contrib/copy-tex.min.css deleted file mode 100644 index 555ed1156b3..00000000000 --- a/docs/static/katex/contrib/copy-tex.min.css +++ /dev/null @@ -1 +0,0 @@ -.katex,.katex-display{user-select:all;-moz-user-select:all;-webkit-user-select:all;-ms-user-select:all} diff --git a/docs/static/katex/contrib/copy-tex.min.js b/docs/static/katex/contrib/copy-tex.min.js deleted file mode 100644 index e0354d6b9f1..00000000000 --- a/docs/static/katex/contrib/copy-tex.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var n=t();for(var r in n)("object"==typeof exports?exports:e)[r]=n[r]}}("undefined"!=typeof self?self:this,function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([function(e,t,n){},function(e,t,n){"use strict";n.r(t);n(0);var r={inline:["$","$"],display:["$$","$$"]},o=function(e,t){void 0===t&&(t=r);for(var n=e.querySelectorAll(".katex-mathml + .katex-html"),o=0;o element). -// Modifies fragment in-place. Useful for writing your own 'copy' handler, -// as in copy-tex.js. - -const katexReplaceWithTex = function katexReplaceWithTex(fragment, copyDelimiters) { - if (copyDelimiters === void 0) { - copyDelimiters = defaultCopyDelimiters; - } - - // Remove .katex-html blocks that are preceded by .katex-mathml blocks - // (which will get replaced below). - const katexHtml = fragment.querySelectorAll('.katex-mathml + .katex-html'); - - for (let i = 0; i < katexHtml.length; i++) { - const element = katexHtml[i]; - - if (element.remove) { - element.remove(null); - } else { - element.parentNode.removeChild(element); - } - } // Replace .katex-mathml elements with their annotation (TeX source) - // descendant, with inline delimiters. - - - const katexMathml = fragment.querySelectorAll('.katex-mathml'); - - for (let i = 0; i < katexMathml.length; i++) { - const element = katexMathml[i]; - const texSource = element.querySelector('annotation'); - - if (texSource) { - if (element.replaceWith) { - element.replaceWith(texSource); - } else { - element.parentNode.replaceChild(texSource, element); - } - - texSource.innerHTML = copyDelimiters.inline[0] + texSource.innerHTML + copyDelimiters.inline[1]; - } - } // Switch display math to display delimiters. - - - const displays = fragment.querySelectorAll('.katex-display annotation'); - - for (let i = 0; i < displays.length; i++) { - const element = displays[i]; - element.innerHTML = copyDelimiters.display[0] + element.innerHTML.substr(copyDelimiters.inline[0].length, element.innerHTML.length - copyDelimiters.inline[0].length - copyDelimiters.inline[1].length) + copyDelimiters.display[1]; - } - - return fragment; -}; - -document.addEventListener('copy', function (event) { - const selection = window.getSelection(); - - if (selection.isCollapsed) { - return; // default action OK if selection is empty - } - - const fragment = selection.getRangeAt(0).cloneContents(); - - if (!fragment.querySelector('.katex-mathml')) { - return; // default action OK if no .katex-mathml elements - } // Preserve usual HTML copy/paste behavior. - - - const html = []; - - for (let i = 0; i < fragment.childNodes.length; i++) { - html.push(fragment.childNodes[i].outerHTML); - } - - event.clipboardData.setData('text/html', html.join('')); // Rewrite plain-text version. - - event.clipboardData.setData('text/plain', katexReplaceWithTex(fragment).textContent); // Prevent normal copy handling. - - event.preventDefault(); -}); diff --git a/docs/static/katex/contrib/mathtex-script-type.js b/docs/static/katex/contrib/mathtex-script-type.js deleted file mode 100644 index daa01a6336c..00000000000 --- a/docs/static/katex/contrib/mathtex-script-type.js +++ /dev/null @@ -1,137 +0,0 @@ -(function webpackUniversalModuleDefinition(root, factory) { - if(typeof exports === 'object' && typeof module === 'object') - module.exports = factory(require("katex")); - else if(typeof define === 'function' && define.amd) - define(["katex"], factory); - else { - var a = typeof exports === 'object' ? factory(require("katex")) : factory(root["katex"]); - for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; - } -})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__0__) { -return /******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); -/******/ } -/******/ }; -/******/ -/******/ // define __esModule on exports -/******/ __webpack_require__.r = function(exports) { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ -/******/ // create a fake namespace object -/******/ // mode & 1: value is a module id, require it -/******/ // mode & 2: merge all properties of value into the ns -/******/ // mode & 4: return value when already ns object -/******/ // mode & 8|1: behave like require -/******/ __webpack_require__.t = function(value, mode) { -/******/ if(mode & 1) value = __webpack_require__(value); -/******/ if(mode & 8) return value; -/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; -/******/ var ns = Object.create(null); -/******/ __webpack_require__.r(ns); -/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); -/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); -/******/ return ns; -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; -/******/ -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = 1); -/******/ }) -/************************************************************************/ -/******/ ([ -/* 0 */ -/***/ (function(module, exports) { - -module.exports = __WEBPACK_EXTERNAL_MODULE__0__; - -/***/ }), -/* 1 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony import */ var katex__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0); -/* harmony import */ var katex__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(katex__WEBPACK_IMPORTED_MODULE_0__); - -var scripts = document.body.getElementsByTagName("script"); -scripts = Array.prototype.slice.call(scripts); -scripts.forEach(function (script) { - if (!script.type || !script.type.match(/math\/tex/i)) { - return -1; - } - - var display = script.type.match(/mode\s*=\s*display(;|\s|\n|$)/) != null; - var katexElement = document.createElement(display ? "div" : "span"); - katexElement.setAttribute("class", display ? "equation" : "inline-equation"); - - try { - katex__WEBPACK_IMPORTED_MODULE_0___default.a.render(script.text, katexElement, { - displayMode: display - }); - } catch (err) { - //console.error(err); linter doesn't like this - katexElement.textContent = script.text; - } - - script.parentNode.replaceChild(katexElement, script); -}); - -/***/ }) -/******/ ])["default"]; -}); \ No newline at end of file diff --git a/docs/static/katex/contrib/mathtex-script-type.min.js b/docs/static/katex/contrib/mathtex-script-type.min.js deleted file mode 100644 index ae9f5286912..00000000000 --- a/docs/static/katex/contrib/mathtex-script-type.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t(require("katex"));else if("function"==typeof define&&define.amd)define(["katex"],t);else{var r="object"==typeof exports?t(require("katex")):t(e.katex);for(var n in r)("object"==typeof exports?exports:e)[n]=r[n]}}("undefined"!=typeof self?self:this,function(e){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=1)}([function(t,r){t.exports=e},function(e,t,r){"use strict";r.r(t);var n=r(0),o=r.n(n),u=document.body.getElementsByTagName("script");(u=Array.prototype.slice.call(u)).forEach(function(e){if(!e.type||!e.type.match(/math\/tex/i))return-1;var t=null!=e.type.match(/mode\s*=\s*display(;|\s|\n|$)/),r=document.createElement(t?"div":"span");r.setAttribute("class",t?"equation":"inline-equation");try{o.a.render(e.text,r,{displayMode:t})}catch(t){r.textContent=e.text}e.parentNode.replaceChild(r,e)})}]).default}); \ No newline at end of file diff --git a/docs/static/katex/contrib/mathtex-script-type.mjs b/docs/static/katex/contrib/mathtex-script-type.mjs deleted file mode 100644 index 7cfb90e98ff..00000000000 --- a/docs/static/katex/contrib/mathtex-script-type.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import katex from '../katex.mjs'; - -let scripts = document.body.getElementsByTagName("script"); -scripts = Array.prototype.slice.call(scripts); -scripts.forEach(function (script) { - if (!script.type || !script.type.match(/math\/tex/i)) { - return -1; - } - - const display = script.type.match(/mode\s*=\s*display(;|\s|\n|$)/) != null; - const katexElement = document.createElement(display ? "div" : "span"); - katexElement.setAttribute("class", display ? "equation" : "inline-equation"); - - try { - katex.render(script.text, katexElement, { - displayMode: display - }); - } catch (err) { - //console.error(err); linter doesn't like this - katexElement.textContent = script.text; - } - - script.parentNode.replaceChild(katexElement, script); -}); diff --git a/docs/static/katex/contrib/mhchem.js b/docs/static/katex/contrib/mhchem.js deleted file mode 100644 index ad2dcf0fc7d..00000000000 --- a/docs/static/katex/contrib/mhchem.js +++ /dev/null @@ -1,3241 +0,0 @@ -(function webpackUniversalModuleDefinition(root, factory) { - if(typeof exports === 'object' && typeof module === 'object') - module.exports = factory(require("katex")); - else if(typeof define === 'function' && define.amd) - define(["katex"], factory); - else { - var a = typeof exports === 'object' ? factory(require("katex")) : factory(root["katex"]); - for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; - } -})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__0__) { -return /******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); -/******/ } -/******/ }; -/******/ -/******/ // define __esModule on exports -/******/ __webpack_require__.r = function(exports) { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ -/******/ // create a fake namespace object -/******/ // mode & 1: value is a module id, require it -/******/ // mode & 2: merge all properties of value into the ns -/******/ // mode & 4: return value when already ns object -/******/ // mode & 8|1: behave like require -/******/ __webpack_require__.t = function(value, mode) { -/******/ if(mode & 1) value = __webpack_require__(value); -/******/ if(mode & 8) return value; -/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; -/******/ var ns = Object.create(null); -/******/ __webpack_require__.r(ns); -/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); -/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); -/******/ return ns; -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; -/******/ -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = 1); -/******/ }) -/************************************************************************/ -/******/ ([ -/* 0 */ -/***/ (function(module, exports) { - -module.exports = __WEBPACK_EXTERNAL_MODULE__0__; - -/***/ }), -/* 1 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony import */ var katex__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0); -/* harmony import */ var katex__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(katex__WEBPACK_IMPORTED_MODULE_0__); -/* eslint-disable */ - -/* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */ - -/* vim: set ts=2 et sw=2 tw=80: */ - -/************************************************************* - * - * KaTeX mhchem.js - * - * This file implements a KaTeX version of mhchem version 3.3.0. - * It is adapted from MathJax/extensions/TeX/mhchem.js - * It differs from the MathJax version as follows: - * 1. The interface is changed so that it can be called from KaTeX, not MathJax. - * 2. \rlap and \llap are replaced with \mathrlap and \mathllap. - * 3. Four lines of code are edited in order to use \raisebox instead of \raise. - * 4. The reaction arrow code is simplified. All reaction arrows are rendered - * using KaTeX extensible arrows instead of building non-extensible arrows. - * 5. \tripledash vertical alignment is slightly adjusted. - * - * This code, as other KaTeX code, is released under the MIT license. - * - * /************************************************************* - * - * MathJax/extensions/TeX/mhchem.js - * - * Implements the \ce command for handling chemical formulas - * from the mhchem LaTeX package. - * - * --------------------------------------------------------------------- - * - * Copyright (c) 2011-2015 The MathJax Consortium - * Copyright (c) 2015-2018 Martin Hensel - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// -// Coding Style -// - use '' for identifiers that can by minified/uglified -// - use "" for strings that need to stay untouched -// version: "3.3.0" for MathJax and KaTeX -// Add \ce, \pu, and \tripledash to the KaTeX macros. -katex__WEBPACK_IMPORTED_MODULE_0___default.a.__defineMacro("\\ce", function (context) { - return chemParse(context.consumeArgs(1)[0], "ce"); -}); - -katex__WEBPACK_IMPORTED_MODULE_0___default.a.__defineMacro("\\pu", function (context) { - return chemParse(context.consumeArgs(1)[0], "pu"); -}); // Needed for \bond for the ~ forms -// Raise by 2.56mu, not 2mu. We're raising a hyphen-minus, U+002D, not -// a mathematical minus, U+2212. So we need that extra 0.56. - - -katex__WEBPACK_IMPORTED_MODULE_0___default.a.__defineMacro("\\tripledash", "{\\vphantom{-}\\raisebox{2.56mu}{$\\mkern2mu" + "\\tiny\\text{-}\\mkern1mu\\text{-}\\mkern1mu\\text{-}\\mkern2mu$}}"); - - // -// This is the main function for handing the \ce and \pu commands. -// It takes the argument to \ce or \pu and returns the corresponding TeX string. -// - -var chemParse = function chemParse(tokens, stateMachine) { - // Recreate the argument string from KaTeX's array of tokens. - var str = ""; - var expectedLoc = tokens[tokens.length - 1].loc.start; - - for (var i = tokens.length - 1; i >= 0; i--) { - if (tokens[i].loc.start > expectedLoc) { - // context.consumeArgs has eaten a space. - str += " "; - expectedLoc = tokens[i].loc.start; - } - - str += tokens[i].text; - expectedLoc += tokens[i].text.length; - } - - var tex = texify.go(mhchemParser.go(str, stateMachine)); - return tex; -}; // -// Core parser for mhchem syntax (recursive) -// - -/** @type {MhchemParser} */ - - -var mhchemParser = { - // - // Parses mchem \ce syntax - // - // Call like - // go("H2O"); - // - go: function go(input, stateMachine) { - if (!input) { - return []; - } - - if (stateMachine === undefined) { - stateMachine = 'ce'; - } - - var state = '0'; // - // String buffers for parsing: - // - // buffer.a == amount - // buffer.o == element - // buffer.b == left-side superscript - // buffer.p == left-side subscript - // buffer.q == right-side subscript - // buffer.d == right-side superscript - // - // buffer.r == arrow - // buffer.rdt == arrow, script above, type - // buffer.rd == arrow, script above, content - // buffer.rqt == arrow, script below, type - // buffer.rq == arrow, script below, content - // - // buffer.text_ - // buffer.rm - // etc. - // - // buffer.parenthesisLevel == int, starting at 0 - // buffer.sb == bool, space before - // buffer.beginsWithBond == bool - // - // These letters are also used as state names. - // - // Other states: - // 0 == begin of main part (arrow/operator unlikely) - // 1 == next entity - // 2 == next entity (arrow/operator unlikely) - // 3 == next atom - // c == macro - // - - /** @type {Buffer} */ - - var buffer = {}; - buffer['parenthesisLevel'] = 0; - input = input.replace(/\n/g, " "); - input = input.replace(/[\u2212\u2013\u2014\u2010]/g, "-"); - input = input.replace(/[\u2026]/g, "..."); // - // Looks through mhchemParser.transitions, to execute a matching action - // (recursive) - // - - var lastInput; - var watchdog = 10; - /** @type {ParserOutput[]} */ - - var output = []; - - while (true) { - if (lastInput !== input) { - watchdog = 10; - lastInput = input; - } else { - watchdog--; - } // - // Find actions in transition table - // - - - var machine = mhchemParser.stateMachines[stateMachine]; - var t = machine.transitions[state] || machine.transitions['*']; - - iterateTransitions: for (var i = 0; i < t.length; i++) { - var matches = mhchemParser.patterns.match_(t[i].pattern, input); - - if (matches) { - // - // Execute actions - // - var task = t[i].task; - - for (var iA = 0; iA < task.action_.length; iA++) { - var o; // - // Find and execute action - // - - if (machine.actions[task.action_[iA].type_]) { - o = machine.actions[task.action_[iA].type_](buffer, matches.match_, task.action_[iA].option); - } else if (mhchemParser.actions[task.action_[iA].type_]) { - o = mhchemParser.actions[task.action_[iA].type_](buffer, matches.match_, task.action_[iA].option); - } else { - throw ["MhchemBugA", "mhchem bug A. Please report. (" + task.action_[iA].type_ + ")"]; // Trying to use non-existing action - } // - // Add output - // - - - mhchemParser.concatArray(output, o); - } // - // Set next state, - // Shorten input, - // Continue with next character - // (= apply only one transition per position) - // - - - state = task.nextState || state; - - if (input.length > 0) { - if (!task.revisit) { - input = matches.remainder; - } - - if (!task.toContinue) { - break iterateTransitions; - } - } else { - return output; - } - } - } // - // Prevent infinite loop - // - - - if (watchdog <= 0) { - throw ["MhchemBugU", "mhchem bug U. Please report."]; // Unexpected character - } - } - }, - concatArray: function concatArray(a, b) { - if (b) { - if (Array.isArray(b)) { - for (var iB = 0; iB < b.length; iB++) { - a.push(b[iB]); - } - } else { - a.push(b); - } - } - }, - patterns: { - // - // Matching patterns - // either regexps or function that return null or {match_:"a", remainder:"bc"} - // - patterns: { - // property names must not look like integers ("2") for correct property traversal order, later on - 'empty': /^$/, - 'else': /^./, - 'else2': /^./, - 'space': /^\s/, - 'space A': /^\s(?=[A-Z\\$])/, - 'space$': /^\s$/, - 'a-z': /^[a-z]/, - 'x': /^x/, - 'x$': /^x$/, - 'i$': /^i$/, - 'letters': /^(?:[a-zA-Z\u03B1-\u03C9\u0391-\u03A9?@]|(?:\\(?:alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)(?:\s+|\{\}|(?![a-zA-Z]))))+/, - '\\greek': /^\\(?:alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)(?:\s+|\{\}|(?![a-zA-Z]))/, - 'one lowercase latin letter $': /^(?:([a-z])(?:$|[^a-zA-Z]))$/, - '$one lowercase latin letter$ $': /^\$(?:([a-z])(?:$|[^a-zA-Z]))\$$/, - 'one lowercase greek letter $': /^(?:\$?[\u03B1-\u03C9]\$?|\$?\\(?:alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega)\s*\$?)(?:\s+|\{\}|(?![a-zA-Z]))$/, - 'digits': /^[0-9]+/, - '-9.,9': /^[+\-]?(?:[0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\.[0-9]+))/, - '-9.,9 no missing 0': /^[+\-]?[0-9]+(?:[.,][0-9]+)?/, - '(-)(9.,9)(e)(99)': function e99(input) { - var m = input.match(/^(\+\-|\+\/\-|\+|\-|\\pm\s?)?([0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\.[0-9]+))?(\((?:[0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\.[0-9]+))\))?(?:([eE]|\s*(\*|x|\\times|\u00D7)\s*10\^)([+\-]?[0-9]+|\{[+\-]?[0-9]+\}))?/); - - if (m && m[0]) { - return { - match_: m.splice(1), - remainder: input.substr(m[0].length) - }; - } - - return null; - }, - '(-)(9)^(-9)': function _(input) { - var m = input.match(/^(\+\-|\+\/\-|\+|\-|\\pm\s?)?([0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\.[0-9]+)?)\^([+\-]?[0-9]+|\{[+\-]?[0-9]+\})/); - - if (m && m[0]) { - return { - match_: m.splice(1), - remainder: input.substr(m[0].length) - }; - } - - return null; - }, - 'state of aggregation $': function stateOfAggregation$(input) { - // ... or crystal system - var a = mhchemParser.patterns.findObserveGroups(input, "", /^\([a-z]{1,3}(?=[\),])/, ")", ""); // (aq), (aq,$\infty$), (aq, sat) - - if (a && a.remainder.match(/^($|[\s,;\)\]\}])/)) { - return a; - } // AND end of 'phrase' - - - var m = input.match(/^(?:\((?:\\ca\s?)?\$[amothc]\$\))/); // OR crystal system ($o$) (\ca$c$) - - if (m) { - return { - match_: m[0], - remainder: input.substr(m[0].length) - }; - } - - return null; - }, - '_{(state of aggregation)}$': /^_\{(\([a-z]{1,3}\))\}/, - '{[(': /^(?:\\\{|\[|\()/, - ')]}': /^(?:\)|\]|\\\})/, - ', ': /^[,;]\s*/, - ',': /^[,;]/, - '.': /^[.]/, - '. ': /^([.\u22C5\u00B7\u2022])\s*/, - '...': /^\.\.\.(?=$|[^.])/, - '* ': /^([*])\s*/, - '^{(...)}': function _(input) { - return mhchemParser.patterns.findObserveGroups(input, "^{", "", "", "}"); - }, - '^($...$)': function $$(input) { - return mhchemParser.patterns.findObserveGroups(input, "^", "$", "$", ""); - }, - '^a': /^\^([0-9]+|[^\\_])/, - '^\\x{}{}': function x(input) { - return mhchemParser.patterns.findObserveGroups(input, "^", /^\\[a-zA-Z]+\{/, "}", "", "", "{", "}", "", true); - }, - '^\\x{}': function x(input) { - return mhchemParser.patterns.findObserveGroups(input, "^", /^\\[a-zA-Z]+\{/, "}", ""); - }, - '^\\x': /^\^(\\[a-zA-Z]+)\s*/, - '^(-1)': /^\^(-?\d+)/, - '\'': /^'/, - '_{(...)}': function _(input) { - return mhchemParser.patterns.findObserveGroups(input, "_{", "", "", "}"); - }, - '_($...$)': function _$$(input) { - return mhchemParser.patterns.findObserveGroups(input, "_", "$", "$", ""); - }, - '_9': /^_([+\-]?[0-9]+|[^\\])/, - '_\\x{}{}': function _X(input) { - return mhchemParser.patterns.findObserveGroups(input, "_", /^\\[a-zA-Z]+\{/, "}", "", "", "{", "}", "", true); - }, - '_\\x{}': function _X(input) { - return mhchemParser.patterns.findObserveGroups(input, "_", /^\\[a-zA-Z]+\{/, "}", ""); - }, - '_\\x': /^_(\\[a-zA-Z]+)\s*/, - '^_': /^(?:\^(?=_)|\_(?=\^)|[\^_]$)/, - '{}': /^\{\}/, - '{...}': function _(input) { - return mhchemParser.patterns.findObserveGroups(input, "", "{", "}", ""); - }, - '{(...)}': function _(input) { - return mhchemParser.patterns.findObserveGroups(input, "{", "", "", "}"); - }, - '$...$': function $$(input) { - return mhchemParser.patterns.findObserveGroups(input, "", "$", "$", ""); - }, - '${(...)}$': function $$(input) { - return mhchemParser.patterns.findObserveGroups(input, "${", "", "", "}$"); - }, - '$(...)$': function $$(input) { - return mhchemParser.patterns.findObserveGroups(input, "$", "", "", "$"); - }, - '=<>': /^[=<>]/, - '#': /^[#\u2261]/, - '+': /^\+/, - '-$': /^-(?=[\s_},;\]/]|$|\([a-z]+\))/, - // -space -, -; -] -/ -$ -state-of-aggregation - '-9': /^-(?=[0-9])/, - '- orbital overlap': /^-(?=(?:[spd]|sp)(?:$|[\s,;\)\]\}]))/, - '-': /^-/, - 'pm-operator': /^(?:\\pm|\$\\pm\$|\+-|\+\/-)/, - 'operator': /^(?:\+|(?:[\-=<>]|<<|>>|\\approx|\$\\approx\$)(?=\s|$|-?[0-9]))/, - 'arrowUpDown': /^(?:v|\(v\)|\^|\(\^\))(?=$|[\s,;\)\]\}])/, - '\\bond{(...)}': function bond(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\bond{", "", "", "}"); - }, - '->': /^(?:<->|<-->|->|<-|<=>>|<<=>|<=>|[\u2192\u27F6\u21CC])/, - 'CMT': /^[CMT](?=\[)/, - '[(...)]': function _(input) { - return mhchemParser.patterns.findObserveGroups(input, "[", "", "", "]"); - }, - '1st-level escape': /^(&|\\\\|\\hline)\s*/, - '\\,': /^(?:\\[,\ ;:])/, - // \\x - but output no space before - '\\x{}{}': function x(input) { - return mhchemParser.patterns.findObserveGroups(input, "", /^\\[a-zA-Z]+\{/, "}", "", "", "{", "}", "", true); - }, - '\\x{}': function x(input) { - return mhchemParser.patterns.findObserveGroups(input, "", /^\\[a-zA-Z]+\{/, "}", ""); - }, - '\\ca': /^\\ca(?:\s+|(?![a-zA-Z]))/, - '\\x': /^(?:\\[a-zA-Z]+\s*|\\[_&{}%])/, - 'orbital': /^(?:[0-9]{1,2}[spdfgh]|[0-9]{0,2}sp)(?=$|[^a-zA-Z])/, - // only those with numbers in front, because the others will be formatted correctly anyway - 'others': /^[\/~|]/, - '\\frac{(...)}': function frac(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\frac{", "", "", "}", "{", "", "", "}"); - }, - '\\overset{(...)}': function overset(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\overset{", "", "", "}", "{", "", "", "}"); - }, - "\\underset{(...)}": function underset(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\underset{", "", "", "}", "{", "", "", "}"); - }, - "\\underbrace{(...)}": function underbrace(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\underbrace{", "", "", "}_", "{", "", "", "}"); - }, - '\\color{(...)}0': function color0(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\color{", "", "", "}"); - }, - '\\color{(...)}{(...)}1': function color1(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\color{", "", "", "}", "{", "", "", "}"); - }, - '\\color(...){(...)}2': function color2(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\color", "\\", "", /^(?=\{)/, "{", "", "", "}"); - }, - '\\ce{(...)}': function ce(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\ce{", "", "", "}"); - }, - 'oxidation$': /^(?:[+-][IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/, - 'd-oxidation$': /^(?:[+-]?\s?[IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/, - // 0 could be oxidation or charge - 'roman numeral': /^[IVX]+/, - '1/2$': /^[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+(?:\$[a-z]\$|[a-z])?$/, - 'amount': function amount(input) { - var match; // e.g. 2, 0.5, 1/2, -2, n/2, +; $a$ could be added later in parsing - - match = input.match(/^(?:(?:(?:\([+\-]?[0-9]+\/[0-9]+\)|[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+|[+\-]?[0-9]+[.,][0-9]+|[+\-]?\.[0-9]+|[+\-]?[0-9]+)(?:[a-z](?=\s*[A-Z]))?)|[+\-]?[a-z](?=\s*[A-Z])|\+(?!\s))/); - - if (match) { - return { - match_: match[0], - remainder: input.substr(match[0].length) - }; - } - - var a = mhchemParser.patterns.findObserveGroups(input, "", "$", "$", ""); - - if (a) { - // e.g. $2n-1$, $-$ - match = a.match_.match(/^\$(?:\(?[+\-]?(?:[0-9]*[a-z]?[+\-])?[0-9]*[a-z](?:[+\-][0-9]*[a-z]?)?\)?|\+|-)\$$/); - - if (match) { - return { - match_: match[0], - remainder: input.substr(match[0].length) - }; - } - } - - return null; - }, - 'amount2': function amount2(input) { - return this['amount'](input); - }, - '(KV letters),': /^(?:[A-Z][a-z]{0,2}|i)(?=,)/, - 'formula$': function formula$(input) { - if (input.match(/^\([a-z]+\)$/)) { - return null; - } // state of aggregation = no formula - - - var match = input.match(/^(?:[a-z]|(?:[0-9\ \+\-\,\.\(\)]+[a-z])+[0-9\ \+\-\,\.\(\)]*|(?:[a-z][0-9\ \+\-\,\.\(\)]+)+[a-z]?)$/); - - if (match) { - return { - match_: match[0], - remainder: input.substr(match[0].length) - }; - } - - return null; - }, - 'uprightEntities': /^(?:pH|pOH|pC|pK|iPr|iBu)(?=$|[^a-zA-Z])/, - '/': /^\s*(\/)\s*/, - '//': /^\s*(\/\/)\s*/, - '*': /^\s*[*.]\s*/ - }, - findObserveGroups: function findObserveGroups(input, begExcl, begIncl, endIncl, endExcl, beg2Excl, beg2Incl, end2Incl, end2Excl, combine) { - /** @type {{(input: string, pattern: string | RegExp): string | string[] | null;}} */ - var _match = function _match(input, pattern) { - if (typeof pattern === "string") { - if (input.indexOf(pattern) !== 0) { - return null; - } - - return pattern; - } else { - var match = input.match(pattern); - - if (!match) { - return null; - } - - return match[0]; - } - }; - /** @type {{(input: string, i: number, endChars: string | RegExp): {endMatchBegin: number, endMatchEnd: number} | null;}} */ - - - var _findObserveGroups = function _findObserveGroups(input, i, endChars) { - var braces = 0; - - while (i < input.length) { - var a = input.charAt(i); - - var match = _match(input.substr(i), endChars); - - if (match !== null && braces === 0) { - return { - endMatchBegin: i, - endMatchEnd: i + match.length - }; - } else if (a === "{") { - braces++; - } else if (a === "}") { - if (braces === 0) { - throw ["ExtraCloseMissingOpen", "Extra close brace or missing open brace"]; - } else { - braces--; - } - } - - i++; - } - - if (braces > 0) { - return null; - } - - return null; - }; - - var match = _match(input, begExcl); - - if (match === null) { - return null; - } - - input = input.substr(match.length); - match = _match(input, begIncl); - - if (match === null) { - return null; - } - - var e = _findObserveGroups(input, match.length, endIncl || endExcl); - - if (e === null) { - return null; - } - - var match1 = input.substring(0, endIncl ? e.endMatchEnd : e.endMatchBegin); - - if (!(beg2Excl || beg2Incl)) { - return { - match_: match1, - remainder: input.substr(e.endMatchEnd) - }; - } else { - var group2 = this.findObserveGroups(input.substr(e.endMatchEnd), beg2Excl, beg2Incl, end2Incl, end2Excl); - - if (group2 === null) { - return null; - } - /** @type {string[]} */ - - - var matchRet = [match1, group2.match_]; - return { - match_: combine ? matchRet.join("") : matchRet, - remainder: group2.remainder - }; - } - }, - // - // Matching function - // e.g. match("a", input) will look for the regexp called "a" and see if it matches - // returns null or {match_:"a", remainder:"bc"} - // - match_: function match_(m, input) { - var pattern = mhchemParser.patterns.patterns[m]; - - if (pattern === undefined) { - throw ["MhchemBugP", "mhchem bug P. Please report. (" + m + ")"]; // Trying to use non-existing pattern - } else if (typeof pattern === "function") { - return mhchemParser.patterns.patterns[m](input); // cannot use cached var pattern here, because some pattern functions need this===mhchemParser - } else { - // RegExp - var match = input.match(pattern); - - if (match) { - var mm; - - if (match[2]) { - mm = [match[1], match[2]]; - } else if (match[1]) { - mm = match[1]; - } else { - mm = match[0]; - } - - return { - match_: mm, - remainder: input.substr(match[0].length) - }; - } - - return null; - } - } - }, - // - // Generic state machine actions - // - actions: { - 'a=': function a(buffer, m) { - buffer.a = (buffer.a || "") + m; - }, - 'b=': function b(buffer, m) { - buffer.b = (buffer.b || "") + m; - }, - 'p=': function p(buffer, m) { - buffer.p = (buffer.p || "") + m; - }, - 'o=': function o(buffer, m) { - buffer.o = (buffer.o || "") + m; - }, - 'q=': function q(buffer, m) { - buffer.q = (buffer.q || "") + m; - }, - 'd=': function d(buffer, m) { - buffer.d = (buffer.d || "") + m; - }, - 'rm=': function rm(buffer, m) { - buffer.rm = (buffer.rm || "") + m; - }, - 'text=': function text(buffer, m) { - buffer.text_ = (buffer.text_ || "") + m; - }, - 'insert': function insert(buffer, m, a) { - return { - type_: a - }; - }, - 'insert+p1': function insertP1(buffer, m, a) { - return { - type_: a, - p1: m - }; - }, - 'insert+p1+p2': function insertP1P2(buffer, m, a) { - return { - type_: a, - p1: m[0], - p2: m[1] - }; - }, - 'copy': function copy(buffer, m) { - return m; - }, - 'rm': function rm(buffer, m) { - return { - type_: 'rm', - p1: m || "" - }; - }, - 'text': function text(buffer, m) { - return mhchemParser.go(m, 'text'); - }, - '{text}': function text(buffer, m) { - var ret = ["{"]; - mhchemParser.concatArray(ret, mhchemParser.go(m, 'text')); - ret.push("}"); - return ret; - }, - 'tex-math': function texMath(buffer, m) { - return mhchemParser.go(m, 'tex-math'); - }, - 'tex-math tight': function texMathTight(buffer, m) { - return mhchemParser.go(m, 'tex-math tight'); - }, - 'bond': function bond(buffer, m, k) { - return { - type_: 'bond', - kind_: k || m - }; - }, - 'color0-output': function color0Output(buffer, m) { - return { - type_: 'color0', - color: m[0] - }; - }, - 'ce': function ce(buffer, m) { - return mhchemParser.go(m); - }, - '1/2': function _(buffer, m) { - /** @type {ParserOutput[]} */ - var ret = []; - - if (m.match(/^[+\-]/)) { - ret.push(m.substr(0, 1)); - m = m.substr(1); - } - - var n = m.match(/^([0-9]+|\$[a-z]\$|[a-z])\/([0-9]+)(\$[a-z]\$|[a-z])?$/); - n[1] = n[1].replace(/\$/g, ""); - ret.push({ - type_: 'frac', - p1: n[1], - p2: n[2] - }); - - if (n[3]) { - n[3] = n[3].replace(/\$/g, ""); - ret.push({ - type_: 'tex-math', - p1: n[3] - }); - } - - return ret; - }, - '9,9': function _(buffer, m) { - return mhchemParser.go(m, '9,9'); - } - }, - // - // createTransitions - // convert { 'letter': { 'state': { action_: 'output' } } } to { 'state' => [ { pattern: 'letter', task: { action_: [{type_: 'output'}] } } ] } - // with expansion of 'a|b' to 'a' and 'b' (at 2 places) - // - createTransitions: function createTransitions(o) { - var pattern, state; - /** @type {string[]} */ - - var stateArray; - var i; // - // 1. Collect all states - // - - /** @type {Transitions} */ - - var transitions = {}; - - for (pattern in o) { - for (state in o[pattern]) { - stateArray = state.split("|"); - o[pattern][state].stateArray = stateArray; - - for (i = 0; i < stateArray.length; i++) { - transitions[stateArray[i]] = []; - } - } - } // - // 2. Fill states - // - - - for (pattern in o) { - for (state in o[pattern]) { - stateArray = o[pattern][state].stateArray || []; - - for (i = 0; i < stateArray.length; i++) { - // - // 2a. Normalize actions into array: 'text=' ==> [{type_:'text='}] - // (Note to myself: Resolving the function here would be problematic. It would need .bind (for *this*) and currying (for *option*).) - // - - /** @type {any} */ - var p = o[pattern][state]; - - if (p.action_) { - p.action_ = [].concat(p.action_); - - for (var k = 0; k < p.action_.length; k++) { - if (typeof p.action_[k] === "string") { - p.action_[k] = { - type_: p.action_[k] - }; - } - } - } else { - p.action_ = []; - } // - // 2.b Multi-insert - // - - - var patternArray = pattern.split("|"); - - for (var j = 0; j < patternArray.length; j++) { - if (stateArray[i] === '*') { - // insert into all - for (var t in transitions) { - transitions[t].push({ - pattern: patternArray[j], - task: p - }); - } - } else { - transitions[stateArray[i]].push({ - pattern: patternArray[j], - task: p - }); - } - } - } - } - } - - return transitions; - }, - stateMachines: {} -}; // -// Definition of state machines -// - -mhchemParser.stateMachines = { - // - // \ce state machines - // - //#region ce - 'ce': { - // main parser - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': { - action_: 'output' - } - }, - 'else': { - '0|1|2': { - action_: 'beginsWithBond=false', - revisit: true, - toContinue: true - } - }, - 'oxidation$': { - '0': { - action_: 'oxidation-output' - } - }, - 'CMT': { - 'r': { - action_: 'rdt=', - nextState: 'rt' - }, - 'rd': { - action_: 'rqt=', - nextState: 'rdt' - } - }, - 'arrowUpDown': { - '0|1|2|as': { - action_: ['sb=false', 'output', 'operator'], - nextState: '1' - } - }, - 'uprightEntities': { - '0|1|2': { - action_: ['o=', 'output'], - nextState: '1' - } - }, - 'orbital': { - '0|1|2|3': { - action_: 'o=', - nextState: 'o' - } - }, - '->': { - '0|1|2|3': { - action_: 'r=', - nextState: 'r' - }, - 'a|as': { - action_: ['output', 'r='], - nextState: 'r' - }, - '*': { - action_: ['output', 'r='], - nextState: 'r' - } - }, - '+': { - 'o': { - action_: 'd= kv', - nextState: 'd' - }, - 'd|D': { - action_: 'd=', - nextState: 'd' - }, - 'q': { - action_: 'd=', - nextState: 'qd' - }, - 'qd|qD': { - action_: 'd=', - nextState: 'qd' - }, - 'dq': { - action_: ['output', 'd='], - nextState: 'd' - }, - '3': { - action_: ['sb=false', 'output', 'operator'], - nextState: '0' - } - }, - 'amount': { - '0|2': { - action_: 'a=', - nextState: 'a' - } - }, - 'pm-operator': { - '0|1|2|a|as': { - action_: ['sb=false', 'output', { - type_: 'operator', - option: '\\pm' - }], - nextState: '0' - } - }, - 'operator': { - '0|1|2|a|as': { - action_: ['sb=false', 'output', 'operator'], - nextState: '0' - } - }, - '-$': { - 'o|q': { - action_: ['charge or bond', 'output'], - nextState: 'qd' - }, - 'd': { - action_: 'd=', - nextState: 'd' - }, - 'D': { - action_: ['output', { - type_: 'bond', - option: "-" - }], - nextState: '3' - }, - 'q': { - action_: 'd=', - nextState: 'qd' - }, - 'qd': { - action_: 'd=', - nextState: 'qd' - }, - 'qD|dq': { - action_: ['output', { - type_: 'bond', - option: "-" - }], - nextState: '3' - } - }, - '-9': { - '3|o': { - action_: ['output', { - type_: 'insert', - option: 'hyphen' - }], - nextState: '3' - } - }, - '- orbital overlap': { - 'o': { - action_: ['output', { - type_: 'insert', - option: 'hyphen' - }], - nextState: '2' - }, - 'd': { - action_: ['output', { - type_: 'insert', - option: 'hyphen' - }], - nextState: '2' - } - }, - '-': { - '0|1|2': { - action_: [{ - type_: 'output', - option: 1 - }, 'beginsWithBond=true', { - type_: 'bond', - option: "-" - }], - nextState: '3' - }, - '3': { - action_: { - type_: 'bond', - option: "-" - } - }, - 'a': { - action_: ['output', { - type_: 'insert', - option: 'hyphen' - }], - nextState: '2' - }, - 'as': { - action_: [{ - type_: 'output', - option: 2 - }, { - type_: 'bond', - option: "-" - }], - nextState: '3' - }, - 'b': { - action_: 'b=' - }, - 'o': { - action_: { - type_: '- after o/d', - option: false - }, - nextState: '2' - }, - 'q': { - action_: { - type_: '- after o/d', - option: false - }, - nextState: '2' - }, - 'd|qd|dq': { - action_: { - type_: '- after o/d', - option: true - }, - nextState: '2' - }, - 'D|qD|p': { - action_: ['output', { - type_: 'bond', - option: "-" - }], - nextState: '3' - } - }, - 'amount2': { - '1|3': { - action_: 'a=', - nextState: 'a' - } - }, - 'letters': { - '0|1|2|3|a|as|b|p|bp|o': { - action_: 'o=', - nextState: 'o' - }, - 'q|dq': { - action_: ['output', 'o='], - nextState: 'o' - }, - 'd|D|qd|qD': { - action_: 'o after d', - nextState: 'o' - } - }, - 'digits': { - 'o': { - action_: 'q=', - nextState: 'q' - }, - 'd|D': { - action_: 'q=', - nextState: 'dq' - }, - 'q': { - action_: ['output', 'o='], - nextState: 'o' - }, - 'a': { - action_: 'o=', - nextState: 'o' - } - }, - 'space A': { - 'b|p|bp': {} - }, - 'space': { - 'a': { - nextState: 'as' - }, - '0': { - action_: 'sb=false' - }, - '1|2': { - action_: 'sb=true' - }, - 'r|rt|rd|rdt|rdq': { - action_: 'output', - nextState: '0' - }, - '*': { - action_: ['output', 'sb=true'], - nextState: '1' - } - }, - '1st-level escape': { - '1|2': { - action_: ['output', { - type_: 'insert+p1', - option: '1st-level escape' - }] - }, - '*': { - action_: ['output', { - type_: 'insert+p1', - option: '1st-level escape' - }], - nextState: '0' - } - }, - '[(...)]': { - 'r|rt': { - action_: 'rd=', - nextState: 'rd' - }, - 'rd|rdt': { - action_: 'rq=', - nextState: 'rdq' - } - }, - '...': { - 'o|d|D|dq|qd|qD': { - action_: ['output', { - type_: 'bond', - option: "..." - }], - nextState: '3' - }, - '*': { - action_: [{ - type_: 'output', - option: 1 - }, { - type_: 'insert', - option: 'ellipsis' - }], - nextState: '1' - } - }, - '. |* ': { - '*': { - action_: ['output', { - type_: 'insert', - option: 'addition compound' - }], - nextState: '1' - } - }, - 'state of aggregation $': { - '*': { - action_: ['output', 'state of aggregation'], - nextState: '1' - } - }, - '{[(': { - 'a|as|o': { - action_: ['o=', 'output', 'parenthesisLevel++'], - nextState: '2' - }, - '0|1|2|3': { - action_: ['o=', 'output', 'parenthesisLevel++'], - nextState: '2' - }, - '*': { - action_: ['output', 'o=', 'output', 'parenthesisLevel++'], - nextState: '2' - } - }, - ')]}': { - '0|1|2|3|b|p|bp|o': { - action_: ['o=', 'parenthesisLevel--'], - nextState: 'o' - }, - 'a|as|d|D|q|qd|qD|dq': { - action_: ['output', 'o=', 'parenthesisLevel--'], - nextState: 'o' - } - }, - ', ': { - '*': { - action_: ['output', 'comma'], - nextState: '0' - } - }, - '^_': { - // ^ and _ without a sensible argument - '*': {} - }, - '^{(...)}|^($...$)': { - '0|1|2|as': { - action_: 'b=', - nextState: 'b' - }, - 'p': { - action_: 'b=', - nextState: 'bp' - }, - '3|o': { - action_: 'd= kv', - nextState: 'D' - }, - 'q': { - action_: 'd=', - nextState: 'qD' - }, - 'd|D|qd|qD|dq': { - action_: ['output', 'd='], - nextState: 'D' - } - }, - '^a|^\\x{}{}|^\\x{}|^\\x|\'': { - '0|1|2|as': { - action_: 'b=', - nextState: 'b' - }, - 'p': { - action_: 'b=', - nextState: 'bp' - }, - '3|o': { - action_: 'd= kv', - nextState: 'd' - }, - 'q': { - action_: 'd=', - nextState: 'qd' - }, - 'd|qd|D|qD': { - action_: 'd=' - }, - 'dq': { - action_: ['output', 'd='], - nextState: 'd' - } - }, - '_{(state of aggregation)}$': { - 'd|D|q|qd|qD|dq': { - action_: ['output', 'q='], - nextState: 'q' - } - }, - '_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x': { - '0|1|2|as': { - action_: 'p=', - nextState: 'p' - }, - 'b': { - action_: 'p=', - nextState: 'bp' - }, - '3|o': { - action_: 'q=', - nextState: 'q' - }, - 'd|D': { - action_: 'q=', - nextState: 'dq' - }, - 'q|qd|qD|dq': { - action_: ['output', 'q='], - nextState: 'q' - } - }, - '=<>': { - '0|1|2|3|a|as|o|q|d|D|qd|qD|dq': { - action_: [{ - type_: 'output', - option: 2 - }, 'bond'], - nextState: '3' - } - }, - '#': { - '0|1|2|3|a|as|o': { - action_: [{ - type_: 'output', - option: 2 - }, { - type_: 'bond', - option: "#" - }], - nextState: '3' - } - }, - '{}': { - '*': { - action_: { - type_: 'output', - option: 1 - }, - nextState: '1' - } - }, - '{...}': { - '0|1|2|3|a|as|b|p|bp': { - action_: 'o=', - nextState: 'o' - }, - 'o|d|D|q|qd|qD|dq': { - action_: ['output', 'o='], - nextState: 'o' - } - }, - '$...$': { - 'a': { - action_: 'a=' - }, - // 2$n$ - '0|1|2|3|as|b|p|bp|o': { - action_: 'o=', - nextState: 'o' - }, - // not 'amount' - 'as|o': { - action_: 'o=' - }, - 'q|d|D|qd|qD|dq': { - action_: ['output', 'o='], - nextState: 'o' - } - }, - '\\bond{(...)}': { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'bond'], - nextState: "3" - } - }, - '\\frac{(...)}': { - '*': { - action_: [{ - type_: 'output', - option: 1 - }, 'frac-output'], - nextState: '3' - } - }, - '\\overset{(...)}': { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'overset-output'], - nextState: '3' - } - }, - "\\underset{(...)}": { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'underset-output'], - nextState: '3' - } - }, - "\\underbrace{(...)}": { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'underbrace-output'], - nextState: '3' - } - }, - '\\color{(...)}{(...)}1|\\color(...){(...)}2': { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'color-output'], - nextState: '3' - } - }, - '\\color{(...)}0': { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'color0-output'] - } - }, - '\\ce{(...)}': { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'ce'], - nextState: '3' - } - }, - '\\,': { - '*': { - action_: [{ - type_: 'output', - option: 1 - }, 'copy'], - nextState: '1' - } - }, - '\\x{}{}|\\x{}|\\x': { - '0|1|2|3|a|as|b|p|bp|o|c0': { - action_: ['o=', 'output'], - nextState: '3' - }, - '*': { - action_: ['output', 'o=', 'output'], - nextState: '3' - } - }, - 'others': { - '*': { - action_: [{ - type_: 'output', - option: 1 - }, 'copy'], - nextState: '3' - } - }, - 'else2': { - 'a': { - action_: 'a to o', - nextState: 'o', - revisit: true - }, - 'as': { - action_: ['output', 'sb=true'], - nextState: '1', - revisit: true - }, - 'r|rt|rd|rdt|rdq': { - action_: ['output'], - nextState: '0', - revisit: true - }, - '*': { - action_: ['output', 'copy'], - nextState: '3' - } - } - }), - actions: { - 'o after d': function oAfterD(buffer, m) { - var ret; - - if ((buffer.d || "").match(/^[0-9]+$/)) { - var tmp = buffer.d; - buffer.d = undefined; - ret = this['output'](buffer); - buffer.b = tmp; - } else { - ret = this['output'](buffer); - } - - mhchemParser.actions['o='](buffer, m); - return ret; - }, - 'd= kv': function dKv(buffer, m) { - buffer.d = m; - buffer.dType = 'kv'; - }, - 'charge or bond': function chargeOrBond(buffer, m) { - if (buffer['beginsWithBond']) { - /** @type {ParserOutput[]} */ - var ret = []; - mhchemParser.concatArray(ret, this['output'](buffer)); - mhchemParser.concatArray(ret, mhchemParser.actions['bond'](buffer, m, "-")); - return ret; - } else { - buffer.d = m; - } - }, - '- after o/d': function afterOD(buffer, m, isAfterD) { - var c1 = mhchemParser.patterns.match_('orbital', buffer.o || ""); - var c2 = mhchemParser.patterns.match_('one lowercase greek letter $', buffer.o || ""); - var c3 = mhchemParser.patterns.match_('one lowercase latin letter $', buffer.o || ""); - var c4 = mhchemParser.patterns.match_('$one lowercase latin letter$ $', buffer.o || ""); - var hyphenFollows = m === "-" && (c1 && c1.remainder === "" || c2 || c3 || c4); - - if (hyphenFollows && !buffer.a && !buffer.b && !buffer.p && !buffer.d && !buffer.q && !c1 && c3) { - buffer.o = '$' + buffer.o + '$'; - } - /** @type {ParserOutput[]} */ - - - var ret = []; - - if (hyphenFollows) { - mhchemParser.concatArray(ret, this['output'](buffer)); - ret.push({ - type_: 'hyphen' - }); - } else { - c1 = mhchemParser.patterns.match_('digits', buffer.d || ""); - - if (isAfterD && c1 && c1.remainder === '') { - mhchemParser.concatArray(ret, mhchemParser.actions['d='](buffer, m)); - mhchemParser.concatArray(ret, this['output'](buffer)); - } else { - mhchemParser.concatArray(ret, this['output'](buffer)); - mhchemParser.concatArray(ret, mhchemParser.actions['bond'](buffer, m, "-")); - } - } - - return ret; - }, - 'a to o': function aToO(buffer) { - buffer.o = buffer.a; - buffer.a = undefined; - }, - 'sb=true': function sbTrue(buffer) { - buffer.sb = true; - }, - 'sb=false': function sbFalse(buffer) { - buffer.sb = false; - }, - 'beginsWithBond=true': function beginsWithBondTrue(buffer) { - buffer['beginsWithBond'] = true; - }, - 'beginsWithBond=false': function beginsWithBondFalse(buffer) { - buffer['beginsWithBond'] = false; - }, - 'parenthesisLevel++': function parenthesisLevel(buffer) { - buffer['parenthesisLevel']++; - }, - 'parenthesisLevel--': function parenthesisLevel(buffer) { - buffer['parenthesisLevel']--; - }, - 'state of aggregation': function stateOfAggregation(buffer, m) { - return { - type_: 'state of aggregation', - p1: mhchemParser.go(m, 'o') - }; - }, - 'comma': function comma(buffer, m) { - var a = m.replace(/\s*$/, ''); - var withSpace = a !== m; - - if (withSpace && buffer['parenthesisLevel'] === 0) { - return { - type_: 'comma enumeration L', - p1: a - }; - } else { - return { - type_: 'comma enumeration M', - p1: a - }; - } - }, - 'output': function output(buffer, m, entityFollows) { - // entityFollows: - // undefined = if we have nothing else to output, also ignore the just read space (buffer.sb) - // 1 = an entity follows, never omit the space if there was one just read before (can only apply to state 1) - // 2 = 1 + the entity can have an amount, so output a\, instead of converting it to o (can only apply to states a|as) - - /** @type {ParserOutput | ParserOutput[]} */ - var ret; - - if (!buffer.r) { - ret = []; - - if (!buffer.a && !buffer.b && !buffer.p && !buffer.o && !buffer.q && !buffer.d && !entityFollows) {//ret = []; - } else { - if (buffer.sb) { - ret.push({ - type_: 'entitySkip' - }); - } - - if (!buffer.o && !buffer.q && !buffer.d && !buffer.b && !buffer.p && entityFollows !== 2) { - buffer.o = buffer.a; - buffer.a = undefined; - } else if (!buffer.o && !buffer.q && !buffer.d && (buffer.b || buffer.p)) { - buffer.o = buffer.a; - buffer.d = buffer.b; - buffer.q = buffer.p; - buffer.a = buffer.b = buffer.p = undefined; - } else { - if (buffer.o && buffer.dType === 'kv' && mhchemParser.patterns.match_('d-oxidation$', buffer.d || "")) { - buffer.dType = 'oxidation'; - } else if (buffer.o && buffer.dType === 'kv' && !buffer.q) { - buffer.dType = undefined; - } - } - - ret.push({ - type_: 'chemfive', - a: mhchemParser.go(buffer.a, 'a'), - b: mhchemParser.go(buffer.b, 'bd'), - p: mhchemParser.go(buffer.p, 'pq'), - o: mhchemParser.go(buffer.o, 'o'), - q: mhchemParser.go(buffer.q, 'pq'), - d: mhchemParser.go(buffer.d, buffer.dType === 'oxidation' ? 'oxidation' : 'bd'), - dType: buffer.dType - }); - } - } else { - // r - - /** @type {ParserOutput[]} */ - var rd; - - if (buffer.rdt === 'M') { - rd = mhchemParser.go(buffer.rd, 'tex-math'); - } else if (buffer.rdt === 'T') { - rd = [{ - type_: 'text', - p1: buffer.rd || "" - }]; - } else { - rd = mhchemParser.go(buffer.rd); - } - /** @type {ParserOutput[]} */ - - - var rq; - - if (buffer.rqt === 'M') { - rq = mhchemParser.go(buffer.rq, 'tex-math'); - } else if (buffer.rqt === 'T') { - rq = [{ - type_: 'text', - p1: buffer.rq || "" - }]; - } else { - rq = mhchemParser.go(buffer.rq); - } - - ret = { - type_: 'arrow', - r: buffer.r, - rd: rd, - rq: rq - }; - } - - for (var p in buffer) { - if (p !== 'parenthesisLevel' && p !== 'beginsWithBond') { - delete buffer[p]; - } - } - - return ret; - }, - 'oxidation-output': function oxidationOutput(buffer, m) { - var ret = ["{"]; - mhchemParser.concatArray(ret, mhchemParser.go(m, 'oxidation')); - ret.push("}"); - return ret; - }, - 'frac-output': function fracOutput(buffer, m) { - return { - type_: 'frac-ce', - p1: mhchemParser.go(m[0]), - p2: mhchemParser.go(m[1]) - }; - }, - 'overset-output': function oversetOutput(buffer, m) { - return { - type_: 'overset', - p1: mhchemParser.go(m[0]), - p2: mhchemParser.go(m[1]) - }; - }, - 'underset-output': function undersetOutput(buffer, m) { - return { - type_: 'underset', - p1: mhchemParser.go(m[0]), - p2: mhchemParser.go(m[1]) - }; - }, - 'underbrace-output': function underbraceOutput(buffer, m) { - return { - type_: 'underbrace', - p1: mhchemParser.go(m[0]), - p2: mhchemParser.go(m[1]) - }; - }, - 'color-output': function colorOutput(buffer, m) { - return { - type_: 'color', - color1: m[0], - color2: mhchemParser.go(m[1]) - }; - }, - 'r=': function r(buffer, m) { - buffer.r = m; - }, - 'rdt=': function rdt(buffer, m) { - buffer.rdt = m; - }, - 'rd=': function rd(buffer, m) { - buffer.rd = m; - }, - 'rqt=': function rqt(buffer, m) { - buffer.rqt = m; - }, - 'rq=': function rq(buffer, m) { - buffer.rq = m; - }, - 'operator': function operator(buffer, m, p1) { - return { - type_: 'operator', - kind_: p1 || m - }; - } - } - }, - 'a': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': {} - }, - '1/2$': { - '0': { - action_: '1/2' - } - }, - 'else': { - '0': { - nextState: '1', - revisit: true - } - }, - '$(...)$': { - '*': { - action_: 'tex-math tight', - nextState: '1' - } - }, - ',': { - '*': { - action_: { - type_: 'insert', - option: 'commaDecimal' - } - } - }, - 'else2': { - '*': { - action_: 'copy' - } - } - }), - actions: {} - }, - 'o': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': {} - }, - '1/2$': { - '0': { - action_: '1/2' - } - }, - 'else': { - '0': { - nextState: '1', - revisit: true - } - }, - 'letters': { - '*': { - action_: 'rm' - } - }, - '\\ca': { - '*': { - action_: { - type_: 'insert', - option: 'circa' - } - } - }, - '\\x{}{}|\\x{}|\\x': { - '*': { - action_: 'copy' - } - }, - '${(...)}$|$(...)$': { - '*': { - action_: 'tex-math' - } - }, - '{(...)}': { - '*': { - action_: '{text}' - } - }, - 'else2': { - '*': { - action_: 'copy' - } - } - }), - actions: {} - }, - 'text': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': { - action_: 'output' - } - }, - '{...}': { - '*': { - action_: 'text=' - } - }, - '${(...)}$|$(...)$': { - '*': { - action_: 'tex-math' - } - }, - '\\greek': { - '*': { - action_: ['output', 'rm'] - } - }, - '\\,|\\x{}{}|\\x{}|\\x': { - '*': { - action_: ['output', 'copy'] - } - }, - 'else': { - '*': { - action_: 'text=' - } - } - }), - actions: { - 'output': function output(buffer) { - if (buffer.text_) { - /** @type {ParserOutput} */ - var ret = { - type_: 'text', - p1: buffer.text_ - }; - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - } - } - } - }, - 'pq': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': {} - }, - 'state of aggregation $': { - '*': { - action_: 'state of aggregation' - } - }, - 'i$': { - '0': { - nextState: '!f', - revisit: true - } - }, - '(KV letters),': { - '0': { - action_: 'rm', - nextState: '0' - } - }, - 'formula$': { - '0': { - nextState: 'f', - revisit: true - } - }, - '1/2$': { - '0': { - action_: '1/2' - } - }, - 'else': { - '0': { - nextState: '!f', - revisit: true - } - }, - '${(...)}$|$(...)$': { - '*': { - action_: 'tex-math' - } - }, - '{(...)}': { - '*': { - action_: 'text' - } - }, - 'a-z': { - 'f': { - action_: 'tex-math' - } - }, - 'letters': { - '*': { - action_: 'rm' - } - }, - '-9.,9': { - '*': { - action_: '9,9' - } - }, - ',': { - '*': { - action_: { - type_: 'insert+p1', - option: 'comma enumeration S' - } - } - }, - '\\color{(...)}{(...)}1|\\color(...){(...)}2': { - '*': { - action_: 'color-output' - } - }, - '\\color{(...)}0': { - '*': { - action_: 'color0-output' - } - }, - '\\ce{(...)}': { - '*': { - action_: 'ce' - } - }, - '\\,|\\x{}{}|\\x{}|\\x': { - '*': { - action_: 'copy' - } - }, - 'else2': { - '*': { - action_: 'copy' - } - } - }), - actions: { - 'state of aggregation': function stateOfAggregation(buffer, m) { - return { - type_: 'state of aggregation subscript', - p1: mhchemParser.go(m, 'o') - }; - }, - 'color-output': function colorOutput(buffer, m) { - return { - type_: 'color', - color1: m[0], - color2: mhchemParser.go(m[1], 'pq') - }; - } - } - }, - 'bd': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': {} - }, - 'x$': { - '0': { - nextState: '!f', - revisit: true - } - }, - 'formula$': { - '0': { - nextState: 'f', - revisit: true - } - }, - 'else': { - '0': { - nextState: '!f', - revisit: true - } - }, - '-9.,9 no missing 0': { - '*': { - action_: '9,9' - } - }, - '.': { - '*': { - action_: { - type_: 'insert', - option: 'electron dot' - } - } - }, - 'a-z': { - 'f': { - action_: 'tex-math' - } - }, - 'x': { - '*': { - action_: { - type_: 'insert', - option: 'KV x' - } - } - }, - 'letters': { - '*': { - action_: 'rm' - } - }, - '\'': { - '*': { - action_: { - type_: 'insert', - option: 'prime' - } - } - }, - '${(...)}$|$(...)$': { - '*': { - action_: 'tex-math' - } - }, - '{(...)}': { - '*': { - action_: 'text' - } - }, - '\\color{(...)}{(...)}1|\\color(...){(...)}2': { - '*': { - action_: 'color-output' - } - }, - '\\color{(...)}0': { - '*': { - action_: 'color0-output' - } - }, - '\\ce{(...)}': { - '*': { - action_: 'ce' - } - }, - '\\,|\\x{}{}|\\x{}|\\x': { - '*': { - action_: 'copy' - } - }, - 'else2': { - '*': { - action_: 'copy' - } - } - }), - actions: { - 'color-output': function colorOutput(buffer, m) { - return { - type_: 'color', - color1: m[0], - color2: mhchemParser.go(m[1], 'bd') - }; - } - } - }, - 'oxidation': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': {} - }, - 'roman numeral': { - '*': { - action_: 'roman-numeral' - } - }, - '${(...)}$|$(...)$': { - '*': { - action_: 'tex-math' - } - }, - 'else': { - '*': { - action_: 'copy' - } - } - }), - actions: { - 'roman-numeral': function romanNumeral(buffer, m) { - return { - type_: 'roman numeral', - p1: m || "" - }; - } - } - }, - 'tex-math': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': { - action_: 'output' - } - }, - '\\ce{(...)}': { - '*': { - action_: ['output', 'ce'] - } - }, - '{...}|\\,|\\x{}{}|\\x{}|\\x': { - '*': { - action_: 'o=' - } - }, - 'else': { - '*': { - action_: 'o=' - } - } - }), - actions: { - 'output': function output(buffer) { - if (buffer.o) { - /** @type {ParserOutput} */ - var ret = { - type_: 'tex-math', - p1: buffer.o - }; - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - } - } - } - }, - 'tex-math tight': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': { - action_: 'output' - } - }, - '\\ce{(...)}': { - '*': { - action_: ['output', 'ce'] - } - }, - '{...}|\\,|\\x{}{}|\\x{}|\\x': { - '*': { - action_: 'o=' - } - }, - '-|+': { - '*': { - action_: 'tight operator' - } - }, - 'else': { - '*': { - action_: 'o=' - } - } - }), - actions: { - 'tight operator': function tightOperator(buffer, m) { - buffer.o = (buffer.o || "") + "{" + m + "}"; - }, - 'output': function output(buffer) { - if (buffer.o) { - /** @type {ParserOutput} */ - var ret = { - type_: 'tex-math', - p1: buffer.o - }; - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - } - } - } - }, - '9,9': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': {} - }, - ',': { - '*': { - action_: 'comma' - } - }, - 'else': { - '*': { - action_: 'copy' - } - } - }), - actions: { - 'comma': function comma() { - return { - type_: 'commaDecimal' - }; - } - } - }, - //#endregion - // - // \pu state machines - // - //#region pu - 'pu': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': { - action_: 'output' - } - }, - 'space$': { - '*': { - action_: ['output', 'space'] - } - }, - '{[(|)]}': { - '0|a': { - action_: 'copy' - } - }, - '(-)(9)^(-9)': { - '0': { - action_: 'number^', - nextState: 'a' - } - }, - '(-)(9.,9)(e)(99)': { - '0': { - action_: 'enumber', - nextState: 'a' - } - }, - 'space': { - '0|a': {} - }, - 'pm-operator': { - '0|a': { - action_: { - type_: 'operator', - option: '\\pm' - }, - nextState: '0' - } - }, - 'operator': { - '0|a': { - action_: 'copy', - nextState: '0' - } - }, - '//': { - 'd': { - action_: 'o=', - nextState: '/' - } - }, - '/': { - 'd': { - action_: 'o=', - nextState: '/' - } - }, - '{...}|else': { - '0|d': { - action_: 'd=', - nextState: 'd' - }, - 'a': { - action_: ['space', 'd='], - nextState: 'd' - }, - '/|q': { - action_: 'q=', - nextState: 'q' - } - } - }), - actions: { - 'enumber': function enumber(buffer, m) { - /** @type {ParserOutput[]} */ - var ret = []; - - if (m[0] === "+-" || m[0] === "+/-") { - ret.push("\\pm "); - } else if (m[0]) { - ret.push(m[0]); - } - - if (m[1]) { - mhchemParser.concatArray(ret, mhchemParser.go(m[1], 'pu-9,9')); - - if (m[2]) { - if (m[2].match(/[,.]/)) { - mhchemParser.concatArray(ret, mhchemParser.go(m[2], 'pu-9,9')); - } else { - ret.push(m[2]); - } - } - - m[3] = m[4] || m[3]; - - if (m[3]) { - m[3] = m[3].trim(); - - if (m[3] === "e" || m[3].substr(0, 1) === "*") { - ret.push({ - type_: 'cdot' - }); - } else { - ret.push({ - type_: 'times' - }); - } - } - } - - if (m[3]) { - ret.push("10^{" + m[5] + "}"); - } - - return ret; - }, - 'number^': function number(buffer, m) { - /** @type {ParserOutput[]} */ - var ret = []; - - if (m[0] === "+-" || m[0] === "+/-") { - ret.push("\\pm "); - } else if (m[0]) { - ret.push(m[0]); - } - - mhchemParser.concatArray(ret, mhchemParser.go(m[1], 'pu-9,9')); - ret.push("^{" + m[2] + "}"); - return ret; - }, - 'operator': function operator(buffer, m, p1) { - return { - type_: 'operator', - kind_: p1 || m - }; - }, - 'space': function space() { - return { - type_: 'pu-space-1' - }; - }, - 'output': function output(buffer) { - /** @type {ParserOutput | ParserOutput[]} */ - var ret; - var md = mhchemParser.patterns.match_('{(...)}', buffer.d || ""); - - if (md && md.remainder === '') { - buffer.d = md.match_; - } - - var mq = mhchemParser.patterns.match_('{(...)}', buffer.q || ""); - - if (mq && mq.remainder === '') { - buffer.q = mq.match_; - } - - if (buffer.d) { - buffer.d = buffer.d.replace(/\u00B0C|\^oC|\^{o}C/g, "{}^{\\circ}C"); - buffer.d = buffer.d.replace(/\u00B0F|\^oF|\^{o}F/g, "{}^{\\circ}F"); - } - - if (buffer.q) { - // fraction - buffer.q = buffer.q.replace(/\u00B0C|\^oC|\^{o}C/g, "{}^{\\circ}C"); - buffer.q = buffer.q.replace(/\u00B0F|\^oF|\^{o}F/g, "{}^{\\circ}F"); - var b5 = { - d: mhchemParser.go(buffer.d, 'pu'), - q: mhchemParser.go(buffer.q, 'pu') - }; - - if (buffer.o === '//') { - ret = { - type_: 'pu-frac', - p1: b5.d, - p2: b5.q - }; - } else { - ret = b5.d; - - if (b5.d.length > 1 || b5.q.length > 1) { - ret.push({ - type_: ' / ' - }); - } else { - ret.push({ - type_: '/' - }); - } - - mhchemParser.concatArray(ret, b5.q); - } - } else { - // no fraction - ret = mhchemParser.go(buffer.d, 'pu-2'); - } - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - } - } - }, - 'pu-2': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': { - action_: 'output' - } - }, - '*': { - '*': { - action_: ['output', 'cdot'], - nextState: '0' - } - }, - '\\x': { - '*': { - action_: 'rm=' - } - }, - 'space': { - '*': { - action_: ['output', 'space'], - nextState: '0' - } - }, - '^{(...)}|^(-1)': { - '1': { - action_: '^(-1)' - } - }, - '-9.,9': { - '0': { - action_: 'rm=', - nextState: '0' - }, - '1': { - action_: '^(-1)', - nextState: '0' - } - }, - '{...}|else': { - '*': { - action_: 'rm=', - nextState: '1' - } - } - }), - actions: { - 'cdot': function cdot() { - return { - type_: 'tight cdot' - }; - }, - '^(-1)': function _(buffer, m) { - buffer.rm += "^{" + m + "}"; - }, - 'space': function space() { - return { - type_: 'pu-space-2' - }; - }, - 'output': function output(buffer) { - /** @type {ParserOutput | ParserOutput[]} */ - var ret = []; - - if (buffer.rm) { - var mrm = mhchemParser.patterns.match_('{(...)}', buffer.rm || ""); - - if (mrm && mrm.remainder === '') { - ret = mhchemParser.go(mrm.match_, 'pu'); - } else { - ret = { - type_: 'rm', - p1: buffer.rm - }; - } - } - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - } - } - }, - 'pu-9,9': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '0': { - action_: 'output-0' - }, - 'o': { - action_: 'output-o' - } - }, - ',': { - '0': { - action_: ['output-0', 'comma'], - nextState: 'o' - } - }, - '.': { - '0': { - action_: ['output-0', 'copy'], - nextState: 'o' - } - }, - 'else': { - '*': { - action_: 'text=' - } - } - }), - actions: { - 'comma': function comma() { - return { - type_: 'commaDecimal' - }; - }, - 'output-0': function output0(buffer) { - /** @type {ParserOutput[]} */ - var ret = []; - buffer.text_ = buffer.text_ || ""; - - if (buffer.text_.length > 4) { - var a = buffer.text_.length % 3; - - if (a === 0) { - a = 3; - } - - for (var i = buffer.text_.length - 3; i > 0; i -= 3) { - ret.push(buffer.text_.substr(i, 3)); - ret.push({ - type_: '1000 separator' - }); - } - - ret.push(buffer.text_.substr(0, a)); - ret.reverse(); - } else { - ret.push(buffer.text_); - } - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - }, - 'output-o': function outputO(buffer) { - /** @type {ParserOutput[]} */ - var ret = []; - buffer.text_ = buffer.text_ || ""; - - if (buffer.text_.length > 4) { - var a = buffer.text_.length - 3; - - for (var i = 0; i < a; i += 3) { - ret.push(buffer.text_.substr(i, 3)); - ret.push({ - type_: '1000 separator' - }); - } - - ret.push(buffer.text_.substr(i)); - } else { - ret.push(buffer.text_); - } - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - } - } //#endregion - - } -}; // -// texify: Take MhchemParser output and convert it to TeX -// - -/** @type {Texify} */ - -var texify = { - go: function go(input, isInner) { - // (recursive, max 4 levels) - if (!input) { - return ""; - } - - var res = ""; - var cee = false; - - for (var i = 0; i < input.length; i++) { - var inputi = input[i]; - - if (typeof inputi === "string") { - res += inputi; - } else { - res += texify._go2(inputi); - - if (inputi.type_ === '1st-level escape') { - cee = true; - } - } - } - - if (!isInner && !cee && res) { - res = "{" + res + "}"; - } - - return res; - }, - _goInner: function _goInner(input) { - if (!input) { - return input; - } - - return texify.go(input, true); - }, - _go2: function _go2(buf) { - /** @type {undefined | string} */ - var res; - - switch (buf.type_) { - case 'chemfive': - res = ""; - var b5 = { - a: texify._goInner(buf.a), - b: texify._goInner(buf.b), - p: texify._goInner(buf.p), - o: texify._goInner(buf.o), - q: texify._goInner(buf.q), - d: texify._goInner(buf.d) - }; // - // a - // - - if (b5.a) { - if (b5.a.match(/^[+\-]/)) { - b5.a = "{" + b5.a + "}"; - } - - res += b5.a + "\\,"; - } // - // b and p - // - - - if (b5.b || b5.p) { - res += "{\\vphantom{X}}"; - res += "^{\\hphantom{" + (b5.b || "") + "}}_{\\hphantom{" + (b5.p || "") + "}}"; - res += "{\\vphantom{X}}"; - res += "^{\\smash[t]{\\vphantom{2}}\\mathllap{" + (b5.b || "") + "}}"; - res += "_{\\vphantom{2}\\mathllap{\\smash[t]{" + (b5.p || "") + "}}}"; - } // - // o - // - - - if (b5.o) { - if (b5.o.match(/^[+\-]/)) { - b5.o = "{" + b5.o + "}"; - } - - res += b5.o; - } // - // q and d - // - - - if (buf.dType === 'kv') { - if (b5.d || b5.q) { - res += "{\\vphantom{X}}"; - } - - if (b5.d) { - res += "^{" + b5.d + "}"; - } - - if (b5.q) { - res += "_{\\smash[t]{" + b5.q + "}}"; - } - } else if (buf.dType === 'oxidation') { - if (b5.d) { - res += "{\\vphantom{X}}"; - res += "^{" + b5.d + "}"; - } - - if (b5.q) { - res += "{\\vphantom{X}}"; - res += "_{\\smash[t]{" + b5.q + "}}"; - } - } else { - if (b5.q) { - res += "{\\vphantom{X}}"; - res += "_{\\smash[t]{" + b5.q + "}}"; - } - - if (b5.d) { - res += "{\\vphantom{X}}"; - res += "^{" + b5.d + "}"; - } - } - - break; - - case 'rm': - res = "\\mathrm{" + buf.p1 + "}"; - break; - - case 'text': - if (buf.p1.match(/[\^_]/)) { - buf.p1 = buf.p1.replace(" ", "~").replace("-", "\\text{-}"); - res = "\\mathrm{" + buf.p1 + "}"; - } else { - res = "\\text{" + buf.p1 + "}"; - } - - break; - - case 'roman numeral': - res = "\\mathrm{" + buf.p1 + "}"; - break; - - case 'state of aggregation': - res = "\\mskip2mu " + texify._goInner(buf.p1); - break; - - case 'state of aggregation subscript': - res = "\\mskip1mu " + texify._goInner(buf.p1); - break; - - case 'bond': - res = texify._getBond(buf.kind_); - - if (!res) { - throw ["MhchemErrorBond", "mhchem Error. Unknown bond type (" + buf.kind_ + ")"]; - } - - break; - - case 'frac': - var c = "\\frac{" + buf.p1 + "}{" + buf.p2 + "}"; - res = "\\mathchoice{\\textstyle" + c + "}{" + c + "}{" + c + "}{" + c + "}"; - break; - - case 'pu-frac': - var d = "\\frac{" + texify._goInner(buf.p1) + "}{" + texify._goInner(buf.p2) + "}"; - res = "\\mathchoice{\\textstyle" + d + "}{" + d + "}{" + d + "}{" + d + "}"; - break; - - case 'tex-math': - res = buf.p1 + " "; - break; - - case 'frac-ce': - res = "\\frac{" + texify._goInner(buf.p1) + "}{" + texify._goInner(buf.p2) + "}"; - break; - - case 'overset': - res = "\\overset{" + texify._goInner(buf.p1) + "}{" + texify._goInner(buf.p2) + "}"; - break; - - case 'underset': - res = "\\underset{" + texify._goInner(buf.p1) + "}{" + texify._goInner(buf.p2) + "}"; - break; - - case 'underbrace': - res = "\\underbrace{" + texify._goInner(buf.p1) + "}_{" + texify._goInner(buf.p2) + "}"; - break; - - case 'color': - res = "{\\color{" + buf.color1 + "}{" + texify._goInner(buf.color2) + "}}"; - break; - - case 'color0': - res = "\\color{" + buf.color + "}"; - break; - - case 'arrow': - var b6 = { - rd: texify._goInner(buf.rd), - rq: texify._goInner(buf.rq) - }; - - var arrow = "\\x" + texify._getArrow(buf.r); - - if (b6.rq) { - arrow += "[{" + b6.rq + "}]"; - } - - if (b6.rd) { - arrow += "{" + b6.rd + "}"; - } else { - arrow += "{}"; - } - - res = arrow; - break; - - case 'operator': - res = texify._getOperator(buf.kind_); - break; - - case '1st-level escape': - res = buf.p1 + " "; // &, \\\\, \\hlin - - break; - - case 'space': - res = " "; - break; - - case 'entitySkip': - res = "~"; - break; - - case 'pu-space-1': - res = "~"; - break; - - case 'pu-space-2': - res = "\\mkern3mu "; - break; - - case '1000 separator': - res = "\\mkern2mu "; - break; - - case 'commaDecimal': - res = "{,}"; - break; - - case 'comma enumeration L': - res = "{" + buf.p1 + "}\\mkern6mu "; - break; - - case 'comma enumeration M': - res = "{" + buf.p1 + "}\\mkern3mu "; - break; - - case 'comma enumeration S': - res = "{" + buf.p1 + "}\\mkern1mu "; - break; - - case 'hyphen': - res = "\\text{-}"; - break; - - case 'addition compound': - res = "\\,{\\cdot}\\,"; - break; - - case 'electron dot': - res = "\\mkern1mu \\bullet\\mkern1mu "; - break; - - case 'KV x': - res = "{\\times}"; - break; - - case 'prime': - res = "\\prime "; - break; - - case 'cdot': - res = "\\cdot "; - break; - - case 'tight cdot': - res = "\\mkern1mu{\\cdot}\\mkern1mu "; - break; - - case 'times': - res = "\\times "; - break; - - case 'circa': - res = "{\\sim}"; - break; - - case '^': - res = "uparrow"; - break; - - case 'v': - res = "downarrow"; - break; - - case 'ellipsis': - res = "\\ldots "; - break; - - case '/': - res = "/"; - break; - - case ' / ': - res = "\\,/\\,"; - break; - - default: - assertNever(buf); - throw ["MhchemBugT", "mhchem bug T. Please report."]; - // Missing texify rule or unknown MhchemParser output - } - - assertString(res); - return res; - }, - _getArrow: function _getArrow(a) { - switch (a) { - case "->": - return "rightarrow"; - - case "\u2192": - return "rightarrow"; - - case "\u27F6": - return "rightarrow"; - - case "<-": - return "leftarrow"; - - case "<->": - return "leftrightarrow"; - - case "<-->": - return "rightleftarrows"; - - case "<=>": - return "rightleftharpoons"; - - case "\u21CC": - return "rightleftharpoons"; - - case "<=>>": - return "rightequilibrium"; - - case "<<=>": - return "leftequilibrium"; - - default: - assertNever(a); - throw ["MhchemBugT", "mhchem bug T. Please report."]; - } - }, - _getBond: function _getBond(a) { - switch (a) { - case "-": - return "{-}"; - - case "1": - return "{-}"; - - case "=": - return "{=}"; - - case "2": - return "{=}"; - - case "#": - return "{\\equiv}"; - - case "3": - return "{\\equiv}"; - - case "~": - return "{\\tripledash}"; - - case "~-": - return "{\\mathrlap{\\raisebox{-.1em}{$-$}}\\raisebox{.1em}{$\\tripledash$}}"; - - case "~=": - return "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}"; - - case "~--": - return "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}"; - - case "-~-": - return "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$-$}}\\tripledash}"; - - case "...": - return "{{\\cdot}{\\cdot}{\\cdot}}"; - - case "....": - return "{{\\cdot}{\\cdot}{\\cdot}{\\cdot}}"; - - case "->": - return "{\\rightarrow}"; - - case "<-": - return "{\\leftarrow}"; - - case "<": - return "{<}"; - - case ">": - return "{>}"; - - default: - assertNever(a); - throw ["MhchemBugT", "mhchem bug T. Please report."]; - } - }, - _getOperator: function _getOperator(a) { - switch (a) { - case "+": - return " {}+{} "; - - case "-": - return " {}-{} "; - - case "=": - return " {}={} "; - - case "<": - return " {}<{} "; - - case ">": - return " {}>{} "; - - case "<<": - return " {}\\ll{} "; - - case ">>": - return " {}\\gg{} "; - - case "\\pm": - return " {}\\pm{} "; - - case "\\approx": - return " {}\\approx{} "; - - case "$\\approx$": - return " {}\\approx{} "; - - case "v": - return " \\downarrow{} "; - - case "(v)": - return " \\downarrow{} "; - - case "^": - return " \\uparrow{} "; - - case "(^)": - return " \\uparrow{} "; - - default: - assertNever(a); - throw ["MhchemBugT", "mhchem bug T. Please report."]; - } - } -}; // -// Helpers for code anaylsis -// Will show type error at calling position -// - -/** @param {number} a */ - -function assertNever(a) {} -/** @param {string} a */ - - -function assertString(a) {} - -/***/ }) -/******/ ])["default"]; -}); \ No newline at end of file diff --git a/docs/static/katex/contrib/mhchem.min.js b/docs/static/katex/contrib/mhchem.min.js deleted file mode 100644 index 432a76af0cd..00000000000 --- a/docs/static/katex/contrib/mhchem.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(t,e){if("object"==typeof exports&&"object"==typeof module)module.exports=e(require("katex"));else if("function"==typeof define&&define.amd)define(["katex"],e);else{var n="object"==typeof exports?e(require("katex")):e(t.katex);for(var o in n)("object"==typeof exports?exports:t)[o]=n[o]}}("undefined"!=typeof self?self:this,function(t){return function(t){var e={};function n(o){if(e[o])return e[o].exports;var a=e[o]={i:o,l:!1,exports:{}};return t[o].call(a.exports,a,a.exports,n),a.l=!0,a.exports}return n.m=t,n.c=e,n.d=function(t,e,o){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:o})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var a in t)n.d(o,a,function(e){return t[e]}.bind(null,a));return o},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=1)}([function(e,n){e.exports=t},function(t,e,n){"use strict";n.r(e);var o=n(0),a=n.n(o);a.a.__defineMacro("\\ce",function(t){return r(t.consumeArgs(1)[0],"ce")}),a.a.__defineMacro("\\pu",function(t){return r(t.consumeArgs(1)[0],"pu")}),a.a.__defineMacro("\\tripledash","{\\vphantom{-}\\raisebox{2.56mu}{$\\mkern2mu\\tiny\\text{-}\\mkern1mu\\text{-}\\mkern1mu\\text{-}\\mkern2mu$}}");var r=function(t,e){for(var n="",o=t[t.length-1].loc.start,a=t.length-1;a>=0;a--)t[a].loc.start>o&&(n+=" ",o=t[a].loc.start),n+=t[a].text,o+=t[a].text.length;return c.go(i.go(n,e))},i={go:function(t,e){if(!t)return[];void 0===e&&(e="ce");var n,o="0",a={};a.parenthesisLevel=0,t=(t=(t=t.replace(/\n/g," ")).replace(/[\u2212\u2013\u2014\u2010]/g,"-")).replace(/[\u2026]/g,"...");for(var r=10,c=[];;){n!==t?(r=10,n=t):r--;var u=i.stateMachines[e],p=u.transitions[o]||u.transitions["*"];t:for(var s=0;s0))return c;if(d.revisit||(t=_.remainder),!d.toContinue)break t}}if(r<=0)throw["MhchemBugU","mhchem bug U. Please report."]}},concatArray:function(t,e){if(e)if(Array.isArray(e))for(var n=0;n":/^[=<>]/,"#":/^[#\u2261]/,"+":/^\+/,"-$":/^-(?=[\s_},;\]/]|$|\([a-z]+\))/,"-9":/^-(?=[0-9])/,"- orbital overlap":/^-(?=(?:[spd]|sp)(?:$|[\s,;\)\]\}]))/,"-":/^-/,"pm-operator":/^(?:\\pm|\$\\pm\$|\+-|\+\/-)/,operator:/^(?:\+|(?:[\-=<>]|<<|>>|\\approx|\$\\approx\$)(?=\s|$|-?[0-9]))/,arrowUpDown:/^(?:v|\(v\)|\^|\(\^\))(?=$|[\s,;\)\]\}])/,"\\bond{(...)}":function(t){return i.patterns.findObserveGroups(t,"\\bond{","","","}")},"->":/^(?:<->|<-->|->|<-|<=>>|<<=>|<=>|[\u2192\u27F6\u21CC])/,CMT:/^[CMT](?=\[)/,"[(...)]":function(t){return i.patterns.findObserveGroups(t,"[","","","]")},"1st-level escape":/^(&|\\\\|\\hline)\s*/,"\\,":/^(?:\\[,\ ;:])/,"\\x{}{}":function(t){return i.patterns.findObserveGroups(t,"",/^\\[a-zA-Z]+\{/,"}","","","{","}","",!0)},"\\x{}":function(t){return i.patterns.findObserveGroups(t,"",/^\\[a-zA-Z]+\{/,"}","")},"\\ca":/^\\ca(?:\s+|(?![a-zA-Z]))/,"\\x":/^(?:\\[a-zA-Z]+\s*|\\[_&{}%])/,orbital:/^(?:[0-9]{1,2}[spdfgh]|[0-9]{0,2}sp)(?=$|[^a-zA-Z])/,others:/^[\/~|]/,"\\frac{(...)}":function(t){return i.patterns.findObserveGroups(t,"\\frac{","","","}","{","","","}")},"\\overset{(...)}":function(t){return i.patterns.findObserveGroups(t,"\\overset{","","","}","{","","","}")},"\\underset{(...)}":function(t){return i.patterns.findObserveGroups(t,"\\underset{","","","}","{","","","}")},"\\underbrace{(...)}":function(t){return i.patterns.findObserveGroups(t,"\\underbrace{","","","}_","{","","","}")},"\\color{(...)}0":function(t){return i.patterns.findObserveGroups(t,"\\color{","","","}")},"\\color{(...)}{(...)}1":function(t){return i.patterns.findObserveGroups(t,"\\color{","","","}","{","","","}")},"\\color(...){(...)}2":function(t){return i.patterns.findObserveGroups(t,"\\color","\\","",/^(?=\{)/,"{","","","}")},"\\ce{(...)}":function(t){return i.patterns.findObserveGroups(t,"\\ce{","","","}")},oxidation$:/^(?:[+-][IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/,"d-oxidation$":/^(?:[+-]?\s?[IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/,"roman numeral":/^[IVX]+/,"1/2$":/^[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+(?:\$[a-z]\$|[a-z])?$/,amount:function(t){var e;if(e=t.match(/^(?:(?:(?:\([+\-]?[0-9]+\/[0-9]+\)|[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+|[+\-]?[0-9]+[.,][0-9]+|[+\-]?\.[0-9]+|[+\-]?[0-9]+)(?:[a-z](?=\s*[A-Z]))?)|[+\-]?[a-z](?=\s*[A-Z])|\+(?!\s))/))return{match_:e[0],remainder:t.substr(e[0].length)};var n=i.patterns.findObserveGroups(t,"","$","$","");return n&&(e=n.match_.match(/^\$(?:\(?[+\-]?(?:[0-9]*[a-z]?[+\-])?[0-9]*[a-z](?:[+\-][0-9]*[a-z]?)?\)?|\+|-)\$$/))?{match_:e[0],remainder:t.substr(e[0].length)}:null},amount2:function(t){return this.amount(t)},"(KV letters),":/^(?:[A-Z][a-z]{0,2}|i)(?=,)/,formula$:function(t){if(t.match(/^\([a-z]+\)$/))return null;var e=t.match(/^(?:[a-z]|(?:[0-9\ \+\-\,\.\(\)]+[a-z])+[0-9\ \+\-\,\.\(\)]*|(?:[a-z][0-9\ \+\-\,\.\(\)]+)+[a-z]?)$/);return e?{match_:e[0],remainder:t.substr(e[0].length)}:null},uprightEntities:/^(?:pH|pOH|pC|pK|iPr|iBu)(?=$|[^a-zA-Z])/,"/":/^\s*(\/)\s*/,"//":/^\s*(\/\/)\s*/,"*":/^\s*[*.]\s*/},findObserveGroups:function(t,e,n,o,a,r,i,c,u,p){var s=function(t,e){if("string"==typeof e)return 0!==t.indexOf(e)?null:e;var n=t.match(e);return n?n[0]:null},_=s(t,e);if(null===_)return null;if(t=t.substr(_.length),null===(_=s(t,n)))return null;var d=function(t,e,n){for(var o=0;e":{"0|1|2|3":{action_:"r=",nextState:"r"},"a|as":{action_:["output","r="],nextState:"r"},"*":{action_:["output","r="],nextState:"r"}},"+":{o:{action_:"d= kv",nextState:"d"},"d|D":{action_:"d=",nextState:"d"},q:{action_:"d=",nextState:"qd"},"qd|qD":{action_:"d=",nextState:"qd"},dq:{action_:["output","d="],nextState:"d"},3:{action_:["sb=false","output","operator"],nextState:"0"}},amount:{"0|2":{action_:"a=",nextState:"a"}},"pm-operator":{"0|1|2|a|as":{action_:["sb=false","output",{type_:"operator",option:"\\pm"}],nextState:"0"}},operator:{"0|1|2|a|as":{action_:["sb=false","output","operator"],nextState:"0"}},"-$":{"o|q":{action_:["charge or bond","output"],nextState:"qd"},d:{action_:"d=",nextState:"d"},D:{action_:["output",{type_:"bond",option:"-"}],nextState:"3"},q:{action_:"d=",nextState:"qd"},qd:{action_:"d=",nextState:"qd"},"qD|dq":{action_:["output",{type_:"bond",option:"-"}],nextState:"3"}},"-9":{"3|o":{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"3"}},"- orbital overlap":{o:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"},d:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"}},"-":{"0|1|2":{action_:[{type_:"output",option:1},"beginsWithBond=true",{type_:"bond",option:"-"}],nextState:"3"},3:{action_:{type_:"bond",option:"-"}},a:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"},as:{action_:[{type_:"output",option:2},{type_:"bond",option:"-"}],nextState:"3"},b:{action_:"b="},o:{action_:{type_:"- after o/d",option:!1},nextState:"2"},q:{action_:{type_:"- after o/d",option:!1},nextState:"2"},"d|qd|dq":{action_:{type_:"- after o/d",option:!0},nextState:"2"},"D|qD|p":{action_:["output",{type_:"bond",option:"-"}],nextState:"3"}},amount2:{"1|3":{action_:"a=",nextState:"a"}},letters:{"0|1|2|3|a|as|b|p|bp|o":{action_:"o=",nextState:"o"},"q|dq":{action_:["output","o="],nextState:"o"},"d|D|qd|qD":{action_:"o after d",nextState:"o"}},digits:{o:{action_:"q=",nextState:"q"},"d|D":{action_:"q=",nextState:"dq"},q:{action_:["output","o="],nextState:"o"},a:{action_:"o=",nextState:"o"}},"space A":{"b|p|bp":{}},space:{a:{nextState:"as"},0:{action_:"sb=false"},"1|2":{action_:"sb=true"},"r|rt|rd|rdt|rdq":{action_:"output",nextState:"0"},"*":{action_:["output","sb=true"],nextState:"1"}},"1st-level escape":{"1|2":{action_:["output",{type_:"insert+p1",option:"1st-level escape"}]},"*":{action_:["output",{type_:"insert+p1",option:"1st-level escape"}],nextState:"0"}},"[(...)]":{"r|rt":{action_:"rd=",nextState:"rd"},"rd|rdt":{action_:"rq=",nextState:"rdq"}},"...":{"o|d|D|dq|qd|qD":{action_:["output",{type_:"bond",option:"..."}],nextState:"3"},"*":{action_:[{type_:"output",option:1},{type_:"insert",option:"ellipsis"}],nextState:"1"}},". |* ":{"*":{action_:["output",{type_:"insert",option:"addition compound"}],nextState:"1"}},"state of aggregation $":{"*":{action_:["output","state of aggregation"],nextState:"1"}},"{[(":{"a|as|o":{action_:["o=","output","parenthesisLevel++"],nextState:"2"},"0|1|2|3":{action_:["o=","output","parenthesisLevel++"],nextState:"2"},"*":{action_:["output","o=","output","parenthesisLevel++"],nextState:"2"}},")]}":{"0|1|2|3|b|p|bp|o":{action_:["o=","parenthesisLevel--"],nextState:"o"},"a|as|d|D|q|qd|qD|dq":{action_:["output","o=","parenthesisLevel--"],nextState:"o"}},", ":{"*":{action_:["output","comma"],nextState:"0"}},"^_":{"*":{}},"^{(...)}|^($...$)":{"0|1|2|as":{action_:"b=",nextState:"b"},p:{action_:"b=",nextState:"bp"},"3|o":{action_:"d= kv",nextState:"D"},q:{action_:"d=",nextState:"qD"},"d|D|qd|qD|dq":{action_:["output","d="],nextState:"D"}},"^a|^\\x{}{}|^\\x{}|^\\x|'":{"0|1|2|as":{action_:"b=",nextState:"b"},p:{action_:"b=",nextState:"bp"},"3|o":{action_:"d= kv",nextState:"d"},q:{action_:"d=",nextState:"qd"},"d|qd|D|qD":{action_:"d="},dq:{action_:["output","d="],nextState:"d"}},"_{(state of aggregation)}$":{"d|D|q|qd|qD|dq":{action_:["output","q="],nextState:"q"}},"_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x":{"0|1|2|as":{action_:"p=",nextState:"p"},b:{action_:"p=",nextState:"bp"},"3|o":{action_:"q=",nextState:"q"},"d|D":{action_:"q=",nextState:"dq"},"q|qd|qD|dq":{action_:["output","q="],nextState:"q"}},"=<>":{"0|1|2|3|a|as|o|q|d|D|qd|qD|dq":{action_:[{type_:"output",option:2},"bond"],nextState:"3"}},"#":{"0|1|2|3|a|as|o":{action_:[{type_:"output",option:2},{type_:"bond",option:"#"}],nextState:"3"}},"{}":{"*":{action_:{type_:"output",option:1},nextState:"1"}},"{...}":{"0|1|2|3|a|as|b|p|bp":{action_:"o=",nextState:"o"},"o|d|D|q|qd|qD|dq":{action_:["output","o="],nextState:"o"}},"$...$":{a:{action_:"a="},"0|1|2|3|as|b|p|bp|o":{action_:"o=",nextState:"o"},"as|o":{action_:"o="},"q|d|D|qd|qD|dq":{action_:["output","o="],nextState:"o"}},"\\bond{(...)}":{"*":{action_:[{type_:"output",option:2},"bond"],nextState:"3"}},"\\frac{(...)}":{"*":{action_:[{type_:"output",option:1},"frac-output"],nextState:"3"}},"\\overset{(...)}":{"*":{action_:[{type_:"output",option:2},"overset-output"],nextState:"3"}},"\\underset{(...)}":{"*":{action_:[{type_:"output",option:2},"underset-output"],nextState:"3"}},"\\underbrace{(...)}":{"*":{action_:[{type_:"output",option:2},"underbrace-output"],nextState:"3"}},"\\color{(...)}{(...)}1|\\color(...){(...)}2":{"*":{action_:[{type_:"output",option:2},"color-output"],nextState:"3"}},"\\color{(...)}0":{"*":{action_:[{type_:"output",option:2},"color0-output"]}},"\\ce{(...)}":{"*":{action_:[{type_:"output",option:2},"ce"],nextState:"3"}},"\\,":{"*":{action_:[{type_:"output",option:1},"copy"],nextState:"1"}},"\\x{}{}|\\x{}|\\x":{"0|1|2|3|a|as|b|p|bp|o|c0":{action_:["o=","output"],nextState:"3"},"*":{action_:["output","o=","output"],nextState:"3"}},others:{"*":{action_:[{type_:"output",option:1},"copy"],nextState:"3"}},else2:{a:{action_:"a to o",nextState:"o",revisit:!0},as:{action_:["output","sb=true"],nextState:"1",revisit:!0},"r|rt|rd|rdt|rdq":{action_:["output"],nextState:"0",revisit:!0},"*":{action_:["output","copy"],nextState:"3"}}}),actions:{"o after d":function(t,e){var n;if((t.d||"").match(/^[0-9]+$/)){var o=t.d;t.d=void 0,n=this.output(t),t.b=o}else n=this.output(t);return i.actions["o="](t,e),n},"d= kv":function(t,e){t.d=e,t.dType="kv"},"charge or bond":function(t,e){if(t.beginsWithBond){var n=[];return i.concatArray(n,this.output(t)),i.concatArray(n,i.actions.bond(t,e,"-")),n}t.d=e},"- after o/d":function(t,e,n){var o=i.patterns.match_("orbital",t.o||""),a=i.patterns.match_("one lowercase greek letter $",t.o||""),r=i.patterns.match_("one lowercase latin letter $",t.o||""),c=i.patterns.match_("$one lowercase latin letter$ $",t.o||""),u="-"===e&&(o&&""===o.remainder||a||r||c);!u||t.a||t.b||t.p||t.d||t.q||o||!r||(t.o="$"+t.o+"$");var p=[];return u?(i.concatArray(p,this.output(t)),p.push({type_:"hyphen"})):(o=i.patterns.match_("digits",t.d||""),n&&o&&""===o.remainder?(i.concatArray(p,i.actions["d="](t,e)),i.concatArray(p,this.output(t))):(i.concatArray(p,this.output(t)),i.concatArray(p,i.actions.bond(t,e,"-")))),p},"a to o":function(t){t.o=t.a,t.a=void 0},"sb=true":function(t){t.sb=!0},"sb=false":function(t){t.sb=!1},"beginsWithBond=true":function(t){t.beginsWithBond=!0},"beginsWithBond=false":function(t){t.beginsWithBond=!1},"parenthesisLevel++":function(t){t.parenthesisLevel++},"parenthesisLevel--":function(t){t.parenthesisLevel--},"state of aggregation":function(t,e){return{type_:"state of aggregation",p1:i.go(e,"o")}},comma:function(t,e){var n=e.replace(/\s*$/,"");return n!==e&&0===t.parenthesisLevel?{type_:"comma enumeration L",p1:n}:{type_:"comma enumeration M",p1:n}},output:function(t,e,n){var o,a,r;t.r?(a="M"===t.rdt?i.go(t.rd,"tex-math"):"T"===t.rdt?[{type_:"text",p1:t.rd||""}]:i.go(t.rd),r="M"===t.rqt?i.go(t.rq,"tex-math"):"T"===t.rqt?[{type_:"text",p1:t.rq||""}]:i.go(t.rq),o={type_:"arrow",r:t.r,rd:a,rq:r}):(o=[],(t.a||t.b||t.p||t.o||t.q||t.d||n)&&(t.sb&&o.push({type_:"entitySkip"}),t.o||t.q||t.d||t.b||t.p||2===n?t.o||t.q||t.d||!t.b&&!t.p?t.o&&"kv"===t.dType&&i.patterns.match_("d-oxidation$",t.d||"")?t.dType="oxidation":t.o&&"kv"===t.dType&&!t.q&&(t.dType=void 0):(t.o=t.a,t.d=t.b,t.q=t.p,t.a=t.b=t.p=void 0):(t.o=t.a,t.a=void 0),o.push({type_:"chemfive",a:i.go(t.a,"a"),b:i.go(t.b,"bd"),p:i.go(t.p,"pq"),o:i.go(t.o,"o"),q:i.go(t.q,"pq"),d:i.go(t.d,"oxidation"===t.dType?"oxidation":"bd"),dType:t.dType})));for(var c in t)"parenthesisLevel"!==c&&"beginsWithBond"!==c&&delete t[c];return o},"oxidation-output":function(t,e){var n=["{"];return i.concatArray(n,i.go(e,"oxidation")),n.push("}"),n},"frac-output":function(t,e){return{type_:"frac-ce",p1:i.go(e[0]),p2:i.go(e[1])}},"overset-output":function(t,e){return{type_:"overset",p1:i.go(e[0]),p2:i.go(e[1])}},"underset-output":function(t,e){return{type_:"underset",p1:i.go(e[0]),p2:i.go(e[1])}},"underbrace-output":function(t,e){return{type_:"underbrace",p1:i.go(e[0]),p2:i.go(e[1])}},"color-output":function(t,e){return{type_:"color",color1:e[0],color2:i.go(e[1])}},"r=":function(t,e){t.r=e},"rdt=":function(t,e){t.rdt=e},"rd=":function(t,e){t.rd=e},"rqt=":function(t,e){t.rqt=e},"rq=":function(t,e){t.rq=e},operator:function(t,e,n){return{type_:"operator",kind_:n||e}}}},a:{transitions:i.createTransitions({empty:{"*":{}},"1/2$":{0:{action_:"1/2"}},else:{0:{nextState:"1",revisit:!0}},"$(...)$":{"*":{action_:"tex-math tight",nextState:"1"}},",":{"*":{action_:{type_:"insert",option:"commaDecimal"}}},else2:{"*":{action_:"copy"}}}),actions:{}},o:{transitions:i.createTransitions({empty:{"*":{}},"1/2$":{0:{action_:"1/2"}},else:{0:{nextState:"1",revisit:!0}},letters:{"*":{action_:"rm"}},"\\ca":{"*":{action_:{type_:"insert",option:"circa"}}},"\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"{text}"}},else2:{"*":{action_:"copy"}}}),actions:{}},text:{transitions:i.createTransitions({empty:{"*":{action_:"output"}},"{...}":{"*":{action_:"text="}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"\\greek":{"*":{action_:["output","rm"]}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:["output","copy"]}},else:{"*":{action_:"text="}}}),actions:{output:function(t){if(t.text_){var e={type_:"text",p1:t.text_};for(var n in t)delete t[n];return e}}}},pq:{transitions:i.createTransitions({empty:{"*":{}},"state of aggregation $":{"*":{action_:"state of aggregation"}},i$:{0:{nextState:"!f",revisit:!0}},"(KV letters),":{0:{action_:"rm",nextState:"0"}},formula$:{0:{nextState:"f",revisit:!0}},"1/2$":{0:{action_:"1/2"}},else:{0:{nextState:"!f",revisit:!0}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"text"}},"a-z":{f:{action_:"tex-math"}},letters:{"*":{action_:"rm"}},"-9.,9":{"*":{action_:"9,9"}},",":{"*":{action_:{type_:"insert+p1",option:"comma enumeration S"}}},"\\color{(...)}{(...)}1|\\color(...){(...)}2":{"*":{action_:"color-output"}},"\\color{(...)}0":{"*":{action_:"color0-output"}},"\\ce{(...)}":{"*":{action_:"ce"}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},else2:{"*":{action_:"copy"}}}),actions:{"state of aggregation":function(t,e){return{type_:"state of aggregation subscript",p1:i.go(e,"o")}},"color-output":function(t,e){return{type_:"color",color1:e[0],color2:i.go(e[1],"pq")}}}},bd:{transitions:i.createTransitions({empty:{"*":{}},x$:{0:{nextState:"!f",revisit:!0}},formula$:{0:{nextState:"f",revisit:!0}},else:{0:{nextState:"!f",revisit:!0}},"-9.,9 no missing 0":{"*":{action_:"9,9"}},".":{"*":{action_:{type_:"insert",option:"electron dot"}}},"a-z":{f:{action_:"tex-math"}},x:{"*":{action_:{type_:"insert",option:"KV x"}}},letters:{"*":{action_:"rm"}},"'":{"*":{action_:{type_:"insert",option:"prime"}}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"text"}},"\\color{(...)}{(...)}1|\\color(...){(...)}2":{"*":{action_:"color-output"}},"\\color{(...)}0":{"*":{action_:"color0-output"}},"\\ce{(...)}":{"*":{action_:"ce"}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},else2:{"*":{action_:"copy"}}}),actions:{"color-output":function(t,e){return{type_:"color",color1:e[0],color2:i.go(e[1],"bd")}}}},oxidation:{transitions:i.createTransitions({empty:{"*":{}},"roman numeral":{"*":{action_:"roman-numeral"}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},else:{"*":{action_:"copy"}}}),actions:{"roman-numeral":function(t,e){return{type_:"roman numeral",p1:e||""}}}},"tex-math":{transitions:i.createTransitions({empty:{"*":{action_:"output"}},"\\ce{(...)}":{"*":{action_:["output","ce"]}},"{...}|\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"o="}},else:{"*":{action_:"o="}}}),actions:{output:function(t){if(t.o){var e={type_:"tex-math",p1:t.o};for(var n in t)delete t[n];return e}}}},"tex-math tight":{transitions:i.createTransitions({empty:{"*":{action_:"output"}},"\\ce{(...)}":{"*":{action_:["output","ce"]}},"{...}|\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"o="}},"-|+":{"*":{action_:"tight operator"}},else:{"*":{action_:"o="}}}),actions:{"tight operator":function(t,e){t.o=(t.o||"")+"{"+e+"}"},output:function(t){if(t.o){var e={type_:"tex-math",p1:t.o};for(var n in t)delete t[n];return e}}}},"9,9":{transitions:i.createTransitions({empty:{"*":{}},",":{"*":{action_:"comma"}},else:{"*":{action_:"copy"}}}),actions:{comma:function(){return{type_:"commaDecimal"}}}},pu:{transitions:i.createTransitions({empty:{"*":{action_:"output"}},space$:{"*":{action_:["output","space"]}},"{[(|)]}":{"0|a":{action_:"copy"}},"(-)(9)^(-9)":{0:{action_:"number^",nextState:"a"}},"(-)(9.,9)(e)(99)":{0:{action_:"enumber",nextState:"a"}},space:{"0|a":{}},"pm-operator":{"0|a":{action_:{type_:"operator",option:"\\pm"},nextState:"0"}},operator:{"0|a":{action_:"copy",nextState:"0"}},"//":{d:{action_:"o=",nextState:"/"}},"/":{d:{action_:"o=",nextState:"/"}},"{...}|else":{"0|d":{action_:"d=",nextState:"d"},a:{action_:["space","d="],nextState:"d"},"/|q":{action_:"q=",nextState:"q"}}}),actions:{enumber:function(t,e){var n=[];return"+-"===e[0]||"+/-"===e[0]?n.push("\\pm "):e[0]&&n.push(e[0]),e[1]&&(i.concatArray(n,i.go(e[1],"pu-9,9")),e[2]&&(e[2].match(/[,.]/)?i.concatArray(n,i.go(e[2],"pu-9,9")):n.push(e[2])),e[3]=e[4]||e[3],e[3]&&(e[3]=e[3].trim(),"e"===e[3]||"*"===e[3].substr(0,1)?n.push({type_:"cdot"}):n.push({type_:"times"}))),e[3]&&n.push("10^{"+e[5]+"}"),n},"number^":function(t,e){var n=[];return"+-"===e[0]||"+/-"===e[0]?n.push("\\pm "):e[0]&&n.push(e[0]),i.concatArray(n,i.go(e[1],"pu-9,9")),n.push("^{"+e[2]+"}"),n},operator:function(t,e,n){return{type_:"operator",kind_:n||e}},space:function(){return{type_:"pu-space-1"}},output:function(t){var e,n=i.patterns.match_("{(...)}",t.d||"");n&&""===n.remainder&&(t.d=n.match_);var o=i.patterns.match_("{(...)}",t.q||"");if(o&&""===o.remainder&&(t.q=o.match_),t.d&&(t.d=t.d.replace(/\u00B0C|\^oC|\^{o}C/g,"{}^{\\circ}C"),t.d=t.d.replace(/\u00B0F|\^oF|\^{o}F/g,"{}^{\\circ}F")),t.q){t.q=t.q.replace(/\u00B0C|\^oC|\^{o}C/g,"{}^{\\circ}C"),t.q=t.q.replace(/\u00B0F|\^oF|\^{o}F/g,"{}^{\\circ}F");var a={d:i.go(t.d,"pu"),q:i.go(t.q,"pu")};"//"===t.o?e={type_:"pu-frac",p1:a.d,p2:a.q}:(e=a.d,a.d.length>1||a.q.length>1?e.push({type_:" / "}):e.push({type_:"/"}),i.concatArray(e,a.q))}else e=i.go(t.d,"pu-2");for(var r in t)delete t[r];return e}}},"pu-2":{transitions:i.createTransitions({empty:{"*":{action_:"output"}},"*":{"*":{action_:["output","cdot"],nextState:"0"}},"\\x":{"*":{action_:"rm="}},space:{"*":{action_:["output","space"],nextState:"0"}},"^{(...)}|^(-1)":{1:{action_:"^(-1)"}},"-9.,9":{0:{action_:"rm=",nextState:"0"},1:{action_:"^(-1)",nextState:"0"}},"{...}|else":{"*":{action_:"rm=",nextState:"1"}}}),actions:{cdot:function(){return{type_:"tight cdot"}},"^(-1)":function(t,e){t.rm+="^{"+e+"}"},space:function(){return{type_:"pu-space-2"}},output:function(t){var e=[];if(t.rm){var n=i.patterns.match_("{(...)}",t.rm||"");e=n&&""===n.remainder?i.go(n.match_,"pu"):{type_:"rm",p1:t.rm}}for(var o in t)delete t[o];return e}}},"pu-9,9":{transitions:i.createTransitions({empty:{0:{action_:"output-0"},o:{action_:"output-o"}},",":{0:{action_:["output-0","comma"],nextState:"o"}},".":{0:{action_:["output-0","copy"],nextState:"o"}},else:{"*":{action_:"text="}}}),actions:{comma:function(){return{type_:"commaDecimal"}},"output-0":function(t){var e=[];if(t.text_=t.text_||"",t.text_.length>4){var n=t.text_.length%3;0===n&&(n=3);for(var o=t.text_.length-3;o>0;o-=3)e.push(t.text_.substr(o,3)),e.push({type_:"1000 separator"});e.push(t.text_.substr(0,n)),e.reverse()}else e.push(t.text_);for(var a in t)delete t[a];return e},"output-o":function(t){var e=[];if(t.text_=t.text_||"",t.text_.length>4){for(var n=t.text_.length-3,o=0;o":case"\u2192":case"\u27f6":return"rightarrow";case"<-":return"leftarrow";case"<->":return"leftrightarrow";case"<--\x3e":return"rightleftarrows";case"<=>":case"\u21cc":return"rightleftharpoons";case"<=>>":return"rightequilibrium";case"<<=>":return"leftequilibrium";default:throw["MhchemBugT","mhchem bug T. Please report."]}},_getBond:function(t){switch(t){case"-":case"1":return"{-}";case"=":case"2":return"{=}";case"#":case"3":return"{\\equiv}";case"~":return"{\\tripledash}";case"~-":return"{\\mathrlap{\\raisebox{-.1em}{$-$}}\\raisebox{.1em}{$\\tripledash$}}";case"~=":case"~--":return"{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}";case"-~-":return"{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$-$}}\\tripledash}";case"...":return"{{\\cdot}{\\cdot}{\\cdot}}";case"....":return"{{\\cdot}{\\cdot}{\\cdot}{\\cdot}}";case"->":return"{\\rightarrow}";case"<-":return"{\\leftarrow}";case"<":return"{<}";case">":return"{>}";default:throw["MhchemBugT","mhchem bug T. Please report."]}},_getOperator:function(t){switch(t){case"+":return" {}+{} ";case"-":return" {}-{} ";case"=":return" {}={} ";case"<":return" {}<{} ";case">":return" {}>{} ";case"<<":return" {}\\ll{} ";case">>":return" {}\\gg{} ";case"\\pm":return" {}\\pm{} ";case"\\approx":case"$\\approx$":return" {}\\approx{} ";case"v":case"(v)":return" \\downarrow{} ";case"^":case"(^)":return" \\uparrow{} ";default:throw["MhchemBugT","mhchem bug T. Please report."]}}}}]).default}); \ No newline at end of file diff --git a/docs/static/katex/contrib/mhchem.mjs b/docs/static/katex/contrib/mhchem.mjs deleted file mode 100644 index 762e15ef0a9..00000000000 --- a/docs/static/katex/contrib/mhchem.mjs +++ /dev/null @@ -1,3109 +0,0 @@ -import katex from '../katex.mjs'; - -/* eslint-disable */ - -/* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */ - -/* vim: set ts=2 et sw=2 tw=80: */ - -/************************************************************* - * - * KaTeX mhchem.js - * - * This file implements a KaTeX version of mhchem version 3.3.0. - * It is adapted from MathJax/extensions/TeX/mhchem.js - * It differs from the MathJax version as follows: - * 1. The interface is changed so that it can be called from KaTeX, not MathJax. - * 2. \rlap and \llap are replaced with \mathrlap and \mathllap. - * 3. Four lines of code are edited in order to use \raisebox instead of \raise. - * 4. The reaction arrow code is simplified. All reaction arrows are rendered - * using KaTeX extensible arrows instead of building non-extensible arrows. - * 5. \tripledash vertical alignment is slightly adjusted. - * - * This code, as other KaTeX code, is released under the MIT license. - * - * /************************************************************* - * - * MathJax/extensions/TeX/mhchem.js - * - * Implements the \ce command for handling chemical formulas - * from the mhchem LaTeX package. - * - * --------------------------------------------------------------------- - * - * Copyright (c) 2011-2015 The MathJax Consortium - * Copyright (c) 2015-2018 Martin Hensel - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// -// Coding Style -// - use '' for identifiers that can by minified/uglified -// - use "" for strings that need to stay untouched -// version: "3.3.0" for MathJax and KaTeX -// Add \ce, \pu, and \tripledash to the KaTeX macros. -katex.__defineMacro("\\ce", function (context) { - return chemParse(context.consumeArgs(1)[0], "ce"); -}); - -katex.__defineMacro("\\pu", function (context) { - return chemParse(context.consumeArgs(1)[0], "pu"); -}); // Needed for \bond for the ~ forms -// Raise by 2.56mu, not 2mu. We're raising a hyphen-minus, U+002D, not -// a mathematical minus, U+2212. So we need that extra 0.56. - - -katex.__defineMacro("\\tripledash", "{\\vphantom{-}\\raisebox{2.56mu}{$\\mkern2mu" + "\\tiny\\text{-}\\mkern1mu\\text{-}\\mkern1mu\\text{-}\\mkern2mu$}}"); -// This is the main function for handing the \ce and \pu commands. -// It takes the argument to \ce or \pu and returns the corresponding TeX string. -// - -var chemParse = function chemParse(tokens, stateMachine) { - // Recreate the argument string from KaTeX's array of tokens. - var str = ""; - var expectedLoc = tokens[tokens.length - 1].loc.start; - - for (var i = tokens.length - 1; i >= 0; i--) { - if (tokens[i].loc.start > expectedLoc) { - // context.consumeArgs has eaten a space. - str += " "; - expectedLoc = tokens[i].loc.start; - } - - str += tokens[i].text; - expectedLoc += tokens[i].text.length; - } - - var tex = texify.go(mhchemParser.go(str, stateMachine)); - return tex; -}; // -// Core parser for mhchem syntax (recursive) -// - -/** @type {MhchemParser} */ - - -var mhchemParser = { - // - // Parses mchem \ce syntax - // - // Call like - // go("H2O"); - // - go: function go(input, stateMachine) { - if (!input) { - return []; - } - - if (stateMachine === undefined) { - stateMachine = 'ce'; - } - - var state = '0'; // - // String buffers for parsing: - // - // buffer.a == amount - // buffer.o == element - // buffer.b == left-side superscript - // buffer.p == left-side subscript - // buffer.q == right-side subscript - // buffer.d == right-side superscript - // - // buffer.r == arrow - // buffer.rdt == arrow, script above, type - // buffer.rd == arrow, script above, content - // buffer.rqt == arrow, script below, type - // buffer.rq == arrow, script below, content - // - // buffer.text_ - // buffer.rm - // etc. - // - // buffer.parenthesisLevel == int, starting at 0 - // buffer.sb == bool, space before - // buffer.beginsWithBond == bool - // - // These letters are also used as state names. - // - // Other states: - // 0 == begin of main part (arrow/operator unlikely) - // 1 == next entity - // 2 == next entity (arrow/operator unlikely) - // 3 == next atom - // c == macro - // - - /** @type {Buffer} */ - - var buffer = {}; - buffer['parenthesisLevel'] = 0; - input = input.replace(/\n/g, " "); - input = input.replace(/[\u2212\u2013\u2014\u2010]/g, "-"); - input = input.replace(/[\u2026]/g, "..."); // - // Looks through mhchemParser.transitions, to execute a matching action - // (recursive) - // - - var lastInput; - var watchdog = 10; - /** @type {ParserOutput[]} */ - - var output = []; - - while (true) { - if (lastInput !== input) { - watchdog = 10; - lastInput = input; - } else { - watchdog--; - } // - // Find actions in transition table - // - - - var machine = mhchemParser.stateMachines[stateMachine]; - var t = machine.transitions[state] || machine.transitions['*']; - - iterateTransitions: for (var i = 0; i < t.length; i++) { - var matches = mhchemParser.patterns.match_(t[i].pattern, input); - - if (matches) { - // - // Execute actions - // - var task = t[i].task; - - for (var iA = 0; iA < task.action_.length; iA++) { - var o; // - // Find and execute action - // - - if (machine.actions[task.action_[iA].type_]) { - o = machine.actions[task.action_[iA].type_](buffer, matches.match_, task.action_[iA].option); - } else if (mhchemParser.actions[task.action_[iA].type_]) { - o = mhchemParser.actions[task.action_[iA].type_](buffer, matches.match_, task.action_[iA].option); - } else { - throw ["MhchemBugA", "mhchem bug A. Please report. (" + task.action_[iA].type_ + ")"]; // Trying to use non-existing action - } // - // Add output - // - - - mhchemParser.concatArray(output, o); - } // - // Set next state, - // Shorten input, - // Continue with next character - // (= apply only one transition per position) - // - - - state = task.nextState || state; - - if (input.length > 0) { - if (!task.revisit) { - input = matches.remainder; - } - - if (!task.toContinue) { - break iterateTransitions; - } - } else { - return output; - } - } - } // - // Prevent infinite loop - // - - - if (watchdog <= 0) { - throw ["MhchemBugU", "mhchem bug U. Please report."]; // Unexpected character - } - } - }, - concatArray: function concatArray(a, b) { - if (b) { - if (Array.isArray(b)) { - for (var iB = 0; iB < b.length; iB++) { - a.push(b[iB]); - } - } else { - a.push(b); - } - } - }, - patterns: { - // - // Matching patterns - // either regexps or function that return null or {match_:"a", remainder:"bc"} - // - patterns: { - // property names must not look like integers ("2") for correct property traversal order, later on - 'empty': /^$/, - 'else': /^./, - 'else2': /^./, - 'space': /^\s/, - 'space A': /^\s(?=[A-Z\\$])/, - 'space$': /^\s$/, - 'a-z': /^[a-z]/, - 'x': /^x/, - 'x$': /^x$/, - 'i$': /^i$/, - 'letters': /^(?:[a-zA-Z\u03B1-\u03C9\u0391-\u03A9?@]|(?:\\(?:alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)(?:\s+|\{\}|(?![a-zA-Z]))))+/, - '\\greek': /^\\(?:alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega)(?:\s+|\{\}|(?![a-zA-Z]))/, - 'one lowercase latin letter $': /^(?:([a-z])(?:$|[^a-zA-Z]))$/, - '$one lowercase latin letter$ $': /^\$(?:([a-z])(?:$|[^a-zA-Z]))\$$/, - 'one lowercase greek letter $': /^(?:\$?[\u03B1-\u03C9]\$?|\$?\\(?:alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega)\s*\$?)(?:\s+|\{\}|(?![a-zA-Z]))$/, - 'digits': /^[0-9]+/, - '-9.,9': /^[+\-]?(?:[0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\.[0-9]+))/, - '-9.,9 no missing 0': /^[+\-]?[0-9]+(?:[.,][0-9]+)?/, - '(-)(9.,9)(e)(99)': function e99(input) { - var m = input.match(/^(\+\-|\+\/\-|\+|\-|\\pm\s?)?([0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\.[0-9]+))?(\((?:[0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\.[0-9]+))\))?(?:([eE]|\s*(\*|x|\\times|\u00D7)\s*10\^)([+\-]?[0-9]+|\{[+\-]?[0-9]+\}))?/); - - if (m && m[0]) { - return { - match_: m.splice(1), - remainder: input.substr(m[0].length) - }; - } - - return null; - }, - '(-)(9)^(-9)': function _(input) { - var m = input.match(/^(\+\-|\+\/\-|\+|\-|\\pm\s?)?([0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\.[0-9]+)?)\^([+\-]?[0-9]+|\{[+\-]?[0-9]+\})/); - - if (m && m[0]) { - return { - match_: m.splice(1), - remainder: input.substr(m[0].length) - }; - } - - return null; - }, - 'state of aggregation $': function stateOfAggregation$(input) { - // ... or crystal system - var a = mhchemParser.patterns.findObserveGroups(input, "", /^\([a-z]{1,3}(?=[\),])/, ")", ""); // (aq), (aq,$\infty$), (aq, sat) - - if (a && a.remainder.match(/^($|[\s,;\)\]\}])/)) { - return a; - } // AND end of 'phrase' - - - var m = input.match(/^(?:\((?:\\ca\s?)?\$[amothc]\$\))/); // OR crystal system ($o$) (\ca$c$) - - if (m) { - return { - match_: m[0], - remainder: input.substr(m[0].length) - }; - } - - return null; - }, - '_{(state of aggregation)}$': /^_\{(\([a-z]{1,3}\))\}/, - '{[(': /^(?:\\\{|\[|\()/, - ')]}': /^(?:\)|\]|\\\})/, - ', ': /^[,;]\s*/, - ',': /^[,;]/, - '.': /^[.]/, - '. ': /^([.\u22C5\u00B7\u2022])\s*/, - '...': /^\.\.\.(?=$|[^.])/, - '* ': /^([*])\s*/, - '^{(...)}': function _(input) { - return mhchemParser.patterns.findObserveGroups(input, "^{", "", "", "}"); - }, - '^($...$)': function $$(input) { - return mhchemParser.patterns.findObserveGroups(input, "^", "$", "$", ""); - }, - '^a': /^\^([0-9]+|[^\\_])/, - '^\\x{}{}': function x(input) { - return mhchemParser.patterns.findObserveGroups(input, "^", /^\\[a-zA-Z]+\{/, "}", "", "", "{", "}", "", true); - }, - '^\\x{}': function x(input) { - return mhchemParser.patterns.findObserveGroups(input, "^", /^\\[a-zA-Z]+\{/, "}", ""); - }, - '^\\x': /^\^(\\[a-zA-Z]+)\s*/, - '^(-1)': /^\^(-?\d+)/, - '\'': /^'/, - '_{(...)}': function _(input) { - return mhchemParser.patterns.findObserveGroups(input, "_{", "", "", "}"); - }, - '_($...$)': function _$$(input) { - return mhchemParser.patterns.findObserveGroups(input, "_", "$", "$", ""); - }, - '_9': /^_([+\-]?[0-9]+|[^\\])/, - '_\\x{}{}': function _X(input) { - return mhchemParser.patterns.findObserveGroups(input, "_", /^\\[a-zA-Z]+\{/, "}", "", "", "{", "}", "", true); - }, - '_\\x{}': function _X(input) { - return mhchemParser.patterns.findObserveGroups(input, "_", /^\\[a-zA-Z]+\{/, "}", ""); - }, - '_\\x': /^_(\\[a-zA-Z]+)\s*/, - '^_': /^(?:\^(?=_)|\_(?=\^)|[\^_]$)/, - '{}': /^\{\}/, - '{...}': function _(input) { - return mhchemParser.patterns.findObserveGroups(input, "", "{", "}", ""); - }, - '{(...)}': function _(input) { - return mhchemParser.patterns.findObserveGroups(input, "{", "", "", "}"); - }, - '$...$': function $$(input) { - return mhchemParser.patterns.findObserveGroups(input, "", "$", "$", ""); - }, - '${(...)}$': function $$(input) { - return mhchemParser.patterns.findObserveGroups(input, "${", "", "", "}$"); - }, - '$(...)$': function $$(input) { - return mhchemParser.patterns.findObserveGroups(input, "$", "", "", "$"); - }, - '=<>': /^[=<>]/, - '#': /^[#\u2261]/, - '+': /^\+/, - '-$': /^-(?=[\s_},;\]/]|$|\([a-z]+\))/, - // -space -, -; -] -/ -$ -state-of-aggregation - '-9': /^-(?=[0-9])/, - '- orbital overlap': /^-(?=(?:[spd]|sp)(?:$|[\s,;\)\]\}]))/, - '-': /^-/, - 'pm-operator': /^(?:\\pm|\$\\pm\$|\+-|\+\/-)/, - 'operator': /^(?:\+|(?:[\-=<>]|<<|>>|\\approx|\$\\approx\$)(?=\s|$|-?[0-9]))/, - 'arrowUpDown': /^(?:v|\(v\)|\^|\(\^\))(?=$|[\s,;\)\]\}])/, - '\\bond{(...)}': function bond(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\bond{", "", "", "}"); - }, - '->': /^(?:<->|<-->|->|<-|<=>>|<<=>|<=>|[\u2192\u27F6\u21CC])/, - 'CMT': /^[CMT](?=\[)/, - '[(...)]': function _(input) { - return mhchemParser.patterns.findObserveGroups(input, "[", "", "", "]"); - }, - '1st-level escape': /^(&|\\\\|\\hline)\s*/, - '\\,': /^(?:\\[,\ ;:])/, - // \\x - but output no space before - '\\x{}{}': function x(input) { - return mhchemParser.patterns.findObserveGroups(input, "", /^\\[a-zA-Z]+\{/, "}", "", "", "{", "}", "", true); - }, - '\\x{}': function x(input) { - return mhchemParser.patterns.findObserveGroups(input, "", /^\\[a-zA-Z]+\{/, "}", ""); - }, - '\\ca': /^\\ca(?:\s+|(?![a-zA-Z]))/, - '\\x': /^(?:\\[a-zA-Z]+\s*|\\[_&{}%])/, - 'orbital': /^(?:[0-9]{1,2}[spdfgh]|[0-9]{0,2}sp)(?=$|[^a-zA-Z])/, - // only those with numbers in front, because the others will be formatted correctly anyway - 'others': /^[\/~|]/, - '\\frac{(...)}': function frac(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\frac{", "", "", "}", "{", "", "", "}"); - }, - '\\overset{(...)}': function overset(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\overset{", "", "", "}", "{", "", "", "}"); - }, - '\\underset{(...)}': function underset(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\underset{", "", "", "}", "{", "", "", "}"); - }, - '\\underbrace{(...)}': function underbrace(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\underbrace{", "", "", "}_", "{", "", "", "}"); - }, - '\\color{(...)}0': function color0(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\color{", "", "", "}"); - }, - '\\color{(...)}{(...)}1': function color1(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\color{", "", "", "}", "{", "", "", "}"); - }, - '\\color(...){(...)}2': function color2(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\color", "\\", "", /^(?=\{)/, "{", "", "", "}"); - }, - '\\ce{(...)}': function ce(input) { - return mhchemParser.patterns.findObserveGroups(input, "\\ce{", "", "", "}"); - }, - 'oxidation$': /^(?:[+-][IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/, - 'd-oxidation$': /^(?:[+-]?\s?[IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/, - // 0 could be oxidation or charge - 'roman numeral': /^[IVX]+/, - '1/2$': /^[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+(?:\$[a-z]\$|[a-z])?$/, - 'amount': function amount(input) { - var match; // e.g. 2, 0.5, 1/2, -2, n/2, +; $a$ could be added later in parsing - - match = input.match(/^(?:(?:(?:\([+\-]?[0-9]+\/[0-9]+\)|[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+|[+\-]?[0-9]+[.,][0-9]+|[+\-]?\.[0-9]+|[+\-]?[0-9]+)(?:[a-z](?=\s*[A-Z]))?)|[+\-]?[a-z](?=\s*[A-Z])|\+(?!\s))/); - - if (match) { - return { - match_: match[0], - remainder: input.substr(match[0].length) - }; - } - - var a = mhchemParser.patterns.findObserveGroups(input, "", "$", "$", ""); - - if (a) { - // e.g. $2n-1$, $-$ - match = a.match_.match(/^\$(?:\(?[+\-]?(?:[0-9]*[a-z]?[+\-])?[0-9]*[a-z](?:[+\-][0-9]*[a-z]?)?\)?|\+|-)\$$/); - - if (match) { - return { - match_: match[0], - remainder: input.substr(match[0].length) - }; - } - } - - return null; - }, - 'amount2': function amount2(input) { - return this['amount'](input); - }, - '(KV letters),': /^(?:[A-Z][a-z]{0,2}|i)(?=,)/, - 'formula$': function formula$(input) { - if (input.match(/^\([a-z]+\)$/)) { - return null; - } // state of aggregation = no formula - - - var match = input.match(/^(?:[a-z]|(?:[0-9\ \+\-\,\.\(\)]+[a-z])+[0-9\ \+\-\,\.\(\)]*|(?:[a-z][0-9\ \+\-\,\.\(\)]+)+[a-z]?)$/); - - if (match) { - return { - match_: match[0], - remainder: input.substr(match[0].length) - }; - } - - return null; - }, - 'uprightEntities': /^(?:pH|pOH|pC|pK|iPr|iBu)(?=$|[^a-zA-Z])/, - '/': /^\s*(\/)\s*/, - '//': /^\s*(\/\/)\s*/, - '*': /^\s*[*.]\s*/ - }, - findObserveGroups: function findObserveGroups(input, begExcl, begIncl, endIncl, endExcl, beg2Excl, beg2Incl, end2Incl, end2Excl, combine) { - /** @type {{(input: string, pattern: string | RegExp): string | string[] | null;}} */ - var _match = function _match(input, pattern) { - if (typeof pattern === "string") { - if (input.indexOf(pattern) !== 0) { - return null; - } - - return pattern; - } else { - var match = input.match(pattern); - - if (!match) { - return null; - } - - return match[0]; - } - }; - /** @type {{(input: string, i: number, endChars: string | RegExp): {endMatchBegin: number, endMatchEnd: number} | null;}} */ - - - var _findObserveGroups = function _findObserveGroups(input, i, endChars) { - var braces = 0; - - while (i < input.length) { - var a = input.charAt(i); - - var match = _match(input.substr(i), endChars); - - if (match !== null && braces === 0) { - return { - endMatchBegin: i, - endMatchEnd: i + match.length - }; - } else if (a === "{") { - braces++; - } else if (a === "}") { - if (braces === 0) { - throw ["ExtraCloseMissingOpen", "Extra close brace or missing open brace"]; - } else { - braces--; - } - } - - i++; - } - - if (braces > 0) { - return null; - } - - return null; - }; - - var match = _match(input, begExcl); - - if (match === null) { - return null; - } - - input = input.substr(match.length); - match = _match(input, begIncl); - - if (match === null) { - return null; - } - - var e = _findObserveGroups(input, match.length, endIncl || endExcl); - - if (e === null) { - return null; - } - - var match1 = input.substring(0, endIncl ? e.endMatchEnd : e.endMatchBegin); - - if (!(beg2Excl || beg2Incl)) { - return { - match_: match1, - remainder: input.substr(e.endMatchEnd) - }; - } else { - var group2 = this.findObserveGroups(input.substr(e.endMatchEnd), beg2Excl, beg2Incl, end2Incl, end2Excl); - - if (group2 === null) { - return null; - } - /** @type {string[]} */ - - - var matchRet = [match1, group2.match_]; - return { - match_: combine ? matchRet.join("") : matchRet, - remainder: group2.remainder - }; - } - }, - // - // Matching function - // e.g. match("a", input) will look for the regexp called "a" and see if it matches - // returns null or {match_:"a", remainder:"bc"} - // - match_: function match_(m, input) { - var pattern = mhchemParser.patterns.patterns[m]; - - if (pattern === undefined) { - throw ["MhchemBugP", "mhchem bug P. Please report. (" + m + ")"]; // Trying to use non-existing pattern - } else if (typeof pattern === "function") { - return mhchemParser.patterns.patterns[m](input); // cannot use cached var pattern here, because some pattern functions need this===mhchemParser - } else { - // RegExp - var match = input.match(pattern); - - if (match) { - var mm; - - if (match[2]) { - mm = [match[1], match[2]]; - } else if (match[1]) { - mm = match[1]; - } else { - mm = match[0]; - } - - return { - match_: mm, - remainder: input.substr(match[0].length) - }; - } - - return null; - } - } - }, - // - // Generic state machine actions - // - actions: { - 'a=': function a(buffer, m) { - buffer.a = (buffer.a || "") + m; - }, - 'b=': function b(buffer, m) { - buffer.b = (buffer.b || "") + m; - }, - 'p=': function p(buffer, m) { - buffer.p = (buffer.p || "") + m; - }, - 'o=': function o(buffer, m) { - buffer.o = (buffer.o || "") + m; - }, - 'q=': function q(buffer, m) { - buffer.q = (buffer.q || "") + m; - }, - 'd=': function d(buffer, m) { - buffer.d = (buffer.d || "") + m; - }, - 'rm=': function rm(buffer, m) { - buffer.rm = (buffer.rm || "") + m; - }, - 'text=': function text(buffer, m) { - buffer.text_ = (buffer.text_ || "") + m; - }, - 'insert': function insert(buffer, m, a) { - return { - type_: a - }; - }, - 'insert+p1': function insertP1(buffer, m, a) { - return { - type_: a, - p1: m - }; - }, - 'insert+p1+p2': function insertP1P2(buffer, m, a) { - return { - type_: a, - p1: m[0], - p2: m[1] - }; - }, - 'copy': function copy(buffer, m) { - return m; - }, - 'rm': function rm(buffer, m) { - return { - type_: 'rm', - p1: m || "" - }; - }, - 'text': function text(buffer, m) { - return mhchemParser.go(m, 'text'); - }, - '{text}': function text(buffer, m) { - var ret = ["{"]; - mhchemParser.concatArray(ret, mhchemParser.go(m, 'text')); - ret.push("}"); - return ret; - }, - 'tex-math': function texMath(buffer, m) { - return mhchemParser.go(m, 'tex-math'); - }, - 'tex-math tight': function texMathTight(buffer, m) { - return mhchemParser.go(m, 'tex-math tight'); - }, - 'bond': function bond(buffer, m, k) { - return { - type_: 'bond', - kind_: k || m - }; - }, - 'color0-output': function color0Output(buffer, m) { - return { - type_: 'color0', - color: m[0] - }; - }, - 'ce': function ce(buffer, m) { - return mhchemParser.go(m); - }, - '1/2': function _(buffer, m) { - /** @type {ParserOutput[]} */ - var ret = []; - - if (m.match(/^[+\-]/)) { - ret.push(m.substr(0, 1)); - m = m.substr(1); - } - - var n = m.match(/^([0-9]+|\$[a-z]\$|[a-z])\/([0-9]+)(\$[a-z]\$|[a-z])?$/); - n[1] = n[1].replace(/\$/g, ""); - ret.push({ - type_: 'frac', - p1: n[1], - p2: n[2] - }); - - if (n[3]) { - n[3] = n[3].replace(/\$/g, ""); - ret.push({ - type_: 'tex-math', - p1: n[3] - }); - } - - return ret; - }, - '9,9': function _(buffer, m) { - return mhchemParser.go(m, '9,9'); - } - }, - // - // createTransitions - // convert { 'letter': { 'state': { action_: 'output' } } } to { 'state' => [ { pattern: 'letter', task: { action_: [{type_: 'output'}] } } ] } - // with expansion of 'a|b' to 'a' and 'b' (at 2 places) - // - createTransitions: function createTransitions(o) { - var pattern, state; - /** @type {string[]} */ - - var stateArray; - var i; // - // 1. Collect all states - // - - /** @type {Transitions} */ - - var transitions = {}; - - for (pattern in o) { - for (state in o[pattern]) { - stateArray = state.split("|"); - o[pattern][state].stateArray = stateArray; - - for (i = 0; i < stateArray.length; i++) { - transitions[stateArray[i]] = []; - } - } - } // - // 2. Fill states - // - - - for (pattern in o) { - for (state in o[pattern]) { - stateArray = o[pattern][state].stateArray || []; - - for (i = 0; i < stateArray.length; i++) { - // - // 2a. Normalize actions into array: 'text=' ==> [{type_:'text='}] - // (Note to myself: Resolving the function here would be problematic. It would need .bind (for *this*) and currying (for *option*).) - // - - /** @type {any} */ - var p = o[pattern][state]; - - if (p.action_) { - p.action_ = [].concat(p.action_); - - for (var k = 0; k < p.action_.length; k++) { - if (typeof p.action_[k] === "string") { - p.action_[k] = { - type_: p.action_[k] - }; - } - } - } else { - p.action_ = []; - } // - // 2.b Multi-insert - // - - - var patternArray = pattern.split("|"); - - for (var j = 0; j < patternArray.length; j++) { - if (stateArray[i] === '*') { - // insert into all - for (var t in transitions) { - transitions[t].push({ - pattern: patternArray[j], - task: p - }); - } - } else { - transitions[stateArray[i]].push({ - pattern: patternArray[j], - task: p - }); - } - } - } - } - } - - return transitions; - }, - stateMachines: {} -}; // -// Definition of state machines -// - -mhchemParser.stateMachines = { - // - // \ce state machines - // - //#region ce - 'ce': { - // main parser - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': { - action_: 'output' - } - }, - 'else': { - '0|1|2': { - action_: 'beginsWithBond=false', - revisit: true, - toContinue: true - } - }, - 'oxidation$': { - '0': { - action_: 'oxidation-output' - } - }, - 'CMT': { - 'r': { - action_: 'rdt=', - nextState: 'rt' - }, - 'rd': { - action_: 'rqt=', - nextState: 'rdt' - } - }, - 'arrowUpDown': { - '0|1|2|as': { - action_: ['sb=false', 'output', 'operator'], - nextState: '1' - } - }, - 'uprightEntities': { - '0|1|2': { - action_: ['o=', 'output'], - nextState: '1' - } - }, - 'orbital': { - '0|1|2|3': { - action_: 'o=', - nextState: 'o' - } - }, - '->': { - '0|1|2|3': { - action_: 'r=', - nextState: 'r' - }, - 'a|as': { - action_: ['output', 'r='], - nextState: 'r' - }, - '*': { - action_: ['output', 'r='], - nextState: 'r' - } - }, - '+': { - 'o': { - action_: 'd= kv', - nextState: 'd' - }, - 'd|D': { - action_: 'd=', - nextState: 'd' - }, - 'q': { - action_: 'd=', - nextState: 'qd' - }, - 'qd|qD': { - action_: 'd=', - nextState: 'qd' - }, - 'dq': { - action_: ['output', 'd='], - nextState: 'd' - }, - '3': { - action_: ['sb=false', 'output', 'operator'], - nextState: '0' - } - }, - 'amount': { - '0|2': { - action_: 'a=', - nextState: 'a' - } - }, - 'pm-operator': { - '0|1|2|a|as': { - action_: ['sb=false', 'output', { - type_: 'operator', - option: '\\pm' - }], - nextState: '0' - } - }, - 'operator': { - '0|1|2|a|as': { - action_: ['sb=false', 'output', 'operator'], - nextState: '0' - } - }, - '-$': { - 'o|q': { - action_: ['charge or bond', 'output'], - nextState: 'qd' - }, - 'd': { - action_: 'd=', - nextState: 'd' - }, - 'D': { - action_: ['output', { - type_: 'bond', - option: "-" - }], - nextState: '3' - }, - 'q': { - action_: 'd=', - nextState: 'qd' - }, - 'qd': { - action_: 'd=', - nextState: 'qd' - }, - 'qD|dq': { - action_: ['output', { - type_: 'bond', - option: "-" - }], - nextState: '3' - } - }, - '-9': { - '3|o': { - action_: ['output', { - type_: 'insert', - option: 'hyphen' - }], - nextState: '3' - } - }, - '- orbital overlap': { - 'o': { - action_: ['output', { - type_: 'insert', - option: 'hyphen' - }], - nextState: '2' - }, - 'd': { - action_: ['output', { - type_: 'insert', - option: 'hyphen' - }], - nextState: '2' - } - }, - '-': { - '0|1|2': { - action_: [{ - type_: 'output', - option: 1 - }, 'beginsWithBond=true', { - type_: 'bond', - option: "-" - }], - nextState: '3' - }, - '3': { - action_: { - type_: 'bond', - option: "-" - } - }, - 'a': { - action_: ['output', { - type_: 'insert', - option: 'hyphen' - }], - nextState: '2' - }, - 'as': { - action_: [{ - type_: 'output', - option: 2 - }, { - type_: 'bond', - option: "-" - }], - nextState: '3' - }, - 'b': { - action_: 'b=' - }, - 'o': { - action_: { - type_: '- after o/d', - option: false - }, - nextState: '2' - }, - 'q': { - action_: { - type_: '- after o/d', - option: false - }, - nextState: '2' - }, - 'd|qd|dq': { - action_: { - type_: '- after o/d', - option: true - }, - nextState: '2' - }, - 'D|qD|p': { - action_: ['output', { - type_: 'bond', - option: "-" - }], - nextState: '3' - } - }, - 'amount2': { - '1|3': { - action_: 'a=', - nextState: 'a' - } - }, - 'letters': { - '0|1|2|3|a|as|b|p|bp|o': { - action_: 'o=', - nextState: 'o' - }, - 'q|dq': { - action_: ['output', 'o='], - nextState: 'o' - }, - 'd|D|qd|qD': { - action_: 'o after d', - nextState: 'o' - } - }, - 'digits': { - 'o': { - action_: 'q=', - nextState: 'q' - }, - 'd|D': { - action_: 'q=', - nextState: 'dq' - }, - 'q': { - action_: ['output', 'o='], - nextState: 'o' - }, - 'a': { - action_: 'o=', - nextState: 'o' - } - }, - 'space A': { - 'b|p|bp': {} - }, - 'space': { - 'a': { - nextState: 'as' - }, - '0': { - action_: 'sb=false' - }, - '1|2': { - action_: 'sb=true' - }, - 'r|rt|rd|rdt|rdq': { - action_: 'output', - nextState: '0' - }, - '*': { - action_: ['output', 'sb=true'], - nextState: '1' - } - }, - '1st-level escape': { - '1|2': { - action_: ['output', { - type_: 'insert+p1', - option: '1st-level escape' - }] - }, - '*': { - action_: ['output', { - type_: 'insert+p1', - option: '1st-level escape' - }], - nextState: '0' - } - }, - '[(...)]': { - 'r|rt': { - action_: 'rd=', - nextState: 'rd' - }, - 'rd|rdt': { - action_: 'rq=', - nextState: 'rdq' - } - }, - '...': { - 'o|d|D|dq|qd|qD': { - action_: ['output', { - type_: 'bond', - option: "..." - }], - nextState: '3' - }, - '*': { - action_: [{ - type_: 'output', - option: 1 - }, { - type_: 'insert', - option: 'ellipsis' - }], - nextState: '1' - } - }, - '. |* ': { - '*': { - action_: ['output', { - type_: 'insert', - option: 'addition compound' - }], - nextState: '1' - } - }, - 'state of aggregation $': { - '*': { - action_: ['output', 'state of aggregation'], - nextState: '1' - } - }, - '{[(': { - 'a|as|o': { - action_: ['o=', 'output', 'parenthesisLevel++'], - nextState: '2' - }, - '0|1|2|3': { - action_: ['o=', 'output', 'parenthesisLevel++'], - nextState: '2' - }, - '*': { - action_: ['output', 'o=', 'output', 'parenthesisLevel++'], - nextState: '2' - } - }, - ')]}': { - '0|1|2|3|b|p|bp|o': { - action_: ['o=', 'parenthesisLevel--'], - nextState: 'o' - }, - 'a|as|d|D|q|qd|qD|dq': { - action_: ['output', 'o=', 'parenthesisLevel--'], - nextState: 'o' - } - }, - ', ': { - '*': { - action_: ['output', 'comma'], - nextState: '0' - } - }, - '^_': { - // ^ and _ without a sensible argument - '*': {} - }, - '^{(...)}|^($...$)': { - '0|1|2|as': { - action_: 'b=', - nextState: 'b' - }, - 'p': { - action_: 'b=', - nextState: 'bp' - }, - '3|o': { - action_: 'd= kv', - nextState: 'D' - }, - 'q': { - action_: 'd=', - nextState: 'qD' - }, - 'd|D|qd|qD|dq': { - action_: ['output', 'd='], - nextState: 'D' - } - }, - '^a|^\\x{}{}|^\\x{}|^\\x|\'': { - '0|1|2|as': { - action_: 'b=', - nextState: 'b' - }, - 'p': { - action_: 'b=', - nextState: 'bp' - }, - '3|o': { - action_: 'd= kv', - nextState: 'd' - }, - 'q': { - action_: 'd=', - nextState: 'qd' - }, - 'd|qd|D|qD': { - action_: 'd=' - }, - 'dq': { - action_: ['output', 'd='], - nextState: 'd' - } - }, - '_{(state of aggregation)}$': { - 'd|D|q|qd|qD|dq': { - action_: ['output', 'q='], - nextState: 'q' - } - }, - '_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x': { - '0|1|2|as': { - action_: 'p=', - nextState: 'p' - }, - 'b': { - action_: 'p=', - nextState: 'bp' - }, - '3|o': { - action_: 'q=', - nextState: 'q' - }, - 'd|D': { - action_: 'q=', - nextState: 'dq' - }, - 'q|qd|qD|dq': { - action_: ['output', 'q='], - nextState: 'q' - } - }, - '=<>': { - '0|1|2|3|a|as|o|q|d|D|qd|qD|dq': { - action_: [{ - type_: 'output', - option: 2 - }, 'bond'], - nextState: '3' - } - }, - '#': { - '0|1|2|3|a|as|o': { - action_: [{ - type_: 'output', - option: 2 - }, { - type_: 'bond', - option: "#" - }], - nextState: '3' - } - }, - '{}': { - '*': { - action_: { - type_: 'output', - option: 1 - }, - nextState: '1' - } - }, - '{...}': { - '0|1|2|3|a|as|b|p|bp': { - action_: 'o=', - nextState: 'o' - }, - 'o|d|D|q|qd|qD|dq': { - action_: ['output', 'o='], - nextState: 'o' - } - }, - '$...$': { - 'a': { - action_: 'a=' - }, - // 2$n$ - '0|1|2|3|as|b|p|bp|o': { - action_: 'o=', - nextState: 'o' - }, - // not 'amount' - 'as|o': { - action_: 'o=' - }, - 'q|d|D|qd|qD|dq': { - action_: ['output', 'o='], - nextState: 'o' - } - }, - '\\bond{(...)}': { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'bond'], - nextState: "3" - } - }, - '\\frac{(...)}': { - '*': { - action_: [{ - type_: 'output', - option: 1 - }, 'frac-output'], - nextState: '3' - } - }, - '\\overset{(...)}': { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'overset-output'], - nextState: '3' - } - }, - '\\underset{(...)}': { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'underset-output'], - nextState: '3' - } - }, - '\\underbrace{(...)}': { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'underbrace-output'], - nextState: '3' - } - }, - '\\color{(...)}{(...)}1|\\color(...){(...)}2': { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'color-output'], - nextState: '3' - } - }, - '\\color{(...)}0': { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'color0-output'] - } - }, - '\\ce{(...)}': { - '*': { - action_: [{ - type_: 'output', - option: 2 - }, 'ce'], - nextState: '3' - } - }, - '\\,': { - '*': { - action_: [{ - type_: 'output', - option: 1 - }, 'copy'], - nextState: '1' - } - }, - '\\x{}{}|\\x{}|\\x': { - '0|1|2|3|a|as|b|p|bp|o|c0': { - action_: ['o=', 'output'], - nextState: '3' - }, - '*': { - action_: ['output', 'o=', 'output'], - nextState: '3' - } - }, - 'others': { - '*': { - action_: [{ - type_: 'output', - option: 1 - }, 'copy'], - nextState: '3' - } - }, - 'else2': { - 'a': { - action_: 'a to o', - nextState: 'o', - revisit: true - }, - 'as': { - action_: ['output', 'sb=true'], - nextState: '1', - revisit: true - }, - 'r|rt|rd|rdt|rdq': { - action_: ['output'], - nextState: '0', - revisit: true - }, - '*': { - action_: ['output', 'copy'], - nextState: '3' - } - } - }), - actions: { - 'o after d': function oAfterD(buffer, m) { - var ret; - - if ((buffer.d || "").match(/^[0-9]+$/)) { - var tmp = buffer.d; - buffer.d = undefined; - ret = this['output'](buffer); - buffer.b = tmp; - } else { - ret = this['output'](buffer); - } - - mhchemParser.actions['o='](buffer, m); - return ret; - }, - 'd= kv': function dKv(buffer, m) { - buffer.d = m; - buffer.dType = 'kv'; - }, - 'charge or bond': function chargeOrBond(buffer, m) { - if (buffer['beginsWithBond']) { - /** @type {ParserOutput[]} */ - var ret = []; - mhchemParser.concatArray(ret, this['output'](buffer)); - mhchemParser.concatArray(ret, mhchemParser.actions['bond'](buffer, m, "-")); - return ret; - } else { - buffer.d = m; - } - }, - '- after o/d': function afterOD(buffer, m, isAfterD) { - var c1 = mhchemParser.patterns.match_('orbital', buffer.o || ""); - var c2 = mhchemParser.patterns.match_('one lowercase greek letter $', buffer.o || ""); - var c3 = mhchemParser.patterns.match_('one lowercase latin letter $', buffer.o || ""); - var c4 = mhchemParser.patterns.match_('$one lowercase latin letter$ $', buffer.o || ""); - var hyphenFollows = m === "-" && (c1 && c1.remainder === "" || c2 || c3 || c4); - - if (hyphenFollows && !buffer.a && !buffer.b && !buffer.p && !buffer.d && !buffer.q && !c1 && c3) { - buffer.o = '$' + buffer.o + '$'; - } - /** @type {ParserOutput[]} */ - - - var ret = []; - - if (hyphenFollows) { - mhchemParser.concatArray(ret, this['output'](buffer)); - ret.push({ - type_: 'hyphen' - }); - } else { - c1 = mhchemParser.patterns.match_('digits', buffer.d || ""); - - if (isAfterD && c1 && c1.remainder === '') { - mhchemParser.concatArray(ret, mhchemParser.actions['d='](buffer, m)); - mhchemParser.concatArray(ret, this['output'](buffer)); - } else { - mhchemParser.concatArray(ret, this['output'](buffer)); - mhchemParser.concatArray(ret, mhchemParser.actions['bond'](buffer, m, "-")); - } - } - - return ret; - }, - 'a to o': function aToO(buffer) { - buffer.o = buffer.a; - buffer.a = undefined; - }, - 'sb=true': function sbTrue(buffer) { - buffer.sb = true; - }, - 'sb=false': function sbFalse(buffer) { - buffer.sb = false; - }, - 'beginsWithBond=true': function beginsWithBondTrue(buffer) { - buffer['beginsWithBond'] = true; - }, - 'beginsWithBond=false': function beginsWithBondFalse(buffer) { - buffer['beginsWithBond'] = false; - }, - 'parenthesisLevel++': function parenthesisLevel(buffer) { - buffer['parenthesisLevel']++; - }, - 'parenthesisLevel--': function parenthesisLevel(buffer) { - buffer['parenthesisLevel']--; - }, - 'state of aggregation': function stateOfAggregation(buffer, m) { - return { - type_: 'state of aggregation', - p1: mhchemParser.go(m, 'o') - }; - }, - 'comma': function comma(buffer, m) { - var a = m.replace(/\s*$/, ''); - var withSpace = a !== m; - - if (withSpace && buffer['parenthesisLevel'] === 0) { - return { - type_: 'comma enumeration L', - p1: a - }; - } else { - return { - type_: 'comma enumeration M', - p1: a - }; - } - }, - 'output': function output(buffer, m, entityFollows) { - // entityFollows: - // undefined = if we have nothing else to output, also ignore the just read space (buffer.sb) - // 1 = an entity follows, never omit the space if there was one just read before (can only apply to state 1) - // 2 = 1 + the entity can have an amount, so output a\, instead of converting it to o (can only apply to states a|as) - - /** @type {ParserOutput | ParserOutput[]} */ - var ret; - - if (!buffer.r) { - ret = []; - - if (!buffer.a && !buffer.b && !buffer.p && !buffer.o && !buffer.q && !buffer.d && !entityFollows) ; else { - if (buffer.sb) { - ret.push({ - type_: 'entitySkip' - }); - } - - if (!buffer.o && !buffer.q && !buffer.d && !buffer.b && !buffer.p && entityFollows !== 2) { - buffer.o = buffer.a; - buffer.a = undefined; - } else if (!buffer.o && !buffer.q && !buffer.d && (buffer.b || buffer.p)) { - buffer.o = buffer.a; - buffer.d = buffer.b; - buffer.q = buffer.p; - buffer.a = buffer.b = buffer.p = undefined; - } else { - if (buffer.o && buffer.dType === 'kv' && mhchemParser.patterns.match_('d-oxidation$', buffer.d || "")) { - buffer.dType = 'oxidation'; - } else if (buffer.o && buffer.dType === 'kv' && !buffer.q) { - buffer.dType = undefined; - } - } - - ret.push({ - type_: 'chemfive', - a: mhchemParser.go(buffer.a, 'a'), - b: mhchemParser.go(buffer.b, 'bd'), - p: mhchemParser.go(buffer.p, 'pq'), - o: mhchemParser.go(buffer.o, 'o'), - q: mhchemParser.go(buffer.q, 'pq'), - d: mhchemParser.go(buffer.d, buffer.dType === 'oxidation' ? 'oxidation' : 'bd'), - dType: buffer.dType - }); - } - } else { - // r - - /** @type {ParserOutput[]} */ - var rd; - - if (buffer.rdt === 'M') { - rd = mhchemParser.go(buffer.rd, 'tex-math'); - } else if (buffer.rdt === 'T') { - rd = [{ - type_: 'text', - p1: buffer.rd || "" - }]; - } else { - rd = mhchemParser.go(buffer.rd); - } - /** @type {ParserOutput[]} */ - - - var rq; - - if (buffer.rqt === 'M') { - rq = mhchemParser.go(buffer.rq, 'tex-math'); - } else if (buffer.rqt === 'T') { - rq = [{ - type_: 'text', - p1: buffer.rq || "" - }]; - } else { - rq = mhchemParser.go(buffer.rq); - } - - ret = { - type_: 'arrow', - r: buffer.r, - rd: rd, - rq: rq - }; - } - - for (var p in buffer) { - if (p !== 'parenthesisLevel' && p !== 'beginsWithBond') { - delete buffer[p]; - } - } - - return ret; - }, - 'oxidation-output': function oxidationOutput(buffer, m) { - var ret = ["{"]; - mhchemParser.concatArray(ret, mhchemParser.go(m, 'oxidation')); - ret.push("}"); - return ret; - }, - 'frac-output': function fracOutput(buffer, m) { - return { - type_: 'frac-ce', - p1: mhchemParser.go(m[0]), - p2: mhchemParser.go(m[1]) - }; - }, - 'overset-output': function oversetOutput(buffer, m) { - return { - type_: 'overset', - p1: mhchemParser.go(m[0]), - p2: mhchemParser.go(m[1]) - }; - }, - 'underset-output': function undersetOutput(buffer, m) { - return { - type_: 'underset', - p1: mhchemParser.go(m[0]), - p2: mhchemParser.go(m[1]) - }; - }, - 'underbrace-output': function underbraceOutput(buffer, m) { - return { - type_: 'underbrace', - p1: mhchemParser.go(m[0]), - p2: mhchemParser.go(m[1]) - }; - }, - 'color-output': function colorOutput(buffer, m) { - return { - type_: 'color', - color1: m[0], - color2: mhchemParser.go(m[1]) - }; - }, - 'r=': function r(buffer, m) { - buffer.r = m; - }, - 'rdt=': function rdt(buffer, m) { - buffer.rdt = m; - }, - 'rd=': function rd(buffer, m) { - buffer.rd = m; - }, - 'rqt=': function rqt(buffer, m) { - buffer.rqt = m; - }, - 'rq=': function rq(buffer, m) { - buffer.rq = m; - }, - 'operator': function operator(buffer, m, p1) { - return { - type_: 'operator', - kind_: p1 || m - }; - } - } - }, - 'a': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': {} - }, - '1/2$': { - '0': { - action_: '1/2' - } - }, - 'else': { - '0': { - nextState: '1', - revisit: true - } - }, - '$(...)$': { - '*': { - action_: 'tex-math tight', - nextState: '1' - } - }, - ',': { - '*': { - action_: { - type_: 'insert', - option: 'commaDecimal' - } - } - }, - 'else2': { - '*': { - action_: 'copy' - } - } - }), - actions: {} - }, - 'o': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': {} - }, - '1/2$': { - '0': { - action_: '1/2' - } - }, - 'else': { - '0': { - nextState: '1', - revisit: true - } - }, - 'letters': { - '*': { - action_: 'rm' - } - }, - '\\ca': { - '*': { - action_: { - type_: 'insert', - option: 'circa' - } - } - }, - '\\x{}{}|\\x{}|\\x': { - '*': { - action_: 'copy' - } - }, - '${(...)}$|$(...)$': { - '*': { - action_: 'tex-math' - } - }, - '{(...)}': { - '*': { - action_: '{text}' - } - }, - 'else2': { - '*': { - action_: 'copy' - } - } - }), - actions: {} - }, - 'text': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': { - action_: 'output' - } - }, - '{...}': { - '*': { - action_: 'text=' - } - }, - '${(...)}$|$(...)$': { - '*': { - action_: 'tex-math' - } - }, - '\\greek': { - '*': { - action_: ['output', 'rm'] - } - }, - '\\,|\\x{}{}|\\x{}|\\x': { - '*': { - action_: ['output', 'copy'] - } - }, - 'else': { - '*': { - action_: 'text=' - } - } - }), - actions: { - 'output': function output(buffer) { - if (buffer.text_) { - /** @type {ParserOutput} */ - var ret = { - type_: 'text', - p1: buffer.text_ - }; - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - } - } - } - }, - 'pq': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': {} - }, - 'state of aggregation $': { - '*': { - action_: 'state of aggregation' - } - }, - 'i$': { - '0': { - nextState: '!f', - revisit: true - } - }, - '(KV letters),': { - '0': { - action_: 'rm', - nextState: '0' - } - }, - 'formula$': { - '0': { - nextState: 'f', - revisit: true - } - }, - '1/2$': { - '0': { - action_: '1/2' - } - }, - 'else': { - '0': { - nextState: '!f', - revisit: true - } - }, - '${(...)}$|$(...)$': { - '*': { - action_: 'tex-math' - } - }, - '{(...)}': { - '*': { - action_: 'text' - } - }, - 'a-z': { - 'f': { - action_: 'tex-math' - } - }, - 'letters': { - '*': { - action_: 'rm' - } - }, - '-9.,9': { - '*': { - action_: '9,9' - } - }, - ',': { - '*': { - action_: { - type_: 'insert+p1', - option: 'comma enumeration S' - } - } - }, - '\\color{(...)}{(...)}1|\\color(...){(...)}2': { - '*': { - action_: 'color-output' - } - }, - '\\color{(...)}0': { - '*': { - action_: 'color0-output' - } - }, - '\\ce{(...)}': { - '*': { - action_: 'ce' - } - }, - '\\,|\\x{}{}|\\x{}|\\x': { - '*': { - action_: 'copy' - } - }, - 'else2': { - '*': { - action_: 'copy' - } - } - }), - actions: { - 'state of aggregation': function stateOfAggregation(buffer, m) { - return { - type_: 'state of aggregation subscript', - p1: mhchemParser.go(m, 'o') - }; - }, - 'color-output': function colorOutput(buffer, m) { - return { - type_: 'color', - color1: m[0], - color2: mhchemParser.go(m[1], 'pq') - }; - } - } - }, - 'bd': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': {} - }, - 'x$': { - '0': { - nextState: '!f', - revisit: true - } - }, - 'formula$': { - '0': { - nextState: 'f', - revisit: true - } - }, - 'else': { - '0': { - nextState: '!f', - revisit: true - } - }, - '-9.,9 no missing 0': { - '*': { - action_: '9,9' - } - }, - '.': { - '*': { - action_: { - type_: 'insert', - option: 'electron dot' - } - } - }, - 'a-z': { - 'f': { - action_: 'tex-math' - } - }, - 'x': { - '*': { - action_: { - type_: 'insert', - option: 'KV x' - } - } - }, - 'letters': { - '*': { - action_: 'rm' - } - }, - '\'': { - '*': { - action_: { - type_: 'insert', - option: 'prime' - } - } - }, - '${(...)}$|$(...)$': { - '*': { - action_: 'tex-math' - } - }, - '{(...)}': { - '*': { - action_: 'text' - } - }, - '\\color{(...)}{(...)}1|\\color(...){(...)}2': { - '*': { - action_: 'color-output' - } - }, - '\\color{(...)}0': { - '*': { - action_: 'color0-output' - } - }, - '\\ce{(...)}': { - '*': { - action_: 'ce' - } - }, - '\\,|\\x{}{}|\\x{}|\\x': { - '*': { - action_: 'copy' - } - }, - 'else2': { - '*': { - action_: 'copy' - } - } - }), - actions: { - 'color-output': function colorOutput(buffer, m) { - return { - type_: 'color', - color1: m[0], - color2: mhchemParser.go(m[1], 'bd') - }; - } - } - }, - 'oxidation': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': {} - }, - 'roman numeral': { - '*': { - action_: 'roman-numeral' - } - }, - '${(...)}$|$(...)$': { - '*': { - action_: 'tex-math' - } - }, - 'else': { - '*': { - action_: 'copy' - } - } - }), - actions: { - 'roman-numeral': function romanNumeral(buffer, m) { - return { - type_: 'roman numeral', - p1: m || "" - }; - } - } - }, - 'tex-math': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': { - action_: 'output' - } - }, - '\\ce{(...)}': { - '*': { - action_: ['output', 'ce'] - } - }, - '{...}|\\,|\\x{}{}|\\x{}|\\x': { - '*': { - action_: 'o=' - } - }, - 'else': { - '*': { - action_: 'o=' - } - } - }), - actions: { - 'output': function output(buffer) { - if (buffer.o) { - /** @type {ParserOutput} */ - var ret = { - type_: 'tex-math', - p1: buffer.o - }; - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - } - } - } - }, - 'tex-math tight': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': { - action_: 'output' - } - }, - '\\ce{(...)}': { - '*': { - action_: ['output', 'ce'] - } - }, - '{...}|\\,|\\x{}{}|\\x{}|\\x': { - '*': { - action_: 'o=' - } - }, - '-|+': { - '*': { - action_: 'tight operator' - } - }, - 'else': { - '*': { - action_: 'o=' - } - } - }), - actions: { - 'tight operator': function tightOperator(buffer, m) { - buffer.o = (buffer.o || "") + "{" + m + "}"; - }, - 'output': function output(buffer) { - if (buffer.o) { - /** @type {ParserOutput} */ - var ret = { - type_: 'tex-math', - p1: buffer.o - }; - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - } - } - } - }, - '9,9': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': {} - }, - ',': { - '*': { - action_: 'comma' - } - }, - 'else': { - '*': { - action_: 'copy' - } - } - }), - actions: { - 'comma': function comma() { - return { - type_: 'commaDecimal' - }; - } - } - }, - //#endregion - // - // \pu state machines - // - //#region pu - 'pu': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': { - action_: 'output' - } - }, - 'space$': { - '*': { - action_: ['output', 'space'] - } - }, - '{[(|)]}': { - '0|a': { - action_: 'copy' - } - }, - '(-)(9)^(-9)': { - '0': { - action_: 'number^', - nextState: 'a' - } - }, - '(-)(9.,9)(e)(99)': { - '0': { - action_: 'enumber', - nextState: 'a' - } - }, - 'space': { - '0|a': {} - }, - 'pm-operator': { - '0|a': { - action_: { - type_: 'operator', - option: '\\pm' - }, - nextState: '0' - } - }, - 'operator': { - '0|a': { - action_: 'copy', - nextState: '0' - } - }, - '//': { - 'd': { - action_: 'o=', - nextState: '/' - } - }, - '/': { - 'd': { - action_: 'o=', - nextState: '/' - } - }, - '{...}|else': { - '0|d': { - action_: 'd=', - nextState: 'd' - }, - 'a': { - action_: ['space', 'd='], - nextState: 'd' - }, - '/|q': { - action_: 'q=', - nextState: 'q' - } - } - }), - actions: { - 'enumber': function enumber(buffer, m) { - /** @type {ParserOutput[]} */ - var ret = []; - - if (m[0] === "+-" || m[0] === "+/-") { - ret.push("\\pm "); - } else if (m[0]) { - ret.push(m[0]); - } - - if (m[1]) { - mhchemParser.concatArray(ret, mhchemParser.go(m[1], 'pu-9,9')); - - if (m[2]) { - if (m[2].match(/[,.]/)) { - mhchemParser.concatArray(ret, mhchemParser.go(m[2], 'pu-9,9')); - } else { - ret.push(m[2]); - } - } - - m[3] = m[4] || m[3]; - - if (m[3]) { - m[3] = m[3].trim(); - - if (m[3] === "e" || m[3].substr(0, 1) === "*") { - ret.push({ - type_: 'cdot' - }); - } else { - ret.push({ - type_: 'times' - }); - } - } - } - - if (m[3]) { - ret.push("10^{" + m[5] + "}"); - } - - return ret; - }, - 'number^': function number(buffer, m) { - /** @type {ParserOutput[]} */ - var ret = []; - - if (m[0] === "+-" || m[0] === "+/-") { - ret.push("\\pm "); - } else if (m[0]) { - ret.push(m[0]); - } - - mhchemParser.concatArray(ret, mhchemParser.go(m[1], 'pu-9,9')); - ret.push("^{" + m[2] + "}"); - return ret; - }, - 'operator': function operator(buffer, m, p1) { - return { - type_: 'operator', - kind_: p1 || m - }; - }, - 'space': function space() { - return { - type_: 'pu-space-1' - }; - }, - 'output': function output(buffer) { - /** @type {ParserOutput | ParserOutput[]} */ - var ret; - var md = mhchemParser.patterns.match_('{(...)}', buffer.d || ""); - - if (md && md.remainder === '') { - buffer.d = md.match_; - } - - var mq = mhchemParser.patterns.match_('{(...)}', buffer.q || ""); - - if (mq && mq.remainder === '') { - buffer.q = mq.match_; - } - - if (buffer.d) { - buffer.d = buffer.d.replace(/\u00B0C|\^oC|\^{o}C/g, "{}^{\\circ}C"); - buffer.d = buffer.d.replace(/\u00B0F|\^oF|\^{o}F/g, "{}^{\\circ}F"); - } - - if (buffer.q) { - // fraction - buffer.q = buffer.q.replace(/\u00B0C|\^oC|\^{o}C/g, "{}^{\\circ}C"); - buffer.q = buffer.q.replace(/\u00B0F|\^oF|\^{o}F/g, "{}^{\\circ}F"); - var b5 = { - d: mhchemParser.go(buffer.d, 'pu'), - q: mhchemParser.go(buffer.q, 'pu') - }; - - if (buffer.o === '//') { - ret = { - type_: 'pu-frac', - p1: b5.d, - p2: b5.q - }; - } else { - ret = b5.d; - - if (b5.d.length > 1 || b5.q.length > 1) { - ret.push({ - type_: ' / ' - }); - } else { - ret.push({ - type_: '/' - }); - } - - mhchemParser.concatArray(ret, b5.q); - } - } else { - // no fraction - ret = mhchemParser.go(buffer.d, 'pu-2'); - } - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - } - } - }, - 'pu-2': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '*': { - action_: 'output' - } - }, - '*': { - '*': { - action_: ['output', 'cdot'], - nextState: '0' - } - }, - '\\x': { - '*': { - action_: 'rm=' - } - }, - 'space': { - '*': { - action_: ['output', 'space'], - nextState: '0' - } - }, - '^{(...)}|^(-1)': { - '1': { - action_: '^(-1)' - } - }, - '-9.,9': { - '0': { - action_: 'rm=', - nextState: '0' - }, - '1': { - action_: '^(-1)', - nextState: '0' - } - }, - '{...}|else': { - '*': { - action_: 'rm=', - nextState: '1' - } - } - }), - actions: { - 'cdot': function cdot() { - return { - type_: 'tight cdot' - }; - }, - '^(-1)': function _(buffer, m) { - buffer.rm += "^{" + m + "}"; - }, - 'space': function space() { - return { - type_: 'pu-space-2' - }; - }, - 'output': function output(buffer) { - /** @type {ParserOutput | ParserOutput[]} */ - var ret = []; - - if (buffer.rm) { - var mrm = mhchemParser.patterns.match_('{(...)}', buffer.rm || ""); - - if (mrm && mrm.remainder === '') { - ret = mhchemParser.go(mrm.match_, 'pu'); - } else { - ret = { - type_: 'rm', - p1: buffer.rm - }; - } - } - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - } - } - }, - 'pu-9,9': { - transitions: mhchemParser.createTransitions({ - 'empty': { - '0': { - action_: 'output-0' - }, - 'o': { - action_: 'output-o' - } - }, - ',': { - '0': { - action_: ['output-0', 'comma'], - nextState: 'o' - } - }, - '.': { - '0': { - action_: ['output-0', 'copy'], - nextState: 'o' - } - }, - 'else': { - '*': { - action_: 'text=' - } - } - }), - actions: { - 'comma': function comma() { - return { - type_: 'commaDecimal' - }; - }, - 'output-0': function output0(buffer) { - /** @type {ParserOutput[]} */ - var ret = []; - buffer.text_ = buffer.text_ || ""; - - if (buffer.text_.length > 4) { - var a = buffer.text_.length % 3; - - if (a === 0) { - a = 3; - } - - for (var i = buffer.text_.length - 3; i > 0; i -= 3) { - ret.push(buffer.text_.substr(i, 3)); - ret.push({ - type_: '1000 separator' - }); - } - - ret.push(buffer.text_.substr(0, a)); - ret.reverse(); - } else { - ret.push(buffer.text_); - } - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - }, - 'output-o': function outputO(buffer) { - /** @type {ParserOutput[]} */ - var ret = []; - buffer.text_ = buffer.text_ || ""; - - if (buffer.text_.length > 4) { - var a = buffer.text_.length - 3; - - for (var i = 0; i < a; i += 3) { - ret.push(buffer.text_.substr(i, 3)); - ret.push({ - type_: '1000 separator' - }); - } - - ret.push(buffer.text_.substr(i)); - } else { - ret.push(buffer.text_); - } - - for (var p in buffer) { - delete buffer[p]; - } - - return ret; - } - } //#endregion - - } -}; // -// texify: Take MhchemParser output and convert it to TeX -// - -/** @type {Texify} */ - -var texify = { - go: function go(input, isInner) { - // (recursive, max 4 levels) - if (!input) { - return ""; - } - - var res = ""; - var cee = false; - - for (var i = 0; i < input.length; i++) { - var inputi = input[i]; - - if (typeof inputi === "string") { - res += inputi; - } else { - res += texify._go2(inputi); - - if (inputi.type_ === '1st-level escape') { - cee = true; - } - } - } - - if (!isInner && !cee && res) { - res = "{" + res + "}"; - } - - return res; - }, - _goInner: function _goInner(input) { - if (!input) { - return input; - } - - return texify.go(input, true); - }, - _go2: function _go2(buf) { - /** @type {undefined | string} */ - var res; - - switch (buf.type_) { - case 'chemfive': - res = ""; - var b5 = { - a: texify._goInner(buf.a), - b: texify._goInner(buf.b), - p: texify._goInner(buf.p), - o: texify._goInner(buf.o), - q: texify._goInner(buf.q), - d: texify._goInner(buf.d) - }; // - // a - // - - if (b5.a) { - if (b5.a.match(/^[+\-]/)) { - b5.a = "{" + b5.a + "}"; - } - - res += b5.a + "\\,"; - } // - // b and p - // - - - if (b5.b || b5.p) { - res += "{\\vphantom{X}}"; - res += "^{\\hphantom{" + (b5.b || "") + "}}_{\\hphantom{" + (b5.p || "") + "}}"; - res += "{\\vphantom{X}}"; - res += "^{\\smash[t]{\\vphantom{2}}\\mathllap{" + (b5.b || "") + "}}"; - res += "_{\\vphantom{2}\\mathllap{\\smash[t]{" + (b5.p || "") + "}}}"; - } // - // o - // - - - if (b5.o) { - if (b5.o.match(/^[+\-]/)) { - b5.o = "{" + b5.o + "}"; - } - - res += b5.o; - } // - // q and d - // - - - if (buf.dType === 'kv') { - if (b5.d || b5.q) { - res += "{\\vphantom{X}}"; - } - - if (b5.d) { - res += "^{" + b5.d + "}"; - } - - if (b5.q) { - res += "_{\\smash[t]{" + b5.q + "}}"; - } - } else if (buf.dType === 'oxidation') { - if (b5.d) { - res += "{\\vphantom{X}}"; - res += "^{" + b5.d + "}"; - } - - if (b5.q) { - res += "{\\vphantom{X}}"; - res += "_{\\smash[t]{" + b5.q + "}}"; - } - } else { - if (b5.q) { - res += "{\\vphantom{X}}"; - res += "_{\\smash[t]{" + b5.q + "}}"; - } - - if (b5.d) { - res += "{\\vphantom{X}}"; - res += "^{" + b5.d + "}"; - } - } - - break; - - case 'rm': - res = "\\mathrm{" + buf.p1 + "}"; - break; - - case 'text': - if (buf.p1.match(/[\^_]/)) { - buf.p1 = buf.p1.replace(" ", "~").replace("-", "\\text{-}"); - res = "\\mathrm{" + buf.p1 + "}"; - } else { - res = "\\text{" + buf.p1 + "}"; - } - - break; - - case 'roman numeral': - res = "\\mathrm{" + buf.p1 + "}"; - break; - - case 'state of aggregation': - res = "\\mskip2mu " + texify._goInner(buf.p1); - break; - - case 'state of aggregation subscript': - res = "\\mskip1mu " + texify._goInner(buf.p1); - break; - - case 'bond': - res = texify._getBond(buf.kind_); - - if (!res) { - throw ["MhchemErrorBond", "mhchem Error. Unknown bond type (" + buf.kind_ + ")"]; - } - - break; - - case 'frac': - var c = "\\frac{" + buf.p1 + "}{" + buf.p2 + "}"; - res = "\\mathchoice{\\textstyle" + c + "}{" + c + "}{" + c + "}{" + c + "}"; - break; - - case 'pu-frac': - var d = "\\frac{" + texify._goInner(buf.p1) + "}{" + texify._goInner(buf.p2) + "}"; - res = "\\mathchoice{\\textstyle" + d + "}{" + d + "}{" + d + "}{" + d + "}"; - break; - - case 'tex-math': - res = buf.p1 + " "; - break; - - case 'frac-ce': - res = "\\frac{" + texify._goInner(buf.p1) + "}{" + texify._goInner(buf.p2) + "}"; - break; - - case 'overset': - res = "\\overset{" + texify._goInner(buf.p1) + "}{" + texify._goInner(buf.p2) + "}"; - break; - - case 'underset': - res = "\\underset{" + texify._goInner(buf.p1) + "}{" + texify._goInner(buf.p2) + "}"; - break; - - case 'underbrace': - res = "\\underbrace{" + texify._goInner(buf.p1) + "}_{" + texify._goInner(buf.p2) + "}"; - break; - - case 'color': - res = "{\\color{" + buf.color1 + "}{" + texify._goInner(buf.color2) + "}}"; - break; - - case 'color0': - res = "\\color{" + buf.color + "}"; - break; - - case 'arrow': - var b6 = { - rd: texify._goInner(buf.rd), - rq: texify._goInner(buf.rq) - }; - - var arrow = "\\x" + texify._getArrow(buf.r); - - if (b6.rq) { - arrow += "[{" + b6.rq + "}]"; - } - - if (b6.rd) { - arrow += "{" + b6.rd + "}"; - } else { - arrow += "{}"; - } - - res = arrow; - break; - - case 'operator': - res = texify._getOperator(buf.kind_); - break; - - case '1st-level escape': - res = buf.p1 + " "; // &, \\\\, \\hlin - - break; - - case 'space': - res = " "; - break; - - case 'entitySkip': - res = "~"; - break; - - case 'pu-space-1': - res = "~"; - break; - - case 'pu-space-2': - res = "\\mkern3mu "; - break; - - case '1000 separator': - res = "\\mkern2mu "; - break; - - case 'commaDecimal': - res = "{,}"; - break; - - case 'comma enumeration L': - res = "{" + buf.p1 + "}\\mkern6mu "; - break; - - case 'comma enumeration M': - res = "{" + buf.p1 + "}\\mkern3mu "; - break; - - case 'comma enumeration S': - res = "{" + buf.p1 + "}\\mkern1mu "; - break; - - case 'hyphen': - res = "\\text{-}"; - break; - - case 'addition compound': - res = "\\,{\\cdot}\\,"; - break; - - case 'electron dot': - res = "\\mkern1mu \\bullet\\mkern1mu "; - break; - - case 'KV x': - res = "{\\times}"; - break; - - case 'prime': - res = "\\prime "; - break; - - case 'cdot': - res = "\\cdot "; - break; - - case 'tight cdot': - res = "\\mkern1mu{\\cdot}\\mkern1mu "; - break; - - case 'times': - res = "\\times "; - break; - - case 'circa': - res = "{\\sim}"; - break; - - case '^': - res = "uparrow"; - break; - - case 'v': - res = "downarrow"; - break; - - case 'ellipsis': - res = "\\ldots "; - break; - - case '/': - res = "/"; - break; - - case ' / ': - res = "\\,/\\,"; - break; - - default: - throw ["MhchemBugT", "mhchem bug T. Please report."]; - // Missing texify rule or unknown MhchemParser output - } - return res; - }, - _getArrow: function _getArrow(a) { - switch (a) { - case "->": - return "rightarrow"; - - case "\u2192": - return "rightarrow"; - - case "\u27F6": - return "rightarrow"; - - case "<-": - return "leftarrow"; - - case "<->": - return "leftrightarrow"; - - case "<-->": - return "rightleftarrows"; - - case "<=>": - return "rightleftharpoons"; - - case "\u21CC": - return "rightleftharpoons"; - - case "<=>>": - return "rightequilibrium"; - - case "<<=>": - return "leftequilibrium"; - - default: - throw ["MhchemBugT", "mhchem bug T. Please report."]; - } - }, - _getBond: function _getBond(a) { - switch (a) { - case "-": - return "{-}"; - - case "1": - return "{-}"; - - case "=": - return "{=}"; - - case "2": - return "{=}"; - - case "#": - return "{\\equiv}"; - - case "3": - return "{\\equiv}"; - - case "~": - return "{\\tripledash}"; - - case "~-": - return "{\\mathrlap{\\raisebox{-.1em}{$-$}}\\raisebox{.1em}{$\\tripledash$}}"; - - case "~=": - return "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}"; - - case "~--": - return "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}"; - - case "-~-": - return "{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$-$}}\\tripledash}"; - - case "...": - return "{{\\cdot}{\\cdot}{\\cdot}}"; - - case "....": - return "{{\\cdot}{\\cdot}{\\cdot}{\\cdot}}"; - - case "->": - return "{\\rightarrow}"; - - case "<-": - return "{\\leftarrow}"; - - case "<": - return "{<}"; - - case ">": - return "{>}"; - - default: - throw ["MhchemBugT", "mhchem bug T. Please report."]; - } - }, - _getOperator: function _getOperator(a) { - switch (a) { - case "+": - return " {}+{} "; - - case "-": - return " {}-{} "; - - case "=": - return " {}={} "; - - case "<": - return " {}<{} "; - - case ">": - return " {}>{} "; - - case "<<": - return " {}\\ll{} "; - - case ">>": - return " {}\\gg{} "; - - case "\\pm": - return " {}\\pm{} "; - - case "\\approx": - return " {}\\approx{} "; - - case "$\\approx$": - return " {}\\approx{} "; - - case "v": - return " \\downarrow{} "; - - case "(v)": - return " \\downarrow{} "; - - case "^": - return " \\uparrow{} "; - - case "(^)": - return " \\uparrow{} "; - - default: - throw ["MhchemBugT", "mhchem bug T. Please report."]; - } - } -}; // diff --git a/docs/static/katex/contrib/render-a11y-string.js b/docs/static/katex/contrib/render-a11y-string.js deleted file mode 100644 index d8b27c77b95..00000000000 --- a/docs/static/katex/contrib/render-a11y-string.js +++ /dev/null @@ -1,870 +0,0 @@ -(function webpackUniversalModuleDefinition(root, factory) { - if(typeof exports === 'object' && typeof module === 'object') - module.exports = factory(require("katex")); - else if(typeof define === 'function' && define.amd) - define(["katex"], factory); - else { - var a = typeof exports === 'object' ? factory(require("katex")) : factory(root["katex"]); - for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; - } -})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__0__) { -return /******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); -/******/ } -/******/ }; -/******/ -/******/ // define __esModule on exports -/******/ __webpack_require__.r = function(exports) { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ -/******/ // create a fake namespace object -/******/ // mode & 1: value is a module id, require it -/******/ // mode & 2: merge all properties of value into the ns -/******/ // mode & 4: return value when already ns object -/******/ // mode & 8|1: behave like require -/******/ __webpack_require__.t = function(value, mode) { -/******/ if(mode & 1) value = __webpack_require__(value); -/******/ if(mode & 8) return value; -/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; -/******/ var ns = Object.create(null); -/******/ __webpack_require__.r(ns); -/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); -/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); -/******/ return ns; -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; -/******/ -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = 1); -/******/ }) -/************************************************************************/ -/******/ ([ -/* 0 */ -/***/ (function(module, exports) { - -module.exports = __WEBPACK_EXTERNAL_MODULE__0__; - -/***/ }), -/* 1 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony import */ var katex__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0); -/* harmony import */ var katex__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(katex__WEBPACK_IMPORTED_MODULE_0__); -/** - * renderA11yString returns a readable string. - * - * In some cases the string will have the proper semantic math - * meaning,: - * renderA11yString("\\frac{1}{2}"") - * -> "start fraction, 1, divided by, 2, end fraction" - * - * However, other cases do not: - * renderA11yString("f(x) = x^2") - * -> "f, left parenthesis, x, right parenthesis, equals, x, squared" - * - * The commas in the string aim to increase ease of understanding - * when read by a screenreader. - */ -// NOTE: since we're importing types here these files won't actually be -// included in the build. -// $FlowIgnore: we import the types directly anyways - -var stringMap = { - "(": "left parenthesis", - ")": "right parenthesis", - "[": "open bracket", - "]": "close bracket", - "\\{": "left brace", - "\\}": "right brace", - "\\lvert": "open vertical bar", - "\\rvert": "close vertical bar", - "|": "vertical bar", - "\\uparrow": "up arrow", - "\\Uparrow": "up arrow", - "\\downarrow": "down arrow", - "\\Downarrow": "down arrow", - "\\updownarrow": "up down arrow", - "\\leftarrow": "left arrow", - "\\Leftarrow": "left arrow", - "\\rightarrow": "right arrow", - "\\Rightarrow": "right arrow", - "\\langle": "open angle", - "\\rangle": "close angle", - "\\lfloor": "open floor", - "\\rfloor": "close floor", - "\\int": "integral", - "\\intop": "integral", - "\\lim": "limit", - "\\ln": "natural log", - "\\log": "log", - "\\sin": "sine", - "\\cos": "cosine", - "\\tan": "tangent", - "\\cot": "cotangent", - "\\sum": "sum", - "/": "slash", - ",": "comma", - ".": "point", - "-": "negative", - "+": "plus", - "~": "tilde", - ":": "colon", - "?": "question mark", - "'": "apostrophe", - "\\%": "percent", - " ": "space", - "\\ ": "space", - "\\$": "dollar sign", - "\\angle": "angle", - "\\degree": "degree", - "\\circ": "circle", - "\\vec": "vector", - "\\triangle": "triangle", - "\\pi": "pi", - "\\prime": "prime", - "\\infty": "infinity", - "\\alpha": "alpha", - "\\beta": "beta", - "\\gamma": "gamma", - "\\omega": "omega", - "\\theta": "theta", - "\\sigma": "sigma", - "\\lambda": "lambda", - "\\tau": "tau", - "\\Delta": "delta", - "\\delta": "delta", - "\\mu": "mu", - "\\rho": "rho", - "\\nabla": "del", - "\\ell": "ell", - "\\ldots": "dots", - // TODO: add entries for all accents - "\\hat": "hat", - "\\acute": "acute" -}; -var powerMap = { - "prime": "prime", - "degree": "degrees", - "circle": "degrees", - "2": "squared", - "3": "cubed" -}; -var openMap = { - "|": "open vertical bar", - ".": "" -}; -var closeMap = { - "|": "close vertical bar", - ".": "" -}; -var binMap = { - "+": "plus", - "-": "minus", - "\\pm": "plus minus", - "\\cdot": "dot", - "*": "times", - "/": "divided by", - "\\times": "times", - "\\div": "divided by", - "\\circ": "circle", - "\\bullet": "bullet" -}; -var relMap = { - "=": "equals", - "\\approx": "approximately equals", - "≠": "does not equal", - "\\geq": "is greater than or equal to", - "\\ge": "is greater than or equal to", - "\\leq": "is less than or equal to", - "\\le": "is less than or equal to", - ">": "is greater than", - "<": "is less than", - "\\leftarrow": "left arrow", - "\\Leftarrow": "left arrow", - "\\rightarrow": "right arrow", - "\\Rightarrow": "right arrow", - ":": "colon" -}; -var accentUnderMap = { - "\\underleftarrow": "left arrow", - "\\underrightarrow": "right arrow", - "\\underleftrightarrow": "left-right arrow", - "\\undergroup": "group", - "\\underlinesegment": "line segment", - "\\utilde": "tilde" -}; - -var buildString = function buildString(str, type, a11yStrings) { - if (!str) { - return; - } - - var ret; - - if (type === "open") { - ret = str in openMap ? openMap[str] : stringMap[str] || str; - } else if (type === "close") { - ret = str in closeMap ? closeMap[str] : stringMap[str] || str; - } else if (type === "bin") { - ret = binMap[str] || str; - } else if (type === "rel") { - ret = relMap[str] || str; - } else { - ret = stringMap[str] || str; - } // If the text to add is a number and there is already a string - // in the list and the last string is a number then we should - // combine them into a single number - - - if (/^\d+$/.test(ret) && a11yStrings.length > 0 && // TODO(kevinb): check that the last item in a11yStrings is a string - // I think we might be able to drop the nested arrays, which would make - // this easier to type - $FlowFixMe - /^\d+$/.test(a11yStrings[a11yStrings.length - 1])) { - a11yStrings[a11yStrings.length - 1] += ret; - } else if (ret) { - a11yStrings.push(ret); - } -}; - -var buildRegion = function buildRegion(a11yStrings, callback) { - var regionStrings = []; - a11yStrings.push(regionStrings); - callback(regionStrings); -}; - -var handleObject = function handleObject(tree, a11yStrings, atomType) { - // Everything else is assumed to be an object... - switch (tree.type) { - case "accent": - { - buildRegion(a11yStrings, function (a11yStrings) { - buildA11yStrings(tree.base, a11yStrings, atomType); - a11yStrings.push("with"); - buildString(tree.label, "normal", a11yStrings); - a11yStrings.push("on top"); - }); - break; - } - - case "accentUnder": - { - buildRegion(a11yStrings, function (a11yStrings) { - buildA11yStrings(tree.base, a11yStrings, atomType); - a11yStrings.push("with"); - buildString(accentUnderMap[tree.label], "normal", a11yStrings); - a11yStrings.push("underneath"); - }); - break; - } - - case "accent-token": - { - // Used internally by accent symbols. - break; - } - - case "atom": - { - var text = tree.text; - - switch (tree.family) { - case "bin": - { - buildString(text, "bin", a11yStrings); - break; - } - - case "close": - { - buildString(text, "close", a11yStrings); - break; - } - // TODO(kevinb): figure out what should be done for inner - - case "inner": - { - buildString(tree.text, "inner", a11yStrings); - break; - } - - case "open": - { - buildString(text, "open", a11yStrings); - break; - } - - case "punct": - { - buildString(text, "punct", a11yStrings); - break; - } - - case "rel": - { - buildString(text, "rel", a11yStrings); - break; - } - - default: - { - tree.family; - throw new Error("\"" + tree.family + "\" is not a valid atom type"); - } - } - - break; - } - - case "color": - { - var color = tree.color.replace(/katex-/, ""); - buildRegion(a11yStrings, function (regionStrings) { - regionStrings.push("start color " + color); - buildA11yStrings(tree.body, regionStrings, atomType); - regionStrings.push("end color " + color); - }); - break; - } - - case "color-token": - { - // Used by \color, \colorbox, and \fcolorbox but not directly rendered. - // It's a leaf node and has no children so just break. - break; - } - - case "delimsizing": - { - if (tree.delim && tree.delim !== ".") { - buildString(tree.delim, "normal", a11yStrings); - } - - break; - } - - case "genfrac": - { - buildRegion(a11yStrings, function (regionStrings) { - // genfrac can have unbalanced delimiters - var leftDelim = tree.leftDelim, - rightDelim = tree.rightDelim; // NOTE: Not sure if this is a safe assumption - // hasBarLine true -> fraction, false -> binomial - - if (tree.hasBarLine) { - regionStrings.push("start fraction"); - leftDelim && buildString(leftDelim, "open", regionStrings); - buildA11yStrings(tree.numer, regionStrings, atomType); - regionStrings.push("divided by"); - buildA11yStrings(tree.denom, regionStrings, atomType); - rightDelim && buildString(rightDelim, "close", regionStrings); - regionStrings.push("end fraction"); - } else { - regionStrings.push("start binomial"); - leftDelim && buildString(leftDelim, "open", regionStrings); - buildA11yStrings(tree.numer, regionStrings, atomType); - regionStrings.push("over"); - buildA11yStrings(tree.denom, regionStrings, atomType); - rightDelim && buildString(rightDelim, "close", regionStrings); - regionStrings.push("end binomial"); - } - }); - break; - } - - case "kern": - { - // No op: we don't attempt to present kerning information - // to the screen reader. - break; - } - - case "leftright": - { - buildRegion(a11yStrings, function (regionStrings) { - buildString(tree.left, "open", regionStrings); - buildA11yStrings(tree.body, regionStrings, atomType); - buildString(tree.right, "close", regionStrings); - }); - break; - } - - case "leftright-right": - { - // TODO: double check that this is a no-op - break; - } - - case "lap": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "mathord": - { - buildString(tree.text, "normal", a11yStrings); - break; - } - - case "op": - { - var body = tree.body, - name = tree.name; - - if (body) { - buildA11yStrings(body, a11yStrings, atomType); - } else if (name) { - buildString(name, "normal", a11yStrings); - } - - break; - } - - case "op-token": - { - // Used internally by operator symbols. - buildString(tree.text, atomType, a11yStrings); - break; - } - - case "ordgroup": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "overline": - { - buildRegion(a11yStrings, function (a11yStrings) { - a11yStrings.push("start overline"); - buildA11yStrings(tree.body, a11yStrings, atomType); - a11yStrings.push("end overline"); - }); - break; - } - - case "phantom": - { - a11yStrings.push("empty space"); - break; - } - - case "raisebox": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "rule": - { - a11yStrings.push("rectangle"); - break; - } - - case "sizing": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "spacing": - { - a11yStrings.push("space"); - break; - } - - case "styling": - { - // We ignore the styling and just pass through the contents - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "sqrt": - { - buildRegion(a11yStrings, function (regionStrings) { - var body = tree.body, - index = tree.index; - - if (index) { - var indexString = flatten(buildA11yStrings(index, [], atomType)).join(","); - - if (indexString === "3") { - regionStrings.push("cube root of"); - buildA11yStrings(body, regionStrings, atomType); - regionStrings.push("end cube root"); - return; - } - - regionStrings.push("root"); - regionStrings.push("start index"); - buildA11yStrings(index, regionStrings, atomType); - regionStrings.push("end index"); - return; - } - - regionStrings.push("square root of"); - buildA11yStrings(body, regionStrings, atomType); - regionStrings.push("end square root"); - }); - break; - } - - case "supsub": - { - var base = tree.base, - sub = tree.sub, - sup = tree.sup; - var isLog = false; - - if (base) { - buildA11yStrings(base, a11yStrings, atomType); - isLog = base.type === "op" && base.name === "\\log"; - } - - if (sub) { - var regionName = isLog ? "base" : "subscript"; - buildRegion(a11yStrings, function (regionStrings) { - regionStrings.push("start " + regionName); - buildA11yStrings(sub, regionStrings, atomType); - regionStrings.push("end " + regionName); - }); - } - - if (sup) { - buildRegion(a11yStrings, function (regionStrings) { - var supString = flatten(buildA11yStrings(sup, [], atomType)).join(","); - - if (supString in powerMap) { - regionStrings.push(powerMap[supString]); - return; - } - - regionStrings.push("start superscript"); - buildA11yStrings(sup, regionStrings, atomType); - regionStrings.push("end superscript"); - }); - } - - break; - } - - case "text": - { - // TODO: handle other fonts - if (tree.font === "\\textbf") { - buildRegion(a11yStrings, function (regionStrings) { - regionStrings.push("start bold text"); - buildA11yStrings(tree.body, regionStrings, atomType); - regionStrings.push("end bold text"); - }); - break; - } - - buildRegion(a11yStrings, function (regionStrings) { - regionStrings.push("start text"); - buildA11yStrings(tree.body, regionStrings, atomType); - regionStrings.push("end text"); - }); - break; - } - - case "textord": - { - buildString(tree.text, atomType, a11yStrings); - break; - } - - case "smash": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "enclose": - { - // TODO: create a map for these. - // TODO: differentiate between a body with a single atom, e.g. - // "cancel a" instead of "start cancel, a, end cancel" - if (/cancel/.test(tree.label)) { - buildRegion(a11yStrings, function (regionStrings) { - regionStrings.push("start cancel"); - buildA11yStrings(tree.body, regionStrings, atomType); - regionStrings.push("end cancel"); - }); - break; - } else if (/box/.test(tree.label)) { - buildRegion(a11yStrings, function (regionStrings) { - regionStrings.push("start box"); - buildA11yStrings(tree.body, regionStrings, atomType); - regionStrings.push("end box"); - }); - break; - } else if (/sout/.test(tree.label)) { - buildRegion(a11yStrings, function (regionStrings) { - regionStrings.push("start strikeout"); - buildA11yStrings(tree.body, regionStrings, atomType); - regionStrings.push("end strikeout"); - }); - break; - } - - throw new Error("KaTeX-a11y: enclose node with " + tree.label + " not supported yet"); - } - - case "vphantom": - { - throw new Error("KaTeX-a11y: vphantom not implemented yet"); - } - - case "hphantom": - { - throw new Error("KaTeX-a11y: hphantom not implemented yet"); - } - - case "operatorname": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "array": - { - throw new Error("KaTeX-a11y: array not implemented yet"); - } - - case "raw": - { - throw new Error("KaTeX-a11y: raw not implemented yet"); - } - - case "size": - { - // Although there are nodes of type "size" in the parse tree, they have - // no semantic meaning and should be ignored. - break; - } - - case "url": - { - throw new Error("KaTeX-a11y: url not implemented yet"); - } - - case "tag": - { - throw new Error("KaTeX-a11y: tag not implemented yet"); - } - - case "verb": - { - buildString("start verbatim", "normal", a11yStrings); - buildString(tree.body, "normal", a11yStrings); - buildString("end verbatim", "normal", a11yStrings); - break; - } - - case "environment": - { - throw new Error("KaTeX-a11y: environment not implemented yet"); - } - - case "horizBrace": - { - buildString("start " + tree.label.slice(1), "normal", a11yStrings); - buildA11yStrings(tree.base, a11yStrings, atomType); - buildString("end " + tree.label.slice(1), "normal", a11yStrings); - break; - } - - case "infix": - { - // All infix nodes are replace with other nodes. - break; - } - - case "includegraphics": - { - throw new Error("KaTeX-a11y: includegraphics not implemented yet"); - } - - case "font": - { - // TODO: callout the start/end of specific fonts - // TODO: map \BBb{N} to "the naturals" or something like that - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "href": - { - throw new Error("KaTeX-a11y: href not implemented yet"); - } - - case "cr": - { - // This is used by environments. - throw new Error("KaTeX-a11y: cr not implemented yet"); - } - - case "underline": - { - buildRegion(a11yStrings, function (a11yStrings) { - a11yStrings.push("start underline"); - buildA11yStrings(tree.body, a11yStrings, atomType); - a11yStrings.push("end underline"); - }); - break; - } - - case "xArrow": - { - throw new Error("KaTeX-a11y: xArrow not implemented yet"); - } - - case "mclass": - { - // \neq and \ne are macros so we let "htmlmathml" render the mathmal - // side of things and extract the text from that. - var _atomType = tree.mclass.slice(1); // $FlowFixMe: drop the leading "m" from the values in mclass - - - buildA11yStrings(tree.body, a11yStrings, _atomType); - break; - } - - case "mathchoice": - { - // TODO: track which which style we're using, e.g. dispaly, text, etc. - // default to text style if even that may not be the correct style - buildA11yStrings(tree.text, a11yStrings, atomType); - break; - } - - case "htmlmathml": - { - buildA11yStrings(tree.mathml, a11yStrings, atomType); - break; - } - - case "middle": - { - buildString(tree.delim, atomType, a11yStrings); - break; - } - - case "internal": - { - // internal nodes are never included in the parse tree - break; - } - - case "html": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - default: - tree.type; - throw new Error("KaTeX a11y un-recognized type: " + tree.type); - } -}; - -var buildA11yStrings = function buildA11yStrings(tree, a11yStrings, atomType) { - if (a11yStrings === void 0) { - a11yStrings = []; - } - - if (tree instanceof Array) { - for (var i = 0; i < tree.length; i++) { - buildA11yStrings(tree[i], a11yStrings, atomType); - } - } else { - handleObject(tree, a11yStrings, atomType); - } - - return a11yStrings; -}; - -var flatten = function flatten(array) { - var result = []; - array.forEach(function (item) { - if (item instanceof Array) { - result = result.concat(flatten(item)); - } else { - result.push(item); - } - }); - return result; -}; - -var renderA11yString = function renderA11yString(text, settings) { - var tree = katex__WEBPACK_IMPORTED_MODULE_0___default.a.__parse(text, settings); - - var a11yStrings = buildA11yStrings(tree, [], "normal"); - return flatten(a11yStrings).join(", "); -}; - -/* harmony default export */ __webpack_exports__["default"] = (renderA11yString); - -/***/ }) -/******/ ])["default"]; -}); \ No newline at end of file diff --git a/docs/static/katex/contrib/render-a11y-string.min.js b/docs/static/katex/contrib/render-a11y-string.min.js deleted file mode 100644 index 5d406a6c6d2..00000000000 --- a/docs/static/katex/contrib/render-a11y-string.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(e,r){if("object"==typeof exports&&"object"==typeof module)module.exports=r(require("katex"));else if("function"==typeof define&&define.amd)define(["katex"],r);else{var t="object"==typeof exports?r(require("katex")):r(e.katex);for(var a in t)("object"==typeof exports?exports:e)[a]=t[a]}}("undefined"!=typeof self?self:this,function(e){return function(e){var r={};function t(a){if(r[a])return r[a].exports;var o=r[a]={i:a,l:!1,exports:{}};return e[a].call(o.exports,o,o.exports,t),o.l=!0,o.exports}return t.m=e,t.c=r,t.d=function(e,r,a){t.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:a})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,r){if(1&r&&(e=t(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var a=Object.create(null);if(t.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var o in e)t.d(a,o,function(r){return e[r]}.bind(null,o));return a},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=1)}([function(r,t){r.exports=e},function(e,r,t){"use strict";t.r(r);var a=t(0),o=t.n(a),n={"(":"left parenthesis",")":"right parenthesis","[":"open bracket","]":"close bracket","\\{":"left brace","\\}":"right brace","\\lvert":"open vertical bar","\\rvert":"close vertical bar","|":"vertical bar","\\uparrow":"up arrow","\\Uparrow":"up arrow","\\downarrow":"down arrow","\\Downarrow":"down arrow","\\updownarrow":"up down arrow","\\leftarrow":"left arrow","\\Leftarrow":"left arrow","\\rightarrow":"right arrow","\\Rightarrow":"right arrow","\\langle":"open angle","\\rangle":"close angle","\\lfloor":"open floor","\\rfloor":"close floor","\\int":"integral","\\intop":"integral","\\lim":"limit","\\ln":"natural log","\\log":"log","\\sin":"sine","\\cos":"cosine","\\tan":"tangent","\\cot":"cotangent","\\sum":"sum","/":"slash",",":"comma",".":"point","-":"negative","+":"plus","~":"tilde",":":"colon","?":"question mark","'":"apostrophe","\\%":"percent"," ":"space","\\ ":"space","\\$":"dollar sign","\\angle":"angle","\\degree":"degree","\\circ":"circle","\\vec":"vector","\\triangle":"triangle","\\pi":"pi","\\prime":"prime","\\infty":"infinity","\\alpha":"alpha","\\beta":"beta","\\gamma":"gamma","\\omega":"omega","\\theta":"theta","\\sigma":"sigma","\\lambda":"lambda","\\tau":"tau","\\Delta":"delta","\\delta":"delta","\\mu":"mu","\\rho":"rho","\\nabla":"del","\\ell":"ell","\\ldots":"dots","\\hat":"hat","\\acute":"acute"},s={prime:"prime",degree:"degrees",circle:"degrees",2:"squared",3:"cubed"},i={"|":"open vertical bar",".":""},l={"|":"close vertical bar",".":""},c={"+":"plus","-":"minus","\\pm":"plus minus","\\cdot":"dot","*":"times","/":"divided by","\\times":"times","\\div":"divided by","\\circ":"circle","\\bullet":"bullet"},u={"=":"equals","\\approx":"approximately equals","\u2260":"does not equal","\\geq":"is greater than or equal to","\\ge":"is greater than or equal to","\\leq":"is less than or equal to","\\le":"is less than or equal to",">":"is greater than","<":"is less than","\\leftarrow":"left arrow","\\Leftarrow":"left arrow","\\rightarrow":"right arrow","\\Rightarrow":"right arrow",":":"colon"},p={"\\underleftarrow":"left arrow","\\underrightarrow":"right arrow","\\underleftrightarrow":"left-right arrow","\\undergroup":"group","\\underlinesegment":"line segment","\\utilde":"tilde"},d=function(e,r,t){var a;e&&(/^\d+$/.test(a="open"===r?e in i?i[e]:n[e]||e:"close"===r?e in l?l[e]:n[e]||e:"bin"===r?c[e]||e:"rel"===r?u[e]||e:n[e]||e)&&t.length>0&&/^\d+$/.test(t[t.length-1])?t[t.length-1]+=a:a&&t.push(a))},b=function(e,r){var t=[];e.push(t),r(t)},h=function(e,r,t){switch(e.type){case"accent":b(r,function(r){f(e.base,r,t),r.push("with"),d(e.label,"normal",r),r.push("on top")});break;case"accentUnder":b(r,function(r){f(e.base,r,t),r.push("with"),d(p[e.label],"normal",r),r.push("underneath")});break;case"accent-token":break;case"atom":var a=e.text;switch(e.family){case"bin":d(a,"bin",r);break;case"close":d(a,"close",r);break;case"inner":d(e.text,"inner",r);break;case"open":d(a,"open",r);break;case"punct":d(a,"punct",r);break;case"rel":d(a,"rel",r);break;default:throw e.family,new Error('"'+e.family+'" is not a valid atom type')}break;case"color":var o=e.color.replace(/katex-/,"");b(r,function(r){r.push("start color "+o),f(e.body,r,t),r.push("end color "+o)});break;case"color-token":break;case"delimsizing":e.delim&&"."!==e.delim&&d(e.delim,"normal",r);break;case"genfrac":b(r,function(r){var a=e.leftDelim,o=e.rightDelim;e.hasBarLine?(r.push("start fraction"),a&&d(a,"open",r),f(e.numer,r,t),r.push("divided by"),f(e.denom,r,t),o&&d(o,"close",r),r.push("end fraction")):(r.push("start binomial"),a&&d(a,"open",r),f(e.numer,r,t),r.push("over"),f(e.denom,r,t),o&&d(o,"close",r),r.push("end binomial"))});break;case"kern":break;case"leftright":b(r,function(r){d(e.left,"open",r),f(e.body,r,t),d(e.right,"close",r)});break;case"leftright-right":break;case"lap":f(e.body,r,t);break;case"mathord":d(e.text,"normal",r);break;case"op":var n=e.body,i=e.name;n?f(n,r,t):i&&d(i,"normal",r);break;case"op-token":d(e.text,t,r);break;case"ordgroup":f(e.body,r,t);break;case"overline":b(r,function(r){r.push("start overline"),f(e.body,r,t),r.push("end overline")});break;case"phantom":r.push("empty space");break;case"raisebox":f(e.body,r,t);break;case"rule":r.push("rectangle");break;case"sizing":f(e.body,r,t);break;case"spacing":r.push("space");break;case"styling":f(e.body,r,t);break;case"sqrt":b(r,function(r){var a=e.body,o=e.index;if(o)return"3"===m(f(o,[],t)).join(",")?(r.push("cube root of"),f(a,r,t),void r.push("end cube root")):(r.push("root"),r.push("start index"),f(o,r,t),void r.push("end index"));r.push("square root of"),f(a,r,t),r.push("end square root")});break;case"supsub":var l=e.base,c=e.sub,u=e.sup,h=!1;if(l&&(f(l,r,t),h="op"===l.type&&"\\log"===l.name),c){var y=h?"base":"subscript";b(r,function(e){e.push("start "+y),f(c,e,t),e.push("end "+y)})}u&&b(r,function(e){var r=m(f(u,[],t)).join(",");r in s?e.push(s[r]):(e.push("start superscript"),f(u,e,t),e.push("end superscript"))});break;case"text":if("\\textbf"===e.font){b(r,function(r){r.push("start bold text"),f(e.body,r,t),r.push("end bold text")});break}b(r,function(r){r.push("start text"),f(e.body,r,t),r.push("end text")});break;case"textord":d(e.text,t,r);break;case"smash":f(e.body,r,t);break;case"enclose":if(/cancel/.test(e.label)){b(r,function(r){r.push("start cancel"),f(e.body,r,t),r.push("end cancel")});break}if(/box/.test(e.label)){b(r,function(r){r.push("start box"),f(e.body,r,t),r.push("end box")});break}if(/sout/.test(e.label)){b(r,function(r){r.push("start strikeout"),f(e.body,r,t),r.push("end strikeout")});break}throw new Error("KaTeX-a11y: enclose node with "+e.label+" not supported yet");case"vphantom":throw new Error("KaTeX-a11y: vphantom not implemented yet");case"hphantom":throw new Error("KaTeX-a11y: hphantom not implemented yet");case"operatorname":f(e.body,r,t);break;case"array":throw new Error("KaTeX-a11y: array not implemented yet");case"raw":throw new Error("KaTeX-a11y: raw not implemented yet");case"size":break;case"url":throw new Error("KaTeX-a11y: url not implemented yet");case"tag":throw new Error("KaTeX-a11y: tag not implemented yet");case"verb":d("start verbatim","normal",r),d(e.body,"normal",r),d("end verbatim","normal",r);break;case"environment":throw new Error("KaTeX-a11y: environment not implemented yet");case"horizBrace":d("start "+e.label.slice(1),"normal",r),f(e.base,r,t),d("end "+e.label.slice(1),"normal",r);break;case"infix":break;case"includegraphics":throw new Error("KaTeX-a11y: includegraphics not implemented yet");case"font":f(e.body,r,t);break;case"href":throw new Error("KaTeX-a11y: href not implemented yet");case"cr":throw new Error("KaTeX-a11y: cr not implemented yet");case"underline":b(r,function(r){r.push("start underline"),f(e.body,r,t),r.push("end underline")});break;case"xArrow":throw new Error("KaTeX-a11y: xArrow not implemented yet");case"mclass":var g=e.mclass.slice(1);f(e.body,r,g);break;case"mathchoice":f(e.text,r,t);break;case"htmlmathml":f(e.mathml,r,t);break;case"middle":d(e.delim,t,r);break;case"internal":break;case"html":f(e.body,r,t);break;default:throw e.type,new Error("KaTeX a11y un-recognized type: "+e.type)}},f=function e(r,t,a){if(void 0===t&&(t=[]),r instanceof Array)for(var o=0;o "start fraction, 1, divided by, 2, end fraction" - * - * However, other cases do not: - * renderA11yString("f(x) = x^2") - * -> "f, left parenthesis, x, right parenthesis, equals, x, squared" - * - * The commas in the string aim to increase ease of understanding - * when read by a screenreader. - */ -const stringMap = { - "(": "left parenthesis", - ")": "right parenthesis", - "[": "open bracket", - "]": "close bracket", - "\\{": "left brace", - "\\}": "right brace", - "\\lvert": "open vertical bar", - "\\rvert": "close vertical bar", - "|": "vertical bar", - "\\uparrow": "up arrow", - "\\Uparrow": "up arrow", - "\\downarrow": "down arrow", - "\\Downarrow": "down arrow", - "\\updownarrow": "up down arrow", - "\\leftarrow": "left arrow", - "\\Leftarrow": "left arrow", - "\\rightarrow": "right arrow", - "\\Rightarrow": "right arrow", - "\\langle": "open angle", - "\\rangle": "close angle", - "\\lfloor": "open floor", - "\\rfloor": "close floor", - "\\int": "integral", - "\\intop": "integral", - "\\lim": "limit", - "\\ln": "natural log", - "\\log": "log", - "\\sin": "sine", - "\\cos": "cosine", - "\\tan": "tangent", - "\\cot": "cotangent", - "\\sum": "sum", - "/": "slash", - ",": "comma", - ".": "point", - "-": "negative", - "+": "plus", - "~": "tilde", - ":": "colon", - "?": "question mark", - "'": "apostrophe", - "\\%": "percent", - " ": "space", - "\\ ": "space", - "\\$": "dollar sign", - "\\angle": "angle", - "\\degree": "degree", - "\\circ": "circle", - "\\vec": "vector", - "\\triangle": "triangle", - "\\pi": "pi", - "\\prime": "prime", - "\\infty": "infinity", - "\\alpha": "alpha", - "\\beta": "beta", - "\\gamma": "gamma", - "\\omega": "omega", - "\\theta": "theta", - "\\sigma": "sigma", - "\\lambda": "lambda", - "\\tau": "tau", - "\\Delta": "delta", - "\\delta": "delta", - "\\mu": "mu", - "\\rho": "rho", - "\\nabla": "del", - "\\ell": "ell", - "\\ldots": "dots", - // TODO: add entries for all accents - "\\hat": "hat", - "\\acute": "acute" -}; -const powerMap = { - "prime": "prime", - "degree": "degrees", - "circle": "degrees", - "2": "squared", - "3": "cubed" -}; -const openMap = { - "|": "open vertical bar", - ".": "" -}; -const closeMap = { - "|": "close vertical bar", - ".": "" -}; -const binMap = { - "+": "plus", - "-": "minus", - "\\pm": "plus minus", - "\\cdot": "dot", - "*": "times", - "/": "divided by", - "\\times": "times", - "\\div": "divided by", - "\\circ": "circle", - "\\bullet": "bullet" -}; -const relMap = { - "=": "equals", - "\\approx": "approximately equals", - "≠": "does not equal", - "\\geq": "is greater than or equal to", - "\\ge": "is greater than or equal to", - "\\leq": "is less than or equal to", - "\\le": "is less than or equal to", - ">": "is greater than", - "<": "is less than", - "\\leftarrow": "left arrow", - "\\Leftarrow": "left arrow", - "\\rightarrow": "right arrow", - "\\Rightarrow": "right arrow", - ":": "colon" -}; -const accentUnderMap = { - "\\underleftarrow": "left arrow", - "\\underrightarrow": "right arrow", - "\\underleftrightarrow": "left-right arrow", - "\\undergroup": "group", - "\\underlinesegment": "line segment", - "\\utilde": "tilde" -}; - -const buildString = (str, type, a11yStrings) => { - if (!str) { - return; - } - - let ret; - - if (type === "open") { - ret = str in openMap ? openMap[str] : stringMap[str] || str; - } else if (type === "close") { - ret = str in closeMap ? closeMap[str] : stringMap[str] || str; - } else if (type === "bin") { - ret = binMap[str] || str; - } else if (type === "rel") { - ret = relMap[str] || str; - } else { - ret = stringMap[str] || str; - } // If the text to add is a number and there is already a string - // in the list and the last string is a number then we should - // combine them into a single number - - - if (/^\d+$/.test(ret) && a11yStrings.length > 0 && // TODO(kevinb): check that the last item in a11yStrings is a string - // I think we might be able to drop the nested arrays, which would make - // this easier to type - $FlowFixMe - /^\d+$/.test(a11yStrings[a11yStrings.length - 1])) { - a11yStrings[a11yStrings.length - 1] += ret; - } else if (ret) { - a11yStrings.push(ret); - } -}; - -const buildRegion = (a11yStrings, callback) => { - const regionStrings = []; - a11yStrings.push(regionStrings); - callback(regionStrings); -}; - -const handleObject = (tree, a11yStrings, atomType) => { - // Everything else is assumed to be an object... - switch (tree.type) { - case "accent": - { - buildRegion(a11yStrings, a11yStrings => { - buildA11yStrings(tree.base, a11yStrings, atomType); - a11yStrings.push("with"); - buildString(tree.label, "normal", a11yStrings); - a11yStrings.push("on top"); - }); - break; - } - - case "accentUnder": - { - buildRegion(a11yStrings, a11yStrings => { - buildA11yStrings(tree.base, a11yStrings, atomType); - a11yStrings.push("with"); - buildString(accentUnderMap[tree.label], "normal", a11yStrings); - a11yStrings.push("underneath"); - }); - break; - } - - case "accent-token": - { - // Used internally by accent symbols. - break; - } - - case "atom": - { - const text = tree.text; - - switch (tree.family) { - case "bin": - { - buildString(text, "bin", a11yStrings); - break; - } - - case "close": - { - buildString(text, "close", a11yStrings); - break; - } - // TODO(kevinb): figure out what should be done for inner - - case "inner": - { - buildString(tree.text, "inner", a11yStrings); - break; - } - - case "open": - { - buildString(text, "open", a11yStrings); - break; - } - - case "punct": - { - buildString(text, "punct", a11yStrings); - break; - } - - case "rel": - { - buildString(text, "rel", a11yStrings); - break; - } - - default: - { - tree.family; - throw new Error(`"${tree.family}" is not a valid atom type`); - } - } - - break; - } - - case "color": - { - const color = tree.color.replace(/katex-/, ""); - buildRegion(a11yStrings, regionStrings => { - regionStrings.push("start color " + color); - buildA11yStrings(tree.body, regionStrings, atomType); - regionStrings.push("end color " + color); - }); - break; - } - - case "color-token": - { - // Used by \color, \colorbox, and \fcolorbox but not directly rendered. - // It's a leaf node and has no children so just break. - break; - } - - case "delimsizing": - { - if (tree.delim && tree.delim !== ".") { - buildString(tree.delim, "normal", a11yStrings); - } - - break; - } - - case "genfrac": - { - buildRegion(a11yStrings, regionStrings => { - // genfrac can have unbalanced delimiters - const leftDelim = tree.leftDelim, - rightDelim = tree.rightDelim; // NOTE: Not sure if this is a safe assumption - // hasBarLine true -> fraction, false -> binomial - - if (tree.hasBarLine) { - regionStrings.push("start fraction"); - leftDelim && buildString(leftDelim, "open", regionStrings); - buildA11yStrings(tree.numer, regionStrings, atomType); - regionStrings.push("divided by"); - buildA11yStrings(tree.denom, regionStrings, atomType); - rightDelim && buildString(rightDelim, "close", regionStrings); - regionStrings.push("end fraction"); - } else { - regionStrings.push("start binomial"); - leftDelim && buildString(leftDelim, "open", regionStrings); - buildA11yStrings(tree.numer, regionStrings, atomType); - regionStrings.push("over"); - buildA11yStrings(tree.denom, regionStrings, atomType); - rightDelim && buildString(rightDelim, "close", regionStrings); - regionStrings.push("end binomial"); - } - }); - break; - } - - case "kern": - { - // No op: we don't attempt to present kerning information - // to the screen reader. - break; - } - - case "leftright": - { - buildRegion(a11yStrings, regionStrings => { - buildString(tree.left, "open", regionStrings); - buildA11yStrings(tree.body, regionStrings, atomType); - buildString(tree.right, "close", regionStrings); - }); - break; - } - - case "leftright-right": - { - // TODO: double check that this is a no-op - break; - } - - case "lap": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "mathord": - { - buildString(tree.text, "normal", a11yStrings); - break; - } - - case "op": - { - const body = tree.body, - name = tree.name; - - if (body) { - buildA11yStrings(body, a11yStrings, atomType); - } else if (name) { - buildString(name, "normal", a11yStrings); - } - - break; - } - - case "op-token": - { - // Used internally by operator symbols. - buildString(tree.text, atomType, a11yStrings); - break; - } - - case "ordgroup": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "overline": - { - buildRegion(a11yStrings, function (a11yStrings) { - a11yStrings.push("start overline"); - buildA11yStrings(tree.body, a11yStrings, atomType); - a11yStrings.push("end overline"); - }); - break; - } - - case "phantom": - { - a11yStrings.push("empty space"); - break; - } - - case "raisebox": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "rule": - { - a11yStrings.push("rectangle"); - break; - } - - case "sizing": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "spacing": - { - a11yStrings.push("space"); - break; - } - - case "styling": - { - // We ignore the styling and just pass through the contents - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "sqrt": - { - buildRegion(a11yStrings, regionStrings => { - const body = tree.body, - index = tree.index; - - if (index) { - const indexString = flatten(buildA11yStrings(index, [], atomType)).join(","); - - if (indexString === "3") { - regionStrings.push("cube root of"); - buildA11yStrings(body, regionStrings, atomType); - regionStrings.push("end cube root"); - return; - } - - regionStrings.push("root"); - regionStrings.push("start index"); - buildA11yStrings(index, regionStrings, atomType); - regionStrings.push("end index"); - return; - } - - regionStrings.push("square root of"); - buildA11yStrings(body, regionStrings, atomType); - regionStrings.push("end square root"); - }); - break; - } - - case "supsub": - { - const base = tree.base, - sub = tree.sub, - sup = tree.sup; - let isLog = false; - - if (base) { - buildA11yStrings(base, a11yStrings, atomType); - isLog = base.type === "op" && base.name === "\\log"; - } - - if (sub) { - const regionName = isLog ? "base" : "subscript"; - buildRegion(a11yStrings, function (regionStrings) { - regionStrings.push(`start ${regionName}`); - buildA11yStrings(sub, regionStrings, atomType); - regionStrings.push(`end ${regionName}`); - }); - } - - if (sup) { - buildRegion(a11yStrings, function (regionStrings) { - const supString = flatten(buildA11yStrings(sup, [], atomType)).join(","); - - if (supString in powerMap) { - regionStrings.push(powerMap[supString]); - return; - } - - regionStrings.push("start superscript"); - buildA11yStrings(sup, regionStrings, atomType); - regionStrings.push("end superscript"); - }); - } - - break; - } - - case "text": - { - // TODO: handle other fonts - if (tree.font === "\\textbf") { - buildRegion(a11yStrings, function (regionStrings) { - regionStrings.push("start bold text"); - buildA11yStrings(tree.body, regionStrings, atomType); - regionStrings.push("end bold text"); - }); - break; - } - - buildRegion(a11yStrings, function (regionStrings) { - regionStrings.push("start text"); - buildA11yStrings(tree.body, regionStrings, atomType); - regionStrings.push("end text"); - }); - break; - } - - case "textord": - { - buildString(tree.text, atomType, a11yStrings); - break; - } - - case "smash": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "enclose": - { - // TODO: create a map for these. - // TODO: differentiate between a body with a single atom, e.g. - // "cancel a" instead of "start cancel, a, end cancel" - if (/cancel/.test(tree.label)) { - buildRegion(a11yStrings, function (regionStrings) { - regionStrings.push("start cancel"); - buildA11yStrings(tree.body, regionStrings, atomType); - regionStrings.push("end cancel"); - }); - break; - } else if (/box/.test(tree.label)) { - buildRegion(a11yStrings, function (regionStrings) { - regionStrings.push("start box"); - buildA11yStrings(tree.body, regionStrings, atomType); - regionStrings.push("end box"); - }); - break; - } else if (/sout/.test(tree.label)) { - buildRegion(a11yStrings, function (regionStrings) { - regionStrings.push("start strikeout"); - buildA11yStrings(tree.body, regionStrings, atomType); - regionStrings.push("end strikeout"); - }); - break; - } - - throw new Error(`KaTeX-a11y: enclose node with ${tree.label} not supported yet`); - } - - case "vphantom": - { - throw new Error("KaTeX-a11y: vphantom not implemented yet"); - } - - case "hphantom": - { - throw new Error("KaTeX-a11y: hphantom not implemented yet"); - } - - case "operatorname": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "array": - { - throw new Error("KaTeX-a11y: array not implemented yet"); - } - - case "raw": - { - throw new Error("KaTeX-a11y: raw not implemented yet"); - } - - case "size": - { - // Although there are nodes of type "size" in the parse tree, they have - // no semantic meaning and should be ignored. - break; - } - - case "url": - { - throw new Error("KaTeX-a11y: url not implemented yet"); - } - - case "tag": - { - throw new Error("KaTeX-a11y: tag not implemented yet"); - } - - case "verb": - { - buildString(`start verbatim`, "normal", a11yStrings); - buildString(tree.body, "normal", a11yStrings); - buildString(`end verbatim`, "normal", a11yStrings); - break; - } - - case "environment": - { - throw new Error("KaTeX-a11y: environment not implemented yet"); - } - - case "horizBrace": - { - buildString(`start ${tree.label.slice(1)}`, "normal", a11yStrings); - buildA11yStrings(tree.base, a11yStrings, atomType); - buildString(`end ${tree.label.slice(1)}`, "normal", a11yStrings); - break; - } - - case "infix": - { - // All infix nodes are replace with other nodes. - break; - } - - case "includegraphics": - { - throw new Error("KaTeX-a11y: includegraphics not implemented yet"); - } - - case "font": - { - // TODO: callout the start/end of specific fonts - // TODO: map \BBb{N} to "the naturals" or something like that - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "href": - { - throw new Error("KaTeX-a11y: href not implemented yet"); - } - - case "cr": - { - // This is used by environments. - throw new Error("KaTeX-a11y: cr not implemented yet"); - } - - case "underline": - { - buildRegion(a11yStrings, function (a11yStrings) { - a11yStrings.push("start underline"); - buildA11yStrings(tree.body, a11yStrings, atomType); - a11yStrings.push("end underline"); - }); - break; - } - - case "xArrow": - { - throw new Error("KaTeX-a11y: xArrow not implemented yet"); - } - - case "mclass": - { - // \neq and \ne are macros so we let "htmlmathml" render the mathmal - // side of things and extract the text from that. - const atomType = tree.mclass.slice(1); // $FlowFixMe: drop the leading "m" from the values in mclass - - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - case "mathchoice": - { - // TODO: track which which style we're using, e.g. dispaly, text, etc. - // default to text style if even that may not be the correct style - buildA11yStrings(tree.text, a11yStrings, atomType); - break; - } - - case "htmlmathml": - { - buildA11yStrings(tree.mathml, a11yStrings, atomType); - break; - } - - case "middle": - { - buildString(tree.delim, atomType, a11yStrings); - break; - } - - case "internal": - { - // internal nodes are never included in the parse tree - break; - } - - case "html": - { - buildA11yStrings(tree.body, a11yStrings, atomType); - break; - } - - default: - tree.type; - throw new Error("KaTeX a11y un-recognized type: " + tree.type); - } -}; - -const buildA11yStrings = function buildA11yStrings(tree, a11yStrings, atomType) { - if (a11yStrings === void 0) { - a11yStrings = []; - } - - if (tree instanceof Array) { - for (let i = 0; i < tree.length; i++) { - buildA11yStrings(tree[i], a11yStrings, atomType); - } - } else { - handleObject(tree, a11yStrings, atomType); - } - - return a11yStrings; -}; - -const flatten = function flatten(array) { - let result = []; - array.forEach(function (item) { - if (item instanceof Array) { - result = result.concat(flatten(item)); - } else { - result.push(item); - } - }); - return result; -}; - -const renderA11yString = function renderA11yString(text, settings) { - const tree = katex.__parse(text, settings); - - const a11yStrings = buildA11yStrings(tree, [], "normal"); - return flatten(a11yStrings).join(", "); -}; - -export default renderA11yString; diff --git a/docs/static/katex/fonts/KaTeX_AMS-Regular.ttf b/docs/static/katex/fonts/KaTeX_AMS-Regular.ttf deleted file mode 100644 index 737cf8eb58a..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_AMS-Regular.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_AMS-Regular.woff b/docs/static/katex/fonts/KaTeX_AMS-Regular.woff deleted file mode 100644 index 38378bfba8d..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_AMS-Regular.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_AMS-Regular.woff2 b/docs/static/katex/fonts/KaTeX_AMS-Regular.woff2 deleted file mode 100644 index a4d1ba64101..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_AMS-Regular.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Caligraphic-Bold.ttf b/docs/static/katex/fonts/KaTeX_Caligraphic-Bold.ttf deleted file mode 100644 index 04d28abd995..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Caligraphic-Bold.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Caligraphic-Bold.woff b/docs/static/katex/fonts/KaTeX_Caligraphic-Bold.woff deleted file mode 100644 index a01ce906060..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Caligraphic-Bold.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 b/docs/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 deleted file mode 100644 index 37927274af1..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Caligraphic-Regular.ttf b/docs/static/katex/fonts/KaTeX_Caligraphic-Regular.ttf deleted file mode 100644 index b2ce555fd5d..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Caligraphic-Regular.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Caligraphic-Regular.woff b/docs/static/katex/fonts/KaTeX_Caligraphic-Regular.woff deleted file mode 100644 index bc169b7cdd8..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Caligraphic-Regular.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 b/docs/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 deleted file mode 100644 index f1e38bba2a7..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Fraktur-Bold.ttf b/docs/static/katex/fonts/KaTeX_Fraktur-Bold.ttf deleted file mode 100644 index c42d169167d..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Fraktur-Bold.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Fraktur-Bold.woff b/docs/static/katex/fonts/KaTeX_Fraktur-Bold.woff deleted file mode 100644 index f30b54b3d1c..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Fraktur-Bold.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 b/docs/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 deleted file mode 100644 index b7a83593a8d..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Fraktur-Regular.ttf b/docs/static/katex/fonts/KaTeX_Fraktur-Regular.ttf deleted file mode 100644 index 413322824e3..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Fraktur-Regular.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Fraktur-Regular.woff b/docs/static/katex/fonts/KaTeX_Fraktur-Regular.woff deleted file mode 100644 index 5af51de9e5d..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Fraktur-Regular.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 b/docs/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 deleted file mode 100644 index 3874f93e8da..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Main-Bold.ttf b/docs/static/katex/fonts/KaTeX_Main-Bold.ttf deleted file mode 100644 index 14390e012a5..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Main-Bold.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Main-Bold.woff b/docs/static/katex/fonts/KaTeX_Main-Bold.woff deleted file mode 100644 index 33b41998e40..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Main-Bold.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Main-Bold.woff2 b/docs/static/katex/fonts/KaTeX_Main-Bold.woff2 deleted file mode 100644 index f9b71cbe742..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Main-Bold.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Main-BoldItalic.ttf b/docs/static/katex/fonts/KaTeX_Main-BoldItalic.ttf deleted file mode 100644 index ad0761f431e..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Main-BoldItalic.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Main-BoldItalic.woff b/docs/static/katex/fonts/KaTeX_Main-BoldItalic.woff deleted file mode 100644 index 115af4f072e..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Main-BoldItalic.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 b/docs/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 deleted file mode 100644 index 5c500c285ab..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Main-Italic.ttf b/docs/static/katex/fonts/KaTeX_Main-Italic.ttf deleted file mode 100644 index fc8625c81c8..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Main-Italic.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Main-Italic.woff b/docs/static/katex/fonts/KaTeX_Main-Italic.woff deleted file mode 100644 index 2d3087ab496..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Main-Italic.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Main-Italic.woff2 b/docs/static/katex/fonts/KaTeX_Main-Italic.woff2 deleted file mode 100644 index 08510d85a77..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Main-Italic.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Main-Regular.ttf b/docs/static/katex/fonts/KaTeX_Main-Regular.ttf deleted file mode 100644 index 5115a044ea4..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Main-Regular.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Main-Regular.woff b/docs/static/katex/fonts/KaTeX_Main-Regular.woff deleted file mode 100644 index 42b74ab1330..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Main-Regular.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Main-Regular.woff2 b/docs/static/katex/fonts/KaTeX_Main-Regular.woff2 deleted file mode 100644 index 18647fa6afb..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Main-Regular.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Math-BoldItalic.ttf b/docs/static/katex/fonts/KaTeX_Math-BoldItalic.ttf deleted file mode 100644 index 326b523bd0e..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Math-BoldItalic.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Math-BoldItalic.woff b/docs/static/katex/fonts/KaTeX_Math-BoldItalic.woff deleted file mode 100644 index 5b4041aa87c..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Math-BoldItalic.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 b/docs/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 deleted file mode 100644 index ba55276d030..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Math-Italic.ttf b/docs/static/katex/fonts/KaTeX_Math-Italic.ttf deleted file mode 100644 index f148fceeb07..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Math-Italic.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Math-Italic.woff b/docs/static/katex/fonts/KaTeX_Math-Italic.woff deleted file mode 100644 index 31d0038498a..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Math-Italic.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Math-Italic.woff2 b/docs/static/katex/fonts/KaTeX_Math-Italic.woff2 deleted file mode 100644 index 9871ab6b835..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Math-Italic.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_SansSerif-Bold.ttf b/docs/static/katex/fonts/KaTeX_SansSerif-Bold.ttf deleted file mode 100644 index dce35c8fdf2..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_SansSerif-Bold.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_SansSerif-Bold.woff b/docs/static/katex/fonts/KaTeX_SansSerif-Bold.woff deleted file mode 100644 index 992cb3d6d0d..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_SansSerif-Bold.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 b/docs/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 deleted file mode 100644 index 6dd10388ada..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_SansSerif-Italic.ttf b/docs/static/katex/fonts/KaTeX_SansSerif-Italic.ttf deleted file mode 100644 index a3eb86c38da..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_SansSerif-Italic.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_SansSerif-Italic.woff b/docs/static/katex/fonts/KaTeX_SansSerif-Italic.woff deleted file mode 100644 index f4fa252a2c1..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_SansSerif-Italic.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 b/docs/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 deleted file mode 100644 index 9f2501a3aac..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_SansSerif-Regular.ttf b/docs/static/katex/fonts/KaTeX_SansSerif-Regular.ttf deleted file mode 100644 index 3be73ce17f8..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_SansSerif-Regular.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_SansSerif-Regular.woff b/docs/static/katex/fonts/KaTeX_SansSerif-Regular.woff deleted file mode 100644 index ec283f418b5..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_SansSerif-Regular.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 b/docs/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 deleted file mode 100644 index e46094fba16..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Script-Regular.ttf b/docs/static/katex/fonts/KaTeX_Script-Regular.ttf deleted file mode 100644 index 40c8a997acc..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Script-Regular.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Script-Regular.woff b/docs/static/katex/fonts/KaTeX_Script-Regular.woff deleted file mode 100644 index 4eafae7583d..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Script-Regular.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Script-Regular.woff2 b/docs/static/katex/fonts/KaTeX_Script-Regular.woff2 deleted file mode 100644 index 69b1754d7e2..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Script-Regular.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Size1-Regular.ttf b/docs/static/katex/fonts/KaTeX_Size1-Regular.ttf deleted file mode 100644 index f0aff83efb3..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Size1-Regular.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Size1-Regular.woff b/docs/static/katex/fonts/KaTeX_Size1-Regular.woff deleted file mode 100644 index 0358ee4a3eb..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Size1-Regular.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Size1-Regular.woff2 b/docs/static/katex/fonts/KaTeX_Size1-Regular.woff2 deleted file mode 100644 index f951ed01694..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Size1-Regular.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Size2-Regular.ttf b/docs/static/katex/fonts/KaTeX_Size2-Regular.ttf deleted file mode 100644 index 4f72f167953..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Size2-Regular.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Size2-Regular.woff b/docs/static/katex/fonts/KaTeX_Size2-Regular.woff deleted file mode 100644 index 8a053d23ae3..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Size2-Regular.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Size2-Regular.woff2 b/docs/static/katex/fonts/KaTeX_Size2-Regular.woff2 deleted file mode 100644 index 181d9625a79..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Size2-Regular.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Size3-Regular.ttf b/docs/static/katex/fonts/KaTeX_Size3-Regular.ttf deleted file mode 100644 index 56d2dc6c5d2..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Size3-Regular.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Size3-Regular.woff b/docs/static/katex/fonts/KaTeX_Size3-Regular.woff deleted file mode 100644 index 0ec99ad1a9f..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Size3-Regular.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Size3-Regular.woff2 b/docs/static/katex/fonts/KaTeX_Size3-Regular.woff2 deleted file mode 100644 index c2985cd380c..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Size3-Regular.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Size4-Regular.ttf b/docs/static/katex/fonts/KaTeX_Size4-Regular.ttf deleted file mode 100644 index baf02091aa2..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Size4-Regular.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Size4-Regular.woff b/docs/static/katex/fonts/KaTeX_Size4-Regular.woff deleted file mode 100644 index ff6731972f9..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Size4-Regular.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Size4-Regular.woff2 b/docs/static/katex/fonts/KaTeX_Size4-Regular.woff2 deleted file mode 100644 index a4e810da5e3..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Size4-Regular.woff2 and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Typewriter-Regular.ttf b/docs/static/katex/fonts/KaTeX_Typewriter-Regular.ttf deleted file mode 100644 index e66c218df5d..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Typewriter-Regular.ttf and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Typewriter-Regular.woff b/docs/static/katex/fonts/KaTeX_Typewriter-Regular.woff deleted file mode 100644 index c66d149d5e2..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Typewriter-Regular.woff and /dev/null differ diff --git a/docs/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 b/docs/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 deleted file mode 100644 index e5bf2ce1ff1..00000000000 Binary files a/docs/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 and /dev/null differ diff --git a/docs/static/katex/katex.css b/docs/static/katex/katex.css deleted file mode 100644 index 5d2dfc28b81..00000000000 --- a/docs/static/katex/katex.css +++ /dev/null @@ -1,1034 +0,0 @@ -/* stylelint-disable font-family-no-missing-generic-family-keyword */ -@font-face { - font-family: 'KaTeX_AMS'; - src: url(fonts/KaTeX_AMS-Regular.woff2) format('woff2'), url(fonts/KaTeX_AMS-Regular.woff) format('woff'), url(fonts/KaTeX_AMS-Regular.ttf) format('truetype'); - font-weight: normal; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_Caligraphic'; - src: url(fonts/KaTeX_Caligraphic-Bold.woff2) format('woff2'), url(fonts/KaTeX_Caligraphic-Bold.woff) format('woff'), url(fonts/KaTeX_Caligraphic-Bold.ttf) format('truetype'); - font-weight: bold; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_Caligraphic'; - src: url(fonts/KaTeX_Caligraphic-Regular.woff2) format('woff2'), url(fonts/KaTeX_Caligraphic-Regular.woff) format('woff'), url(fonts/KaTeX_Caligraphic-Regular.ttf) format('truetype'); - font-weight: normal; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_Fraktur'; - src: url(fonts/KaTeX_Fraktur-Bold.woff2) format('woff2'), url(fonts/KaTeX_Fraktur-Bold.woff) format('woff'), url(fonts/KaTeX_Fraktur-Bold.ttf) format('truetype'); - font-weight: bold; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_Fraktur'; - src: url(fonts/KaTeX_Fraktur-Regular.woff2) format('woff2'), url(fonts/KaTeX_Fraktur-Regular.woff) format('woff'), url(fonts/KaTeX_Fraktur-Regular.ttf) format('truetype'); - font-weight: normal; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_Main'; - src: url(fonts/KaTeX_Main-Bold.woff2) format('woff2'), url(fonts/KaTeX_Main-Bold.woff) format('woff'), url(fonts/KaTeX_Main-Bold.ttf) format('truetype'); - font-weight: bold; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_Main'; - src: url(fonts/KaTeX_Main-BoldItalic.woff2) format('woff2'), url(fonts/KaTeX_Main-BoldItalic.woff) format('woff'), url(fonts/KaTeX_Main-BoldItalic.ttf) format('truetype'); - font-weight: bold; - font-style: italic; -} -@font-face { - font-family: 'KaTeX_Main'; - src: url(fonts/KaTeX_Main-Italic.woff2) format('woff2'), url(fonts/KaTeX_Main-Italic.woff) format('woff'), url(fonts/KaTeX_Main-Italic.ttf) format('truetype'); - font-weight: normal; - font-style: italic; -} -@font-face { - font-family: 'KaTeX_Main'; - src: url(fonts/KaTeX_Main-Regular.woff2) format('woff2'), url(fonts/KaTeX_Main-Regular.woff) format('woff'), url(fonts/KaTeX_Main-Regular.ttf) format('truetype'); - font-weight: normal; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_Math'; - src: url(fonts/KaTeX_Math-BoldItalic.woff2) format('woff2'), url(fonts/KaTeX_Math-BoldItalic.woff) format('woff'), url(fonts/KaTeX_Math-BoldItalic.ttf) format('truetype'); - font-weight: bold; - font-style: italic; -} -@font-face { - font-family: 'KaTeX_Math'; - src: url(fonts/KaTeX_Math-Italic.woff2) format('woff2'), url(fonts/KaTeX_Math-Italic.woff) format('woff'), url(fonts/KaTeX_Math-Italic.ttf) format('truetype'); - font-weight: normal; - font-style: italic; -} -@font-face { - font-family: 'KaTeX_SansSerif'; - src: url(fonts/KaTeX_SansSerif-Bold.woff2) format('woff2'), url(fonts/KaTeX_SansSerif-Bold.woff) format('woff'), url(fonts/KaTeX_SansSerif-Bold.ttf) format('truetype'); - font-weight: bold; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_SansSerif'; - src: url(fonts/KaTeX_SansSerif-Italic.woff2) format('woff2'), url(fonts/KaTeX_SansSerif-Italic.woff) format('woff'), url(fonts/KaTeX_SansSerif-Italic.ttf) format('truetype'); - font-weight: normal; - font-style: italic; -} -@font-face { - font-family: 'KaTeX_SansSerif'; - src: url(fonts/KaTeX_SansSerif-Regular.woff2) format('woff2'), url(fonts/KaTeX_SansSerif-Regular.woff) format('woff'), url(fonts/KaTeX_SansSerif-Regular.ttf) format('truetype'); - font-weight: normal; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_Script'; - src: url(fonts/KaTeX_Script-Regular.woff2) format('woff2'), url(fonts/KaTeX_Script-Regular.woff) format('woff'), url(fonts/KaTeX_Script-Regular.ttf) format('truetype'); - font-weight: normal; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_Size1'; - src: url(fonts/KaTeX_Size1-Regular.woff2) format('woff2'), url(fonts/KaTeX_Size1-Regular.woff) format('woff'), url(fonts/KaTeX_Size1-Regular.ttf) format('truetype'); - font-weight: normal; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_Size2'; - src: url(fonts/KaTeX_Size2-Regular.woff2) format('woff2'), url(fonts/KaTeX_Size2-Regular.woff) format('woff'), url(fonts/KaTeX_Size2-Regular.ttf) format('truetype'); - font-weight: normal; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_Size3'; - src: url(fonts/KaTeX_Size3-Regular.woff2) format('woff2'), url(fonts/KaTeX_Size3-Regular.woff) format('woff'), url(fonts/KaTeX_Size3-Regular.ttf) format('truetype'); - font-weight: normal; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_Size4'; - src: url(fonts/KaTeX_Size4-Regular.woff2) format('woff2'), url(fonts/KaTeX_Size4-Regular.woff) format('woff'), url(fonts/KaTeX_Size4-Regular.ttf) format('truetype'); - font-weight: normal; - font-style: normal; -} -@font-face { - font-family: 'KaTeX_Typewriter'; - src: url(fonts/KaTeX_Typewriter-Regular.woff2) format('woff2'), url(fonts/KaTeX_Typewriter-Regular.woff) format('woff'), url(fonts/KaTeX_Typewriter-Regular.ttf) format('truetype'); - font-weight: normal; - font-style: normal; -} -.katex { - font: normal 1.21em KaTeX_Main, Times New Roman, serif; - line-height: 1.2; - text-indent: 0; - text-rendering: auto; - border-color: currentColor; -} -.katex * { - -ms-high-contrast-adjust: none !important; -} -.katex .katex-version::after { - content: "0.12.0"; -} -.katex .katex-mathml { - position: absolute; - clip: rect(1px, 1px, 1px, 1px); - padding: 0; - border: 0; - height: 1px; - width: 1px; - overflow: hidden; -} -.katex .katex-html { - /* \newline is an empty block at top level, between .base elements */ -} -.katex .katex-html > .newline { - display: block; -} -.katex .base { - position: relative; - display: inline-block; - white-space: nowrap; - width: min-content; -} -.katex .strut { - display: inline-block; -} -.katex .textbf { - font-weight: bold; -} -.katex .textit { - font-style: italic; -} -.katex .textrm { - font-family: KaTeX_Main; -} -.katex .textsf { - font-family: KaTeX_SansSerif; -} -.katex .texttt { - font-family: KaTeX_Typewriter; -} -.katex .mathnormal { - font-family: KaTeX_Math; - font-style: italic; -} -.katex .mathit { - font-family: KaTeX_Main; - font-style: italic; -} -.katex .mathrm { - font-style: normal; -} -.katex .mathbf { - font-family: KaTeX_Main; - font-weight: bold; -} -.katex .boldsymbol { - font-family: KaTeX_Math; - font-weight: bold; - font-style: italic; -} -.katex .amsrm { - font-family: KaTeX_AMS; -} -.katex .mathbb, -.katex .textbb { - font-family: KaTeX_AMS; -} -.katex .mathcal { - font-family: KaTeX_Caligraphic; -} -.katex .mathfrak, -.katex .textfrak { - font-family: KaTeX_Fraktur; -} -.katex .mathtt { - font-family: KaTeX_Typewriter; -} -.katex .mathscr, -.katex .textscr { - font-family: KaTeX_Script; -} -.katex .mathsf, -.katex .textsf { - font-family: KaTeX_SansSerif; -} -.katex .mathboldsf, -.katex .textboldsf { - font-family: KaTeX_SansSerif; - font-weight: bold; -} -.katex .mathitsf, -.katex .textitsf { - font-family: KaTeX_SansSerif; - font-style: italic; -} -.katex .mainrm { - font-family: KaTeX_Main; - font-style: normal; -} -.katex .vlist-t { - display: inline-table; - table-layout: fixed; - border-collapse: collapse; -} -.katex .vlist-r { - display: table-row; -} -.katex .vlist { - display: table-cell; - vertical-align: bottom; - position: relative; -} -.katex .vlist > span { - display: block; - height: 0; - position: relative; -} -.katex .vlist > span > span { - display: inline-block; -} -.katex .vlist > span > .pstrut { - overflow: hidden; - width: 0; -} -.katex .vlist-t2 { - margin-right: -2px; -} -.katex .vlist-s { - display: table-cell; - vertical-align: bottom; - font-size: 1px; - width: 2px; - min-width: 2px; -} -.katex .vbox { - display: -ms-inline-flexbox; - display: inline-flex; - -ms-flex-direction: column; - flex-direction: column; - align-items: baseline; -} -.katex .hbox { - display: -ms-inline-flexbox; - display: inline-flex; - -ms-flex-direction: row; - flex-direction: row; - width: 100%; -} -.katex .thinbox { - display: inline-flex; - flex-direction: row; - width: 0; - max-width: 0; -} -.katex .msupsub { - text-align: left; -} -.katex .mfrac > span > span { - text-align: center; -} -.katex .mfrac .frac-line { - display: inline-block; - width: 100%; - border-bottom-style: solid; -} -.katex .mfrac .frac-line, -.katex .overline .overline-line, -.katex .underline .underline-line, -.katex .hline, -.katex .hdashline, -.katex .rule { - min-height: 1px; -} -.katex .mspace { - display: inline-block; -} -.katex .llap, -.katex .rlap, -.katex .clap { - width: 0; - position: relative; -} -.katex .llap > .inner, -.katex .rlap > .inner, -.katex .clap > .inner { - position: absolute; -} -.katex .llap > .fix, -.katex .rlap > .fix, -.katex .clap > .fix { - display: inline-block; -} -.katex .llap > .inner { - right: 0; -} -.katex .rlap > .inner, -.katex .clap > .inner { - left: 0; -} -.katex .clap > .inner > span { - margin-left: -50%; - margin-right: 50%; -} -.katex .rule { - display: inline-block; - border: solid 0; - position: relative; -} -.katex .overline .overline-line, -.katex .underline .underline-line, -.katex .hline { - display: inline-block; - width: 100%; - border-bottom-style: solid; -} -.katex .hdashline { - display: inline-block; - width: 100%; - border-bottom-style: dashed; -} -.katex .sqrt > .root { - margin-left: 0.27777778em; - margin-right: -0.55555556em; -} -.katex .sizing.reset-size1.size1, -.katex .fontsize-ensurer.reset-size1.size1 { - font-size: 1em; -} -.katex .sizing.reset-size1.size2, -.katex .fontsize-ensurer.reset-size1.size2 { - font-size: 1.2em; -} -.katex .sizing.reset-size1.size3, -.katex .fontsize-ensurer.reset-size1.size3 { - font-size: 1.4em; -} -.katex .sizing.reset-size1.size4, -.katex .fontsize-ensurer.reset-size1.size4 { - font-size: 1.6em; -} -.katex .sizing.reset-size1.size5, -.katex .fontsize-ensurer.reset-size1.size5 { - font-size: 1.8em; -} -.katex .sizing.reset-size1.size6, -.katex .fontsize-ensurer.reset-size1.size6 { - font-size: 2em; -} -.katex .sizing.reset-size1.size7, -.katex .fontsize-ensurer.reset-size1.size7 { - font-size: 2.4em; -} -.katex .sizing.reset-size1.size8, -.katex .fontsize-ensurer.reset-size1.size8 { - font-size: 2.88em; -} -.katex .sizing.reset-size1.size9, -.katex .fontsize-ensurer.reset-size1.size9 { - font-size: 3.456em; -} -.katex .sizing.reset-size1.size10, -.katex .fontsize-ensurer.reset-size1.size10 { - font-size: 4.148em; -} -.katex .sizing.reset-size1.size11, -.katex .fontsize-ensurer.reset-size1.size11 { - font-size: 4.976em; -} -.katex .sizing.reset-size2.size1, -.katex .fontsize-ensurer.reset-size2.size1 { - font-size: 0.83333333em; -} -.katex .sizing.reset-size2.size2, -.katex .fontsize-ensurer.reset-size2.size2 { - font-size: 1em; -} -.katex .sizing.reset-size2.size3, -.katex .fontsize-ensurer.reset-size2.size3 { - font-size: 1.16666667em; -} -.katex .sizing.reset-size2.size4, -.katex .fontsize-ensurer.reset-size2.size4 { - font-size: 1.33333333em; -} -.katex .sizing.reset-size2.size5, -.katex .fontsize-ensurer.reset-size2.size5 { - font-size: 1.5em; -} -.katex .sizing.reset-size2.size6, -.katex .fontsize-ensurer.reset-size2.size6 { - font-size: 1.66666667em; -} -.katex .sizing.reset-size2.size7, -.katex .fontsize-ensurer.reset-size2.size7 { - font-size: 2em; -} -.katex .sizing.reset-size2.size8, -.katex .fontsize-ensurer.reset-size2.size8 { - font-size: 2.4em; -} -.katex .sizing.reset-size2.size9, -.katex .fontsize-ensurer.reset-size2.size9 { - font-size: 2.88em; -} -.katex .sizing.reset-size2.size10, -.katex .fontsize-ensurer.reset-size2.size10 { - font-size: 3.45666667em; -} -.katex .sizing.reset-size2.size11, -.katex .fontsize-ensurer.reset-size2.size11 { - font-size: 4.14666667em; -} -.katex .sizing.reset-size3.size1, -.katex .fontsize-ensurer.reset-size3.size1 { - font-size: 0.71428571em; -} -.katex .sizing.reset-size3.size2, -.katex .fontsize-ensurer.reset-size3.size2 { - font-size: 0.85714286em; -} -.katex .sizing.reset-size3.size3, -.katex .fontsize-ensurer.reset-size3.size3 { - font-size: 1em; -} -.katex .sizing.reset-size3.size4, -.katex .fontsize-ensurer.reset-size3.size4 { - font-size: 1.14285714em; -} -.katex .sizing.reset-size3.size5, -.katex .fontsize-ensurer.reset-size3.size5 { - font-size: 1.28571429em; -} -.katex .sizing.reset-size3.size6, -.katex .fontsize-ensurer.reset-size3.size6 { - font-size: 1.42857143em; -} -.katex .sizing.reset-size3.size7, -.katex .fontsize-ensurer.reset-size3.size7 { - font-size: 1.71428571em; -} -.katex .sizing.reset-size3.size8, -.katex .fontsize-ensurer.reset-size3.size8 { - font-size: 2.05714286em; -} -.katex .sizing.reset-size3.size9, -.katex .fontsize-ensurer.reset-size3.size9 { - font-size: 2.46857143em; -} -.katex .sizing.reset-size3.size10, -.katex .fontsize-ensurer.reset-size3.size10 { - font-size: 2.96285714em; -} -.katex .sizing.reset-size3.size11, -.katex .fontsize-ensurer.reset-size3.size11 { - font-size: 3.55428571em; -} -.katex .sizing.reset-size4.size1, -.katex .fontsize-ensurer.reset-size4.size1 { - font-size: 0.625em; -} -.katex .sizing.reset-size4.size2, -.katex .fontsize-ensurer.reset-size4.size2 { - font-size: 0.75em; -} -.katex .sizing.reset-size4.size3, -.katex .fontsize-ensurer.reset-size4.size3 { - font-size: 0.875em; -} -.katex .sizing.reset-size4.size4, -.katex .fontsize-ensurer.reset-size4.size4 { - font-size: 1em; -} -.katex .sizing.reset-size4.size5, -.katex .fontsize-ensurer.reset-size4.size5 { - font-size: 1.125em; -} -.katex .sizing.reset-size4.size6, -.katex .fontsize-ensurer.reset-size4.size6 { - font-size: 1.25em; -} -.katex .sizing.reset-size4.size7, -.katex .fontsize-ensurer.reset-size4.size7 { - font-size: 1.5em; -} -.katex .sizing.reset-size4.size8, -.katex .fontsize-ensurer.reset-size4.size8 { - font-size: 1.8em; -} -.katex .sizing.reset-size4.size9, -.katex .fontsize-ensurer.reset-size4.size9 { - font-size: 2.16em; -} -.katex .sizing.reset-size4.size10, -.katex .fontsize-ensurer.reset-size4.size10 { - font-size: 2.5925em; -} -.katex .sizing.reset-size4.size11, -.katex .fontsize-ensurer.reset-size4.size11 { - font-size: 3.11em; -} -.katex .sizing.reset-size5.size1, -.katex .fontsize-ensurer.reset-size5.size1 { - font-size: 0.55555556em; -} -.katex .sizing.reset-size5.size2, -.katex .fontsize-ensurer.reset-size5.size2 { - font-size: 0.66666667em; -} -.katex .sizing.reset-size5.size3, -.katex .fontsize-ensurer.reset-size5.size3 { - font-size: 0.77777778em; -} -.katex .sizing.reset-size5.size4, -.katex .fontsize-ensurer.reset-size5.size4 { - font-size: 0.88888889em; -} -.katex .sizing.reset-size5.size5, -.katex .fontsize-ensurer.reset-size5.size5 { - font-size: 1em; -} -.katex .sizing.reset-size5.size6, -.katex .fontsize-ensurer.reset-size5.size6 { - font-size: 1.11111111em; -} -.katex .sizing.reset-size5.size7, -.katex .fontsize-ensurer.reset-size5.size7 { - font-size: 1.33333333em; -} -.katex .sizing.reset-size5.size8, -.katex .fontsize-ensurer.reset-size5.size8 { - font-size: 1.6em; -} -.katex .sizing.reset-size5.size9, -.katex .fontsize-ensurer.reset-size5.size9 { - font-size: 1.92em; -} -.katex .sizing.reset-size5.size10, -.katex .fontsize-ensurer.reset-size5.size10 { - font-size: 2.30444444em; -} -.katex .sizing.reset-size5.size11, -.katex .fontsize-ensurer.reset-size5.size11 { - font-size: 2.76444444em; -} -.katex .sizing.reset-size6.size1, -.katex .fontsize-ensurer.reset-size6.size1 { - font-size: 0.5em; -} -.katex .sizing.reset-size6.size2, -.katex .fontsize-ensurer.reset-size6.size2 { - font-size: 0.6em; -} -.katex .sizing.reset-size6.size3, -.katex .fontsize-ensurer.reset-size6.size3 { - font-size: 0.7em; -} -.katex .sizing.reset-size6.size4, -.katex .fontsize-ensurer.reset-size6.size4 { - font-size: 0.8em; -} -.katex .sizing.reset-size6.size5, -.katex .fontsize-ensurer.reset-size6.size5 { - font-size: 0.9em; -} -.katex .sizing.reset-size6.size6, -.katex .fontsize-ensurer.reset-size6.size6 { - font-size: 1em; -} -.katex .sizing.reset-size6.size7, -.katex .fontsize-ensurer.reset-size6.size7 { - font-size: 1.2em; -} -.katex .sizing.reset-size6.size8, -.katex .fontsize-ensurer.reset-size6.size8 { - font-size: 1.44em; -} -.katex .sizing.reset-size6.size9, -.katex .fontsize-ensurer.reset-size6.size9 { - font-size: 1.728em; -} -.katex .sizing.reset-size6.size10, -.katex .fontsize-ensurer.reset-size6.size10 { - font-size: 2.074em; -} -.katex .sizing.reset-size6.size11, -.katex .fontsize-ensurer.reset-size6.size11 { - font-size: 2.488em; -} -.katex .sizing.reset-size7.size1, -.katex .fontsize-ensurer.reset-size7.size1 { - font-size: 0.41666667em; -} -.katex .sizing.reset-size7.size2, -.katex .fontsize-ensurer.reset-size7.size2 { - font-size: 0.5em; -} -.katex .sizing.reset-size7.size3, -.katex .fontsize-ensurer.reset-size7.size3 { - font-size: 0.58333333em; -} -.katex .sizing.reset-size7.size4, -.katex .fontsize-ensurer.reset-size7.size4 { - font-size: 0.66666667em; -} -.katex .sizing.reset-size7.size5, -.katex .fontsize-ensurer.reset-size7.size5 { - font-size: 0.75em; -} -.katex .sizing.reset-size7.size6, -.katex .fontsize-ensurer.reset-size7.size6 { - font-size: 0.83333333em; -} -.katex .sizing.reset-size7.size7, -.katex .fontsize-ensurer.reset-size7.size7 { - font-size: 1em; -} -.katex .sizing.reset-size7.size8, -.katex .fontsize-ensurer.reset-size7.size8 { - font-size: 1.2em; -} -.katex .sizing.reset-size7.size9, -.katex .fontsize-ensurer.reset-size7.size9 { - font-size: 1.44em; -} -.katex .sizing.reset-size7.size10, -.katex .fontsize-ensurer.reset-size7.size10 { - font-size: 1.72833333em; -} -.katex .sizing.reset-size7.size11, -.katex .fontsize-ensurer.reset-size7.size11 { - font-size: 2.07333333em; -} -.katex .sizing.reset-size8.size1, -.katex .fontsize-ensurer.reset-size8.size1 { - font-size: 0.34722222em; -} -.katex .sizing.reset-size8.size2, -.katex .fontsize-ensurer.reset-size8.size2 { - font-size: 0.41666667em; -} -.katex .sizing.reset-size8.size3, -.katex .fontsize-ensurer.reset-size8.size3 { - font-size: 0.48611111em; -} -.katex .sizing.reset-size8.size4, -.katex .fontsize-ensurer.reset-size8.size4 { - font-size: 0.55555556em; -} -.katex .sizing.reset-size8.size5, -.katex .fontsize-ensurer.reset-size8.size5 { - font-size: 0.625em; -} -.katex .sizing.reset-size8.size6, -.katex .fontsize-ensurer.reset-size8.size6 { - font-size: 0.69444444em; -} -.katex .sizing.reset-size8.size7, -.katex .fontsize-ensurer.reset-size8.size7 { - font-size: 0.83333333em; -} -.katex .sizing.reset-size8.size8, -.katex .fontsize-ensurer.reset-size8.size8 { - font-size: 1em; -} -.katex .sizing.reset-size8.size9, -.katex .fontsize-ensurer.reset-size8.size9 { - font-size: 1.2em; -} -.katex .sizing.reset-size8.size10, -.katex .fontsize-ensurer.reset-size8.size10 { - font-size: 1.44027778em; -} -.katex .sizing.reset-size8.size11, -.katex .fontsize-ensurer.reset-size8.size11 { - font-size: 1.72777778em; -} -.katex .sizing.reset-size9.size1, -.katex .fontsize-ensurer.reset-size9.size1 { - font-size: 0.28935185em; -} -.katex .sizing.reset-size9.size2, -.katex .fontsize-ensurer.reset-size9.size2 { - font-size: 0.34722222em; -} -.katex .sizing.reset-size9.size3, -.katex .fontsize-ensurer.reset-size9.size3 { - font-size: 0.40509259em; -} -.katex .sizing.reset-size9.size4, -.katex .fontsize-ensurer.reset-size9.size4 { - font-size: 0.46296296em; -} -.katex .sizing.reset-size9.size5, -.katex .fontsize-ensurer.reset-size9.size5 { - font-size: 0.52083333em; -} -.katex .sizing.reset-size9.size6, -.katex .fontsize-ensurer.reset-size9.size6 { - font-size: 0.5787037em; -} -.katex .sizing.reset-size9.size7, -.katex .fontsize-ensurer.reset-size9.size7 { - font-size: 0.69444444em; -} -.katex .sizing.reset-size9.size8, -.katex .fontsize-ensurer.reset-size9.size8 { - font-size: 0.83333333em; -} -.katex .sizing.reset-size9.size9, -.katex .fontsize-ensurer.reset-size9.size9 { - font-size: 1em; -} -.katex .sizing.reset-size9.size10, -.katex .fontsize-ensurer.reset-size9.size10 { - font-size: 1.20023148em; -} -.katex .sizing.reset-size9.size11, -.katex .fontsize-ensurer.reset-size9.size11 { - font-size: 1.43981481em; -} -.katex .sizing.reset-size10.size1, -.katex .fontsize-ensurer.reset-size10.size1 { - font-size: 0.24108004em; -} -.katex .sizing.reset-size10.size2, -.katex .fontsize-ensurer.reset-size10.size2 { - font-size: 0.28929605em; -} -.katex .sizing.reset-size10.size3, -.katex .fontsize-ensurer.reset-size10.size3 { - font-size: 0.33751205em; -} -.katex .sizing.reset-size10.size4, -.katex .fontsize-ensurer.reset-size10.size4 { - font-size: 0.38572806em; -} -.katex .sizing.reset-size10.size5, -.katex .fontsize-ensurer.reset-size10.size5 { - font-size: 0.43394407em; -} -.katex .sizing.reset-size10.size6, -.katex .fontsize-ensurer.reset-size10.size6 { - font-size: 0.48216008em; -} -.katex .sizing.reset-size10.size7, -.katex .fontsize-ensurer.reset-size10.size7 { - font-size: 0.57859209em; -} -.katex .sizing.reset-size10.size8, -.katex .fontsize-ensurer.reset-size10.size8 { - font-size: 0.69431051em; -} -.katex .sizing.reset-size10.size9, -.katex .fontsize-ensurer.reset-size10.size9 { - font-size: 0.83317261em; -} -.katex .sizing.reset-size10.size10, -.katex .fontsize-ensurer.reset-size10.size10 { - font-size: 1em; -} -.katex .sizing.reset-size10.size11, -.katex .fontsize-ensurer.reset-size10.size11 { - font-size: 1.19961427em; -} -.katex .sizing.reset-size11.size1, -.katex .fontsize-ensurer.reset-size11.size1 { - font-size: 0.20096463em; -} -.katex .sizing.reset-size11.size2, -.katex .fontsize-ensurer.reset-size11.size2 { - font-size: 0.24115756em; -} -.katex .sizing.reset-size11.size3, -.katex .fontsize-ensurer.reset-size11.size3 { - font-size: 0.28135048em; -} -.katex .sizing.reset-size11.size4, -.katex .fontsize-ensurer.reset-size11.size4 { - font-size: 0.32154341em; -} -.katex .sizing.reset-size11.size5, -.katex .fontsize-ensurer.reset-size11.size5 { - font-size: 0.36173633em; -} -.katex .sizing.reset-size11.size6, -.katex .fontsize-ensurer.reset-size11.size6 { - font-size: 0.40192926em; -} -.katex .sizing.reset-size11.size7, -.katex .fontsize-ensurer.reset-size11.size7 { - font-size: 0.48231511em; -} -.katex .sizing.reset-size11.size8, -.katex .fontsize-ensurer.reset-size11.size8 { - font-size: 0.57877814em; -} -.katex .sizing.reset-size11.size9, -.katex .fontsize-ensurer.reset-size11.size9 { - font-size: 0.69453376em; -} -.katex .sizing.reset-size11.size10, -.katex .fontsize-ensurer.reset-size11.size10 { - font-size: 0.83360129em; -} -.katex .sizing.reset-size11.size11, -.katex .fontsize-ensurer.reset-size11.size11 { - font-size: 1em; -} -.katex .delimsizing.size1 { - font-family: KaTeX_Size1; -} -.katex .delimsizing.size2 { - font-family: KaTeX_Size2; -} -.katex .delimsizing.size3 { - font-family: KaTeX_Size3; -} -.katex .delimsizing.size4 { - font-family: KaTeX_Size4; -} -.katex .delimsizing.mult .delim-size1 > span { - font-family: KaTeX_Size1; -} -.katex .delimsizing.mult .delim-size4 > span { - font-family: KaTeX_Size4; -} -.katex .nulldelimiter { - display: inline-block; - width: 0.12em; -} -.katex .delimcenter { - position: relative; -} -.katex .op-symbol { - position: relative; -} -.katex .op-symbol.small-op { - font-family: KaTeX_Size1; -} -.katex .op-symbol.large-op { - font-family: KaTeX_Size2; -} -.katex .op-limits > .vlist-t { - text-align: center; -} -.katex .accent > .vlist-t { - text-align: center; -} -.katex .accent .accent-body { - position: relative; -} -.katex .accent .accent-body:not(.accent-full) { - width: 0; -} -.katex .overlay { - display: block; -} -.katex .mtable .vertical-separator { - display: inline-block; - min-width: 1px; -} -.katex .mtable .arraycolsep { - display: inline-block; -} -.katex .mtable .col-align-c > .vlist-t { - text-align: center; -} -.katex .mtable .col-align-l > .vlist-t { - text-align: left; -} -.katex .mtable .col-align-r > .vlist-t { - text-align: right; -} -.katex .svg-align { - text-align: left; -} -.katex svg { - display: block; - position: absolute; - width: 100%; - height: inherit; - fill: currentColor; - stroke: currentColor; - fill-rule: nonzero; - fill-opacity: 1; - stroke-width: 1; - stroke-linecap: butt; - stroke-linejoin: miter; - stroke-miterlimit: 4; - stroke-dasharray: none; - stroke-dashoffset: 0; - stroke-opacity: 1; -} -.katex svg path { - stroke: none; -} -.katex img { - border-style: none; - min-width: 0; - min-height: 0; - max-width: none; - max-height: none; -} -.katex .stretchy { - width: 100%; - display: block; - position: relative; - overflow: hidden; -} -.katex .stretchy::before, -.katex .stretchy::after { - content: ""; -} -.katex .hide-tail { - width: 100%; - position: relative; - overflow: hidden; -} -.katex .halfarrow-left { - position: absolute; - left: 0; - width: 50.2%; - overflow: hidden; -} -.katex .halfarrow-right { - position: absolute; - right: 0; - width: 50.2%; - overflow: hidden; -} -.katex .brace-left { - position: absolute; - left: 0; - width: 25.1%; - overflow: hidden; -} -.katex .brace-center { - position: absolute; - left: 25%; - width: 50%; - overflow: hidden; -} -.katex .brace-right { - position: absolute; - right: 0; - width: 25.1%; - overflow: hidden; -} -.katex .x-arrow-pad { - padding: 0 0.5em; -} -.katex .x-arrow, -.katex .mover, -.katex .munder { - text-align: center; -} -.katex .boxpad { - padding: 0 0.3em 0 0.3em; -} -.katex .fbox, -.katex .fcolorbox { - box-sizing: border-box; - border: 0.04em solid; -} -.katex .cancel-pad { - padding: 0 0.2em 0 0.2em; -} -.katex .cancel-lap { - margin-left: -0.2em; - margin-right: -0.2em; -} -.katex .sout { - border-bottom-style: solid; - border-bottom-width: 0.08em; -} -.katex-display { - display: block; - margin: 1em 0; - text-align: center; -} -.katex-display > .katex { - display: block; - text-align: center; - white-space: nowrap; -} -.katex-display > .katex > .katex-html { - display: block; - position: relative; -} -.katex-display > .katex > .katex-html > .tag { - position: absolute; - right: 0; -} -.katex-display.leqno > .katex > .katex-html > .tag { - left: 0; - right: auto; -} -.katex-display.fleqn > .katex { - text-align: left; - padding-left: 2em; -} diff --git a/docs/static/katex/katex.js b/docs/static/katex/katex.js deleted file mode 100644 index b289a482ae5..00000000000 --- a/docs/static/katex/katex.js +++ /dev/null @@ -1,17308 +0,0 @@ -(function webpackUniversalModuleDefinition(root, factory) { - if(typeof exports === 'object' && typeof module === 'object') - module.exports = factory(); - else if(typeof define === 'function' && define.amd) - define([], factory); - else if(typeof exports === 'object') - exports["katex"] = factory(); - else - root["katex"] = factory(); -})((typeof self !== 'undefined' ? self : this), function() { -return /******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); -/******/ } -/******/ }; -/******/ -/******/ // define __esModule on exports -/******/ __webpack_require__.r = function(exports) { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ -/******/ // create a fake namespace object -/******/ // mode & 1: value is a module id, require it -/******/ // mode & 2: merge all properties of value into the ns -/******/ // mode & 4: return value when already ns object -/******/ // mode & 8|1: behave like require -/******/ __webpack_require__.t = function(value, mode) { -/******/ if(mode & 1) value = __webpack_require__(value); -/******/ if(mode & 8) return value; -/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; -/******/ var ns = Object.create(null); -/******/ __webpack_require__.r(ns); -/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); -/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); -/******/ return ns; -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; -/******/ -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = 1); -/******/ }) -/************************************************************************/ -/******/ ([ -/* 0 */ -/***/ (function(module, exports, __webpack_require__) { - -// extracted by mini-css-extract-plugin - -/***/ }), -/* 1 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); - -// EXTERNAL MODULE: ./src/katex.less -var katex = __webpack_require__(0); - -// CONCATENATED MODULE: ./src/SourceLocation.js -/** - * Lexing or parsing positional information for error reporting. - * This object is immutable. - */ -var SourceLocation = -/*#__PURE__*/ -function () { - // The + prefix indicates that these fields aren't writeable - // Lexer holding the input string. - // Start offset, zero-based inclusive. - // End offset, zero-based exclusive. - function SourceLocation(lexer, start, end) { - this.lexer = void 0; - this.start = void 0; - this.end = void 0; - this.lexer = lexer; - this.start = start; - this.end = end; - } - /** - * Merges two `SourceLocation`s from location providers, given they are - * provided in order of appearance. - * - Returns the first one's location if only the first is provided. - * - Returns a merged range of the first and the last if both are provided - * and their lexers match. - * - Otherwise, returns null. - */ - - - SourceLocation.range = function range(first, second) { - if (!second) { - return first && first.loc; - } else if (!first || !first.loc || !second.loc || first.loc.lexer !== second.loc.lexer) { - return null; - } else { - return new SourceLocation(first.loc.lexer, first.loc.start, second.loc.end); - } - }; - - return SourceLocation; -}(); - - -// CONCATENATED MODULE: ./src/Token.js - -/** - * Interface required to break circular dependency between Token, Lexer, and - * ParseError. - */ - -/** - * The resulting token returned from `lex`. - * - * It consists of the token text plus some position information. - * The position information is essentially a range in an input string, - * but instead of referencing the bare input string, we refer to the lexer. - * That way it is possible to attach extra metadata to the input string, - * like for example a file name or similar. - * - * The position information is optional, so it is OK to construct synthetic - * tokens if appropriate. Not providing available position information may - * lead to degraded error reporting, though. - */ -var Token_Token = -/*#__PURE__*/ -function () { - // don't expand the token - // used in \noexpand - function Token(text, // the text of this token - loc) { - this.text = void 0; - this.loc = void 0; - this.noexpand = void 0; - this.treatAsRelax = void 0; - this.text = text; - this.loc = loc; - } - /** - * Given a pair of tokens (this and endToken), compute a `Token` encompassing - * the whole input range enclosed by these two. - */ - - - var _proto = Token.prototype; - - _proto.range = function range(endToken, // last token of the range, inclusive - text) // the text of the newly constructed token - { - return new Token(text, SourceLocation.range(this, endToken)); - }; - - return Token; -}(); -// CONCATENATED MODULE: ./src/ParseError.js - - -/** - * This is the ParseError class, which is the main error thrown by KaTeX - * functions when something has gone wrong. This is used to distinguish internal - * errors from errors in the expression that the user provided. - * - * If possible, a caller should provide a Token or ParseNode with information - * about where in the source string the problem occurred. - */ -var ParseError = // Error position based on passed-in Token or ParseNode. -function ParseError(message, // The error message -token) // An object providing position information -{ - this.position = void 0; - var error = "KaTeX parse error: " + message; - var start; - var loc = token && token.loc; - - if (loc && loc.start <= loc.end) { - // If we have the input and a position, make the error a bit fancier - // Get the input - var input = loc.lexer.input; // Prepend some information - - start = loc.start; - var end = loc.end; - - if (start === input.length) { - error += " at end of input: "; - } else { - error += " at position " + (start + 1) + ": "; - } // Underline token in question using combining underscores - - - var underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332"); // Extract some context from the input and add it to the error - - var left; - - if (start > 15) { - left = "…" + input.slice(start - 15, start); - } else { - left = input.slice(0, start); - } - - var right; - - if (end + 15 < input.length) { - right = input.slice(end, end + 15) + "…"; - } else { - right = input.slice(end); - } - - error += left + underlined + right; - } // Some hackery to make ParseError a prototype of Error - // See http://stackoverflow.com/a/8460753 - - - var self = new Error(error); - self.name = "ParseError"; // $FlowFixMe - - self.__proto__ = ParseError.prototype; // $FlowFixMe - - self.position = start; - return self; -}; // $FlowFixMe More hackery - - -ParseError.prototype.__proto__ = Error.prototype; -/* harmony default export */ var src_ParseError = (ParseError); -// CONCATENATED MODULE: ./src/utils.js -/** - * This file contains a list of utility functions which are useful in other - * files. - */ - -/** - * Return whether an element is contained in a list - */ -var contains = function contains(list, elem) { - return list.indexOf(elem) !== -1; -}; -/** - * Provide a default value if a setting is undefined - * NOTE: Couldn't use `T` as the output type due to facebook/flow#5022. - */ - - -var deflt = function deflt(setting, defaultIfUndefined) { - return setting === undefined ? defaultIfUndefined : setting; -}; // hyphenate and escape adapted from Facebook's React under Apache 2 license - - -var uppercase = /([A-Z])/g; - -var hyphenate = function hyphenate(str) { - return str.replace(uppercase, "-$1").toLowerCase(); -}; - -var ESCAPE_LOOKUP = { - "&": "&", - ">": ">", - "<": "<", - "\"": """, - "'": "'" -}; -var ESCAPE_REGEX = /[&><"']/g; -/** - * Escapes text to prevent scripting attacks. - */ - -function utils_escape(text) { - return String(text).replace(ESCAPE_REGEX, function (match) { - return ESCAPE_LOOKUP[match]; - }); -} -/** - * Sometimes we want to pull out the innermost element of a group. In most - * cases, this will just be the group itself, but when ordgroups and colors have - * a single element, we want to pull that out. - */ - - -var getBaseElem = function getBaseElem(group) { - if (group.type === "ordgroup") { - if (group.body.length === 1) { - return getBaseElem(group.body[0]); - } else { - return group; - } - } else if (group.type === "color") { - if (group.body.length === 1) { - return getBaseElem(group.body[0]); - } else { - return group; - } - } else if (group.type === "font") { - return getBaseElem(group.body); - } else { - return group; - } -}; -/** - * TeXbook algorithms often reference "character boxes", which are simply groups - * with a single character in them. To decide if something is a character box, - * we find its innermost group, and see if it is a single character. - */ - - -var utils_isCharacterBox = function isCharacterBox(group) { - var baseElem = getBaseElem(group); // These are all they types of groups which hold single characters - - return baseElem.type === "mathord" || baseElem.type === "textord" || baseElem.type === "atom"; -}; - -var assert = function assert(value) { - if (!value) { - throw new Error('Expected non-null, but got ' + String(value)); - } - - return value; -}; -/** - * Return the protocol of a URL, or "_relative" if the URL does not specify a - * protocol (and thus is relative). - */ - -var protocolFromUrl = function protocolFromUrl(url) { - var protocol = /^\s*([^\\/#]*?)(?::|�*58|�*3a)/i.exec(url); - return protocol != null ? protocol[1] : "_relative"; -}; -/* harmony default export */ var utils = ({ - contains: contains, - deflt: deflt, - escape: utils_escape, - hyphenate: hyphenate, - getBaseElem: getBaseElem, - isCharacterBox: utils_isCharacterBox, - protocolFromUrl: protocolFromUrl -}); -// CONCATENATED MODULE: ./src/Settings.js -/* eslint no-console:0 */ - -/** - * This is a module for storing settings passed into KaTeX. It correctly handles - * default settings. - */ - - - - -/** - * The main Settings object - * - * The current options stored are: - * - displayMode: Whether the expression should be typeset as inline math - * (false, the default), meaning that the math starts in - * \textstyle and is placed in an inline-block); or as display - * math (true), meaning that the math starts in \displaystyle - * and is placed in a block with vertical margin. - */ -var Settings_Settings = -/*#__PURE__*/ -function () { - function Settings(options) { - this.displayMode = void 0; - this.output = void 0; - this.leqno = void 0; - this.fleqn = void 0; - this.throwOnError = void 0; - this.errorColor = void 0; - this.macros = void 0; - this.minRuleThickness = void 0; - this.colorIsTextColor = void 0; - this.strict = void 0; - this.trust = void 0; - this.maxSize = void 0; - this.maxExpand = void 0; - this.globalGroup = void 0; - // allow null options - options = options || {}; - this.displayMode = utils.deflt(options.displayMode, false); - this.output = utils.deflt(options.output, "htmlAndMathml"); - this.leqno = utils.deflt(options.leqno, false); - this.fleqn = utils.deflt(options.fleqn, false); - this.throwOnError = utils.deflt(options.throwOnError, true); - this.errorColor = utils.deflt(options.errorColor, "#cc0000"); - this.macros = options.macros || {}; - this.minRuleThickness = Math.max(0, utils.deflt(options.minRuleThickness, 0)); - this.colorIsTextColor = utils.deflt(options.colorIsTextColor, false); - this.strict = utils.deflt(options.strict, "warn"); - this.trust = utils.deflt(options.trust, false); - this.maxSize = Math.max(0, utils.deflt(options.maxSize, Infinity)); - this.maxExpand = Math.max(0, utils.deflt(options.maxExpand, 1000)); - this.globalGroup = utils.deflt(options.globalGroup, false); - } - /** - * Report nonstrict (non-LaTeX-compatible) input. - * Can safely not be called if `this.strict` is false in JavaScript. - */ - - - var _proto = Settings.prototype; - - _proto.reportNonstrict = function reportNonstrict(errorCode, errorMsg, token) { - var strict = this.strict; - - if (typeof strict === "function") { - // Allow return value of strict function to be boolean or string - // (or null/undefined, meaning no further processing). - strict = strict(errorCode, errorMsg, token); - } - - if (!strict || strict === "ignore") { - return; - } else if (strict === true || strict === "error") { - throw new src_ParseError("LaTeX-incompatible input and strict mode is set to 'error': " + (errorMsg + " [" + errorCode + "]"), token); - } else if (strict === "warn") { - typeof console !== "undefined" && console.warn("LaTeX-incompatible input and strict mode is set to 'warn': " + (errorMsg + " [" + errorCode + "]")); - } else { - // won't happen in type-safe code - typeof console !== "undefined" && console.warn("LaTeX-incompatible input and strict mode is set to " + ("unrecognized '" + strict + "': " + errorMsg + " [" + errorCode + "]")); - } - } - /** - * Check whether to apply strict (LaTeX-adhering) behavior for unusual - * input (like `\\`). Unlike `nonstrict`, will not throw an error; - * instead, "error" translates to a return value of `true`, while "ignore" - * translates to a return value of `false`. May still print a warning: - * "warn" prints a warning and returns `false`. - * This is for the second category of `errorCode`s listed in the README. - */ - ; - - _proto.useStrictBehavior = function useStrictBehavior(errorCode, errorMsg, token) { - var strict = this.strict; - - if (typeof strict === "function") { - // Allow return value of strict function to be boolean or string - // (or null/undefined, meaning no further processing). - // But catch any exceptions thrown by function, treating them - // like "error". - try { - strict = strict(errorCode, errorMsg, token); - } catch (error) { - strict = "error"; - } - } - - if (!strict || strict === "ignore") { - return false; - } else if (strict === true || strict === "error") { - return true; - } else if (strict === "warn") { - typeof console !== "undefined" && console.warn("LaTeX-incompatible input and strict mode is set to 'warn': " + (errorMsg + " [" + errorCode + "]")); - return false; - } else { - // won't happen in type-safe code - typeof console !== "undefined" && console.warn("LaTeX-incompatible input and strict mode is set to " + ("unrecognized '" + strict + "': " + errorMsg + " [" + errorCode + "]")); - return false; - } - } - /** - * Check whether to test potentially dangerous input, and return - * `true` (trusted) or `false` (untrusted). The sole argument `context` - * should be an object with `command` field specifying the relevant LaTeX - * command (as a string starting with `\`), and any other arguments, etc. - * If `context` has a `url` field, a `protocol` field will automatically - * get added by this function (changing the specified object). - */ - ; - - _proto.isTrusted = function isTrusted(context) { - if (context.url && !context.protocol) { - context.protocol = utils.protocolFromUrl(context.url); - } - - var trust = typeof this.trust === "function" ? this.trust(context) : this.trust; - return Boolean(trust); - }; - - return Settings; -}(); - - -// CONCATENATED MODULE: ./src/Style.js -/** - * This file contains information and classes for the various kinds of styles - * used in TeX. It provides a generic `Style` class, which holds information - * about a specific style. It then provides instances of all the different kinds - * of styles possible, and provides functions to move between them and get - * information about them. - */ - -/** - * The main style class. Contains a unique id for the style, a size (which is - * the same for cramped and uncramped version of a style), and a cramped flag. - */ -var Style = -/*#__PURE__*/ -function () { - function Style(id, size, cramped) { - this.id = void 0; - this.size = void 0; - this.cramped = void 0; - this.id = id; - this.size = size; - this.cramped = cramped; - } - /** - * Get the style of a superscript given a base in the current style. - */ - - - var _proto = Style.prototype; - - _proto.sup = function sup() { - return Style_styles[_sup[this.id]]; - } - /** - * Get the style of a subscript given a base in the current style. - */ - ; - - _proto.sub = function sub() { - return Style_styles[_sub[this.id]]; - } - /** - * Get the style of a fraction numerator given the fraction in the current - * style. - */ - ; - - _proto.fracNum = function fracNum() { - return Style_styles[_fracNum[this.id]]; - } - /** - * Get the style of a fraction denominator given the fraction in the current - * style. - */ - ; - - _proto.fracDen = function fracDen() { - return Style_styles[_fracDen[this.id]]; - } - /** - * Get the cramped version of a style (in particular, cramping a cramped style - * doesn't change the style). - */ - ; - - _proto.cramp = function cramp() { - return Style_styles[_cramp[this.id]]; - } - /** - * Get a text or display version of this style. - */ - ; - - _proto.text = function text() { - return Style_styles[_text[this.id]]; - } - /** - * Return true if this style is tightly spaced (scriptstyle/scriptscriptstyle) - */ - ; - - _proto.isTight = function isTight() { - return this.size >= 2; - }; - - return Style; -}(); // Export an interface for type checking, but don't expose the implementation. -// This way, no more styles can be generated. - - -// IDs of the different styles -var D = 0; -var Dc = 1; -var T = 2; -var Tc = 3; -var S = 4; -var Sc = 5; -var SS = 6; -var SSc = 7; // Instances of the different styles - -var Style_styles = [new Style(D, 0, false), new Style(Dc, 0, true), new Style(T, 1, false), new Style(Tc, 1, true), new Style(S, 2, false), new Style(Sc, 2, true), new Style(SS, 3, false), new Style(SSc, 3, true)]; // Lookup tables for switching from one style to another - -var _sup = [S, Sc, S, Sc, SS, SSc, SS, SSc]; -var _sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc]; -var _fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc]; -var _fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc]; -var _cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc]; -var _text = [D, Dc, T, Tc, T, Tc, T, Tc]; // We only export some of the styles. - -/* harmony default export */ var src_Style = ({ - DISPLAY: Style_styles[D], - TEXT: Style_styles[T], - SCRIPT: Style_styles[S], - SCRIPTSCRIPT: Style_styles[SS] -}); -// CONCATENATED MODULE: ./src/unicodeScripts.js -/* - * This file defines the Unicode scripts and script families that we - * support. To add new scripts or families, just add a new entry to the - * scriptData array below. Adding scripts to the scriptData array allows - * characters from that script to appear in \text{} environments. - */ - -/** - * Each script or script family has a name and an array of blocks. - * Each block is an array of two numbers which specify the start and - * end points (inclusive) of a block of Unicode codepoints. - */ - -/** - * Unicode block data for the families of scripts we support in \text{}. - * Scripts only need to appear here if they do not have font metrics. - */ -var scriptData = [{ - // Latin characters beyond the Latin-1 characters we have metrics for. - // Needed for Czech, Hungarian and Turkish text, for example. - name: 'latin', - blocks: [[0x0100, 0x024f], // Latin Extended-A and Latin Extended-B - [0x0300, 0x036f]] -}, { - // The Cyrillic script used by Russian and related languages. - // A Cyrillic subset used to be supported as explicitly defined - // symbols in symbols.js - name: 'cyrillic', - blocks: [[0x0400, 0x04ff]] -}, { - // The Brahmic scripts of South and Southeast Asia - // Devanagari (0900–097F) - // Bengali (0980–09FF) - // Gurmukhi (0A00–0A7F) - // Gujarati (0A80–0AFF) - // Oriya (0B00–0B7F) - // Tamil (0B80–0BFF) - // Telugu (0C00–0C7F) - // Kannada (0C80–0CFF) - // Malayalam (0D00–0D7F) - // Sinhala (0D80–0DFF) - // Thai (0E00–0E7F) - // Lao (0E80–0EFF) - // Tibetan (0F00–0FFF) - // Myanmar (1000–109F) - name: 'brahmic', - blocks: [[0x0900, 0x109F]] -}, { - name: 'georgian', - blocks: [[0x10A0, 0x10ff]] -}, { - // Chinese and Japanese. - // The "k" in cjk is for Korean, but we've separated Korean out - name: "cjk", - blocks: [[0x3000, 0x30FF], // CJK symbols and punctuation, Hiragana, Katakana - [0x4E00, 0x9FAF], // CJK ideograms - [0xFF00, 0xFF60]] -}, { - // Korean - name: 'hangul', - blocks: [[0xAC00, 0xD7AF]] -}]; -/** - * Given a codepoint, return the name of the script or script family - * it is from, or null if it is not part of a known block - */ - -function scriptFromCodepoint(codepoint) { - for (var i = 0; i < scriptData.length; i++) { - var script = scriptData[i]; - - for (var _i = 0; _i < script.blocks.length; _i++) { - var block = script.blocks[_i]; - - if (codepoint >= block[0] && codepoint <= block[1]) { - return script.name; - } - } - } - - return null; -} -/** - * A flattened version of all the supported blocks in a single array. - * This is an optimization to make supportedCodepoint() fast. - */ - -var allBlocks = []; -scriptData.forEach(function (s) { - return s.blocks.forEach(function (b) { - return allBlocks.push.apply(allBlocks, b); - }); -}); -/** - * Given a codepoint, return true if it falls within one of the - * scripts or script families defined above and false otherwise. - * - * Micro benchmarks shows that this is faster than - * /[\u3000-\u30FF\u4E00-\u9FAF\uFF00-\uFF60\uAC00-\uD7AF\u0900-\u109F]/.test() - * in Firefox, Chrome and Node. - */ - -function supportedCodepoint(codepoint) { - for (var i = 0; i < allBlocks.length; i += 2) { - if (codepoint >= allBlocks[i] && codepoint <= allBlocks[i + 1]) { - return true; - } - } - - return false; -} -// CONCATENATED MODULE: ./src/svgGeometry.js -/** - * This file provides support to domTree.js and delimiter.js. - * It's a storehouse of path geometry for SVG images. - */ -// In all paths below, the viewBox-to-em scale is 1000:1. -var hLinePad = 80; // padding above a sqrt viniculum. Prevents image cropping. -// The viniculum of a \sqrt can be made thicker by a KaTeX rendering option. -// Think of variable extraViniculum as two detours in the SVG path. -// The detour begins at the lower left of the area labeled extraViniculum below. -// The detour proceeds one extraViniculum distance up and slightly to the right, -// displacing the radiused corner between surd and viniculum. The radius is -// traversed as usual, then the detour resumes. It goes right, to the end of -// the very long viniculumn, then down one extraViniculum distance, -// after which it resumes regular path geometry for the radical. - -/* viniculum - / - /▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒←extraViniculum - / █████████████████████←0.04em (40 unit) std viniculum thickness - / / - / / - / /\ - / / surd -*/ - -var sqrtMain = function sqrtMain(extraViniculum, hLinePad) { - // sqrtMain path geometry is from glyph U221A in the font KaTeX Main - return "M95," + (622 + extraViniculum + hLinePad) + "\nc-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14\nc0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54\nc44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10\ns173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429\nc69,-144,104.5,-217.7,106.5,-221\nl" + extraViniculum / 2.075 + " -" + extraViniculum + "\nc5.3,-9.3,12,-14,20,-14\nH400000v" + (40 + extraViniculum) + "H845.2724\ns-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7\nc-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z\nM" + (834 + extraViniculum) + " " + hLinePad + "h400000v" + (40 + extraViniculum) + "h-400000z"; -}; - -var sqrtSize1 = function sqrtSize1(extraViniculum, hLinePad) { - // size1 is from glyph U221A in the font KaTeX_Size1-Regular - return "M263," + (601 + extraViniculum + hLinePad) + "c0.7,0,18,39.7,52,119\nc34,79.3,68.167,158.7,102.5,238c34.3,79.3,51.8,119.3,52.5,120\nc340,-704.7,510.7,-1060.3,512,-1067\nl" + extraViniculum / 2.084 + " -" + extraViniculum + "\nc4.7,-7.3,11,-11,19,-11\nH40000v" + (40 + extraViniculum) + "H1012.3\ns-271.3,567,-271.3,567c-38.7,80.7,-84,175,-136,283c-52,108,-89.167,185.3,-111.5,232\nc-22.3,46.7,-33.8,70.3,-34.5,71c-4.7,4.7,-12.3,7,-23,7s-12,-1,-12,-1\ns-109,-253,-109,-253c-72.7,-168,-109.3,-252,-110,-252c-10.7,8,-22,16.7,-34,26\nc-22,17.3,-33.3,26,-34,26s-26,-26,-26,-26s76,-59,76,-59s76,-60,76,-60z\nM" + (1001 + extraViniculum) + " " + hLinePad + "h400000v" + (40 + extraViniculum) + "h-400000z"; -}; - -var sqrtSize2 = function sqrtSize2(extraViniculum, hLinePad) { - // size2 is from glyph U221A in the font KaTeX_Size2-Regular - return "M983 " + (10 + extraViniculum + hLinePad) + "\nl" + extraViniculum / 3.13 + " -" + extraViniculum + "\nc4,-6.7,10,-10,18,-10 H400000v" + (40 + extraViniculum) + "\nH1013.1s-83.4,268,-264.1,840c-180.7,572,-277,876.3,-289,913c-4.7,4.7,-12.7,7,-24,7\ns-12,0,-12,0c-1.3,-3.3,-3.7,-11.7,-7,-25c-35.3,-125.3,-106.7,-373.3,-214,-744\nc-10,12,-21,25,-33,39s-32,39,-32,39c-6,-5.3,-15,-14,-27,-26s25,-30,25,-30\nc26.7,-32.7,52,-63,76,-91s52,-60,52,-60s208,722,208,722\nc56,-175.3,126.3,-397.3,211,-666c84.7,-268.7,153.8,-488.2,207.5,-658.5\nc53.7,-170.3,84.5,-266.8,92.5,-289.5z\nM" + (1001 + extraViniculum) + " " + hLinePad + "h400000v" + (40 + extraViniculum) + "h-400000z"; -}; - -var sqrtSize3 = function sqrtSize3(extraViniculum, hLinePad) { - // size3 is from glyph U221A in the font KaTeX_Size3-Regular - return "M424," + (2398 + extraViniculum + hLinePad) + "\nc-1.3,-0.7,-38.5,-172,-111.5,-514c-73,-342,-109.8,-513.3,-110.5,-514\nc0,-2,-10.7,14.3,-32,49c-4.7,7.3,-9.8,15.7,-15.5,25c-5.7,9.3,-9.8,16,-12.5,20\ns-5,7,-5,7c-4,-3.3,-8.3,-7.7,-13,-13s-13,-13,-13,-13s76,-122,76,-122s77,-121,77,-121\ns209,968,209,968c0,-2,84.7,-361.7,254,-1079c169.3,-717.3,254.7,-1077.7,256,-1081\nl" + extraViniculum / 4.223 + " -" + extraViniculum + "c4,-6.7,10,-10,18,-10 H400000\nv" + (40 + extraViniculum) + "H1014.6\ns-87.3,378.7,-272.6,1166c-185.3,787.3,-279.3,1182.3,-282,1185\nc-2,6,-10,9,-24,9\nc-8,0,-12,-0.7,-12,-2z M" + (1001 + extraViniculum) + " " + hLinePad + "\nh400000v" + (40 + extraViniculum) + "h-400000z"; -}; - -var sqrtSize4 = function sqrtSize4(extraViniculum, hLinePad) { - // size4 is from glyph U221A in the font KaTeX_Size4-Regular - return "M473," + (2713 + extraViniculum + hLinePad) + "\nc339.3,-1799.3,509.3,-2700,510,-2702 l" + extraViniculum / 5.298 + " -" + extraViniculum + "\nc3.3,-7.3,9.3,-11,18,-11 H400000v" + (40 + extraViniculum) + "H1017.7\ns-90.5,478,-276.2,1466c-185.7,988,-279.5,1483,-281.5,1485c-2,6,-10,9,-24,9\nc-8,0,-12,-0.7,-12,-2c0,-1.3,-5.3,-32,-16,-92c-50.7,-293.3,-119.7,-693.3,-207,-1200\nc0,-1.3,-5.3,8.7,-16,30c-10.7,21.3,-21.3,42.7,-32,64s-16,33,-16,33s-26,-26,-26,-26\ns76,-153,76,-153s77,-151,77,-151c0.7,0.7,35.7,202,105,604c67.3,400.7,102,602.7,104,\n606zM" + (1001 + extraViniculum) + " " + hLinePad + "h400000v" + (40 + extraViniculum) + "H1017.7z"; -}; - -var sqrtTall = function sqrtTall(extraViniculum, hLinePad, viewBoxHeight) { - // sqrtTall is from glyph U23B7 in the font KaTeX_Size4-Regular - // One path edge has a variable length. It runs vertically from the viniculumn - // to a point near (14 units) the bottom of the surd. The viniculum - // is normally 40 units thick. So the length of the line in question is: - var vertSegment = viewBoxHeight - 54 - hLinePad - extraViniculum; - return "M702 " + (extraViniculum + hLinePad) + "H400000" + (40 + extraViniculum) + "\nH742v" + vertSegment + "l-4 4-4 4c-.667.7 -2 1.5-4 2.5s-4.167 1.833-6.5 2.5-5.5 1-9.5 1\nh-12l-28-84c-16.667-52-96.667 -294.333-240-727l-212 -643 -85 170\nc-4-3.333-8.333-7.667-13 -13l-13-13l77-155 77-156c66 199.333 139 419.667\n219 661 l218 661zM702 " + hLinePad + "H400000v" + (40 + extraViniculum) + "H742z"; -}; - -var sqrtPath = function sqrtPath(size, extraViniculum, viewBoxHeight) { - extraViniculum = 1000 * extraViniculum; // Convert from document ems to viewBox. - - var path = ""; - - switch (size) { - case "sqrtMain": - path = sqrtMain(extraViniculum, hLinePad); - break; - - case "sqrtSize1": - path = sqrtSize1(extraViniculum, hLinePad); - break; - - case "sqrtSize2": - path = sqrtSize2(extraViniculum, hLinePad); - break; - - case "sqrtSize3": - path = sqrtSize3(extraViniculum, hLinePad); - break; - - case "sqrtSize4": - path = sqrtSize4(extraViniculum, hLinePad); - break; - - case "sqrtTall": - path = sqrtTall(extraViniculum, hLinePad, viewBoxHeight); - } - - return path; -}; -var svgGeometry_path = { - // Two paths that cover gaps in built-up parentheses. - leftParenInner: "M291 0 H417 V300 H291 z", - rightParenInner: "M457 0 H583 V300 H457 z", - // The doubleleftarrow geometry is from glyph U+21D0 in the font KaTeX Main - doubleleftarrow: "M262 157\nl10-10c34-36 62.7-77 86-123 3.3-8 5-13.3 5-16 0-5.3-6.7-8-20-8-7.3\n 0-12.2.5-14.5 1.5-2.3 1-4.8 4.5-7.5 10.5-49.3 97.3-121.7 169.3-217 216-28\n 14-57.3 25-88 33-6.7 2-11 3.8-13 5.5-2 1.7-3 4.2-3 7.5s1 5.8 3 7.5\nc2 1.7 6.3 3.5 13 5.5 68 17.3 128.2 47.8 180.5 91.5 52.3 43.7 93.8 96.2 124.5\n 157.5 9.3 8 15.3 12.3 18 13h6c12-.7 18-4 18-10 0-2-1.7-7-5-15-23.3-46-52-87\n-86-123l-10-10h399738v-40H218c328 0 0 0 0 0l-10-8c-26.7-20-65.7-43-117-69 2.7\n-2 6-3.7 10-5 36.7-16 72.3-37.3 107-64l10-8h399782v-40z\nm8 0v40h399730v-40zm0 194v40h399730v-40z", - // doublerightarrow is from glyph U+21D2 in font KaTeX Main - doublerightarrow: "M399738 392l\n-10 10c-34 36-62.7 77-86 123-3.3 8-5 13.3-5 16 0 5.3 6.7 8 20 8 7.3 0 12.2-.5\n 14.5-1.5 2.3-1 4.8-4.5 7.5-10.5 49.3-97.3 121.7-169.3 217-216 28-14 57.3-25 88\n-33 6.7-2 11-3.8 13-5.5 2-1.7 3-4.2 3-7.5s-1-5.8-3-7.5c-2-1.7-6.3-3.5-13-5.5-68\n-17.3-128.2-47.8-180.5-91.5-52.3-43.7-93.8-96.2-124.5-157.5-9.3-8-15.3-12.3-18\n-13h-6c-12 .7-18 4-18 10 0 2 1.7 7 5 15 23.3 46 52 87 86 123l10 10H0v40h399782\nc-328 0 0 0 0 0l10 8c26.7 20 65.7 43 117 69-2.7 2-6 3.7-10 5-36.7 16-72.3 37.3\n-107 64l-10 8H0v40zM0 157v40h399730v-40zm0 194v40h399730v-40z", - // leftarrow is from glyph U+2190 in font KaTeX Main - leftarrow: "M400000 241H110l3-3c68.7-52.7 113.7-120\n 135-202 4-14.7 6-23 6-25 0-7.3-7-11-21-11-8 0-13.2.8-15.5 2.5-2.3 1.7-4.2 5.8\n-5.5 12.5-1.3 4.7-2.7 10.3-4 17-12 48.7-34.8 92-68.5 130S65.3 228.3 18 247\nc-10 4-16 7.7-18 11 0 8.7 6 14.3 18 17 47.3 18.7 87.8 47 121.5 85S196 441.3 208\n 490c.7 2 1.3 5 2 9s1.2 6.7 1.5 8c.3 1.3 1 3.3 2 6s2.2 4.5 3.5 5.5c1.3 1 3.3\n 1.8 6 2.5s6 1 10 1c14 0 21-3.7 21-11 0-2-2-10.3-6-25-20-79.3-65-146.7-135-202\n l-3-3h399890zM100 241v40h399900v-40z", - // overbrace is from glyphs U+23A9/23A8/23A7 in font KaTeX_Size4-Regular - leftbrace: "M6 548l-6-6v-35l6-11c56-104 135.3-181.3 238-232 57.3-28.7 117\n-45 179-50h399577v120H403c-43.3 7-81 15-113 26-100.7 33-179.7 91-237 174-2.7\n 5-6 9-10 13-.7 1-7.3 1-20 1H6z", - leftbraceunder: "M0 6l6-6h17c12.688 0 19.313.3 20 1 4 4 7.313 8.3 10 13\n 35.313 51.3 80.813 93.8 136.5 127.5 55.688 33.7 117.188 55.8 184.5 66.5.688\n 0 2 .3 4 1 18.688 2.7 76 4.3 172 5h399450v120H429l-6-1c-124.688-8-235-61.7\n-331-161C60.687 138.7 32.312 99.3 7 54L0 41V6z", - // overgroup is from the MnSymbol package (public domain) - leftgroup: "M400000 80\nH435C64 80 168.3 229.4 21 260c-5.9 1.2-18 0-18 0-2 0-3-1-3-3v-38C76 61 257 0\n 435 0h399565z", - leftgroupunder: "M400000 262\nH435C64 262 168.3 112.6 21 82c-5.9-1.2-18 0-18 0-2 0-3 1-3 3v38c76 158 257 219\n 435 219h399565z", - // Harpoons are from glyph U+21BD in font KaTeX Main - leftharpoon: "M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3\n-3.3 10.2-9.5 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5\n-18.3 3-21-1.3-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7\n-196 228-6.7 4.7-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40z", - leftharpoonplus: "M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3-3.3 10.2-9.5\n 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5-18.3 3-21-1.3\n-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7-196 228-6.7 4.7\n-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40zM0 435v40h400000v-40z\nm0 0v40h400000v-40z", - leftharpoondown: "M7 241c-4 4-6.333 8.667-7 14 0 5.333.667 9 2 11s5.333\n 5.333 12 10c90.667 54 156 130 196 228 3.333 10.667 6.333 16.333 9 17 2 .667 5\n 1 9 1h5c10.667 0 16.667-2 18-6 2-2.667 1-9.667-3-21-32-87.333-82.667-157.667\n-152-211l-3-3h399907v-40zM93 281 H400000 v-40L7 241z", - leftharpoondownplus: "M7 435c-4 4-6.3 8.7-7 14 0 5.3.7 9 2 11s5.3 5.3 12\n 10c90.7 54 156 130 196 228 3.3 10.7 6.3 16.3 9 17 2 .7 5 1 9 1h5c10.7 0 16.7\n-2 18-6 2-2.7 1-9.7-3-21-32-87.3-82.7-157.7-152-211l-3-3h399907v-40H7zm93 0\nv40h399900v-40zM0 241v40h399900v-40zm0 0v40h399900v-40z", - // hook is from glyph U+21A9 in font KaTeX Main - lefthook: "M400000 281 H103s-33-11.2-61-33.5S0 197.3 0 164s14.2-61.2 42.5\n-83.5C70.8 58.2 104 47 142 47 c16.7 0 25 6.7 25 20 0 12-8.7 18.7-26 20-40 3.3\n-68.7 15.7-86 37-10 12-15 25.3-15 40 0 22.7 9.8 40.7 29.5 54 19.7 13.3 43.5 21\n 71.5 23h399859zM103 281v-40h399897v40z", - leftlinesegment: "M40 281 V428 H0 V94 H40 V241 H400000 v40z\nM40 281 V428 H0 V94 H40 V241 H400000 v40z", - leftmapsto: "M40 281 V448H0V74H40V241H400000v40z\nM40 281 V448H0V74H40V241H400000v40z", - // tofrom is from glyph U+21C4 in font KaTeX AMS Regular - leftToFrom: "M0 147h400000v40H0zm0 214c68 40 115.7 95.7 143 167h22c15.3 0 23\n-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69-70-101l-7-8h399905v-40H95l7-8\nc28.7-32 52-65.7 70-101 10.7-23.3 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 265.3\n 68 321 0 361zm0-174v-40h399900v40zm100 154v40h399900v-40z", - longequal: "M0 50 h400000 v40H0z m0 194h40000v40H0z\nM0 50 h400000 v40H0z m0 194h40000v40H0z", - midbrace: "M200428 334\nc-100.7-8.3-195.3-44-280-108-55.3-42-101.7-93-139-153l-9-14c-2.7 4-5.7 8.7-9 14\n-53.3 86.7-123.7 153-211 199-66.7 36-137.3 56.3-212 62H0V214h199568c178.3-11.7\n 311.7-78.3 403-201 6-8 9.7-12 11-12 .7-.7 6.7-1 18-1s17.3.3 18 1c1.3 0 5 4 11\n 12 44.7 59.3 101.3 106.3 170 141s145.3 54.3 229 60h199572v120z", - midbraceunder: "M199572 214\nc100.7 8.3 195.3 44 280 108 55.3 42 101.7 93 139 153l9 14c2.7-4 5.7-8.7 9-14\n 53.3-86.7 123.7-153 211-199 66.7-36 137.3-56.3 212-62h199568v120H200432c-178.3\n 11.7-311.7 78.3-403 201-6 8-9.7 12-11 12-.7.7-6.7 1-18 1s-17.3-.3-18-1c-1.3 0\n-5-4-11-12-44.7-59.3-101.3-106.3-170-141s-145.3-54.3-229-60H0V214z", - oiintSize1: "M512.6 71.6c272.6 0 320.3 106.8 320.3 178.2 0 70.8-47.7 177.6\n-320.3 177.6S193.1 320.6 193.1 249.8c0-71.4 46.9-178.2 319.5-178.2z\nm368.1 178.2c0-86.4-60.9-215.4-368.1-215.4-306.4 0-367.3 129-367.3 215.4 0 85.8\n60.9 214.8 367.3 214.8 307.2 0 368.1-129 368.1-214.8z", - oiintSize2: "M757.8 100.1c384.7 0 451.1 137.6 451.1 230 0 91.3-66.4 228.8\n-451.1 228.8-386.3 0-452.7-137.5-452.7-228.8 0-92.4 66.4-230 452.7-230z\nm502.4 230c0-111.2-82.4-277.2-502.4-277.2s-504 166-504 277.2\nc0 110 84 276 504 276s502.4-166 502.4-276z", - oiiintSize1: "M681.4 71.6c408.9 0 480.5 106.8 480.5 178.2 0 70.8-71.6 177.6\n-480.5 177.6S202.1 320.6 202.1 249.8c0-71.4 70.5-178.2 479.3-178.2z\nm525.8 178.2c0-86.4-86.8-215.4-525.7-215.4-437.9 0-524.7 129-524.7 215.4 0\n85.8 86.8 214.8 524.7 214.8 438.9 0 525.7-129 525.7-214.8z", - oiiintSize2: "M1021.2 53c603.6 0 707.8 165.8 707.8 277.2 0 110-104.2 275.8\n-707.8 275.8-606 0-710.2-165.8-710.2-275.8C311 218.8 415.2 53 1021.2 53z\nm770.4 277.1c0-131.2-126.4-327.6-770.5-327.6S248.4 198.9 248.4 330.1\nc0 130 128.8 326.4 772.7 326.4s770.5-196.4 770.5-326.4z", - rightarrow: "M0 241v40h399891c-47.3 35.3-84 78-110 128\n-16.7 32-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20\n 11 8 0 13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7\n 39-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85\n-40.5-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5\n-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67\n 151.7 139 205zm0 0v40h399900v-40z", - rightbrace: "M400000 542l\n-6 6h-17c-12.7 0-19.3-.3-20-1-4-4-7.3-8.3-10-13-35.3-51.3-80.8-93.8-136.5-127.5\ns-117.2-55.8-184.5-66.5c-.7 0-2-.3-4-1-18.7-2.7-76-4.3-172-5H0V214h399571l6 1\nc124.7 8 235 61.7 331 161 31.3 33.3 59.7 72.7 85 118l7 13v35z", - rightbraceunder: "M399994 0l6 6v35l-6 11c-56 104-135.3 181.3-238 232-57.3\n 28.7-117 45-179 50H-300V214h399897c43.3-7 81-15 113-26 100.7-33 179.7-91 237\n-174 2.7-5 6-9 10-13 .7-1 7.3-1 20-1h17z", - rightgroup: "M0 80h399565c371 0 266.7 149.4 414 180 5.9 1.2 18 0 18 0 2 0\n 3-1 3-3v-38c-76-158-257-219-435-219H0z", - rightgroupunder: "M0 262h399565c371 0 266.7-149.4 414-180 5.9-1.2 18 0 18\n 0 2 0 3 1 3 3v38c-76 158-257 219-435 219H0z", - rightharpoon: "M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3\n-3.7-15.3-11-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2\n-10.7 0-16.7 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58\n 69.2 92 94.5zm0 0v40h399900v-40z", - rightharpoonplus: "M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3-3.7-15.3-11\n-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2-10.7 0-16.7\n 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58 69.2 92 94.5z\nm0 0v40h399900v-40z m100 194v40h399900v-40zm0 0v40h399900v-40z", - rightharpoondown: "M399747 511c0 7.3 6.7 11 20 11 8 0 13-.8 15-2.5s4.7-6.8\n 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3 8.5-5.8 9.5\n-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3-64.7 57-92 95\n-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 241v40h399900v-40z", - rightharpoondownplus: "M399747 705c0 7.3 6.7 11 20 11 8 0 13-.8\n 15-2.5s4.7-6.8 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3\n 8.5-5.8 9.5-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3\n-64.7 57-92 95-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 435v40h399900v-40z\nm0-194v40h400000v-40zm0 0v40h400000v-40z", - righthook: "M399859 241c-764 0 0 0 0 0 40-3.3 68.7-15.7 86-37 10-12 15-25.3\n 15-40 0-22.7-9.8-40.7-29.5-54-19.7-13.3-43.5-21-71.5-23-17.3-1.3-26-8-26-20 0\n-13.3 8.7-20 26-20 38 0 71 11.2 99 33.5 0 0 7 5.6 21 16.7 14 11.2 21 33.5 21\n 66.8s-14 61.2-42 83.5c-28 22.3-61 33.5-99 33.5L0 241z M0 281v-40h399859v40z", - rightlinesegment: "M399960 241 V94 h40 V428 h-40 V281 H0 v-40z\nM399960 241 V94 h40 V428 h-40 V281 H0 v-40z", - rightToFrom: "M400000 167c-70.7-42-118-97.7-142-167h-23c-15.3 0-23 .3-23\n 1 0 1.3 5.3 13.7 16 37 18 35.3 41.3 69 70 101l7 8H0v40h399905l-7 8c-28.7 32\n-52 65.7-70 101-10.7 23.3-16 35.7-16 37 0 .7 7.7 1 23 1h23c24-69.3 71.3-125 142\n-167z M100 147v40h399900v-40zM0 341v40h399900v-40z", - // twoheadleftarrow is from glyph U+219E in font KaTeX AMS Regular - twoheadleftarrow: "M0 167c68 40\n 115.7 95.7 143 167h22c15.3 0 23-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69\n-70-101l-7-8h125l9 7c50.7 39.3 85 86 103 140h46c0-4.7-6.3-18.7-19-42-18-35.3\n-40-67.3-66-96l-9-9h399716v-40H284l9-9c26-28.7 48-60.7 66-96 12.7-23.333 19\n-37.333 19-42h-46c-18 54-52.3 100.7-103 140l-9 7H95l7-8c28.7-32 52-65.7 70-101\n 10.7-23.333 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 71.3 68 127 0 167z", - twoheadrightarrow: "M400000 167\nc-68-40-115.7-95.7-143-167h-22c-15.3 0-23 .3-23 1 0 1.3 5.3 13.7 16 37 18 35.3\n 41.3 69 70 101l7 8h-125l-9-7c-50.7-39.3-85-86-103-140h-46c0 4.7 6.3 18.7 19 42\n 18 35.3 40 67.3 66 96l9 9H0v40h399716l-9 9c-26 28.7-48 60.7-66 96-12.7 23.333\n-19 37.333-19 42h46c18-54 52.3-100.7 103-140l9-7h125l-7 8c-28.7 32-52 65.7-70\n 101-10.7 23.333-16 35.7-16 37 0 .7 7.7 1 23 1h22c27.3-71.3 75-127 143-167z", - // tilde1 is a modified version of a glyph from the MnSymbol package - tilde1: "M200 55.538c-77 0-168 73.953-177 73.953-3 0-7\n-2.175-9-5.437L2 97c-1-2-2-4-2-6 0-4 2-7 5-9l20-12C116 12 171 0 207 0c86 0\n 114 68 191 68 78 0 168-68 177-68 4 0 7 2 9 5l12 19c1 2.175 2 4.35 2 6.525 0\n 4.35-2 7.613-5 9.788l-19 13.05c-92 63.077-116.937 75.308-183 76.128\n-68.267.847-113-73.952-191-73.952z", - // ditto tilde2, tilde3, & tilde4 - tilde2: "M344 55.266c-142 0-300.638 81.316-311.5 86.418\n-8.01 3.762-22.5 10.91-23.5 5.562L1 120c-1-2-1-3-1-4 0-5 3-9 8-10l18.4-9C160.9\n 31.9 283 0 358 0c148 0 188 122 331 122s314-97 326-97c4 0 8 2 10 7l7 21.114\nc1 2.14 1 3.21 1 4.28 0 5.347-3 9.626-7 10.696l-22.3 12.622C852.6 158.372 751\n 181.476 676 181.476c-149 0-189-126.21-332-126.21z", - tilde3: "M786 59C457 59 32 175.242 13 175.242c-6 0-10-3.457\n-11-10.37L.15 138c-1-7 3-12 10-13l19.2-6.4C378.4 40.7 634.3 0 804.3 0c337 0\n 411.8 157 746.8 157 328 0 754-112 773-112 5 0 10 3 11 9l1 14.075c1 8.066-.697\n 16.595-6.697 17.492l-21.052 7.31c-367.9 98.146-609.15 122.696-778.15 122.696\n -338 0-409-156.573-744-156.573z", - tilde4: "M786 58C457 58 32 177.487 13 177.487c-6 0-10-3.345\n-11-10.035L.15 143c-1-7 3-12 10-13l22-6.7C381.2 35 637.15 0 807.15 0c337 0 409\n 177 744 177 328 0 754-127 773-127 5 0 10 3 11 9l1 14.794c1 7.805-3 13.38-9\n 14.495l-20.7 5.574c-366.85 99.79-607.3 139.372-776.3 139.372-338 0-409\n -175.236-744-175.236z", - // vec is from glyph U+20D7 in font KaTeX Main - vec: "M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5\n3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11\n10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63\n-1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1\n-7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59\nH213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359\nc-16-25.333-24-45-24-59z", - // widehat1 is a modified version of a glyph from the MnSymbol package - widehat1: "M529 0h5l519 115c5 1 9 5 9 10 0 1-1 2-1 3l-4 22\nc-1 5-5 9-11 9h-2L532 67 19 159h-2c-5 0-9-4-11-9l-5-22c-1-6 2-12 8-13z", - // ditto widehat2, widehat3, & widehat4 - widehat2: "M1181 0h2l1171 176c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 220h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z", - widehat3: "M1181 0h2l1171 236c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 280h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z", - widehat4: "M1181 0h2l1171 296c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 340h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z", - // widecheck paths are all inverted versions of widehat - widecheck1: "M529,159h5l519,-115c5,-1,9,-5,9,-10c0,-1,-1,-2,-1,-3l-4,-22c-1,\n-5,-5,-9,-11,-9h-2l-512,92l-513,-92h-2c-5,0,-9,4,-11,9l-5,22c-1,6,2,12,8,13z", - widecheck2: "M1181,220h2l1171,-176c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,153l-1167,-153h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z", - widecheck3: "M1181,280h2l1171,-236c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,213l-1167,-213h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z", - widecheck4: "M1181,340h2l1171,-296c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,273l-1167,-273h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z", - // The next ten paths support reaction arrows from the mhchem package. - // Arrows for \ce{<-->} are offset from xAxis by 0.22ex, per mhchem in LaTeX - // baraboveleftarrow is mostly from from glyph U+2190 in font KaTeX Main - baraboveleftarrow: "M400000 620h-399890l3 -3c68.7 -52.7 113.7 -120 135 -202\nc4 -14.7 6 -23 6 -25c0 -7.3 -7 -11 -21 -11c-8 0 -13.2 0.8 -15.5 2.5\nc-2.3 1.7 -4.2 5.8 -5.5 12.5c-1.3 4.7 -2.7 10.3 -4 17c-12 48.7 -34.8 92 -68.5 130\ns-74.2 66.3 -121.5 85c-10 4 -16 7.7 -18 11c0 8.7 6 14.3 18 17c47.3 18.7 87.8 47\n121.5 85s56.5 81.3 68.5 130c0.7 2 1.3 5 2 9s1.2 6.7 1.5 8c0.3 1.3 1 3.3 2 6\ns2.2 4.5 3.5 5.5c1.3 1 3.3 1.8 6 2.5s6 1 10 1c14 0 21 -3.7 21 -11\nc0 -2 -2 -10.3 -6 -25c-20 -79.3 -65 -146.7 -135 -202l-3 -3h399890z\nM100 620v40h399900v-40z M0 241v40h399900v-40zM0 241v40h399900v-40z", - // rightarrowabovebar is mostly from glyph U+2192, KaTeX Main - rightarrowabovebar: "M0 241v40h399891c-47.3 35.3-84 78-110 128-16.7 32\n-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 11 8 0\n13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 39\n-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85-40.5\n-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5\n-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67\n151.7 139 205zm96 379h399894v40H0zm0 0h399904v40H0z", - // The short left harpoon has 0.5em (i.e. 500 units) kern on the left end. - // Ref from mhchem.sty: \rlap{\raisebox{-.22ex}{$\kern0.5em - baraboveshortleftharpoon: "M507,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11\nc1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17\nc2,0.7,5,1,9,1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21\nc-32,-87.3,-82.7,-157.7,-152,-211c0,0,-3,-3,-3,-3l399351,0l0,-40\nc-398570,0,-399437,0,-399437,0z M593 435 v40 H399500 v-40z\nM0 281 v-40 H399908 v40z M0 281 v-40 H399908 v40z", - rightharpoonaboveshortbar: "M0,241 l0,40c399126,0,399993,0,399993,0\nc4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,\n-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6\nc-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z\nM0 241 v40 H399908 v-40z M0 475 v-40 H399500 v40z M0 475 v-40 H399500 v40z", - shortbaraboveleftharpoon: "M7,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11\nc1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17c2,0.7,5,1,9,\n1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21c-32,-87.3,-82.7,-157.7,\n-152,-211c0,0,-3,-3,-3,-3l399907,0l0,-40c-399126,0,-399993,0,-399993,0z\nM93 435 v40 H400000 v-40z M500 241 v40 H400000 v-40z M500 241 v40 H400000 v-40z", - shortrightharpoonabovebar: "M53,241l0,40c398570,0,399437,0,399437,0\nc4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,\n-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6\nc-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z\nM500 241 v40 H399408 v-40z M500 435 v40 H400000 v-40z" -}; -// CONCATENATED MODULE: ./src/tree.js - - -/** - * This node represents a document fragment, which contains elements, but when - * placed into the DOM doesn't have any representation itself. It only contains - * children and doesn't have any DOM node properties. - */ -var tree_DocumentFragment = -/*#__PURE__*/ -function () { - // HtmlDomNode - // Never used; needed for satisfying interface. - function DocumentFragment(children) { - this.children = void 0; - this.classes = void 0; - this.height = void 0; - this.depth = void 0; - this.maxFontSize = void 0; - this.style = void 0; - this.children = children; - this.classes = []; - this.height = 0; - this.depth = 0; - this.maxFontSize = 0; - this.style = {}; - } - - var _proto = DocumentFragment.prototype; - - _proto.hasClass = function hasClass(className) { - return utils.contains(this.classes, className); - } - /** Convert the fragment into a node. */ - ; - - _proto.toNode = function toNode() { - var frag = document.createDocumentFragment(); - - for (var i = 0; i < this.children.length; i++) { - frag.appendChild(this.children[i].toNode()); - } - - return frag; - } - /** Convert the fragment into HTML markup. */ - ; - - _proto.toMarkup = function toMarkup() { - var markup = ""; // Simply concatenate the markup for the children together. - - for (var i = 0; i < this.children.length; i++) { - markup += this.children[i].toMarkup(); - } - - return markup; - } - /** - * Converts the math node into a string, similar to innerText. Applies to - * MathDomNode's only. - */ - ; - - _proto.toText = function toText() { - // To avoid this, we would subclass documentFragment separately for - // MathML, but polyfills for subclassing is expensive per PR 1469. - // $FlowFixMe: Only works for ChildType = MathDomNode. - var toText = function toText(child) { - return child.toText(); - }; - - return this.children.map(toText).join(""); - }; - - return DocumentFragment; -}(); -// CONCATENATED MODULE: ./src/domTree.js -/** - * These objects store the data about the DOM nodes we create, as well as some - * extra data. They can then be transformed into real DOM nodes with the - * `toNode` function or HTML markup using `toMarkup`. They are useful for both - * storing extra properties on the nodes, as well as providing a way to easily - * work with the DOM. - * - * Similar functions for working with MathML nodes exist in mathMLTree.js. - * - * TODO: refactor `span` and `anchor` into common superclass when - * target environments support class inheritance - */ - - - - - -/** - * Create an HTML className based on a list of classes. In addition to joining - * with spaces, we also remove empty classes. - */ -var createClass = function createClass(classes) { - return classes.filter(function (cls) { - return cls; - }).join(" "); -}; - -var initNode = function initNode(classes, options, style) { - this.classes = classes || []; - this.attributes = {}; - this.height = 0; - this.depth = 0; - this.maxFontSize = 0; - this.style = style || {}; - - if (options) { - if (options.style.isTight()) { - this.classes.push("mtight"); - } - - var color = options.getColor(); - - if (color) { - this.style.color = color; - } - } -}; -/** - * Convert into an HTML node - */ - - -var _toNode = function toNode(tagName) { - var node = document.createElement(tagName); // Apply the class - - node.className = createClass(this.classes); // Apply inline styles - - for (var style in this.style) { - if (this.style.hasOwnProperty(style)) { - // $FlowFixMe Flow doesn't seem to understand span.style's type. - node.style[style] = this.style[style]; - } - } // Apply attributes - - - for (var attr in this.attributes) { - if (this.attributes.hasOwnProperty(attr)) { - node.setAttribute(attr, this.attributes[attr]); - } - } // Append the children, also as HTML nodes - - - for (var i = 0; i < this.children.length; i++) { - node.appendChild(this.children[i].toNode()); - } - - return node; -}; -/** - * Convert into an HTML markup string - */ - - -var _toMarkup = function toMarkup(tagName) { - var markup = "<" + tagName; // Add the class - - if (this.classes.length) { - markup += " class=\"" + utils.escape(createClass(this.classes)) + "\""; - } - - var styles = ""; // Add the styles, after hyphenation - - for (var style in this.style) { - if (this.style.hasOwnProperty(style)) { - styles += utils.hyphenate(style) + ":" + this.style[style] + ";"; - } - } - - if (styles) { - markup += " style=\"" + utils.escape(styles) + "\""; - } // Add the attributes - - - for (var attr in this.attributes) { - if (this.attributes.hasOwnProperty(attr)) { - markup += " " + attr + "=\"" + utils.escape(this.attributes[attr]) + "\""; - } - } - - markup += ">"; // Add the markup of the children, also as markup - - for (var i = 0; i < this.children.length; i++) { - markup += this.children[i].toMarkup(); - } - - markup += ""; - return markup; -}; // Making the type below exact with all optional fields doesn't work due to -// - https://github.com/facebook/flow/issues/4582 -// - https://github.com/facebook/flow/issues/5688 -// However, since *all* fields are optional, $Shape<> works as suggested in 5688 -// above. -// This type does not include all CSS properties. Additional properties should -// be added as needed. - - -/** - * This node represents a span node, with a className, a list of children, and - * an inline style. It also contains information about its height, depth, and - * maxFontSize. - * - * Represents two types with different uses: SvgSpan to wrap an SVG and DomSpan - * otherwise. This typesafety is important when HTML builders access a span's - * children. - */ -var domTree_Span = -/*#__PURE__*/ -function () { - function Span(classes, children, options, style) { - this.children = void 0; - this.attributes = void 0; - this.classes = void 0; - this.height = void 0; - this.depth = void 0; - this.width = void 0; - this.maxFontSize = void 0; - this.style = void 0; - initNode.call(this, classes, options, style); - this.children = children || []; - } - /** - * Sets an arbitrary attribute on the span. Warning: use this wisely. Not - * all browsers support attributes the same, and having too many custom - * attributes is probably bad. - */ - - - var _proto = Span.prototype; - - _proto.setAttribute = function setAttribute(attribute, value) { - this.attributes[attribute] = value; - }; - - _proto.hasClass = function hasClass(className) { - return utils.contains(this.classes, className); - }; - - _proto.toNode = function toNode() { - return _toNode.call(this, "span"); - }; - - _proto.toMarkup = function toMarkup() { - return _toMarkup.call(this, "span"); - }; - - return Span; -}(); -/** - * This node represents an anchor () element with a hyperlink. See `span` - * for further details. - */ - -var domTree_Anchor = -/*#__PURE__*/ -function () { - function Anchor(href, classes, children, options) { - this.children = void 0; - this.attributes = void 0; - this.classes = void 0; - this.height = void 0; - this.depth = void 0; - this.maxFontSize = void 0; - this.style = void 0; - initNode.call(this, classes, options); - this.children = children || []; - this.setAttribute('href', href); - } - - var _proto2 = Anchor.prototype; - - _proto2.setAttribute = function setAttribute(attribute, value) { - this.attributes[attribute] = value; - }; - - _proto2.hasClass = function hasClass(className) { - return utils.contains(this.classes, className); - }; - - _proto2.toNode = function toNode() { - return _toNode.call(this, "a"); - }; - - _proto2.toMarkup = function toMarkup() { - return _toMarkup.call(this, "a"); - }; - - return Anchor; -}(); -/** - * This node represents an image embed () element. - */ - -var domTree_Img = -/*#__PURE__*/ -function () { - function Img(src, alt, style) { - this.src = void 0; - this.alt = void 0; - this.classes = void 0; - this.height = void 0; - this.depth = void 0; - this.maxFontSize = void 0; - this.style = void 0; - this.alt = alt; - this.src = src; - this.classes = ["mord"]; - this.style = style; - } - - var _proto3 = Img.prototype; - - _proto3.hasClass = function hasClass(className) { - return utils.contains(this.classes, className); - }; - - _proto3.toNode = function toNode() { - var node = document.createElement("img"); - node.src = this.src; - node.alt = this.alt; - node.className = "mord"; // Apply inline styles - - for (var style in this.style) { - if (this.style.hasOwnProperty(style)) { - // $FlowFixMe - node.style[style] = this.style[style]; - } - } - - return node; - }; - - _proto3.toMarkup = function toMarkup() { - var markup = "" + this.alt + " 0) { - span = document.createElement("span"); - span.style.marginRight = this.italic + "em"; - } - - if (this.classes.length > 0) { - span = span || document.createElement("span"); - span.className = createClass(this.classes); - } - - for (var style in this.style) { - if (this.style.hasOwnProperty(style)) { - span = span || document.createElement("span"); // $FlowFixMe Flow doesn't seem to understand span.style's type. - - span.style[style] = this.style[style]; - } - } - - if (span) { - span.appendChild(node); - return span; - } else { - return node; - } - } - /** - * Creates markup for a symbol node. - */ - ; - - _proto4.toMarkup = function toMarkup() { - // TODO(alpert): More duplication than I'd like from - // span.prototype.toMarkup and symbolNode.prototype.toNode... - var needsSpan = false; - var markup = " 0) { - styles += "margin-right:" + this.italic + "em;"; - } - - for (var style in this.style) { - if (this.style.hasOwnProperty(style)) { - styles += utils.hyphenate(style) + ":" + this.style[style] + ";"; - } - } - - if (styles) { - needsSpan = true; - markup += " style=\"" + utils.escape(styles) + "\""; - } - - var escaped = utils.escape(this.text); - - if (needsSpan) { - markup += ">"; - markup += escaped; - markup += ""; - return markup; - } else { - return escaped; - } - }; - - return SymbolNode; -}(); -/** - * SVG nodes are used to render stretchy wide elements. - */ - -var SvgNode = -/*#__PURE__*/ -function () { - function SvgNode(children, attributes) { - this.children = void 0; - this.attributes = void 0; - this.children = children || []; - this.attributes = attributes || {}; - } - - var _proto5 = SvgNode.prototype; - - _proto5.toNode = function toNode() { - var svgNS = "http://www.w3.org/2000/svg"; - var node = document.createElementNS(svgNS, "svg"); // Apply attributes - - for (var attr in this.attributes) { - if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { - node.setAttribute(attr, this.attributes[attr]); - } - } - - for (var i = 0; i < this.children.length; i++) { - node.appendChild(this.children[i].toNode()); - } - - return node; - }; - - _proto5.toMarkup = function toMarkup() { - var markup = ""; - } else { - return ""; - } - }; - - return PathNode; -}(); -var LineNode = -/*#__PURE__*/ -function () { - function LineNode(attributes) { - this.attributes = void 0; - this.attributes = attributes || {}; - } - - var _proto7 = LineNode.prototype; - - _proto7.toNode = function toNode() { - var svgNS = "http://www.w3.org/2000/svg"; - var node = document.createElementNS(svgNS, "line"); // Apply attributes - - for (var attr in this.attributes) { - if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { - node.setAttribute(attr, this.attributes[attr]); - } - } - - return node; - }; - - _proto7.toMarkup = function toMarkup() { - var markup = " but got " + String(group) + "."); - } -} -// CONCATENATED MODULE: ./submodules/katex-fonts/fontMetricsData.js -// This file is GENERATED by buildMetrics.sh. DO NOT MODIFY. -/* harmony default export */ var fontMetricsData = ({ - "AMS-Regular": { - "32": [0, 0, 0, 0, 0.25], - "65": [0, 0.68889, 0, 0, 0.72222], - "66": [0, 0.68889, 0, 0, 0.66667], - "67": [0, 0.68889, 0, 0, 0.72222], - "68": [0, 0.68889, 0, 0, 0.72222], - "69": [0, 0.68889, 0, 0, 0.66667], - "70": [0, 0.68889, 0, 0, 0.61111], - "71": [0, 0.68889, 0, 0, 0.77778], - "72": [0, 0.68889, 0, 0, 0.77778], - "73": [0, 0.68889, 0, 0, 0.38889], - "74": [0.16667, 0.68889, 0, 0, 0.5], - "75": [0, 0.68889, 0, 0, 0.77778], - "76": [0, 0.68889, 0, 0, 0.66667], - "77": [0, 0.68889, 0, 0, 0.94445], - "78": [0, 0.68889, 0, 0, 0.72222], - "79": [0.16667, 0.68889, 0, 0, 0.77778], - "80": [0, 0.68889, 0, 0, 0.61111], - "81": [0.16667, 0.68889, 0, 0, 0.77778], - "82": [0, 0.68889, 0, 0, 0.72222], - "83": [0, 0.68889, 0, 0, 0.55556], - "84": [0, 0.68889, 0, 0, 0.66667], - "85": [0, 0.68889, 0, 0, 0.72222], - "86": [0, 0.68889, 0, 0, 0.72222], - "87": [0, 0.68889, 0, 0, 1.0], - "88": [0, 0.68889, 0, 0, 0.72222], - "89": [0, 0.68889, 0, 0, 0.72222], - "90": [0, 0.68889, 0, 0, 0.66667], - "107": [0, 0.68889, 0, 0, 0.55556], - "160": [0, 0, 0, 0, 0.25], - "165": [0, 0.675, 0.025, 0, 0.75], - "174": [0.15559, 0.69224, 0, 0, 0.94666], - "240": [0, 0.68889, 0, 0, 0.55556], - "295": [0, 0.68889, 0, 0, 0.54028], - "710": [0, 0.825, 0, 0, 2.33334], - "732": [0, 0.9, 0, 0, 2.33334], - "770": [0, 0.825, 0, 0, 2.33334], - "771": [0, 0.9, 0, 0, 2.33334], - "989": [0.08167, 0.58167, 0, 0, 0.77778], - "1008": [0, 0.43056, 0.04028, 0, 0.66667], - "8245": [0, 0.54986, 0, 0, 0.275], - "8463": [0, 0.68889, 0, 0, 0.54028], - "8487": [0, 0.68889, 0, 0, 0.72222], - "8498": [0, 0.68889, 0, 0, 0.55556], - "8502": [0, 0.68889, 0, 0, 0.66667], - "8503": [0, 0.68889, 0, 0, 0.44445], - "8504": [0, 0.68889, 0, 0, 0.66667], - "8513": [0, 0.68889, 0, 0, 0.63889], - "8592": [-0.03598, 0.46402, 0, 0, 0.5], - "8594": [-0.03598, 0.46402, 0, 0, 0.5], - "8602": [-0.13313, 0.36687, 0, 0, 1.0], - "8603": [-0.13313, 0.36687, 0, 0, 1.0], - "8606": [0.01354, 0.52239, 0, 0, 1.0], - "8608": [0.01354, 0.52239, 0, 0, 1.0], - "8610": [0.01354, 0.52239, 0, 0, 1.11111], - "8611": [0.01354, 0.52239, 0, 0, 1.11111], - "8619": [0, 0.54986, 0, 0, 1.0], - "8620": [0, 0.54986, 0, 0, 1.0], - "8621": [-0.13313, 0.37788, 0, 0, 1.38889], - "8622": [-0.13313, 0.36687, 0, 0, 1.0], - "8624": [0, 0.69224, 0, 0, 0.5], - "8625": [0, 0.69224, 0, 0, 0.5], - "8630": [0, 0.43056, 0, 0, 1.0], - "8631": [0, 0.43056, 0, 0, 1.0], - "8634": [0.08198, 0.58198, 0, 0, 0.77778], - "8635": [0.08198, 0.58198, 0, 0, 0.77778], - "8638": [0.19444, 0.69224, 0, 0, 0.41667], - "8639": [0.19444, 0.69224, 0, 0, 0.41667], - "8642": [0.19444, 0.69224, 0, 0, 0.41667], - "8643": [0.19444, 0.69224, 0, 0, 0.41667], - "8644": [0.1808, 0.675, 0, 0, 1.0], - "8646": [0.1808, 0.675, 0, 0, 1.0], - "8647": [0.1808, 0.675, 0, 0, 1.0], - "8648": [0.19444, 0.69224, 0, 0, 0.83334], - "8649": [0.1808, 0.675, 0, 0, 1.0], - "8650": [0.19444, 0.69224, 0, 0, 0.83334], - "8651": [0.01354, 0.52239, 0, 0, 1.0], - "8652": [0.01354, 0.52239, 0, 0, 1.0], - "8653": [-0.13313, 0.36687, 0, 0, 1.0], - "8654": [-0.13313, 0.36687, 0, 0, 1.0], - "8655": [-0.13313, 0.36687, 0, 0, 1.0], - "8666": [0.13667, 0.63667, 0, 0, 1.0], - "8667": [0.13667, 0.63667, 0, 0, 1.0], - "8669": [-0.13313, 0.37788, 0, 0, 1.0], - "8672": [-0.064, 0.437, 0, 0, 1.334], - "8674": [-0.064, 0.437, 0, 0, 1.334], - "8705": [0, 0.825, 0, 0, 0.5], - "8708": [0, 0.68889, 0, 0, 0.55556], - "8709": [0.08167, 0.58167, 0, 0, 0.77778], - "8717": [0, 0.43056, 0, 0, 0.42917], - "8722": [-0.03598, 0.46402, 0, 0, 0.5], - "8724": [0.08198, 0.69224, 0, 0, 0.77778], - "8726": [0.08167, 0.58167, 0, 0, 0.77778], - "8733": [0, 0.69224, 0, 0, 0.77778], - "8736": [0, 0.69224, 0, 0, 0.72222], - "8737": [0, 0.69224, 0, 0, 0.72222], - "8738": [0.03517, 0.52239, 0, 0, 0.72222], - "8739": [0.08167, 0.58167, 0, 0, 0.22222], - "8740": [0.25142, 0.74111, 0, 0, 0.27778], - "8741": [0.08167, 0.58167, 0, 0, 0.38889], - "8742": [0.25142, 0.74111, 0, 0, 0.5], - "8756": [0, 0.69224, 0, 0, 0.66667], - "8757": [0, 0.69224, 0, 0, 0.66667], - "8764": [-0.13313, 0.36687, 0, 0, 0.77778], - "8765": [-0.13313, 0.37788, 0, 0, 0.77778], - "8769": [-0.13313, 0.36687, 0, 0, 0.77778], - "8770": [-0.03625, 0.46375, 0, 0, 0.77778], - "8774": [0.30274, 0.79383, 0, 0, 0.77778], - "8776": [-0.01688, 0.48312, 0, 0, 0.77778], - "8778": [0.08167, 0.58167, 0, 0, 0.77778], - "8782": [0.06062, 0.54986, 0, 0, 0.77778], - "8783": [0.06062, 0.54986, 0, 0, 0.77778], - "8785": [0.08198, 0.58198, 0, 0, 0.77778], - "8786": [0.08198, 0.58198, 0, 0, 0.77778], - "8787": [0.08198, 0.58198, 0, 0, 0.77778], - "8790": [0, 0.69224, 0, 0, 0.77778], - "8791": [0.22958, 0.72958, 0, 0, 0.77778], - "8796": [0.08198, 0.91667, 0, 0, 0.77778], - "8806": [0.25583, 0.75583, 0, 0, 0.77778], - "8807": [0.25583, 0.75583, 0, 0, 0.77778], - "8808": [0.25142, 0.75726, 0, 0, 0.77778], - "8809": [0.25142, 0.75726, 0, 0, 0.77778], - "8812": [0.25583, 0.75583, 0, 0, 0.5], - "8814": [0.20576, 0.70576, 0, 0, 0.77778], - "8815": [0.20576, 0.70576, 0, 0, 0.77778], - "8816": [0.30274, 0.79383, 0, 0, 0.77778], - "8817": [0.30274, 0.79383, 0, 0, 0.77778], - "8818": [0.22958, 0.72958, 0, 0, 0.77778], - "8819": [0.22958, 0.72958, 0, 0, 0.77778], - "8822": [0.1808, 0.675, 0, 0, 0.77778], - "8823": [0.1808, 0.675, 0, 0, 0.77778], - "8828": [0.13667, 0.63667, 0, 0, 0.77778], - "8829": [0.13667, 0.63667, 0, 0, 0.77778], - "8830": [0.22958, 0.72958, 0, 0, 0.77778], - "8831": [0.22958, 0.72958, 0, 0, 0.77778], - "8832": [0.20576, 0.70576, 0, 0, 0.77778], - "8833": [0.20576, 0.70576, 0, 0, 0.77778], - "8840": [0.30274, 0.79383, 0, 0, 0.77778], - "8841": [0.30274, 0.79383, 0, 0, 0.77778], - "8842": [0.13597, 0.63597, 0, 0, 0.77778], - "8843": [0.13597, 0.63597, 0, 0, 0.77778], - "8847": [0.03517, 0.54986, 0, 0, 0.77778], - "8848": [0.03517, 0.54986, 0, 0, 0.77778], - "8858": [0.08198, 0.58198, 0, 0, 0.77778], - "8859": [0.08198, 0.58198, 0, 0, 0.77778], - "8861": [0.08198, 0.58198, 0, 0, 0.77778], - "8862": [0, 0.675, 0, 0, 0.77778], - "8863": [0, 0.675, 0, 0, 0.77778], - "8864": [0, 0.675, 0, 0, 0.77778], - "8865": [0, 0.675, 0, 0, 0.77778], - "8872": [0, 0.69224, 0, 0, 0.61111], - "8873": [0, 0.69224, 0, 0, 0.72222], - "8874": [0, 0.69224, 0, 0, 0.88889], - "8876": [0, 0.68889, 0, 0, 0.61111], - "8877": [0, 0.68889, 0, 0, 0.61111], - "8878": [0, 0.68889, 0, 0, 0.72222], - "8879": [0, 0.68889, 0, 0, 0.72222], - "8882": [0.03517, 0.54986, 0, 0, 0.77778], - "8883": [0.03517, 0.54986, 0, 0, 0.77778], - "8884": [0.13667, 0.63667, 0, 0, 0.77778], - "8885": [0.13667, 0.63667, 0, 0, 0.77778], - "8888": [0, 0.54986, 0, 0, 1.11111], - "8890": [0.19444, 0.43056, 0, 0, 0.55556], - "8891": [0.19444, 0.69224, 0, 0, 0.61111], - "8892": [0.19444, 0.69224, 0, 0, 0.61111], - "8901": [0, 0.54986, 0, 0, 0.27778], - "8903": [0.08167, 0.58167, 0, 0, 0.77778], - "8905": [0.08167, 0.58167, 0, 0, 0.77778], - "8906": [0.08167, 0.58167, 0, 0, 0.77778], - "8907": [0, 0.69224, 0, 0, 0.77778], - "8908": [0, 0.69224, 0, 0, 0.77778], - "8909": [-0.03598, 0.46402, 0, 0, 0.77778], - "8910": [0, 0.54986, 0, 0, 0.76042], - "8911": [0, 0.54986, 0, 0, 0.76042], - "8912": [0.03517, 0.54986, 0, 0, 0.77778], - "8913": [0.03517, 0.54986, 0, 0, 0.77778], - "8914": [0, 0.54986, 0, 0, 0.66667], - "8915": [0, 0.54986, 0, 0, 0.66667], - "8916": [0, 0.69224, 0, 0, 0.66667], - "8918": [0.0391, 0.5391, 0, 0, 0.77778], - "8919": [0.0391, 0.5391, 0, 0, 0.77778], - "8920": [0.03517, 0.54986, 0, 0, 1.33334], - "8921": [0.03517, 0.54986, 0, 0, 1.33334], - "8922": [0.38569, 0.88569, 0, 0, 0.77778], - "8923": [0.38569, 0.88569, 0, 0, 0.77778], - "8926": [0.13667, 0.63667, 0, 0, 0.77778], - "8927": [0.13667, 0.63667, 0, 0, 0.77778], - "8928": [0.30274, 0.79383, 0, 0, 0.77778], - "8929": [0.30274, 0.79383, 0, 0, 0.77778], - "8934": [0.23222, 0.74111, 0, 0, 0.77778], - "8935": [0.23222, 0.74111, 0, 0, 0.77778], - "8936": [0.23222, 0.74111, 0, 0, 0.77778], - "8937": [0.23222, 0.74111, 0, 0, 0.77778], - "8938": [0.20576, 0.70576, 0, 0, 0.77778], - "8939": [0.20576, 0.70576, 0, 0, 0.77778], - "8940": [0.30274, 0.79383, 0, 0, 0.77778], - "8941": [0.30274, 0.79383, 0, 0, 0.77778], - "8994": [0.19444, 0.69224, 0, 0, 0.77778], - "8995": [0.19444, 0.69224, 0, 0, 0.77778], - "9416": [0.15559, 0.69224, 0, 0, 0.90222], - "9484": [0, 0.69224, 0, 0, 0.5], - "9488": [0, 0.69224, 0, 0, 0.5], - "9492": [0, 0.37788, 0, 0, 0.5], - "9496": [0, 0.37788, 0, 0, 0.5], - "9585": [0.19444, 0.68889, 0, 0, 0.88889], - "9586": [0.19444, 0.74111, 0, 0, 0.88889], - "9632": [0, 0.675, 0, 0, 0.77778], - "9633": [0, 0.675, 0, 0, 0.77778], - "9650": [0, 0.54986, 0, 0, 0.72222], - "9651": [0, 0.54986, 0, 0, 0.72222], - "9654": [0.03517, 0.54986, 0, 0, 0.77778], - "9660": [0, 0.54986, 0, 0, 0.72222], - "9661": [0, 0.54986, 0, 0, 0.72222], - "9664": [0.03517, 0.54986, 0, 0, 0.77778], - "9674": [0.11111, 0.69224, 0, 0, 0.66667], - "9733": [0.19444, 0.69224, 0, 0, 0.94445], - "10003": [0, 0.69224, 0, 0, 0.83334], - "10016": [0, 0.69224, 0, 0, 0.83334], - "10731": [0.11111, 0.69224, 0, 0, 0.66667], - "10846": [0.19444, 0.75583, 0, 0, 0.61111], - "10877": [0.13667, 0.63667, 0, 0, 0.77778], - "10878": [0.13667, 0.63667, 0, 0, 0.77778], - "10885": [0.25583, 0.75583, 0, 0, 0.77778], - "10886": [0.25583, 0.75583, 0, 0, 0.77778], - "10887": [0.13597, 0.63597, 0, 0, 0.77778], - "10888": [0.13597, 0.63597, 0, 0, 0.77778], - "10889": [0.26167, 0.75726, 0, 0, 0.77778], - "10890": [0.26167, 0.75726, 0, 0, 0.77778], - "10891": [0.48256, 0.98256, 0, 0, 0.77778], - "10892": [0.48256, 0.98256, 0, 0, 0.77778], - "10901": [0.13667, 0.63667, 0, 0, 0.77778], - "10902": [0.13667, 0.63667, 0, 0, 0.77778], - "10933": [0.25142, 0.75726, 0, 0, 0.77778], - "10934": [0.25142, 0.75726, 0, 0, 0.77778], - "10935": [0.26167, 0.75726, 0, 0, 0.77778], - "10936": [0.26167, 0.75726, 0, 0, 0.77778], - "10937": [0.26167, 0.75726, 0, 0, 0.77778], - "10938": [0.26167, 0.75726, 0, 0, 0.77778], - "10949": [0.25583, 0.75583, 0, 0, 0.77778], - "10950": [0.25583, 0.75583, 0, 0, 0.77778], - "10955": [0.28481, 0.79383, 0, 0, 0.77778], - "10956": [0.28481, 0.79383, 0, 0, 0.77778], - "57350": [0.08167, 0.58167, 0, 0, 0.22222], - "57351": [0.08167, 0.58167, 0, 0, 0.38889], - "57352": [0.08167, 0.58167, 0, 0, 0.77778], - "57353": [0, 0.43056, 0.04028, 0, 0.66667], - "57356": [0.25142, 0.75726, 0, 0, 0.77778], - "57357": [0.25142, 0.75726, 0, 0, 0.77778], - "57358": [0.41951, 0.91951, 0, 0, 0.77778], - "57359": [0.30274, 0.79383, 0, 0, 0.77778], - "57360": [0.30274, 0.79383, 0, 0, 0.77778], - "57361": [0.41951, 0.91951, 0, 0, 0.77778], - "57366": [0.25142, 0.75726, 0, 0, 0.77778], - "57367": [0.25142, 0.75726, 0, 0, 0.77778], - "57368": [0.25142, 0.75726, 0, 0, 0.77778], - "57369": [0.25142, 0.75726, 0, 0, 0.77778], - "57370": [0.13597, 0.63597, 0, 0, 0.77778], - "57371": [0.13597, 0.63597, 0, 0, 0.77778] - }, - "Caligraphic-Regular": { - "32": [0, 0, 0, 0, 0.25], - "65": [0, 0.68333, 0, 0.19445, 0.79847], - "66": [0, 0.68333, 0.03041, 0.13889, 0.65681], - "67": [0, 0.68333, 0.05834, 0.13889, 0.52653], - "68": [0, 0.68333, 0.02778, 0.08334, 0.77139], - "69": [0, 0.68333, 0.08944, 0.11111, 0.52778], - "70": [0, 0.68333, 0.09931, 0.11111, 0.71875], - "71": [0.09722, 0.68333, 0.0593, 0.11111, 0.59487], - "72": [0, 0.68333, 0.00965, 0.11111, 0.84452], - "73": [0, 0.68333, 0.07382, 0, 0.54452], - "74": [0.09722, 0.68333, 0.18472, 0.16667, 0.67778], - "75": [0, 0.68333, 0.01445, 0.05556, 0.76195], - "76": [0, 0.68333, 0, 0.13889, 0.68972], - "77": [0, 0.68333, 0, 0.13889, 1.2009], - "78": [0, 0.68333, 0.14736, 0.08334, 0.82049], - "79": [0, 0.68333, 0.02778, 0.11111, 0.79611], - "80": [0, 0.68333, 0.08222, 0.08334, 0.69556], - "81": [0.09722, 0.68333, 0, 0.11111, 0.81667], - "82": [0, 0.68333, 0, 0.08334, 0.8475], - "83": [0, 0.68333, 0.075, 0.13889, 0.60556], - "84": [0, 0.68333, 0.25417, 0, 0.54464], - "85": [0, 0.68333, 0.09931, 0.08334, 0.62583], - "86": [0, 0.68333, 0.08222, 0, 0.61278], - "87": [0, 0.68333, 0.08222, 0.08334, 0.98778], - "88": [0, 0.68333, 0.14643, 0.13889, 0.7133], - "89": [0.09722, 0.68333, 0.08222, 0.08334, 0.66834], - "90": [0, 0.68333, 0.07944, 0.13889, 0.72473], - "160": [0, 0, 0, 0, 0.25] - }, - "Fraktur-Regular": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69141, 0, 0, 0.29574], - "34": [0, 0.69141, 0, 0, 0.21471], - "38": [0, 0.69141, 0, 0, 0.73786], - "39": [0, 0.69141, 0, 0, 0.21201], - "40": [0.24982, 0.74947, 0, 0, 0.38865], - "41": [0.24982, 0.74947, 0, 0, 0.38865], - "42": [0, 0.62119, 0, 0, 0.27764], - "43": [0.08319, 0.58283, 0, 0, 0.75623], - "44": [0, 0.10803, 0, 0, 0.27764], - "45": [0.08319, 0.58283, 0, 0, 0.75623], - "46": [0, 0.10803, 0, 0, 0.27764], - "47": [0.24982, 0.74947, 0, 0, 0.50181], - "48": [0, 0.47534, 0, 0, 0.50181], - "49": [0, 0.47534, 0, 0, 0.50181], - "50": [0, 0.47534, 0, 0, 0.50181], - "51": [0.18906, 0.47534, 0, 0, 0.50181], - "52": [0.18906, 0.47534, 0, 0, 0.50181], - "53": [0.18906, 0.47534, 0, 0, 0.50181], - "54": [0, 0.69141, 0, 0, 0.50181], - "55": [0.18906, 0.47534, 0, 0, 0.50181], - "56": [0, 0.69141, 0, 0, 0.50181], - "57": [0.18906, 0.47534, 0, 0, 0.50181], - "58": [0, 0.47534, 0, 0, 0.21606], - "59": [0.12604, 0.47534, 0, 0, 0.21606], - "61": [-0.13099, 0.36866, 0, 0, 0.75623], - "63": [0, 0.69141, 0, 0, 0.36245], - "65": [0, 0.69141, 0, 0, 0.7176], - "66": [0, 0.69141, 0, 0, 0.88397], - "67": [0, 0.69141, 0, 0, 0.61254], - "68": [0, 0.69141, 0, 0, 0.83158], - "69": [0, 0.69141, 0, 0, 0.66278], - "70": [0.12604, 0.69141, 0, 0, 0.61119], - "71": [0, 0.69141, 0, 0, 0.78539], - "72": [0.06302, 0.69141, 0, 0, 0.7203], - "73": [0, 0.69141, 0, 0, 0.55448], - "74": [0.12604, 0.69141, 0, 0, 0.55231], - "75": [0, 0.69141, 0, 0, 0.66845], - "76": [0, 0.69141, 0, 0, 0.66602], - "77": [0, 0.69141, 0, 0, 1.04953], - "78": [0, 0.69141, 0, 0, 0.83212], - "79": [0, 0.69141, 0, 0, 0.82699], - "80": [0.18906, 0.69141, 0, 0, 0.82753], - "81": [0.03781, 0.69141, 0, 0, 0.82699], - "82": [0, 0.69141, 0, 0, 0.82807], - "83": [0, 0.69141, 0, 0, 0.82861], - "84": [0, 0.69141, 0, 0, 0.66899], - "85": [0, 0.69141, 0, 0, 0.64576], - "86": [0, 0.69141, 0, 0, 0.83131], - "87": [0, 0.69141, 0, 0, 1.04602], - "88": [0, 0.69141, 0, 0, 0.71922], - "89": [0.18906, 0.69141, 0, 0, 0.83293], - "90": [0.12604, 0.69141, 0, 0, 0.60201], - "91": [0.24982, 0.74947, 0, 0, 0.27764], - "93": [0.24982, 0.74947, 0, 0, 0.27764], - "94": [0, 0.69141, 0, 0, 0.49965], - "97": [0, 0.47534, 0, 0, 0.50046], - "98": [0, 0.69141, 0, 0, 0.51315], - "99": [0, 0.47534, 0, 0, 0.38946], - "100": [0, 0.62119, 0, 0, 0.49857], - "101": [0, 0.47534, 0, 0, 0.40053], - "102": [0.18906, 0.69141, 0, 0, 0.32626], - "103": [0.18906, 0.47534, 0, 0, 0.5037], - "104": [0.18906, 0.69141, 0, 0, 0.52126], - "105": [0, 0.69141, 0, 0, 0.27899], - "106": [0, 0.69141, 0, 0, 0.28088], - "107": [0, 0.69141, 0, 0, 0.38946], - "108": [0, 0.69141, 0, 0, 0.27953], - "109": [0, 0.47534, 0, 0, 0.76676], - "110": [0, 0.47534, 0, 0, 0.52666], - "111": [0, 0.47534, 0, 0, 0.48885], - "112": [0.18906, 0.52396, 0, 0, 0.50046], - "113": [0.18906, 0.47534, 0, 0, 0.48912], - "114": [0, 0.47534, 0, 0, 0.38919], - "115": [0, 0.47534, 0, 0, 0.44266], - "116": [0, 0.62119, 0, 0, 0.33301], - "117": [0, 0.47534, 0, 0, 0.5172], - "118": [0, 0.52396, 0, 0, 0.5118], - "119": [0, 0.52396, 0, 0, 0.77351], - "120": [0.18906, 0.47534, 0, 0, 0.38865], - "121": [0.18906, 0.47534, 0, 0, 0.49884], - "122": [0.18906, 0.47534, 0, 0, 0.39054], - "160": [0, 0, 0, 0, 0.25], - "8216": [0, 0.69141, 0, 0, 0.21471], - "8217": [0, 0.69141, 0, 0, 0.21471], - "58112": [0, 0.62119, 0, 0, 0.49749], - "58113": [0, 0.62119, 0, 0, 0.4983], - "58114": [0.18906, 0.69141, 0, 0, 0.33328], - "58115": [0.18906, 0.69141, 0, 0, 0.32923], - "58116": [0.18906, 0.47534, 0, 0, 0.50343], - "58117": [0, 0.69141, 0, 0, 0.33301], - "58118": [0, 0.62119, 0, 0, 0.33409], - "58119": [0, 0.47534, 0, 0, 0.50073] - }, - "Main-Bold": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0, 0, 0.35], - "34": [0, 0.69444, 0, 0, 0.60278], - "35": [0.19444, 0.69444, 0, 0, 0.95833], - "36": [0.05556, 0.75, 0, 0, 0.575], - "37": [0.05556, 0.75, 0, 0, 0.95833], - "38": [0, 0.69444, 0, 0, 0.89444], - "39": [0, 0.69444, 0, 0, 0.31944], - "40": [0.25, 0.75, 0, 0, 0.44722], - "41": [0.25, 0.75, 0, 0, 0.44722], - "42": [0, 0.75, 0, 0, 0.575], - "43": [0.13333, 0.63333, 0, 0, 0.89444], - "44": [0.19444, 0.15556, 0, 0, 0.31944], - "45": [0, 0.44444, 0, 0, 0.38333], - "46": [0, 0.15556, 0, 0, 0.31944], - "47": [0.25, 0.75, 0, 0, 0.575], - "48": [0, 0.64444, 0, 0, 0.575], - "49": [0, 0.64444, 0, 0, 0.575], - "50": [0, 0.64444, 0, 0, 0.575], - "51": [0, 0.64444, 0, 0, 0.575], - "52": [0, 0.64444, 0, 0, 0.575], - "53": [0, 0.64444, 0, 0, 0.575], - "54": [0, 0.64444, 0, 0, 0.575], - "55": [0, 0.64444, 0, 0, 0.575], - "56": [0, 0.64444, 0, 0, 0.575], - "57": [0, 0.64444, 0, 0, 0.575], - "58": [0, 0.44444, 0, 0, 0.31944], - "59": [0.19444, 0.44444, 0, 0, 0.31944], - "60": [0.08556, 0.58556, 0, 0, 0.89444], - "61": [-0.10889, 0.39111, 0, 0, 0.89444], - "62": [0.08556, 0.58556, 0, 0, 0.89444], - "63": [0, 0.69444, 0, 0, 0.54305], - "64": [0, 0.69444, 0, 0, 0.89444], - "65": [0, 0.68611, 0, 0, 0.86944], - "66": [0, 0.68611, 0, 0, 0.81805], - "67": [0, 0.68611, 0, 0, 0.83055], - "68": [0, 0.68611, 0, 0, 0.88194], - "69": [0, 0.68611, 0, 0, 0.75555], - "70": [0, 0.68611, 0, 0, 0.72361], - "71": [0, 0.68611, 0, 0, 0.90416], - "72": [0, 0.68611, 0, 0, 0.9], - "73": [0, 0.68611, 0, 0, 0.43611], - "74": [0, 0.68611, 0, 0, 0.59444], - "75": [0, 0.68611, 0, 0, 0.90138], - "76": [0, 0.68611, 0, 0, 0.69166], - "77": [0, 0.68611, 0, 0, 1.09166], - "78": [0, 0.68611, 0, 0, 0.9], - "79": [0, 0.68611, 0, 0, 0.86388], - "80": [0, 0.68611, 0, 0, 0.78611], - "81": [0.19444, 0.68611, 0, 0, 0.86388], - "82": [0, 0.68611, 0, 0, 0.8625], - "83": [0, 0.68611, 0, 0, 0.63889], - "84": [0, 0.68611, 0, 0, 0.8], - "85": [0, 0.68611, 0, 0, 0.88472], - "86": [0, 0.68611, 0.01597, 0, 0.86944], - "87": [0, 0.68611, 0.01597, 0, 1.18888], - "88": [0, 0.68611, 0, 0, 0.86944], - "89": [0, 0.68611, 0.02875, 0, 0.86944], - "90": [0, 0.68611, 0, 0, 0.70277], - "91": [0.25, 0.75, 0, 0, 0.31944], - "92": [0.25, 0.75, 0, 0, 0.575], - "93": [0.25, 0.75, 0, 0, 0.31944], - "94": [0, 0.69444, 0, 0, 0.575], - "95": [0.31, 0.13444, 0.03194, 0, 0.575], - "97": [0, 0.44444, 0, 0, 0.55902], - "98": [0, 0.69444, 0, 0, 0.63889], - "99": [0, 0.44444, 0, 0, 0.51111], - "100": [0, 0.69444, 0, 0, 0.63889], - "101": [0, 0.44444, 0, 0, 0.52708], - "102": [0, 0.69444, 0.10903, 0, 0.35139], - "103": [0.19444, 0.44444, 0.01597, 0, 0.575], - "104": [0, 0.69444, 0, 0, 0.63889], - "105": [0, 0.69444, 0, 0, 0.31944], - "106": [0.19444, 0.69444, 0, 0, 0.35139], - "107": [0, 0.69444, 0, 0, 0.60694], - "108": [0, 0.69444, 0, 0, 0.31944], - "109": [0, 0.44444, 0, 0, 0.95833], - "110": [0, 0.44444, 0, 0, 0.63889], - "111": [0, 0.44444, 0, 0, 0.575], - "112": [0.19444, 0.44444, 0, 0, 0.63889], - "113": [0.19444, 0.44444, 0, 0, 0.60694], - "114": [0, 0.44444, 0, 0, 0.47361], - "115": [0, 0.44444, 0, 0, 0.45361], - "116": [0, 0.63492, 0, 0, 0.44722], - "117": [0, 0.44444, 0, 0, 0.63889], - "118": [0, 0.44444, 0.01597, 0, 0.60694], - "119": [0, 0.44444, 0.01597, 0, 0.83055], - "120": [0, 0.44444, 0, 0, 0.60694], - "121": [0.19444, 0.44444, 0.01597, 0, 0.60694], - "122": [0, 0.44444, 0, 0, 0.51111], - "123": [0.25, 0.75, 0, 0, 0.575], - "124": [0.25, 0.75, 0, 0, 0.31944], - "125": [0.25, 0.75, 0, 0, 0.575], - "126": [0.35, 0.34444, 0, 0, 0.575], - "160": [0, 0, 0, 0, 0.25], - "163": [0, 0.69444, 0, 0, 0.86853], - "168": [0, 0.69444, 0, 0, 0.575], - "172": [0, 0.44444, 0, 0, 0.76666], - "176": [0, 0.69444, 0, 0, 0.86944], - "177": [0.13333, 0.63333, 0, 0, 0.89444], - "184": [0.17014, 0, 0, 0, 0.51111], - "198": [0, 0.68611, 0, 0, 1.04166], - "215": [0.13333, 0.63333, 0, 0, 0.89444], - "216": [0.04861, 0.73472, 0, 0, 0.89444], - "223": [0, 0.69444, 0, 0, 0.59722], - "230": [0, 0.44444, 0, 0, 0.83055], - "247": [0.13333, 0.63333, 0, 0, 0.89444], - "248": [0.09722, 0.54167, 0, 0, 0.575], - "305": [0, 0.44444, 0, 0, 0.31944], - "338": [0, 0.68611, 0, 0, 1.16944], - "339": [0, 0.44444, 0, 0, 0.89444], - "567": [0.19444, 0.44444, 0, 0, 0.35139], - "710": [0, 0.69444, 0, 0, 0.575], - "711": [0, 0.63194, 0, 0, 0.575], - "713": [0, 0.59611, 0, 0, 0.575], - "714": [0, 0.69444, 0, 0, 0.575], - "715": [0, 0.69444, 0, 0, 0.575], - "728": [0, 0.69444, 0, 0, 0.575], - "729": [0, 0.69444, 0, 0, 0.31944], - "730": [0, 0.69444, 0, 0, 0.86944], - "732": [0, 0.69444, 0, 0, 0.575], - "733": [0, 0.69444, 0, 0, 0.575], - "915": [0, 0.68611, 0, 0, 0.69166], - "916": [0, 0.68611, 0, 0, 0.95833], - "920": [0, 0.68611, 0, 0, 0.89444], - "923": [0, 0.68611, 0, 0, 0.80555], - "926": [0, 0.68611, 0, 0, 0.76666], - "928": [0, 0.68611, 0, 0, 0.9], - "931": [0, 0.68611, 0, 0, 0.83055], - "933": [0, 0.68611, 0, 0, 0.89444], - "934": [0, 0.68611, 0, 0, 0.83055], - "936": [0, 0.68611, 0, 0, 0.89444], - "937": [0, 0.68611, 0, 0, 0.83055], - "8211": [0, 0.44444, 0.03194, 0, 0.575], - "8212": [0, 0.44444, 0.03194, 0, 1.14999], - "8216": [0, 0.69444, 0, 0, 0.31944], - "8217": [0, 0.69444, 0, 0, 0.31944], - "8220": [0, 0.69444, 0, 0, 0.60278], - "8221": [0, 0.69444, 0, 0, 0.60278], - "8224": [0.19444, 0.69444, 0, 0, 0.51111], - "8225": [0.19444, 0.69444, 0, 0, 0.51111], - "8242": [0, 0.55556, 0, 0, 0.34444], - "8407": [0, 0.72444, 0.15486, 0, 0.575], - "8463": [0, 0.69444, 0, 0, 0.66759], - "8465": [0, 0.69444, 0, 0, 0.83055], - "8467": [0, 0.69444, 0, 0, 0.47361], - "8472": [0.19444, 0.44444, 0, 0, 0.74027], - "8476": [0, 0.69444, 0, 0, 0.83055], - "8501": [0, 0.69444, 0, 0, 0.70277], - "8592": [-0.10889, 0.39111, 0, 0, 1.14999], - "8593": [0.19444, 0.69444, 0, 0, 0.575], - "8594": [-0.10889, 0.39111, 0, 0, 1.14999], - "8595": [0.19444, 0.69444, 0, 0, 0.575], - "8596": [-0.10889, 0.39111, 0, 0, 1.14999], - "8597": [0.25, 0.75, 0, 0, 0.575], - "8598": [0.19444, 0.69444, 0, 0, 1.14999], - "8599": [0.19444, 0.69444, 0, 0, 1.14999], - "8600": [0.19444, 0.69444, 0, 0, 1.14999], - "8601": [0.19444, 0.69444, 0, 0, 1.14999], - "8636": [-0.10889, 0.39111, 0, 0, 1.14999], - "8637": [-0.10889, 0.39111, 0, 0, 1.14999], - "8640": [-0.10889, 0.39111, 0, 0, 1.14999], - "8641": [-0.10889, 0.39111, 0, 0, 1.14999], - "8656": [-0.10889, 0.39111, 0, 0, 1.14999], - "8657": [0.19444, 0.69444, 0, 0, 0.70277], - "8658": [-0.10889, 0.39111, 0, 0, 1.14999], - "8659": [0.19444, 0.69444, 0, 0, 0.70277], - "8660": [-0.10889, 0.39111, 0, 0, 1.14999], - "8661": [0.25, 0.75, 0, 0, 0.70277], - "8704": [0, 0.69444, 0, 0, 0.63889], - "8706": [0, 0.69444, 0.06389, 0, 0.62847], - "8707": [0, 0.69444, 0, 0, 0.63889], - "8709": [0.05556, 0.75, 0, 0, 0.575], - "8711": [0, 0.68611, 0, 0, 0.95833], - "8712": [0.08556, 0.58556, 0, 0, 0.76666], - "8715": [0.08556, 0.58556, 0, 0, 0.76666], - "8722": [0.13333, 0.63333, 0, 0, 0.89444], - "8723": [0.13333, 0.63333, 0, 0, 0.89444], - "8725": [0.25, 0.75, 0, 0, 0.575], - "8726": [0.25, 0.75, 0, 0, 0.575], - "8727": [-0.02778, 0.47222, 0, 0, 0.575], - "8728": [-0.02639, 0.47361, 0, 0, 0.575], - "8729": [-0.02639, 0.47361, 0, 0, 0.575], - "8730": [0.18, 0.82, 0, 0, 0.95833], - "8733": [0, 0.44444, 0, 0, 0.89444], - "8734": [0, 0.44444, 0, 0, 1.14999], - "8736": [0, 0.69224, 0, 0, 0.72222], - "8739": [0.25, 0.75, 0, 0, 0.31944], - "8741": [0.25, 0.75, 0, 0, 0.575], - "8743": [0, 0.55556, 0, 0, 0.76666], - "8744": [0, 0.55556, 0, 0, 0.76666], - "8745": [0, 0.55556, 0, 0, 0.76666], - "8746": [0, 0.55556, 0, 0, 0.76666], - "8747": [0.19444, 0.69444, 0.12778, 0, 0.56875], - "8764": [-0.10889, 0.39111, 0, 0, 0.89444], - "8768": [0.19444, 0.69444, 0, 0, 0.31944], - "8771": [0.00222, 0.50222, 0, 0, 0.89444], - "8776": [0.02444, 0.52444, 0, 0, 0.89444], - "8781": [0.00222, 0.50222, 0, 0, 0.89444], - "8801": [0.00222, 0.50222, 0, 0, 0.89444], - "8804": [0.19667, 0.69667, 0, 0, 0.89444], - "8805": [0.19667, 0.69667, 0, 0, 0.89444], - "8810": [0.08556, 0.58556, 0, 0, 1.14999], - "8811": [0.08556, 0.58556, 0, 0, 1.14999], - "8826": [0.08556, 0.58556, 0, 0, 0.89444], - "8827": [0.08556, 0.58556, 0, 0, 0.89444], - "8834": [0.08556, 0.58556, 0, 0, 0.89444], - "8835": [0.08556, 0.58556, 0, 0, 0.89444], - "8838": [0.19667, 0.69667, 0, 0, 0.89444], - "8839": [0.19667, 0.69667, 0, 0, 0.89444], - "8846": [0, 0.55556, 0, 0, 0.76666], - "8849": [0.19667, 0.69667, 0, 0, 0.89444], - "8850": [0.19667, 0.69667, 0, 0, 0.89444], - "8851": [0, 0.55556, 0, 0, 0.76666], - "8852": [0, 0.55556, 0, 0, 0.76666], - "8853": [0.13333, 0.63333, 0, 0, 0.89444], - "8854": [0.13333, 0.63333, 0, 0, 0.89444], - "8855": [0.13333, 0.63333, 0, 0, 0.89444], - "8856": [0.13333, 0.63333, 0, 0, 0.89444], - "8857": [0.13333, 0.63333, 0, 0, 0.89444], - "8866": [0, 0.69444, 0, 0, 0.70277], - "8867": [0, 0.69444, 0, 0, 0.70277], - "8868": [0, 0.69444, 0, 0, 0.89444], - "8869": [0, 0.69444, 0, 0, 0.89444], - "8900": [-0.02639, 0.47361, 0, 0, 0.575], - "8901": [-0.02639, 0.47361, 0, 0, 0.31944], - "8902": [-0.02778, 0.47222, 0, 0, 0.575], - "8968": [0.25, 0.75, 0, 0, 0.51111], - "8969": [0.25, 0.75, 0, 0, 0.51111], - "8970": [0.25, 0.75, 0, 0, 0.51111], - "8971": [0.25, 0.75, 0, 0, 0.51111], - "8994": [-0.13889, 0.36111, 0, 0, 1.14999], - "8995": [-0.13889, 0.36111, 0, 0, 1.14999], - "9651": [0.19444, 0.69444, 0, 0, 1.02222], - "9657": [-0.02778, 0.47222, 0, 0, 0.575], - "9661": [0.19444, 0.69444, 0, 0, 1.02222], - "9667": [-0.02778, 0.47222, 0, 0, 0.575], - "9711": [0.19444, 0.69444, 0, 0, 1.14999], - "9824": [0.12963, 0.69444, 0, 0, 0.89444], - "9825": [0.12963, 0.69444, 0, 0, 0.89444], - "9826": [0.12963, 0.69444, 0, 0, 0.89444], - "9827": [0.12963, 0.69444, 0, 0, 0.89444], - "9837": [0, 0.75, 0, 0, 0.44722], - "9838": [0.19444, 0.69444, 0, 0, 0.44722], - "9839": [0.19444, 0.69444, 0, 0, 0.44722], - "10216": [0.25, 0.75, 0, 0, 0.44722], - "10217": [0.25, 0.75, 0, 0, 0.44722], - "10815": [0, 0.68611, 0, 0, 0.9], - "10927": [0.19667, 0.69667, 0, 0, 0.89444], - "10928": [0.19667, 0.69667, 0, 0, 0.89444], - "57376": [0.19444, 0.69444, 0, 0, 0] - }, - "Main-BoldItalic": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0.11417, 0, 0.38611], - "34": [0, 0.69444, 0.07939, 0, 0.62055], - "35": [0.19444, 0.69444, 0.06833, 0, 0.94444], - "37": [0.05556, 0.75, 0.12861, 0, 0.94444], - "38": [0, 0.69444, 0.08528, 0, 0.88555], - "39": [0, 0.69444, 0.12945, 0, 0.35555], - "40": [0.25, 0.75, 0.15806, 0, 0.47333], - "41": [0.25, 0.75, 0.03306, 0, 0.47333], - "42": [0, 0.75, 0.14333, 0, 0.59111], - "43": [0.10333, 0.60333, 0.03306, 0, 0.88555], - "44": [0.19444, 0.14722, 0, 0, 0.35555], - "45": [0, 0.44444, 0.02611, 0, 0.41444], - "46": [0, 0.14722, 0, 0, 0.35555], - "47": [0.25, 0.75, 0.15806, 0, 0.59111], - "48": [0, 0.64444, 0.13167, 0, 0.59111], - "49": [0, 0.64444, 0.13167, 0, 0.59111], - "50": [0, 0.64444, 0.13167, 0, 0.59111], - "51": [0, 0.64444, 0.13167, 0, 0.59111], - "52": [0.19444, 0.64444, 0.13167, 0, 0.59111], - "53": [0, 0.64444, 0.13167, 0, 0.59111], - "54": [0, 0.64444, 0.13167, 0, 0.59111], - "55": [0.19444, 0.64444, 0.13167, 0, 0.59111], - "56": [0, 0.64444, 0.13167, 0, 0.59111], - "57": [0, 0.64444, 0.13167, 0, 0.59111], - "58": [0, 0.44444, 0.06695, 0, 0.35555], - "59": [0.19444, 0.44444, 0.06695, 0, 0.35555], - "61": [-0.10889, 0.39111, 0.06833, 0, 0.88555], - "63": [0, 0.69444, 0.11472, 0, 0.59111], - "64": [0, 0.69444, 0.09208, 0, 0.88555], - "65": [0, 0.68611, 0, 0, 0.86555], - "66": [0, 0.68611, 0.0992, 0, 0.81666], - "67": [0, 0.68611, 0.14208, 0, 0.82666], - "68": [0, 0.68611, 0.09062, 0, 0.87555], - "69": [0, 0.68611, 0.11431, 0, 0.75666], - "70": [0, 0.68611, 0.12903, 0, 0.72722], - "71": [0, 0.68611, 0.07347, 0, 0.89527], - "72": [0, 0.68611, 0.17208, 0, 0.8961], - "73": [0, 0.68611, 0.15681, 0, 0.47166], - "74": [0, 0.68611, 0.145, 0, 0.61055], - "75": [0, 0.68611, 0.14208, 0, 0.89499], - "76": [0, 0.68611, 0, 0, 0.69777], - "77": [0, 0.68611, 0.17208, 0, 1.07277], - "78": [0, 0.68611, 0.17208, 0, 0.8961], - "79": [0, 0.68611, 0.09062, 0, 0.85499], - "80": [0, 0.68611, 0.0992, 0, 0.78721], - "81": [0.19444, 0.68611, 0.09062, 0, 0.85499], - "82": [0, 0.68611, 0.02559, 0, 0.85944], - "83": [0, 0.68611, 0.11264, 0, 0.64999], - "84": [0, 0.68611, 0.12903, 0, 0.7961], - "85": [0, 0.68611, 0.17208, 0, 0.88083], - "86": [0, 0.68611, 0.18625, 0, 0.86555], - "87": [0, 0.68611, 0.18625, 0, 1.15999], - "88": [0, 0.68611, 0.15681, 0, 0.86555], - "89": [0, 0.68611, 0.19803, 0, 0.86555], - "90": [0, 0.68611, 0.14208, 0, 0.70888], - "91": [0.25, 0.75, 0.1875, 0, 0.35611], - "93": [0.25, 0.75, 0.09972, 0, 0.35611], - "94": [0, 0.69444, 0.06709, 0, 0.59111], - "95": [0.31, 0.13444, 0.09811, 0, 0.59111], - "97": [0, 0.44444, 0.09426, 0, 0.59111], - "98": [0, 0.69444, 0.07861, 0, 0.53222], - "99": [0, 0.44444, 0.05222, 0, 0.53222], - "100": [0, 0.69444, 0.10861, 0, 0.59111], - "101": [0, 0.44444, 0.085, 0, 0.53222], - "102": [0.19444, 0.69444, 0.21778, 0, 0.4], - "103": [0.19444, 0.44444, 0.105, 0, 0.53222], - "104": [0, 0.69444, 0.09426, 0, 0.59111], - "105": [0, 0.69326, 0.11387, 0, 0.35555], - "106": [0.19444, 0.69326, 0.1672, 0, 0.35555], - "107": [0, 0.69444, 0.11111, 0, 0.53222], - "108": [0, 0.69444, 0.10861, 0, 0.29666], - "109": [0, 0.44444, 0.09426, 0, 0.94444], - "110": [0, 0.44444, 0.09426, 0, 0.64999], - "111": [0, 0.44444, 0.07861, 0, 0.59111], - "112": [0.19444, 0.44444, 0.07861, 0, 0.59111], - "113": [0.19444, 0.44444, 0.105, 0, 0.53222], - "114": [0, 0.44444, 0.11111, 0, 0.50167], - "115": [0, 0.44444, 0.08167, 0, 0.48694], - "116": [0, 0.63492, 0.09639, 0, 0.385], - "117": [0, 0.44444, 0.09426, 0, 0.62055], - "118": [0, 0.44444, 0.11111, 0, 0.53222], - "119": [0, 0.44444, 0.11111, 0, 0.76777], - "120": [0, 0.44444, 0.12583, 0, 0.56055], - "121": [0.19444, 0.44444, 0.105, 0, 0.56166], - "122": [0, 0.44444, 0.13889, 0, 0.49055], - "126": [0.35, 0.34444, 0.11472, 0, 0.59111], - "160": [0, 0, 0, 0, 0.25], - "168": [0, 0.69444, 0.11473, 0, 0.59111], - "176": [0, 0.69444, 0, 0, 0.94888], - "184": [0.17014, 0, 0, 0, 0.53222], - "198": [0, 0.68611, 0.11431, 0, 1.02277], - "216": [0.04861, 0.73472, 0.09062, 0, 0.88555], - "223": [0.19444, 0.69444, 0.09736, 0, 0.665], - "230": [0, 0.44444, 0.085, 0, 0.82666], - "248": [0.09722, 0.54167, 0.09458, 0, 0.59111], - "305": [0, 0.44444, 0.09426, 0, 0.35555], - "338": [0, 0.68611, 0.11431, 0, 1.14054], - "339": [0, 0.44444, 0.085, 0, 0.82666], - "567": [0.19444, 0.44444, 0.04611, 0, 0.385], - "710": [0, 0.69444, 0.06709, 0, 0.59111], - "711": [0, 0.63194, 0.08271, 0, 0.59111], - "713": [0, 0.59444, 0.10444, 0, 0.59111], - "714": [0, 0.69444, 0.08528, 0, 0.59111], - "715": [0, 0.69444, 0, 0, 0.59111], - "728": [0, 0.69444, 0.10333, 0, 0.59111], - "729": [0, 0.69444, 0.12945, 0, 0.35555], - "730": [0, 0.69444, 0, 0, 0.94888], - "732": [0, 0.69444, 0.11472, 0, 0.59111], - "733": [0, 0.69444, 0.11472, 0, 0.59111], - "915": [0, 0.68611, 0.12903, 0, 0.69777], - "916": [0, 0.68611, 0, 0, 0.94444], - "920": [0, 0.68611, 0.09062, 0, 0.88555], - "923": [0, 0.68611, 0, 0, 0.80666], - "926": [0, 0.68611, 0.15092, 0, 0.76777], - "928": [0, 0.68611, 0.17208, 0, 0.8961], - "931": [0, 0.68611, 0.11431, 0, 0.82666], - "933": [0, 0.68611, 0.10778, 0, 0.88555], - "934": [0, 0.68611, 0.05632, 0, 0.82666], - "936": [0, 0.68611, 0.10778, 0, 0.88555], - "937": [0, 0.68611, 0.0992, 0, 0.82666], - "8211": [0, 0.44444, 0.09811, 0, 0.59111], - "8212": [0, 0.44444, 0.09811, 0, 1.18221], - "8216": [0, 0.69444, 0.12945, 0, 0.35555], - "8217": [0, 0.69444, 0.12945, 0, 0.35555], - "8220": [0, 0.69444, 0.16772, 0, 0.62055], - "8221": [0, 0.69444, 0.07939, 0, 0.62055] - }, - "Main-Italic": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0.12417, 0, 0.30667], - "34": [0, 0.69444, 0.06961, 0, 0.51444], - "35": [0.19444, 0.69444, 0.06616, 0, 0.81777], - "37": [0.05556, 0.75, 0.13639, 0, 0.81777], - "38": [0, 0.69444, 0.09694, 0, 0.76666], - "39": [0, 0.69444, 0.12417, 0, 0.30667], - "40": [0.25, 0.75, 0.16194, 0, 0.40889], - "41": [0.25, 0.75, 0.03694, 0, 0.40889], - "42": [0, 0.75, 0.14917, 0, 0.51111], - "43": [0.05667, 0.56167, 0.03694, 0, 0.76666], - "44": [0.19444, 0.10556, 0, 0, 0.30667], - "45": [0, 0.43056, 0.02826, 0, 0.35778], - "46": [0, 0.10556, 0, 0, 0.30667], - "47": [0.25, 0.75, 0.16194, 0, 0.51111], - "48": [0, 0.64444, 0.13556, 0, 0.51111], - "49": [0, 0.64444, 0.13556, 0, 0.51111], - "50": [0, 0.64444, 0.13556, 0, 0.51111], - "51": [0, 0.64444, 0.13556, 0, 0.51111], - "52": [0.19444, 0.64444, 0.13556, 0, 0.51111], - "53": [0, 0.64444, 0.13556, 0, 0.51111], - "54": [0, 0.64444, 0.13556, 0, 0.51111], - "55": [0.19444, 0.64444, 0.13556, 0, 0.51111], - "56": [0, 0.64444, 0.13556, 0, 0.51111], - "57": [0, 0.64444, 0.13556, 0, 0.51111], - "58": [0, 0.43056, 0.0582, 0, 0.30667], - "59": [0.19444, 0.43056, 0.0582, 0, 0.30667], - "61": [-0.13313, 0.36687, 0.06616, 0, 0.76666], - "63": [0, 0.69444, 0.1225, 0, 0.51111], - "64": [0, 0.69444, 0.09597, 0, 0.76666], - "65": [0, 0.68333, 0, 0, 0.74333], - "66": [0, 0.68333, 0.10257, 0, 0.70389], - "67": [0, 0.68333, 0.14528, 0, 0.71555], - "68": [0, 0.68333, 0.09403, 0, 0.755], - "69": [0, 0.68333, 0.12028, 0, 0.67833], - "70": [0, 0.68333, 0.13305, 0, 0.65277], - "71": [0, 0.68333, 0.08722, 0, 0.77361], - "72": [0, 0.68333, 0.16389, 0, 0.74333], - "73": [0, 0.68333, 0.15806, 0, 0.38555], - "74": [0, 0.68333, 0.14028, 0, 0.525], - "75": [0, 0.68333, 0.14528, 0, 0.76888], - "76": [0, 0.68333, 0, 0, 0.62722], - "77": [0, 0.68333, 0.16389, 0, 0.89666], - "78": [0, 0.68333, 0.16389, 0, 0.74333], - "79": [0, 0.68333, 0.09403, 0, 0.76666], - "80": [0, 0.68333, 0.10257, 0, 0.67833], - "81": [0.19444, 0.68333, 0.09403, 0, 0.76666], - "82": [0, 0.68333, 0.03868, 0, 0.72944], - "83": [0, 0.68333, 0.11972, 0, 0.56222], - "84": [0, 0.68333, 0.13305, 0, 0.71555], - "85": [0, 0.68333, 0.16389, 0, 0.74333], - "86": [0, 0.68333, 0.18361, 0, 0.74333], - "87": [0, 0.68333, 0.18361, 0, 0.99888], - "88": [0, 0.68333, 0.15806, 0, 0.74333], - "89": [0, 0.68333, 0.19383, 0, 0.74333], - "90": [0, 0.68333, 0.14528, 0, 0.61333], - "91": [0.25, 0.75, 0.1875, 0, 0.30667], - "93": [0.25, 0.75, 0.10528, 0, 0.30667], - "94": [0, 0.69444, 0.06646, 0, 0.51111], - "95": [0.31, 0.12056, 0.09208, 0, 0.51111], - "97": [0, 0.43056, 0.07671, 0, 0.51111], - "98": [0, 0.69444, 0.06312, 0, 0.46], - "99": [0, 0.43056, 0.05653, 0, 0.46], - "100": [0, 0.69444, 0.10333, 0, 0.51111], - "101": [0, 0.43056, 0.07514, 0, 0.46], - "102": [0.19444, 0.69444, 0.21194, 0, 0.30667], - "103": [0.19444, 0.43056, 0.08847, 0, 0.46], - "104": [0, 0.69444, 0.07671, 0, 0.51111], - "105": [0, 0.65536, 0.1019, 0, 0.30667], - "106": [0.19444, 0.65536, 0.14467, 0, 0.30667], - "107": [0, 0.69444, 0.10764, 0, 0.46], - "108": [0, 0.69444, 0.10333, 0, 0.25555], - "109": [0, 0.43056, 0.07671, 0, 0.81777], - "110": [0, 0.43056, 0.07671, 0, 0.56222], - "111": [0, 0.43056, 0.06312, 0, 0.51111], - "112": [0.19444, 0.43056, 0.06312, 0, 0.51111], - "113": [0.19444, 0.43056, 0.08847, 0, 0.46], - "114": [0, 0.43056, 0.10764, 0, 0.42166], - "115": [0, 0.43056, 0.08208, 0, 0.40889], - "116": [0, 0.61508, 0.09486, 0, 0.33222], - "117": [0, 0.43056, 0.07671, 0, 0.53666], - "118": [0, 0.43056, 0.10764, 0, 0.46], - "119": [0, 0.43056, 0.10764, 0, 0.66444], - "120": [0, 0.43056, 0.12042, 0, 0.46389], - "121": [0.19444, 0.43056, 0.08847, 0, 0.48555], - "122": [0, 0.43056, 0.12292, 0, 0.40889], - "126": [0.35, 0.31786, 0.11585, 0, 0.51111], - "160": [0, 0, 0, 0, 0.25], - "168": [0, 0.66786, 0.10474, 0, 0.51111], - "176": [0, 0.69444, 0, 0, 0.83129], - "184": [0.17014, 0, 0, 0, 0.46], - "198": [0, 0.68333, 0.12028, 0, 0.88277], - "216": [0.04861, 0.73194, 0.09403, 0, 0.76666], - "223": [0.19444, 0.69444, 0.10514, 0, 0.53666], - "230": [0, 0.43056, 0.07514, 0, 0.71555], - "248": [0.09722, 0.52778, 0.09194, 0, 0.51111], - "338": [0, 0.68333, 0.12028, 0, 0.98499], - "339": [0, 0.43056, 0.07514, 0, 0.71555], - "710": [0, 0.69444, 0.06646, 0, 0.51111], - "711": [0, 0.62847, 0.08295, 0, 0.51111], - "713": [0, 0.56167, 0.10333, 0, 0.51111], - "714": [0, 0.69444, 0.09694, 0, 0.51111], - "715": [0, 0.69444, 0, 0, 0.51111], - "728": [0, 0.69444, 0.10806, 0, 0.51111], - "729": [0, 0.66786, 0.11752, 0, 0.30667], - "730": [0, 0.69444, 0, 0, 0.83129], - "732": [0, 0.66786, 0.11585, 0, 0.51111], - "733": [0, 0.69444, 0.1225, 0, 0.51111], - "915": [0, 0.68333, 0.13305, 0, 0.62722], - "916": [0, 0.68333, 0, 0, 0.81777], - "920": [0, 0.68333, 0.09403, 0, 0.76666], - "923": [0, 0.68333, 0, 0, 0.69222], - "926": [0, 0.68333, 0.15294, 0, 0.66444], - "928": [0, 0.68333, 0.16389, 0, 0.74333], - "931": [0, 0.68333, 0.12028, 0, 0.71555], - "933": [0, 0.68333, 0.11111, 0, 0.76666], - "934": [0, 0.68333, 0.05986, 0, 0.71555], - "936": [0, 0.68333, 0.11111, 0, 0.76666], - "937": [0, 0.68333, 0.10257, 0, 0.71555], - "8211": [0, 0.43056, 0.09208, 0, 0.51111], - "8212": [0, 0.43056, 0.09208, 0, 1.02222], - "8216": [0, 0.69444, 0.12417, 0, 0.30667], - "8217": [0, 0.69444, 0.12417, 0, 0.30667], - "8220": [0, 0.69444, 0.1685, 0, 0.51444], - "8221": [0, 0.69444, 0.06961, 0, 0.51444], - "8463": [0, 0.68889, 0, 0, 0.54028] - }, - "Main-Regular": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0, 0, 0.27778], - "34": [0, 0.69444, 0, 0, 0.5], - "35": [0.19444, 0.69444, 0, 0, 0.83334], - "36": [0.05556, 0.75, 0, 0, 0.5], - "37": [0.05556, 0.75, 0, 0, 0.83334], - "38": [0, 0.69444, 0, 0, 0.77778], - "39": [0, 0.69444, 0, 0, 0.27778], - "40": [0.25, 0.75, 0, 0, 0.38889], - "41": [0.25, 0.75, 0, 0, 0.38889], - "42": [0, 0.75, 0, 0, 0.5], - "43": [0.08333, 0.58333, 0, 0, 0.77778], - "44": [0.19444, 0.10556, 0, 0, 0.27778], - "45": [0, 0.43056, 0, 0, 0.33333], - "46": [0, 0.10556, 0, 0, 0.27778], - "47": [0.25, 0.75, 0, 0, 0.5], - "48": [0, 0.64444, 0, 0, 0.5], - "49": [0, 0.64444, 0, 0, 0.5], - "50": [0, 0.64444, 0, 0, 0.5], - "51": [0, 0.64444, 0, 0, 0.5], - "52": [0, 0.64444, 0, 0, 0.5], - "53": [0, 0.64444, 0, 0, 0.5], - "54": [0, 0.64444, 0, 0, 0.5], - "55": [0, 0.64444, 0, 0, 0.5], - "56": [0, 0.64444, 0, 0, 0.5], - "57": [0, 0.64444, 0, 0, 0.5], - "58": [0, 0.43056, 0, 0, 0.27778], - "59": [0.19444, 0.43056, 0, 0, 0.27778], - "60": [0.0391, 0.5391, 0, 0, 0.77778], - "61": [-0.13313, 0.36687, 0, 0, 0.77778], - "62": [0.0391, 0.5391, 0, 0, 0.77778], - "63": [0, 0.69444, 0, 0, 0.47222], - "64": [0, 0.69444, 0, 0, 0.77778], - "65": [0, 0.68333, 0, 0, 0.75], - "66": [0, 0.68333, 0, 0, 0.70834], - "67": [0, 0.68333, 0, 0, 0.72222], - "68": [0, 0.68333, 0, 0, 0.76389], - "69": [0, 0.68333, 0, 0, 0.68056], - "70": [0, 0.68333, 0, 0, 0.65278], - "71": [0, 0.68333, 0, 0, 0.78472], - "72": [0, 0.68333, 0, 0, 0.75], - "73": [0, 0.68333, 0, 0, 0.36111], - "74": [0, 0.68333, 0, 0, 0.51389], - "75": [0, 0.68333, 0, 0, 0.77778], - "76": [0, 0.68333, 0, 0, 0.625], - "77": [0, 0.68333, 0, 0, 0.91667], - "78": [0, 0.68333, 0, 0, 0.75], - "79": [0, 0.68333, 0, 0, 0.77778], - "80": [0, 0.68333, 0, 0, 0.68056], - "81": [0.19444, 0.68333, 0, 0, 0.77778], - "82": [0, 0.68333, 0, 0, 0.73611], - "83": [0, 0.68333, 0, 0, 0.55556], - "84": [0, 0.68333, 0, 0, 0.72222], - "85": [0, 0.68333, 0, 0, 0.75], - "86": [0, 0.68333, 0.01389, 0, 0.75], - "87": [0, 0.68333, 0.01389, 0, 1.02778], - "88": [0, 0.68333, 0, 0, 0.75], - "89": [0, 0.68333, 0.025, 0, 0.75], - "90": [0, 0.68333, 0, 0, 0.61111], - "91": [0.25, 0.75, 0, 0, 0.27778], - "92": [0.25, 0.75, 0, 0, 0.5], - "93": [0.25, 0.75, 0, 0, 0.27778], - "94": [0, 0.69444, 0, 0, 0.5], - "95": [0.31, 0.12056, 0.02778, 0, 0.5], - "97": [0, 0.43056, 0, 0, 0.5], - "98": [0, 0.69444, 0, 0, 0.55556], - "99": [0, 0.43056, 0, 0, 0.44445], - "100": [0, 0.69444, 0, 0, 0.55556], - "101": [0, 0.43056, 0, 0, 0.44445], - "102": [0, 0.69444, 0.07778, 0, 0.30556], - "103": [0.19444, 0.43056, 0.01389, 0, 0.5], - "104": [0, 0.69444, 0, 0, 0.55556], - "105": [0, 0.66786, 0, 0, 0.27778], - "106": [0.19444, 0.66786, 0, 0, 0.30556], - "107": [0, 0.69444, 0, 0, 0.52778], - "108": [0, 0.69444, 0, 0, 0.27778], - "109": [0, 0.43056, 0, 0, 0.83334], - "110": [0, 0.43056, 0, 0, 0.55556], - "111": [0, 0.43056, 0, 0, 0.5], - "112": [0.19444, 0.43056, 0, 0, 0.55556], - "113": [0.19444, 0.43056, 0, 0, 0.52778], - "114": [0, 0.43056, 0, 0, 0.39167], - "115": [0, 0.43056, 0, 0, 0.39445], - "116": [0, 0.61508, 0, 0, 0.38889], - "117": [0, 0.43056, 0, 0, 0.55556], - "118": [0, 0.43056, 0.01389, 0, 0.52778], - "119": [0, 0.43056, 0.01389, 0, 0.72222], - "120": [0, 0.43056, 0, 0, 0.52778], - "121": [0.19444, 0.43056, 0.01389, 0, 0.52778], - "122": [0, 0.43056, 0, 0, 0.44445], - "123": [0.25, 0.75, 0, 0, 0.5], - "124": [0.25, 0.75, 0, 0, 0.27778], - "125": [0.25, 0.75, 0, 0, 0.5], - "126": [0.35, 0.31786, 0, 0, 0.5], - "160": [0, 0, 0, 0, 0.25], - "163": [0, 0.69444, 0, 0, 0.76909], - "167": [0.19444, 0.69444, 0, 0, 0.44445], - "168": [0, 0.66786, 0, 0, 0.5], - "172": [0, 0.43056, 0, 0, 0.66667], - "176": [0, 0.69444, 0, 0, 0.75], - "177": [0.08333, 0.58333, 0, 0, 0.77778], - "182": [0.19444, 0.69444, 0, 0, 0.61111], - "184": [0.17014, 0, 0, 0, 0.44445], - "198": [0, 0.68333, 0, 0, 0.90278], - "215": [0.08333, 0.58333, 0, 0, 0.77778], - "216": [0.04861, 0.73194, 0, 0, 0.77778], - "223": [0, 0.69444, 0, 0, 0.5], - "230": [0, 0.43056, 0, 0, 0.72222], - "247": [0.08333, 0.58333, 0, 0, 0.77778], - "248": [0.09722, 0.52778, 0, 0, 0.5], - "305": [0, 0.43056, 0, 0, 0.27778], - "338": [0, 0.68333, 0, 0, 1.01389], - "339": [0, 0.43056, 0, 0, 0.77778], - "567": [0.19444, 0.43056, 0, 0, 0.30556], - "710": [0, 0.69444, 0, 0, 0.5], - "711": [0, 0.62847, 0, 0, 0.5], - "713": [0, 0.56778, 0, 0, 0.5], - "714": [0, 0.69444, 0, 0, 0.5], - "715": [0, 0.69444, 0, 0, 0.5], - "728": [0, 0.69444, 0, 0, 0.5], - "729": [0, 0.66786, 0, 0, 0.27778], - "730": [0, 0.69444, 0, 0, 0.75], - "732": [0, 0.66786, 0, 0, 0.5], - "733": [0, 0.69444, 0, 0, 0.5], - "915": [0, 0.68333, 0, 0, 0.625], - "916": [0, 0.68333, 0, 0, 0.83334], - "920": [0, 0.68333, 0, 0, 0.77778], - "923": [0, 0.68333, 0, 0, 0.69445], - "926": [0, 0.68333, 0, 0, 0.66667], - "928": [0, 0.68333, 0, 0, 0.75], - "931": [0, 0.68333, 0, 0, 0.72222], - "933": [0, 0.68333, 0, 0, 0.77778], - "934": [0, 0.68333, 0, 0, 0.72222], - "936": [0, 0.68333, 0, 0, 0.77778], - "937": [0, 0.68333, 0, 0, 0.72222], - "8211": [0, 0.43056, 0.02778, 0, 0.5], - "8212": [0, 0.43056, 0.02778, 0, 1.0], - "8216": [0, 0.69444, 0, 0, 0.27778], - "8217": [0, 0.69444, 0, 0, 0.27778], - "8220": [0, 0.69444, 0, 0, 0.5], - "8221": [0, 0.69444, 0, 0, 0.5], - "8224": [0.19444, 0.69444, 0, 0, 0.44445], - "8225": [0.19444, 0.69444, 0, 0, 0.44445], - "8230": [0, 0.12, 0, 0, 1.172], - "8242": [0, 0.55556, 0, 0, 0.275], - "8407": [0, 0.71444, 0.15382, 0, 0.5], - "8463": [0, 0.68889, 0, 0, 0.54028], - "8465": [0, 0.69444, 0, 0, 0.72222], - "8467": [0, 0.69444, 0, 0.11111, 0.41667], - "8472": [0.19444, 0.43056, 0, 0.11111, 0.63646], - "8476": [0, 0.69444, 0, 0, 0.72222], - "8501": [0, 0.69444, 0, 0, 0.61111], - "8592": [-0.13313, 0.36687, 0, 0, 1.0], - "8593": [0.19444, 0.69444, 0, 0, 0.5], - "8594": [-0.13313, 0.36687, 0, 0, 1.0], - "8595": [0.19444, 0.69444, 0, 0, 0.5], - "8596": [-0.13313, 0.36687, 0, 0, 1.0], - "8597": [0.25, 0.75, 0, 0, 0.5], - "8598": [0.19444, 0.69444, 0, 0, 1.0], - "8599": [0.19444, 0.69444, 0, 0, 1.0], - "8600": [0.19444, 0.69444, 0, 0, 1.0], - "8601": [0.19444, 0.69444, 0, 0, 1.0], - "8614": [0.011, 0.511, 0, 0, 1.0], - "8617": [0.011, 0.511, 0, 0, 1.126], - "8618": [0.011, 0.511, 0, 0, 1.126], - "8636": [-0.13313, 0.36687, 0, 0, 1.0], - "8637": [-0.13313, 0.36687, 0, 0, 1.0], - "8640": [-0.13313, 0.36687, 0, 0, 1.0], - "8641": [-0.13313, 0.36687, 0, 0, 1.0], - "8652": [0.011, 0.671, 0, 0, 1.0], - "8656": [-0.13313, 0.36687, 0, 0, 1.0], - "8657": [0.19444, 0.69444, 0, 0, 0.61111], - "8658": [-0.13313, 0.36687, 0, 0, 1.0], - "8659": [0.19444, 0.69444, 0, 0, 0.61111], - "8660": [-0.13313, 0.36687, 0, 0, 1.0], - "8661": [0.25, 0.75, 0, 0, 0.61111], - "8704": [0, 0.69444, 0, 0, 0.55556], - "8706": [0, 0.69444, 0.05556, 0.08334, 0.5309], - "8707": [0, 0.69444, 0, 0, 0.55556], - "8709": [0.05556, 0.75, 0, 0, 0.5], - "8711": [0, 0.68333, 0, 0, 0.83334], - "8712": [0.0391, 0.5391, 0, 0, 0.66667], - "8715": [0.0391, 0.5391, 0, 0, 0.66667], - "8722": [0.08333, 0.58333, 0, 0, 0.77778], - "8723": [0.08333, 0.58333, 0, 0, 0.77778], - "8725": [0.25, 0.75, 0, 0, 0.5], - "8726": [0.25, 0.75, 0, 0, 0.5], - "8727": [-0.03472, 0.46528, 0, 0, 0.5], - "8728": [-0.05555, 0.44445, 0, 0, 0.5], - "8729": [-0.05555, 0.44445, 0, 0, 0.5], - "8730": [0.2, 0.8, 0, 0, 0.83334], - "8733": [0, 0.43056, 0, 0, 0.77778], - "8734": [0, 0.43056, 0, 0, 1.0], - "8736": [0, 0.69224, 0, 0, 0.72222], - "8739": [0.25, 0.75, 0, 0, 0.27778], - "8741": [0.25, 0.75, 0, 0, 0.5], - "8743": [0, 0.55556, 0, 0, 0.66667], - "8744": [0, 0.55556, 0, 0, 0.66667], - "8745": [0, 0.55556, 0, 0, 0.66667], - "8746": [0, 0.55556, 0, 0, 0.66667], - "8747": [0.19444, 0.69444, 0.11111, 0, 0.41667], - "8764": [-0.13313, 0.36687, 0, 0, 0.77778], - "8768": [0.19444, 0.69444, 0, 0, 0.27778], - "8771": [-0.03625, 0.46375, 0, 0, 0.77778], - "8773": [-0.022, 0.589, 0, 0, 1.0], - "8776": [-0.01688, 0.48312, 0, 0, 0.77778], - "8781": [-0.03625, 0.46375, 0, 0, 0.77778], - "8784": [-0.133, 0.67, 0, 0, 0.778], - "8801": [-0.03625, 0.46375, 0, 0, 0.77778], - "8804": [0.13597, 0.63597, 0, 0, 0.77778], - "8805": [0.13597, 0.63597, 0, 0, 0.77778], - "8810": [0.0391, 0.5391, 0, 0, 1.0], - "8811": [0.0391, 0.5391, 0, 0, 1.0], - "8826": [0.0391, 0.5391, 0, 0, 0.77778], - "8827": [0.0391, 0.5391, 0, 0, 0.77778], - "8834": [0.0391, 0.5391, 0, 0, 0.77778], - "8835": [0.0391, 0.5391, 0, 0, 0.77778], - "8838": [0.13597, 0.63597, 0, 0, 0.77778], - "8839": [0.13597, 0.63597, 0, 0, 0.77778], - "8846": [0, 0.55556, 0, 0, 0.66667], - "8849": [0.13597, 0.63597, 0, 0, 0.77778], - "8850": [0.13597, 0.63597, 0, 0, 0.77778], - "8851": [0, 0.55556, 0, 0, 0.66667], - "8852": [0, 0.55556, 0, 0, 0.66667], - "8853": [0.08333, 0.58333, 0, 0, 0.77778], - "8854": [0.08333, 0.58333, 0, 0, 0.77778], - "8855": [0.08333, 0.58333, 0, 0, 0.77778], - "8856": [0.08333, 0.58333, 0, 0, 0.77778], - "8857": [0.08333, 0.58333, 0, 0, 0.77778], - "8866": [0, 0.69444, 0, 0, 0.61111], - "8867": [0, 0.69444, 0, 0, 0.61111], - "8868": [0, 0.69444, 0, 0, 0.77778], - "8869": [0, 0.69444, 0, 0, 0.77778], - "8872": [0.249, 0.75, 0, 0, 0.867], - "8900": [-0.05555, 0.44445, 0, 0, 0.5], - "8901": [-0.05555, 0.44445, 0, 0, 0.27778], - "8902": [-0.03472, 0.46528, 0, 0, 0.5], - "8904": [0.005, 0.505, 0, 0, 0.9], - "8942": [0.03, 0.9, 0, 0, 0.278], - "8943": [-0.19, 0.31, 0, 0, 1.172], - "8945": [-0.1, 0.82, 0, 0, 1.282], - "8968": [0.25, 0.75, 0, 0, 0.44445], - "8969": [0.25, 0.75, 0, 0, 0.44445], - "8970": [0.25, 0.75, 0, 0, 0.44445], - "8971": [0.25, 0.75, 0, 0, 0.44445], - "8994": [-0.14236, 0.35764, 0, 0, 1.0], - "8995": [-0.14236, 0.35764, 0, 0, 1.0], - "9136": [0.244, 0.744, 0, 0, 0.412], - "9137": [0.244, 0.744, 0, 0, 0.412], - "9651": [0.19444, 0.69444, 0, 0, 0.88889], - "9657": [-0.03472, 0.46528, 0, 0, 0.5], - "9661": [0.19444, 0.69444, 0, 0, 0.88889], - "9667": [-0.03472, 0.46528, 0, 0, 0.5], - "9711": [0.19444, 0.69444, 0, 0, 1.0], - "9824": [0.12963, 0.69444, 0, 0, 0.77778], - "9825": [0.12963, 0.69444, 0, 0, 0.77778], - "9826": [0.12963, 0.69444, 0, 0, 0.77778], - "9827": [0.12963, 0.69444, 0, 0, 0.77778], - "9837": [0, 0.75, 0, 0, 0.38889], - "9838": [0.19444, 0.69444, 0, 0, 0.38889], - "9839": [0.19444, 0.69444, 0, 0, 0.38889], - "10216": [0.25, 0.75, 0, 0, 0.38889], - "10217": [0.25, 0.75, 0, 0, 0.38889], - "10222": [0.244, 0.744, 0, 0, 0.412], - "10223": [0.244, 0.744, 0, 0, 0.412], - "10229": [0.011, 0.511, 0, 0, 1.609], - "10230": [0.011, 0.511, 0, 0, 1.638], - "10231": [0.011, 0.511, 0, 0, 1.859], - "10232": [0.024, 0.525, 0, 0, 1.609], - "10233": [0.024, 0.525, 0, 0, 1.638], - "10234": [0.024, 0.525, 0, 0, 1.858], - "10236": [0.011, 0.511, 0, 0, 1.638], - "10815": [0, 0.68333, 0, 0, 0.75], - "10927": [0.13597, 0.63597, 0, 0, 0.77778], - "10928": [0.13597, 0.63597, 0, 0, 0.77778], - "57376": [0.19444, 0.69444, 0, 0, 0] - }, - "Math-BoldItalic": { - "32": [0, 0, 0, 0, 0.25], - "48": [0, 0.44444, 0, 0, 0.575], - "49": [0, 0.44444, 0, 0, 0.575], - "50": [0, 0.44444, 0, 0, 0.575], - "51": [0.19444, 0.44444, 0, 0, 0.575], - "52": [0.19444, 0.44444, 0, 0, 0.575], - "53": [0.19444, 0.44444, 0, 0, 0.575], - "54": [0, 0.64444, 0, 0, 0.575], - "55": [0.19444, 0.44444, 0, 0, 0.575], - "56": [0, 0.64444, 0, 0, 0.575], - "57": [0.19444, 0.44444, 0, 0, 0.575], - "65": [0, 0.68611, 0, 0, 0.86944], - "66": [0, 0.68611, 0.04835, 0, 0.8664], - "67": [0, 0.68611, 0.06979, 0, 0.81694], - "68": [0, 0.68611, 0.03194, 0, 0.93812], - "69": [0, 0.68611, 0.05451, 0, 0.81007], - "70": [0, 0.68611, 0.15972, 0, 0.68889], - "71": [0, 0.68611, 0, 0, 0.88673], - "72": [0, 0.68611, 0.08229, 0, 0.98229], - "73": [0, 0.68611, 0.07778, 0, 0.51111], - "74": [0, 0.68611, 0.10069, 0, 0.63125], - "75": [0, 0.68611, 0.06979, 0, 0.97118], - "76": [0, 0.68611, 0, 0, 0.75555], - "77": [0, 0.68611, 0.11424, 0, 1.14201], - "78": [0, 0.68611, 0.11424, 0, 0.95034], - "79": [0, 0.68611, 0.03194, 0, 0.83666], - "80": [0, 0.68611, 0.15972, 0, 0.72309], - "81": [0.19444, 0.68611, 0, 0, 0.86861], - "82": [0, 0.68611, 0.00421, 0, 0.87235], - "83": [0, 0.68611, 0.05382, 0, 0.69271], - "84": [0, 0.68611, 0.15972, 0, 0.63663], - "85": [0, 0.68611, 0.11424, 0, 0.80027], - "86": [0, 0.68611, 0.25555, 0, 0.67778], - "87": [0, 0.68611, 0.15972, 0, 1.09305], - "88": [0, 0.68611, 0.07778, 0, 0.94722], - "89": [0, 0.68611, 0.25555, 0, 0.67458], - "90": [0, 0.68611, 0.06979, 0, 0.77257], - "97": [0, 0.44444, 0, 0, 0.63287], - "98": [0, 0.69444, 0, 0, 0.52083], - "99": [0, 0.44444, 0, 0, 0.51342], - "100": [0, 0.69444, 0, 0, 0.60972], - "101": [0, 0.44444, 0, 0, 0.55361], - "102": [0.19444, 0.69444, 0.11042, 0, 0.56806], - "103": [0.19444, 0.44444, 0.03704, 0, 0.5449], - "104": [0, 0.69444, 0, 0, 0.66759], - "105": [0, 0.69326, 0, 0, 0.4048], - "106": [0.19444, 0.69326, 0.0622, 0, 0.47083], - "107": [0, 0.69444, 0.01852, 0, 0.6037], - "108": [0, 0.69444, 0.0088, 0, 0.34815], - "109": [0, 0.44444, 0, 0, 1.0324], - "110": [0, 0.44444, 0, 0, 0.71296], - "111": [0, 0.44444, 0, 0, 0.58472], - "112": [0.19444, 0.44444, 0, 0, 0.60092], - "113": [0.19444, 0.44444, 0.03704, 0, 0.54213], - "114": [0, 0.44444, 0.03194, 0, 0.5287], - "115": [0, 0.44444, 0, 0, 0.53125], - "116": [0, 0.63492, 0, 0, 0.41528], - "117": [0, 0.44444, 0, 0, 0.68102], - "118": [0, 0.44444, 0.03704, 0, 0.56666], - "119": [0, 0.44444, 0.02778, 0, 0.83148], - "120": [0, 0.44444, 0, 0, 0.65903], - "121": [0.19444, 0.44444, 0.03704, 0, 0.59028], - "122": [0, 0.44444, 0.04213, 0, 0.55509], - "160": [0, 0, 0, 0, 0.25], - "915": [0, 0.68611, 0.15972, 0, 0.65694], - "916": [0, 0.68611, 0, 0, 0.95833], - "920": [0, 0.68611, 0.03194, 0, 0.86722], - "923": [0, 0.68611, 0, 0, 0.80555], - "926": [0, 0.68611, 0.07458, 0, 0.84125], - "928": [0, 0.68611, 0.08229, 0, 0.98229], - "931": [0, 0.68611, 0.05451, 0, 0.88507], - "933": [0, 0.68611, 0.15972, 0, 0.67083], - "934": [0, 0.68611, 0, 0, 0.76666], - "936": [0, 0.68611, 0.11653, 0, 0.71402], - "937": [0, 0.68611, 0.04835, 0, 0.8789], - "945": [0, 0.44444, 0, 0, 0.76064], - "946": [0.19444, 0.69444, 0.03403, 0, 0.65972], - "947": [0.19444, 0.44444, 0.06389, 0, 0.59003], - "948": [0, 0.69444, 0.03819, 0, 0.52222], - "949": [0, 0.44444, 0, 0, 0.52882], - "950": [0.19444, 0.69444, 0.06215, 0, 0.50833], - "951": [0.19444, 0.44444, 0.03704, 0, 0.6], - "952": [0, 0.69444, 0.03194, 0, 0.5618], - "953": [0, 0.44444, 0, 0, 0.41204], - "954": [0, 0.44444, 0, 0, 0.66759], - "955": [0, 0.69444, 0, 0, 0.67083], - "956": [0.19444, 0.44444, 0, 0, 0.70787], - "957": [0, 0.44444, 0.06898, 0, 0.57685], - "958": [0.19444, 0.69444, 0.03021, 0, 0.50833], - "959": [0, 0.44444, 0, 0, 0.58472], - "960": [0, 0.44444, 0.03704, 0, 0.68241], - "961": [0.19444, 0.44444, 0, 0, 0.6118], - "962": [0.09722, 0.44444, 0.07917, 0, 0.42361], - "963": [0, 0.44444, 0.03704, 0, 0.68588], - "964": [0, 0.44444, 0.13472, 0, 0.52083], - "965": [0, 0.44444, 0.03704, 0, 0.63055], - "966": [0.19444, 0.44444, 0, 0, 0.74722], - "967": [0.19444, 0.44444, 0, 0, 0.71805], - "968": [0.19444, 0.69444, 0.03704, 0, 0.75833], - "969": [0, 0.44444, 0.03704, 0, 0.71782], - "977": [0, 0.69444, 0, 0, 0.69155], - "981": [0.19444, 0.69444, 0, 0, 0.7125], - "982": [0, 0.44444, 0.03194, 0, 0.975], - "1009": [0.19444, 0.44444, 0, 0, 0.6118], - "1013": [0, 0.44444, 0, 0, 0.48333], - "57649": [0, 0.44444, 0, 0, 0.39352], - "57911": [0.19444, 0.44444, 0, 0, 0.43889] - }, - "Math-Italic": { - "32": [0, 0, 0, 0, 0.25], - "48": [0, 0.43056, 0, 0, 0.5], - "49": [0, 0.43056, 0, 0, 0.5], - "50": [0, 0.43056, 0, 0, 0.5], - "51": [0.19444, 0.43056, 0, 0, 0.5], - "52": [0.19444, 0.43056, 0, 0, 0.5], - "53": [0.19444, 0.43056, 0, 0, 0.5], - "54": [0, 0.64444, 0, 0, 0.5], - "55": [0.19444, 0.43056, 0, 0, 0.5], - "56": [0, 0.64444, 0, 0, 0.5], - "57": [0.19444, 0.43056, 0, 0, 0.5], - "65": [0, 0.68333, 0, 0.13889, 0.75], - "66": [0, 0.68333, 0.05017, 0.08334, 0.75851], - "67": [0, 0.68333, 0.07153, 0.08334, 0.71472], - "68": [0, 0.68333, 0.02778, 0.05556, 0.82792], - "69": [0, 0.68333, 0.05764, 0.08334, 0.7382], - "70": [0, 0.68333, 0.13889, 0.08334, 0.64306], - "71": [0, 0.68333, 0, 0.08334, 0.78625], - "72": [0, 0.68333, 0.08125, 0.05556, 0.83125], - "73": [0, 0.68333, 0.07847, 0.11111, 0.43958], - "74": [0, 0.68333, 0.09618, 0.16667, 0.55451], - "75": [0, 0.68333, 0.07153, 0.05556, 0.84931], - "76": [0, 0.68333, 0, 0.02778, 0.68056], - "77": [0, 0.68333, 0.10903, 0.08334, 0.97014], - "78": [0, 0.68333, 0.10903, 0.08334, 0.80347], - "79": [0, 0.68333, 0.02778, 0.08334, 0.76278], - "80": [0, 0.68333, 0.13889, 0.08334, 0.64201], - "81": [0.19444, 0.68333, 0, 0.08334, 0.79056], - "82": [0, 0.68333, 0.00773, 0.08334, 0.75929], - "83": [0, 0.68333, 0.05764, 0.08334, 0.6132], - "84": [0, 0.68333, 0.13889, 0.08334, 0.58438], - "85": [0, 0.68333, 0.10903, 0.02778, 0.68278], - "86": [0, 0.68333, 0.22222, 0, 0.58333], - "87": [0, 0.68333, 0.13889, 0, 0.94445], - "88": [0, 0.68333, 0.07847, 0.08334, 0.82847], - "89": [0, 0.68333, 0.22222, 0, 0.58056], - "90": [0, 0.68333, 0.07153, 0.08334, 0.68264], - "97": [0, 0.43056, 0, 0, 0.52859], - "98": [0, 0.69444, 0, 0, 0.42917], - "99": [0, 0.43056, 0, 0.05556, 0.43276], - "100": [0, 0.69444, 0, 0.16667, 0.52049], - "101": [0, 0.43056, 0, 0.05556, 0.46563], - "102": [0.19444, 0.69444, 0.10764, 0.16667, 0.48959], - "103": [0.19444, 0.43056, 0.03588, 0.02778, 0.47697], - "104": [0, 0.69444, 0, 0, 0.57616], - "105": [0, 0.65952, 0, 0, 0.34451], - "106": [0.19444, 0.65952, 0.05724, 0, 0.41181], - "107": [0, 0.69444, 0.03148, 0, 0.5206], - "108": [0, 0.69444, 0.01968, 0.08334, 0.29838], - "109": [0, 0.43056, 0, 0, 0.87801], - "110": [0, 0.43056, 0, 0, 0.60023], - "111": [0, 0.43056, 0, 0.05556, 0.48472], - "112": [0.19444, 0.43056, 0, 0.08334, 0.50313], - "113": [0.19444, 0.43056, 0.03588, 0.08334, 0.44641], - "114": [0, 0.43056, 0.02778, 0.05556, 0.45116], - "115": [0, 0.43056, 0, 0.05556, 0.46875], - "116": [0, 0.61508, 0, 0.08334, 0.36111], - "117": [0, 0.43056, 0, 0.02778, 0.57246], - "118": [0, 0.43056, 0.03588, 0.02778, 0.48472], - "119": [0, 0.43056, 0.02691, 0.08334, 0.71592], - "120": [0, 0.43056, 0, 0.02778, 0.57153], - "121": [0.19444, 0.43056, 0.03588, 0.05556, 0.49028], - "122": [0, 0.43056, 0.04398, 0.05556, 0.46505], - "160": [0, 0, 0, 0, 0.25], - "915": [0, 0.68333, 0.13889, 0.08334, 0.61528], - "916": [0, 0.68333, 0, 0.16667, 0.83334], - "920": [0, 0.68333, 0.02778, 0.08334, 0.76278], - "923": [0, 0.68333, 0, 0.16667, 0.69445], - "926": [0, 0.68333, 0.07569, 0.08334, 0.74236], - "928": [0, 0.68333, 0.08125, 0.05556, 0.83125], - "931": [0, 0.68333, 0.05764, 0.08334, 0.77986], - "933": [0, 0.68333, 0.13889, 0.05556, 0.58333], - "934": [0, 0.68333, 0, 0.08334, 0.66667], - "936": [0, 0.68333, 0.11, 0.05556, 0.61222], - "937": [0, 0.68333, 0.05017, 0.08334, 0.7724], - "945": [0, 0.43056, 0.0037, 0.02778, 0.6397], - "946": [0.19444, 0.69444, 0.05278, 0.08334, 0.56563], - "947": [0.19444, 0.43056, 0.05556, 0, 0.51773], - "948": [0, 0.69444, 0.03785, 0.05556, 0.44444], - "949": [0, 0.43056, 0, 0.08334, 0.46632], - "950": [0.19444, 0.69444, 0.07378, 0.08334, 0.4375], - "951": [0.19444, 0.43056, 0.03588, 0.05556, 0.49653], - "952": [0, 0.69444, 0.02778, 0.08334, 0.46944], - "953": [0, 0.43056, 0, 0.05556, 0.35394], - "954": [0, 0.43056, 0, 0, 0.57616], - "955": [0, 0.69444, 0, 0, 0.58334], - "956": [0.19444, 0.43056, 0, 0.02778, 0.60255], - "957": [0, 0.43056, 0.06366, 0.02778, 0.49398], - "958": [0.19444, 0.69444, 0.04601, 0.11111, 0.4375], - "959": [0, 0.43056, 0, 0.05556, 0.48472], - "960": [0, 0.43056, 0.03588, 0, 0.57003], - "961": [0.19444, 0.43056, 0, 0.08334, 0.51702], - "962": [0.09722, 0.43056, 0.07986, 0.08334, 0.36285], - "963": [0, 0.43056, 0.03588, 0, 0.57141], - "964": [0, 0.43056, 0.1132, 0.02778, 0.43715], - "965": [0, 0.43056, 0.03588, 0.02778, 0.54028], - "966": [0.19444, 0.43056, 0, 0.08334, 0.65417], - "967": [0.19444, 0.43056, 0, 0.05556, 0.62569], - "968": [0.19444, 0.69444, 0.03588, 0.11111, 0.65139], - "969": [0, 0.43056, 0.03588, 0, 0.62245], - "977": [0, 0.69444, 0, 0.08334, 0.59144], - "981": [0.19444, 0.69444, 0, 0.08334, 0.59583], - "982": [0, 0.43056, 0.02778, 0, 0.82813], - "1009": [0.19444, 0.43056, 0, 0.08334, 0.51702], - "1013": [0, 0.43056, 0, 0.05556, 0.4059], - "57649": [0, 0.43056, 0, 0.02778, 0.32246], - "57911": [0.19444, 0.43056, 0, 0.08334, 0.38403] - }, - "SansSerif-Bold": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0, 0, 0.36667], - "34": [0, 0.69444, 0, 0, 0.55834], - "35": [0.19444, 0.69444, 0, 0, 0.91667], - "36": [0.05556, 0.75, 0, 0, 0.55], - "37": [0.05556, 0.75, 0, 0, 1.02912], - "38": [0, 0.69444, 0, 0, 0.83056], - "39": [0, 0.69444, 0, 0, 0.30556], - "40": [0.25, 0.75, 0, 0, 0.42778], - "41": [0.25, 0.75, 0, 0, 0.42778], - "42": [0, 0.75, 0, 0, 0.55], - "43": [0.11667, 0.61667, 0, 0, 0.85556], - "44": [0.10556, 0.13056, 0, 0, 0.30556], - "45": [0, 0.45833, 0, 0, 0.36667], - "46": [0, 0.13056, 0, 0, 0.30556], - "47": [0.25, 0.75, 0, 0, 0.55], - "48": [0, 0.69444, 0, 0, 0.55], - "49": [0, 0.69444, 0, 0, 0.55], - "50": [0, 0.69444, 0, 0, 0.55], - "51": [0, 0.69444, 0, 0, 0.55], - "52": [0, 0.69444, 0, 0, 0.55], - "53": [0, 0.69444, 0, 0, 0.55], - "54": [0, 0.69444, 0, 0, 0.55], - "55": [0, 0.69444, 0, 0, 0.55], - "56": [0, 0.69444, 0, 0, 0.55], - "57": [0, 0.69444, 0, 0, 0.55], - "58": [0, 0.45833, 0, 0, 0.30556], - "59": [0.10556, 0.45833, 0, 0, 0.30556], - "61": [-0.09375, 0.40625, 0, 0, 0.85556], - "63": [0, 0.69444, 0, 0, 0.51945], - "64": [0, 0.69444, 0, 0, 0.73334], - "65": [0, 0.69444, 0, 0, 0.73334], - "66": [0, 0.69444, 0, 0, 0.73334], - "67": [0, 0.69444, 0, 0, 0.70278], - "68": [0, 0.69444, 0, 0, 0.79445], - "69": [0, 0.69444, 0, 0, 0.64167], - "70": [0, 0.69444, 0, 0, 0.61111], - "71": [0, 0.69444, 0, 0, 0.73334], - "72": [0, 0.69444, 0, 0, 0.79445], - "73": [0, 0.69444, 0, 0, 0.33056], - "74": [0, 0.69444, 0, 0, 0.51945], - "75": [0, 0.69444, 0, 0, 0.76389], - "76": [0, 0.69444, 0, 0, 0.58056], - "77": [0, 0.69444, 0, 0, 0.97778], - "78": [0, 0.69444, 0, 0, 0.79445], - "79": [0, 0.69444, 0, 0, 0.79445], - "80": [0, 0.69444, 0, 0, 0.70278], - "81": [0.10556, 0.69444, 0, 0, 0.79445], - "82": [0, 0.69444, 0, 0, 0.70278], - "83": [0, 0.69444, 0, 0, 0.61111], - "84": [0, 0.69444, 0, 0, 0.73334], - "85": [0, 0.69444, 0, 0, 0.76389], - "86": [0, 0.69444, 0.01528, 0, 0.73334], - "87": [0, 0.69444, 0.01528, 0, 1.03889], - "88": [0, 0.69444, 0, 0, 0.73334], - "89": [0, 0.69444, 0.0275, 0, 0.73334], - "90": [0, 0.69444, 0, 0, 0.67223], - "91": [0.25, 0.75, 0, 0, 0.34306], - "93": [0.25, 0.75, 0, 0, 0.34306], - "94": [0, 0.69444, 0, 0, 0.55], - "95": [0.35, 0.10833, 0.03056, 0, 0.55], - "97": [0, 0.45833, 0, 0, 0.525], - "98": [0, 0.69444, 0, 0, 0.56111], - "99": [0, 0.45833, 0, 0, 0.48889], - "100": [0, 0.69444, 0, 0, 0.56111], - "101": [0, 0.45833, 0, 0, 0.51111], - "102": [0, 0.69444, 0.07639, 0, 0.33611], - "103": [0.19444, 0.45833, 0.01528, 0, 0.55], - "104": [0, 0.69444, 0, 0, 0.56111], - "105": [0, 0.69444, 0, 0, 0.25556], - "106": [0.19444, 0.69444, 0, 0, 0.28611], - "107": [0, 0.69444, 0, 0, 0.53056], - "108": [0, 0.69444, 0, 0, 0.25556], - "109": [0, 0.45833, 0, 0, 0.86667], - "110": [0, 0.45833, 0, 0, 0.56111], - "111": [0, 0.45833, 0, 0, 0.55], - "112": [0.19444, 0.45833, 0, 0, 0.56111], - "113": [0.19444, 0.45833, 0, 0, 0.56111], - "114": [0, 0.45833, 0.01528, 0, 0.37222], - "115": [0, 0.45833, 0, 0, 0.42167], - "116": [0, 0.58929, 0, 0, 0.40417], - "117": [0, 0.45833, 0, 0, 0.56111], - "118": [0, 0.45833, 0.01528, 0, 0.5], - "119": [0, 0.45833, 0.01528, 0, 0.74445], - "120": [0, 0.45833, 0, 0, 0.5], - "121": [0.19444, 0.45833, 0.01528, 0, 0.5], - "122": [0, 0.45833, 0, 0, 0.47639], - "126": [0.35, 0.34444, 0, 0, 0.55], - "160": [0, 0, 0, 0, 0.25], - "168": [0, 0.69444, 0, 0, 0.55], - "176": [0, 0.69444, 0, 0, 0.73334], - "180": [0, 0.69444, 0, 0, 0.55], - "184": [0.17014, 0, 0, 0, 0.48889], - "305": [0, 0.45833, 0, 0, 0.25556], - "567": [0.19444, 0.45833, 0, 0, 0.28611], - "710": [0, 0.69444, 0, 0, 0.55], - "711": [0, 0.63542, 0, 0, 0.55], - "713": [0, 0.63778, 0, 0, 0.55], - "728": [0, 0.69444, 0, 0, 0.55], - "729": [0, 0.69444, 0, 0, 0.30556], - "730": [0, 0.69444, 0, 0, 0.73334], - "732": [0, 0.69444, 0, 0, 0.55], - "733": [0, 0.69444, 0, 0, 0.55], - "915": [0, 0.69444, 0, 0, 0.58056], - "916": [0, 0.69444, 0, 0, 0.91667], - "920": [0, 0.69444, 0, 0, 0.85556], - "923": [0, 0.69444, 0, 0, 0.67223], - "926": [0, 0.69444, 0, 0, 0.73334], - "928": [0, 0.69444, 0, 0, 0.79445], - "931": [0, 0.69444, 0, 0, 0.79445], - "933": [0, 0.69444, 0, 0, 0.85556], - "934": [0, 0.69444, 0, 0, 0.79445], - "936": [0, 0.69444, 0, 0, 0.85556], - "937": [0, 0.69444, 0, 0, 0.79445], - "8211": [0, 0.45833, 0.03056, 0, 0.55], - "8212": [0, 0.45833, 0.03056, 0, 1.10001], - "8216": [0, 0.69444, 0, 0, 0.30556], - "8217": [0, 0.69444, 0, 0, 0.30556], - "8220": [0, 0.69444, 0, 0, 0.55834], - "8221": [0, 0.69444, 0, 0, 0.55834] - }, - "SansSerif-Italic": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0.05733, 0, 0.31945], - "34": [0, 0.69444, 0.00316, 0, 0.5], - "35": [0.19444, 0.69444, 0.05087, 0, 0.83334], - "36": [0.05556, 0.75, 0.11156, 0, 0.5], - "37": [0.05556, 0.75, 0.03126, 0, 0.83334], - "38": [0, 0.69444, 0.03058, 0, 0.75834], - "39": [0, 0.69444, 0.07816, 0, 0.27778], - "40": [0.25, 0.75, 0.13164, 0, 0.38889], - "41": [0.25, 0.75, 0.02536, 0, 0.38889], - "42": [0, 0.75, 0.11775, 0, 0.5], - "43": [0.08333, 0.58333, 0.02536, 0, 0.77778], - "44": [0.125, 0.08333, 0, 0, 0.27778], - "45": [0, 0.44444, 0.01946, 0, 0.33333], - "46": [0, 0.08333, 0, 0, 0.27778], - "47": [0.25, 0.75, 0.13164, 0, 0.5], - "48": [0, 0.65556, 0.11156, 0, 0.5], - "49": [0, 0.65556, 0.11156, 0, 0.5], - "50": [0, 0.65556, 0.11156, 0, 0.5], - "51": [0, 0.65556, 0.11156, 0, 0.5], - "52": [0, 0.65556, 0.11156, 0, 0.5], - "53": [0, 0.65556, 0.11156, 0, 0.5], - "54": [0, 0.65556, 0.11156, 0, 0.5], - "55": [0, 0.65556, 0.11156, 0, 0.5], - "56": [0, 0.65556, 0.11156, 0, 0.5], - "57": [0, 0.65556, 0.11156, 0, 0.5], - "58": [0, 0.44444, 0.02502, 0, 0.27778], - "59": [0.125, 0.44444, 0.02502, 0, 0.27778], - "61": [-0.13, 0.37, 0.05087, 0, 0.77778], - "63": [0, 0.69444, 0.11809, 0, 0.47222], - "64": [0, 0.69444, 0.07555, 0, 0.66667], - "65": [0, 0.69444, 0, 0, 0.66667], - "66": [0, 0.69444, 0.08293, 0, 0.66667], - "67": [0, 0.69444, 0.11983, 0, 0.63889], - "68": [0, 0.69444, 0.07555, 0, 0.72223], - "69": [0, 0.69444, 0.11983, 0, 0.59722], - "70": [0, 0.69444, 0.13372, 0, 0.56945], - "71": [0, 0.69444, 0.11983, 0, 0.66667], - "72": [0, 0.69444, 0.08094, 0, 0.70834], - "73": [0, 0.69444, 0.13372, 0, 0.27778], - "74": [0, 0.69444, 0.08094, 0, 0.47222], - "75": [0, 0.69444, 0.11983, 0, 0.69445], - "76": [0, 0.69444, 0, 0, 0.54167], - "77": [0, 0.69444, 0.08094, 0, 0.875], - "78": [0, 0.69444, 0.08094, 0, 0.70834], - "79": [0, 0.69444, 0.07555, 0, 0.73611], - "80": [0, 0.69444, 0.08293, 0, 0.63889], - "81": [0.125, 0.69444, 0.07555, 0, 0.73611], - "82": [0, 0.69444, 0.08293, 0, 0.64584], - "83": [0, 0.69444, 0.09205, 0, 0.55556], - "84": [0, 0.69444, 0.13372, 0, 0.68056], - "85": [0, 0.69444, 0.08094, 0, 0.6875], - "86": [0, 0.69444, 0.1615, 0, 0.66667], - "87": [0, 0.69444, 0.1615, 0, 0.94445], - "88": [0, 0.69444, 0.13372, 0, 0.66667], - "89": [0, 0.69444, 0.17261, 0, 0.66667], - "90": [0, 0.69444, 0.11983, 0, 0.61111], - "91": [0.25, 0.75, 0.15942, 0, 0.28889], - "93": [0.25, 0.75, 0.08719, 0, 0.28889], - "94": [0, 0.69444, 0.0799, 0, 0.5], - "95": [0.35, 0.09444, 0.08616, 0, 0.5], - "97": [0, 0.44444, 0.00981, 0, 0.48056], - "98": [0, 0.69444, 0.03057, 0, 0.51667], - "99": [0, 0.44444, 0.08336, 0, 0.44445], - "100": [0, 0.69444, 0.09483, 0, 0.51667], - "101": [0, 0.44444, 0.06778, 0, 0.44445], - "102": [0, 0.69444, 0.21705, 0, 0.30556], - "103": [0.19444, 0.44444, 0.10836, 0, 0.5], - "104": [0, 0.69444, 0.01778, 0, 0.51667], - "105": [0, 0.67937, 0.09718, 0, 0.23889], - "106": [0.19444, 0.67937, 0.09162, 0, 0.26667], - "107": [0, 0.69444, 0.08336, 0, 0.48889], - "108": [0, 0.69444, 0.09483, 0, 0.23889], - "109": [0, 0.44444, 0.01778, 0, 0.79445], - "110": [0, 0.44444, 0.01778, 0, 0.51667], - "111": [0, 0.44444, 0.06613, 0, 0.5], - "112": [0.19444, 0.44444, 0.0389, 0, 0.51667], - "113": [0.19444, 0.44444, 0.04169, 0, 0.51667], - "114": [0, 0.44444, 0.10836, 0, 0.34167], - "115": [0, 0.44444, 0.0778, 0, 0.38333], - "116": [0, 0.57143, 0.07225, 0, 0.36111], - "117": [0, 0.44444, 0.04169, 0, 0.51667], - "118": [0, 0.44444, 0.10836, 0, 0.46111], - "119": [0, 0.44444, 0.10836, 0, 0.68334], - "120": [0, 0.44444, 0.09169, 0, 0.46111], - "121": [0.19444, 0.44444, 0.10836, 0, 0.46111], - "122": [0, 0.44444, 0.08752, 0, 0.43472], - "126": [0.35, 0.32659, 0.08826, 0, 0.5], - "160": [0, 0, 0, 0, 0.25], - "168": [0, 0.67937, 0.06385, 0, 0.5], - "176": [0, 0.69444, 0, 0, 0.73752], - "184": [0.17014, 0, 0, 0, 0.44445], - "305": [0, 0.44444, 0.04169, 0, 0.23889], - "567": [0.19444, 0.44444, 0.04169, 0, 0.26667], - "710": [0, 0.69444, 0.0799, 0, 0.5], - "711": [0, 0.63194, 0.08432, 0, 0.5], - "713": [0, 0.60889, 0.08776, 0, 0.5], - "714": [0, 0.69444, 0.09205, 0, 0.5], - "715": [0, 0.69444, 0, 0, 0.5], - "728": [0, 0.69444, 0.09483, 0, 0.5], - "729": [0, 0.67937, 0.07774, 0, 0.27778], - "730": [0, 0.69444, 0, 0, 0.73752], - "732": [0, 0.67659, 0.08826, 0, 0.5], - "733": [0, 0.69444, 0.09205, 0, 0.5], - "915": [0, 0.69444, 0.13372, 0, 0.54167], - "916": [0, 0.69444, 0, 0, 0.83334], - "920": [0, 0.69444, 0.07555, 0, 0.77778], - "923": [0, 0.69444, 0, 0, 0.61111], - "926": [0, 0.69444, 0.12816, 0, 0.66667], - "928": [0, 0.69444, 0.08094, 0, 0.70834], - "931": [0, 0.69444, 0.11983, 0, 0.72222], - "933": [0, 0.69444, 0.09031, 0, 0.77778], - "934": [0, 0.69444, 0.04603, 0, 0.72222], - "936": [0, 0.69444, 0.09031, 0, 0.77778], - "937": [0, 0.69444, 0.08293, 0, 0.72222], - "8211": [0, 0.44444, 0.08616, 0, 0.5], - "8212": [0, 0.44444, 0.08616, 0, 1.0], - "8216": [0, 0.69444, 0.07816, 0, 0.27778], - "8217": [0, 0.69444, 0.07816, 0, 0.27778], - "8220": [0, 0.69444, 0.14205, 0, 0.5], - "8221": [0, 0.69444, 0.00316, 0, 0.5] - }, - "SansSerif-Regular": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0, 0, 0.31945], - "34": [0, 0.69444, 0, 0, 0.5], - "35": [0.19444, 0.69444, 0, 0, 0.83334], - "36": [0.05556, 0.75, 0, 0, 0.5], - "37": [0.05556, 0.75, 0, 0, 0.83334], - "38": [0, 0.69444, 0, 0, 0.75834], - "39": [0, 0.69444, 0, 0, 0.27778], - "40": [0.25, 0.75, 0, 0, 0.38889], - "41": [0.25, 0.75, 0, 0, 0.38889], - "42": [0, 0.75, 0, 0, 0.5], - "43": [0.08333, 0.58333, 0, 0, 0.77778], - "44": [0.125, 0.08333, 0, 0, 0.27778], - "45": [0, 0.44444, 0, 0, 0.33333], - "46": [0, 0.08333, 0, 0, 0.27778], - "47": [0.25, 0.75, 0, 0, 0.5], - "48": [0, 0.65556, 0, 0, 0.5], - "49": [0, 0.65556, 0, 0, 0.5], - "50": [0, 0.65556, 0, 0, 0.5], - "51": [0, 0.65556, 0, 0, 0.5], - "52": [0, 0.65556, 0, 0, 0.5], - "53": [0, 0.65556, 0, 0, 0.5], - "54": [0, 0.65556, 0, 0, 0.5], - "55": [0, 0.65556, 0, 0, 0.5], - "56": [0, 0.65556, 0, 0, 0.5], - "57": [0, 0.65556, 0, 0, 0.5], - "58": [0, 0.44444, 0, 0, 0.27778], - "59": [0.125, 0.44444, 0, 0, 0.27778], - "61": [-0.13, 0.37, 0, 0, 0.77778], - "63": [0, 0.69444, 0, 0, 0.47222], - "64": [0, 0.69444, 0, 0, 0.66667], - "65": [0, 0.69444, 0, 0, 0.66667], - "66": [0, 0.69444, 0, 0, 0.66667], - "67": [0, 0.69444, 0, 0, 0.63889], - "68": [0, 0.69444, 0, 0, 0.72223], - "69": [0, 0.69444, 0, 0, 0.59722], - "70": [0, 0.69444, 0, 0, 0.56945], - "71": [0, 0.69444, 0, 0, 0.66667], - "72": [0, 0.69444, 0, 0, 0.70834], - "73": [0, 0.69444, 0, 0, 0.27778], - "74": [0, 0.69444, 0, 0, 0.47222], - "75": [0, 0.69444, 0, 0, 0.69445], - "76": [0, 0.69444, 0, 0, 0.54167], - "77": [0, 0.69444, 0, 0, 0.875], - "78": [0, 0.69444, 0, 0, 0.70834], - "79": [0, 0.69444, 0, 0, 0.73611], - "80": [0, 0.69444, 0, 0, 0.63889], - "81": [0.125, 0.69444, 0, 0, 0.73611], - "82": [0, 0.69444, 0, 0, 0.64584], - "83": [0, 0.69444, 0, 0, 0.55556], - "84": [0, 0.69444, 0, 0, 0.68056], - "85": [0, 0.69444, 0, 0, 0.6875], - "86": [0, 0.69444, 0.01389, 0, 0.66667], - "87": [0, 0.69444, 0.01389, 0, 0.94445], - "88": [0, 0.69444, 0, 0, 0.66667], - "89": [0, 0.69444, 0.025, 0, 0.66667], - "90": [0, 0.69444, 0, 0, 0.61111], - "91": [0.25, 0.75, 0, 0, 0.28889], - "93": [0.25, 0.75, 0, 0, 0.28889], - "94": [0, 0.69444, 0, 0, 0.5], - "95": [0.35, 0.09444, 0.02778, 0, 0.5], - "97": [0, 0.44444, 0, 0, 0.48056], - "98": [0, 0.69444, 0, 0, 0.51667], - "99": [0, 0.44444, 0, 0, 0.44445], - "100": [0, 0.69444, 0, 0, 0.51667], - "101": [0, 0.44444, 0, 0, 0.44445], - "102": [0, 0.69444, 0.06944, 0, 0.30556], - "103": [0.19444, 0.44444, 0.01389, 0, 0.5], - "104": [0, 0.69444, 0, 0, 0.51667], - "105": [0, 0.67937, 0, 0, 0.23889], - "106": [0.19444, 0.67937, 0, 0, 0.26667], - "107": [0, 0.69444, 0, 0, 0.48889], - "108": [0, 0.69444, 0, 0, 0.23889], - "109": [0, 0.44444, 0, 0, 0.79445], - "110": [0, 0.44444, 0, 0, 0.51667], - "111": [0, 0.44444, 0, 0, 0.5], - "112": [0.19444, 0.44444, 0, 0, 0.51667], - "113": [0.19444, 0.44444, 0, 0, 0.51667], - "114": [0, 0.44444, 0.01389, 0, 0.34167], - "115": [0, 0.44444, 0, 0, 0.38333], - "116": [0, 0.57143, 0, 0, 0.36111], - "117": [0, 0.44444, 0, 0, 0.51667], - "118": [0, 0.44444, 0.01389, 0, 0.46111], - "119": [0, 0.44444, 0.01389, 0, 0.68334], - "120": [0, 0.44444, 0, 0, 0.46111], - "121": [0.19444, 0.44444, 0.01389, 0, 0.46111], - "122": [0, 0.44444, 0, 0, 0.43472], - "126": [0.35, 0.32659, 0, 0, 0.5], - "160": [0, 0, 0, 0, 0.25], - "168": [0, 0.67937, 0, 0, 0.5], - "176": [0, 0.69444, 0, 0, 0.66667], - "184": [0.17014, 0, 0, 0, 0.44445], - "305": [0, 0.44444, 0, 0, 0.23889], - "567": [0.19444, 0.44444, 0, 0, 0.26667], - "710": [0, 0.69444, 0, 0, 0.5], - "711": [0, 0.63194, 0, 0, 0.5], - "713": [0, 0.60889, 0, 0, 0.5], - "714": [0, 0.69444, 0, 0, 0.5], - "715": [0, 0.69444, 0, 0, 0.5], - "728": [0, 0.69444, 0, 0, 0.5], - "729": [0, 0.67937, 0, 0, 0.27778], - "730": [0, 0.69444, 0, 0, 0.66667], - "732": [0, 0.67659, 0, 0, 0.5], - "733": [0, 0.69444, 0, 0, 0.5], - "915": [0, 0.69444, 0, 0, 0.54167], - "916": [0, 0.69444, 0, 0, 0.83334], - "920": [0, 0.69444, 0, 0, 0.77778], - "923": [0, 0.69444, 0, 0, 0.61111], - "926": [0, 0.69444, 0, 0, 0.66667], - "928": [0, 0.69444, 0, 0, 0.70834], - "931": [0, 0.69444, 0, 0, 0.72222], - "933": [0, 0.69444, 0, 0, 0.77778], - "934": [0, 0.69444, 0, 0, 0.72222], - "936": [0, 0.69444, 0, 0, 0.77778], - "937": [0, 0.69444, 0, 0, 0.72222], - "8211": [0, 0.44444, 0.02778, 0, 0.5], - "8212": [0, 0.44444, 0.02778, 0, 1.0], - "8216": [0, 0.69444, 0, 0, 0.27778], - "8217": [0, 0.69444, 0, 0, 0.27778], - "8220": [0, 0.69444, 0, 0, 0.5], - "8221": [0, 0.69444, 0, 0, 0.5] - }, - "Script-Regular": { - "32": [0, 0, 0, 0, 0.25], - "65": [0, 0.7, 0.22925, 0, 0.80253], - "66": [0, 0.7, 0.04087, 0, 0.90757], - "67": [0, 0.7, 0.1689, 0, 0.66619], - "68": [0, 0.7, 0.09371, 0, 0.77443], - "69": [0, 0.7, 0.18583, 0, 0.56162], - "70": [0, 0.7, 0.13634, 0, 0.89544], - "71": [0, 0.7, 0.17322, 0, 0.60961], - "72": [0, 0.7, 0.29694, 0, 0.96919], - "73": [0, 0.7, 0.19189, 0, 0.80907], - "74": [0.27778, 0.7, 0.19189, 0, 1.05159], - "75": [0, 0.7, 0.31259, 0, 0.91364], - "76": [0, 0.7, 0.19189, 0, 0.87373], - "77": [0, 0.7, 0.15981, 0, 1.08031], - "78": [0, 0.7, 0.3525, 0, 0.9015], - "79": [0, 0.7, 0.08078, 0, 0.73787], - "80": [0, 0.7, 0.08078, 0, 1.01262], - "81": [0, 0.7, 0.03305, 0, 0.88282], - "82": [0, 0.7, 0.06259, 0, 0.85], - "83": [0, 0.7, 0.19189, 0, 0.86767], - "84": [0, 0.7, 0.29087, 0, 0.74697], - "85": [0, 0.7, 0.25815, 0, 0.79996], - "86": [0, 0.7, 0.27523, 0, 0.62204], - "87": [0, 0.7, 0.27523, 0, 0.80532], - "88": [0, 0.7, 0.26006, 0, 0.94445], - "89": [0, 0.7, 0.2939, 0, 0.70961], - "90": [0, 0.7, 0.24037, 0, 0.8212], - "160": [0, 0, 0, 0, 0.25] - }, - "Size1-Regular": { - "32": [0, 0, 0, 0, 0.25], - "40": [0.35001, 0.85, 0, 0, 0.45834], - "41": [0.35001, 0.85, 0, 0, 0.45834], - "47": [0.35001, 0.85, 0, 0, 0.57778], - "91": [0.35001, 0.85, 0, 0, 0.41667], - "92": [0.35001, 0.85, 0, 0, 0.57778], - "93": [0.35001, 0.85, 0, 0, 0.41667], - "123": [0.35001, 0.85, 0, 0, 0.58334], - "125": [0.35001, 0.85, 0, 0, 0.58334], - "160": [0, 0, 0, 0, 0.25], - "710": [0, 0.72222, 0, 0, 0.55556], - "732": [0, 0.72222, 0, 0, 0.55556], - "770": [0, 0.72222, 0, 0, 0.55556], - "771": [0, 0.72222, 0, 0, 0.55556], - "8214": [-0.00099, 0.601, 0, 0, 0.77778], - "8593": [1e-05, 0.6, 0, 0, 0.66667], - "8595": [1e-05, 0.6, 0, 0, 0.66667], - "8657": [1e-05, 0.6, 0, 0, 0.77778], - "8659": [1e-05, 0.6, 0, 0, 0.77778], - "8719": [0.25001, 0.75, 0, 0, 0.94445], - "8720": [0.25001, 0.75, 0, 0, 0.94445], - "8721": [0.25001, 0.75, 0, 0, 1.05556], - "8730": [0.35001, 0.85, 0, 0, 1.0], - "8739": [-0.00599, 0.606, 0, 0, 0.33333], - "8741": [-0.00599, 0.606, 0, 0, 0.55556], - "8747": [0.30612, 0.805, 0.19445, 0, 0.47222], - "8748": [0.306, 0.805, 0.19445, 0, 0.47222], - "8749": [0.306, 0.805, 0.19445, 0, 0.47222], - "8750": [0.30612, 0.805, 0.19445, 0, 0.47222], - "8896": [0.25001, 0.75, 0, 0, 0.83334], - "8897": [0.25001, 0.75, 0, 0, 0.83334], - "8898": [0.25001, 0.75, 0, 0, 0.83334], - "8899": [0.25001, 0.75, 0, 0, 0.83334], - "8968": [0.35001, 0.85, 0, 0, 0.47222], - "8969": [0.35001, 0.85, 0, 0, 0.47222], - "8970": [0.35001, 0.85, 0, 0, 0.47222], - "8971": [0.35001, 0.85, 0, 0, 0.47222], - "9168": [-0.00099, 0.601, 0, 0, 0.66667], - "10216": [0.35001, 0.85, 0, 0, 0.47222], - "10217": [0.35001, 0.85, 0, 0, 0.47222], - "10752": [0.25001, 0.75, 0, 0, 1.11111], - "10753": [0.25001, 0.75, 0, 0, 1.11111], - "10754": [0.25001, 0.75, 0, 0, 1.11111], - "10756": [0.25001, 0.75, 0, 0, 0.83334], - "10758": [0.25001, 0.75, 0, 0, 0.83334] - }, - "Size2-Regular": { - "32": [0, 0, 0, 0, 0.25], - "40": [0.65002, 1.15, 0, 0, 0.59722], - "41": [0.65002, 1.15, 0, 0, 0.59722], - "47": [0.65002, 1.15, 0, 0, 0.81111], - "91": [0.65002, 1.15, 0, 0, 0.47222], - "92": [0.65002, 1.15, 0, 0, 0.81111], - "93": [0.65002, 1.15, 0, 0, 0.47222], - "123": [0.65002, 1.15, 0, 0, 0.66667], - "125": [0.65002, 1.15, 0, 0, 0.66667], - "160": [0, 0, 0, 0, 0.25], - "710": [0, 0.75, 0, 0, 1.0], - "732": [0, 0.75, 0, 0, 1.0], - "770": [0, 0.75, 0, 0, 1.0], - "771": [0, 0.75, 0, 0, 1.0], - "8719": [0.55001, 1.05, 0, 0, 1.27778], - "8720": [0.55001, 1.05, 0, 0, 1.27778], - "8721": [0.55001, 1.05, 0, 0, 1.44445], - "8730": [0.65002, 1.15, 0, 0, 1.0], - "8747": [0.86225, 1.36, 0.44445, 0, 0.55556], - "8748": [0.862, 1.36, 0.44445, 0, 0.55556], - "8749": [0.862, 1.36, 0.44445, 0, 0.55556], - "8750": [0.86225, 1.36, 0.44445, 0, 0.55556], - "8896": [0.55001, 1.05, 0, 0, 1.11111], - "8897": [0.55001, 1.05, 0, 0, 1.11111], - "8898": [0.55001, 1.05, 0, 0, 1.11111], - "8899": [0.55001, 1.05, 0, 0, 1.11111], - "8968": [0.65002, 1.15, 0, 0, 0.52778], - "8969": [0.65002, 1.15, 0, 0, 0.52778], - "8970": [0.65002, 1.15, 0, 0, 0.52778], - "8971": [0.65002, 1.15, 0, 0, 0.52778], - "10216": [0.65002, 1.15, 0, 0, 0.61111], - "10217": [0.65002, 1.15, 0, 0, 0.61111], - "10752": [0.55001, 1.05, 0, 0, 1.51112], - "10753": [0.55001, 1.05, 0, 0, 1.51112], - "10754": [0.55001, 1.05, 0, 0, 1.51112], - "10756": [0.55001, 1.05, 0, 0, 1.11111], - "10758": [0.55001, 1.05, 0, 0, 1.11111] - }, - "Size3-Regular": { - "32": [0, 0, 0, 0, 0.25], - "40": [0.95003, 1.45, 0, 0, 0.73611], - "41": [0.95003, 1.45, 0, 0, 0.73611], - "47": [0.95003, 1.45, 0, 0, 1.04445], - "91": [0.95003, 1.45, 0, 0, 0.52778], - "92": [0.95003, 1.45, 0, 0, 1.04445], - "93": [0.95003, 1.45, 0, 0, 0.52778], - "123": [0.95003, 1.45, 0, 0, 0.75], - "125": [0.95003, 1.45, 0, 0, 0.75], - "160": [0, 0, 0, 0, 0.25], - "710": [0, 0.75, 0, 0, 1.44445], - "732": [0, 0.75, 0, 0, 1.44445], - "770": [0, 0.75, 0, 0, 1.44445], - "771": [0, 0.75, 0, 0, 1.44445], - "8730": [0.95003, 1.45, 0, 0, 1.0], - "8968": [0.95003, 1.45, 0, 0, 0.58334], - "8969": [0.95003, 1.45, 0, 0, 0.58334], - "8970": [0.95003, 1.45, 0, 0, 0.58334], - "8971": [0.95003, 1.45, 0, 0, 0.58334], - "10216": [0.95003, 1.45, 0, 0, 0.75], - "10217": [0.95003, 1.45, 0, 0, 0.75] - }, - "Size4-Regular": { - "32": [0, 0, 0, 0, 0.25], - "40": [1.25003, 1.75, 0, 0, 0.79167], - "41": [1.25003, 1.75, 0, 0, 0.79167], - "47": [1.25003, 1.75, 0, 0, 1.27778], - "91": [1.25003, 1.75, 0, 0, 0.58334], - "92": [1.25003, 1.75, 0, 0, 1.27778], - "93": [1.25003, 1.75, 0, 0, 0.58334], - "123": [1.25003, 1.75, 0, 0, 0.80556], - "125": [1.25003, 1.75, 0, 0, 0.80556], - "160": [0, 0, 0, 0, 0.25], - "710": [0, 0.825, 0, 0, 1.8889], - "732": [0, 0.825, 0, 0, 1.8889], - "770": [0, 0.825, 0, 0, 1.8889], - "771": [0, 0.825, 0, 0, 1.8889], - "8730": [1.25003, 1.75, 0, 0, 1.0], - "8968": [1.25003, 1.75, 0, 0, 0.63889], - "8969": [1.25003, 1.75, 0, 0, 0.63889], - "8970": [1.25003, 1.75, 0, 0, 0.63889], - "8971": [1.25003, 1.75, 0, 0, 0.63889], - "9115": [0.64502, 1.155, 0, 0, 0.875], - "9116": [1e-05, 0.6, 0, 0, 0.875], - "9117": [0.64502, 1.155, 0, 0, 0.875], - "9118": [0.64502, 1.155, 0, 0, 0.875], - "9119": [1e-05, 0.6, 0, 0, 0.875], - "9120": [0.64502, 1.155, 0, 0, 0.875], - "9121": [0.64502, 1.155, 0, 0, 0.66667], - "9122": [-0.00099, 0.601, 0, 0, 0.66667], - "9123": [0.64502, 1.155, 0, 0, 0.66667], - "9124": [0.64502, 1.155, 0, 0, 0.66667], - "9125": [-0.00099, 0.601, 0, 0, 0.66667], - "9126": [0.64502, 1.155, 0, 0, 0.66667], - "9127": [1e-05, 0.9, 0, 0, 0.88889], - "9128": [0.65002, 1.15, 0, 0, 0.88889], - "9129": [0.90001, 0, 0, 0, 0.88889], - "9130": [0, 0.3, 0, 0, 0.88889], - "9131": [1e-05, 0.9, 0, 0, 0.88889], - "9132": [0.65002, 1.15, 0, 0, 0.88889], - "9133": [0.90001, 0, 0, 0, 0.88889], - "9143": [0.88502, 0.915, 0, 0, 1.05556], - "10216": [1.25003, 1.75, 0, 0, 0.80556], - "10217": [1.25003, 1.75, 0, 0, 0.80556], - "57344": [-0.00499, 0.605, 0, 0, 1.05556], - "57345": [-0.00499, 0.605, 0, 0, 1.05556], - "57680": [0, 0.12, 0, 0, 0.45], - "57681": [0, 0.12, 0, 0, 0.45], - "57682": [0, 0.12, 0, 0, 0.45], - "57683": [0, 0.12, 0, 0, 0.45] - }, - "Typewriter-Regular": { - "32": [0, 0, 0, 0, 0.525], - "33": [0, 0.61111, 0, 0, 0.525], - "34": [0, 0.61111, 0, 0, 0.525], - "35": [0, 0.61111, 0, 0, 0.525], - "36": [0.08333, 0.69444, 0, 0, 0.525], - "37": [0.08333, 0.69444, 0, 0, 0.525], - "38": [0, 0.61111, 0, 0, 0.525], - "39": [0, 0.61111, 0, 0, 0.525], - "40": [0.08333, 0.69444, 0, 0, 0.525], - "41": [0.08333, 0.69444, 0, 0, 0.525], - "42": [0, 0.52083, 0, 0, 0.525], - "43": [-0.08056, 0.53055, 0, 0, 0.525], - "44": [0.13889, 0.125, 0, 0, 0.525], - "45": [-0.08056, 0.53055, 0, 0, 0.525], - "46": [0, 0.125, 0, 0, 0.525], - "47": [0.08333, 0.69444, 0, 0, 0.525], - "48": [0, 0.61111, 0, 0, 0.525], - "49": [0, 0.61111, 0, 0, 0.525], - "50": [0, 0.61111, 0, 0, 0.525], - "51": [0, 0.61111, 0, 0, 0.525], - "52": [0, 0.61111, 0, 0, 0.525], - "53": [0, 0.61111, 0, 0, 0.525], - "54": [0, 0.61111, 0, 0, 0.525], - "55": [0, 0.61111, 0, 0, 0.525], - "56": [0, 0.61111, 0, 0, 0.525], - "57": [0, 0.61111, 0, 0, 0.525], - "58": [0, 0.43056, 0, 0, 0.525], - "59": [0.13889, 0.43056, 0, 0, 0.525], - "60": [-0.05556, 0.55556, 0, 0, 0.525], - "61": [-0.19549, 0.41562, 0, 0, 0.525], - "62": [-0.05556, 0.55556, 0, 0, 0.525], - "63": [0, 0.61111, 0, 0, 0.525], - "64": [0, 0.61111, 0, 0, 0.525], - "65": [0, 0.61111, 0, 0, 0.525], - "66": [0, 0.61111, 0, 0, 0.525], - "67": [0, 0.61111, 0, 0, 0.525], - "68": [0, 0.61111, 0, 0, 0.525], - "69": [0, 0.61111, 0, 0, 0.525], - "70": [0, 0.61111, 0, 0, 0.525], - "71": [0, 0.61111, 0, 0, 0.525], - "72": [0, 0.61111, 0, 0, 0.525], - "73": [0, 0.61111, 0, 0, 0.525], - "74": [0, 0.61111, 0, 0, 0.525], - "75": [0, 0.61111, 0, 0, 0.525], - "76": [0, 0.61111, 0, 0, 0.525], - "77": [0, 0.61111, 0, 0, 0.525], - "78": [0, 0.61111, 0, 0, 0.525], - "79": [0, 0.61111, 0, 0, 0.525], - "80": [0, 0.61111, 0, 0, 0.525], - "81": [0.13889, 0.61111, 0, 0, 0.525], - "82": [0, 0.61111, 0, 0, 0.525], - "83": [0, 0.61111, 0, 0, 0.525], - "84": [0, 0.61111, 0, 0, 0.525], - "85": [0, 0.61111, 0, 0, 0.525], - "86": [0, 0.61111, 0, 0, 0.525], - "87": [0, 0.61111, 0, 0, 0.525], - "88": [0, 0.61111, 0, 0, 0.525], - "89": [0, 0.61111, 0, 0, 0.525], - "90": [0, 0.61111, 0, 0, 0.525], - "91": [0.08333, 0.69444, 0, 0, 0.525], - "92": [0.08333, 0.69444, 0, 0, 0.525], - "93": [0.08333, 0.69444, 0, 0, 0.525], - "94": [0, 0.61111, 0, 0, 0.525], - "95": [0.09514, 0, 0, 0, 0.525], - "96": [0, 0.61111, 0, 0, 0.525], - "97": [0, 0.43056, 0, 0, 0.525], - "98": [0, 0.61111, 0, 0, 0.525], - "99": [0, 0.43056, 0, 0, 0.525], - "100": [0, 0.61111, 0, 0, 0.525], - "101": [0, 0.43056, 0, 0, 0.525], - "102": [0, 0.61111, 0, 0, 0.525], - "103": [0.22222, 0.43056, 0, 0, 0.525], - "104": [0, 0.61111, 0, 0, 0.525], - "105": [0, 0.61111, 0, 0, 0.525], - "106": [0.22222, 0.61111, 0, 0, 0.525], - "107": [0, 0.61111, 0, 0, 0.525], - "108": [0, 0.61111, 0, 0, 0.525], - "109": [0, 0.43056, 0, 0, 0.525], - "110": [0, 0.43056, 0, 0, 0.525], - "111": [0, 0.43056, 0, 0, 0.525], - "112": [0.22222, 0.43056, 0, 0, 0.525], - "113": [0.22222, 0.43056, 0, 0, 0.525], - "114": [0, 0.43056, 0, 0, 0.525], - "115": [0, 0.43056, 0, 0, 0.525], - "116": [0, 0.55358, 0, 0, 0.525], - "117": [0, 0.43056, 0, 0, 0.525], - "118": [0, 0.43056, 0, 0, 0.525], - "119": [0, 0.43056, 0, 0, 0.525], - "120": [0, 0.43056, 0, 0, 0.525], - "121": [0.22222, 0.43056, 0, 0, 0.525], - "122": [0, 0.43056, 0, 0, 0.525], - "123": [0.08333, 0.69444, 0, 0, 0.525], - "124": [0.08333, 0.69444, 0, 0, 0.525], - "125": [0.08333, 0.69444, 0, 0, 0.525], - "126": [0, 0.61111, 0, 0, 0.525], - "127": [0, 0.61111, 0, 0, 0.525], - "160": [0, 0, 0, 0, 0.525], - "176": [0, 0.61111, 0, 0, 0.525], - "184": [0.19445, 0, 0, 0, 0.525], - "305": [0, 0.43056, 0, 0, 0.525], - "567": [0.22222, 0.43056, 0, 0, 0.525], - "711": [0, 0.56597, 0, 0, 0.525], - "713": [0, 0.56555, 0, 0, 0.525], - "714": [0, 0.61111, 0, 0, 0.525], - "715": [0, 0.61111, 0, 0, 0.525], - "728": [0, 0.61111, 0, 0, 0.525], - "730": [0, 0.61111, 0, 0, 0.525], - "770": [0, 0.61111, 0, 0, 0.525], - "771": [0, 0.61111, 0, 0, 0.525], - "776": [0, 0.61111, 0, 0, 0.525], - "915": [0, 0.61111, 0, 0, 0.525], - "916": [0, 0.61111, 0, 0, 0.525], - "920": [0, 0.61111, 0, 0, 0.525], - "923": [0, 0.61111, 0, 0, 0.525], - "926": [0, 0.61111, 0, 0, 0.525], - "928": [0, 0.61111, 0, 0, 0.525], - "931": [0, 0.61111, 0, 0, 0.525], - "933": [0, 0.61111, 0, 0, 0.525], - "934": [0, 0.61111, 0, 0, 0.525], - "936": [0, 0.61111, 0, 0, 0.525], - "937": [0, 0.61111, 0, 0, 0.525], - "8216": [0, 0.61111, 0, 0, 0.525], - "8217": [0, 0.61111, 0, 0, 0.525], - "8242": [0, 0.61111, 0, 0, 0.525], - "9251": [0.11111, 0.21944, 0, 0, 0.525] - } -}); -// CONCATENATED MODULE: ./src/fontMetrics.js - - -/** - * This file contains metrics regarding fonts and individual symbols. The sigma - * and xi variables, as well as the metricMap map contain data extracted from - * TeX, TeX font metrics, and the TTF files. These data are then exposed via the - * `metrics` variable and the getCharacterMetrics function. - */ -// In TeX, there are actually three sets of dimensions, one for each of -// textstyle (size index 5 and higher: >=9pt), scriptstyle (size index 3 and 4: -// 7-8pt), and scriptscriptstyle (size index 1 and 2: 5-6pt). These are -// provided in the arrays below, in that order. -// -// The font metrics are stored in fonts cmsy10, cmsy7, and cmsy5 respsectively. -// This was determined by running the following script: -// -// latex -interaction=nonstopmode \ -// '\documentclass{article}\usepackage{amsmath}\begin{document}' \ -// '$a$ \expandafter\show\the\textfont2' \ -// '\expandafter\show\the\scriptfont2' \ -// '\expandafter\show\the\scriptscriptfont2' \ -// '\stop' -// -// The metrics themselves were retrieved using the following commands: -// -// tftopl cmsy10 -// tftopl cmsy7 -// tftopl cmsy5 -// -// The output of each of these commands is quite lengthy. The only part we -// care about is the FONTDIMEN section. Each value is measured in EMs. -var sigmasAndXis = { - slant: [0.250, 0.250, 0.250], - // sigma1 - space: [0.000, 0.000, 0.000], - // sigma2 - stretch: [0.000, 0.000, 0.000], - // sigma3 - shrink: [0.000, 0.000, 0.000], - // sigma4 - xHeight: [0.431, 0.431, 0.431], - // sigma5 - quad: [1.000, 1.171, 1.472], - // sigma6 - extraSpace: [0.000, 0.000, 0.000], - // sigma7 - num1: [0.677, 0.732, 0.925], - // sigma8 - num2: [0.394, 0.384, 0.387], - // sigma9 - num3: [0.444, 0.471, 0.504], - // sigma10 - denom1: [0.686, 0.752, 1.025], - // sigma11 - denom2: [0.345, 0.344, 0.532], - // sigma12 - sup1: [0.413, 0.503, 0.504], - // sigma13 - sup2: [0.363, 0.431, 0.404], - // sigma14 - sup3: [0.289, 0.286, 0.294], - // sigma15 - sub1: [0.150, 0.143, 0.200], - // sigma16 - sub2: [0.247, 0.286, 0.400], - // sigma17 - supDrop: [0.386, 0.353, 0.494], - // sigma18 - subDrop: [0.050, 0.071, 0.100], - // sigma19 - delim1: [2.390, 1.700, 1.980], - // sigma20 - delim2: [1.010, 1.157, 1.420], - // sigma21 - axisHeight: [0.250, 0.250, 0.250], - // sigma22 - // These font metrics are extracted from TeX by using tftopl on cmex10.tfm; - // they correspond to the font parameters of the extension fonts (family 3). - // See the TeXbook, page 441. In AMSTeX, the extension fonts scale; to - // match cmex7, we'd use cmex7.tfm values for script and scriptscript - // values. - defaultRuleThickness: [0.04, 0.049, 0.049], - // xi8; cmex7: 0.049 - bigOpSpacing1: [0.111, 0.111, 0.111], - // xi9 - bigOpSpacing2: [0.166, 0.166, 0.166], - // xi10 - bigOpSpacing3: [0.2, 0.2, 0.2], - // xi11 - bigOpSpacing4: [0.6, 0.611, 0.611], - // xi12; cmex7: 0.611 - bigOpSpacing5: [0.1, 0.143, 0.143], - // xi13; cmex7: 0.143 - // The \sqrt rule width is taken from the height of the surd character. - // Since we use the same font at all sizes, this thickness doesn't scale. - sqrtRuleThickness: [0.04, 0.04, 0.04], - // This value determines how large a pt is, for metrics which are defined - // in terms of pts. - // This value is also used in katex.less; if you change it make sure the - // values match. - ptPerEm: [10.0, 10.0, 10.0], - // The space between adjacent `|` columns in an array definition. From - // `\showthe\doublerulesep` in LaTeX. Equals 2.0 / ptPerEm. - doubleRuleSep: [0.2, 0.2, 0.2], - // The width of separator lines in {array} environments. From - // `\showthe\arrayrulewidth` in LaTeX. Equals 0.4 / ptPerEm. - arrayRuleWidth: [0.04, 0.04, 0.04], - // Two values from LaTeX source2e: - fboxsep: [0.3, 0.3, 0.3], - // 3 pt / ptPerEm - fboxrule: [0.04, 0.04, 0.04] // 0.4 pt / ptPerEm - -}; // This map contains a mapping from font name and character code to character -// metrics, including height, depth, italic correction, and skew (kern from the -// character to the corresponding \skewchar) -// This map is generated via `make metrics`. It should not be changed manually. - - // These are very rough approximations. We default to Times New Roman which -// should have Latin-1 and Cyrillic characters, but may not depending on the -// operating system. The metrics do not account for extra height from the -// accents. In the case of Cyrillic characters which have both ascenders and -// descenders we prefer approximations with ascenders, primarily to prevent -// the fraction bar or root line from intersecting the glyph. -// TODO(kevinb) allow union of multiple glyph metrics for better accuracy. - -var extraCharacterMap = { - // Latin-1 - 'Å': 'A', - 'Ç': 'C', - 'Ð': 'D', - 'Þ': 'o', - 'å': 'a', - 'ç': 'c', - 'ð': 'd', - 'þ': 'o', - // Cyrillic - 'А': 'A', - 'Б': 'B', - 'В': 'B', - 'Г': 'F', - 'Д': 'A', - 'Е': 'E', - 'Ж': 'K', - 'З': '3', - 'И': 'N', - 'Й': 'N', - 'К': 'K', - 'Л': 'N', - 'М': 'M', - 'Н': 'H', - 'О': 'O', - 'П': 'N', - 'Р': 'P', - 'С': 'C', - 'Т': 'T', - 'У': 'y', - 'Ф': 'O', - 'Х': 'X', - 'Ц': 'U', - 'Ч': 'h', - 'Ш': 'W', - 'Щ': 'W', - 'Ъ': 'B', - 'Ы': 'X', - 'Ь': 'B', - 'Э': '3', - 'Ю': 'X', - 'Я': 'R', - 'а': 'a', - 'б': 'b', - 'в': 'a', - 'г': 'r', - 'д': 'y', - 'е': 'e', - 'ж': 'm', - 'з': 'e', - 'и': 'n', - 'й': 'n', - 'к': 'n', - 'л': 'n', - 'м': 'm', - 'н': 'n', - 'о': 'o', - 'п': 'n', - 'р': 'p', - 'с': 'c', - 'т': 'o', - 'у': 'y', - 'ф': 'b', - 'х': 'x', - 'ц': 'n', - 'ч': 'n', - 'ш': 'w', - 'щ': 'w', - 'ъ': 'a', - 'ы': 'm', - 'ь': 'a', - 'э': 'e', - 'ю': 'm', - 'я': 'r' -}; - -/** - * This function adds new font metrics to default metricMap - * It can also override existing metrics - */ -function setFontMetrics(fontName, metrics) { - fontMetricsData[fontName] = metrics; -} -/** - * This function is a convenience function for looking up information in the - * metricMap table. It takes a character as a string, and a font. - * - * Note: the `width` property may be undefined if fontMetricsData.js wasn't - * built using `Make extended_metrics`. - */ - -function getCharacterMetrics(character, font, mode) { - if (!fontMetricsData[font]) { - throw new Error("Font metrics not found for font: " + font + "."); - } - - var ch = character.charCodeAt(0); - var metrics = fontMetricsData[font][ch]; - - if (!metrics && character[0] in extraCharacterMap) { - ch = extraCharacterMap[character[0]].charCodeAt(0); - metrics = fontMetricsData[font][ch]; - } - - if (!metrics && mode === 'text') { - // We don't typically have font metrics for Asian scripts. - // But since we support them in text mode, we need to return - // some sort of metrics. - // So if the character is in a script we support but we - // don't have metrics for it, just use the metrics for - // the Latin capital letter M. This is close enough because - // we (currently) only care about the height of the glpyh - // not its width. - if (supportedCodepoint(ch)) { - metrics = fontMetricsData[font][77]; // 77 is the charcode for 'M' - } - } - - if (metrics) { - return { - depth: metrics[0], - height: metrics[1], - italic: metrics[2], - skew: metrics[3], - width: metrics[4] - }; - } -} -var fontMetricsBySizeIndex = {}; -/** - * Get the font metrics for a given size. - */ - -function getGlobalMetrics(size) { - var sizeIndex; - - if (size >= 5) { - sizeIndex = 0; - } else if (size >= 3) { - sizeIndex = 1; - } else { - sizeIndex = 2; - } - - if (!fontMetricsBySizeIndex[sizeIndex]) { - var metrics = fontMetricsBySizeIndex[sizeIndex] = { - cssEmPerMu: sigmasAndXis.quad[sizeIndex] / 18 - }; - - for (var key in sigmasAndXis) { - if (sigmasAndXis.hasOwnProperty(key)) { - metrics[key] = sigmasAndXis[key][sizeIndex]; - } - } - } - - return fontMetricsBySizeIndex[sizeIndex]; -} -// CONCATENATED MODULE: ./src/symbols.js -/** - * This file holds a list of all no-argument functions and single-character - * symbols (like 'a' or ';'). - * - * For each of the symbols, there are three properties they can have: - * - font (required): the font to be used for this symbol. Either "main" (the - normal font), or "ams" (the ams fonts). - * - group (required): the ParseNode group type the symbol should have (i.e. - "textord", "mathord", etc). - See https://github.com/KaTeX/KaTeX/wiki/Examining-TeX#group-types - * - replace: the character that this symbol or function should be - * replaced with (i.e. "\phi" has a replace value of "\u03d5", the phi - * character in the main font). - * - * The outermost map in the table indicates what mode the symbols should be - * accepted in (e.g. "math" or "text"). - */ -// Some of these have a "-token" suffix since these are also used as `ParseNode` -// types for raw text tokens, and we want to avoid conflicts with higher-level -// `ParseNode` types. These `ParseNode`s are constructed within `Parser` by -// looking up the `symbols` map. -var ATOMS = { - "bin": 1, - "close": 1, - "inner": 1, - "open": 1, - "punct": 1, - "rel": 1 -}; -var NON_ATOMS = { - "accent-token": 1, - "mathord": 1, - "op-token": 1, - "spacing": 1, - "textord": 1 -}; -var symbols = { - "math": {}, - "text": {} -}; -/* harmony default export */ var src_symbols = (symbols); -/** `acceptUnicodeChar = true` is only applicable if `replace` is set. */ - -function defineSymbol(mode, font, group, replace, name, acceptUnicodeChar) { - symbols[mode][name] = { - font: font, - group: group, - replace: replace - }; - - if (acceptUnicodeChar && replace) { - symbols[mode][replace] = symbols[mode][name]; - } -} // Some abbreviations for commonly used strings. -// This helps minify the code, and also spotting typos using jshint. -// modes: - -var symbols_math = "math"; -var symbols_text = "text"; // fonts: - -var main = "main"; -var ams = "ams"; // groups: - -var symbols_accent = "accent-token"; -var bin = "bin"; -var symbols_close = "close"; -var symbols_inner = "inner"; -var mathord = "mathord"; -var op = "op-token"; -var symbols_open = "open"; -var punct = "punct"; -var rel = "rel"; -var symbols_spacing = "spacing"; -var symbols_textord = "textord"; // Now comes the symbol table -// Relation Symbols - -defineSymbol(symbols_math, main, rel, "\u2261", "\\equiv", true); -defineSymbol(symbols_math, main, rel, "\u227A", "\\prec", true); -defineSymbol(symbols_math, main, rel, "\u227B", "\\succ", true); -defineSymbol(symbols_math, main, rel, "\u223C", "\\sim", true); -defineSymbol(symbols_math, main, rel, "\u22A5", "\\perp"); -defineSymbol(symbols_math, main, rel, "\u2AAF", "\\preceq", true); -defineSymbol(symbols_math, main, rel, "\u2AB0", "\\succeq", true); -defineSymbol(symbols_math, main, rel, "\u2243", "\\simeq", true); -defineSymbol(symbols_math, main, rel, "\u2223", "\\mid", true); -defineSymbol(symbols_math, main, rel, "\u226A", "\\ll", true); -defineSymbol(symbols_math, main, rel, "\u226B", "\\gg", true); -defineSymbol(symbols_math, main, rel, "\u224D", "\\asymp", true); -defineSymbol(symbols_math, main, rel, "\u2225", "\\parallel"); -defineSymbol(symbols_math, main, rel, "\u22C8", "\\bowtie", true); -defineSymbol(symbols_math, main, rel, "\u2323", "\\smile", true); -defineSymbol(symbols_math, main, rel, "\u2291", "\\sqsubseteq", true); -defineSymbol(symbols_math, main, rel, "\u2292", "\\sqsupseteq", true); -defineSymbol(symbols_math, main, rel, "\u2250", "\\doteq", true); -defineSymbol(symbols_math, main, rel, "\u2322", "\\frown", true); -defineSymbol(symbols_math, main, rel, "\u220B", "\\ni", true); -defineSymbol(symbols_math, main, rel, "\u221D", "\\propto", true); -defineSymbol(symbols_math, main, rel, "\u22A2", "\\vdash", true); -defineSymbol(symbols_math, main, rel, "\u22A3", "\\dashv", true); -defineSymbol(symbols_math, main, rel, "\u220B", "\\owns"); // Punctuation - -defineSymbol(symbols_math, main, punct, ".", "\\ldotp"); -defineSymbol(symbols_math, main, punct, "\u22C5", "\\cdotp"); // Misc Symbols - -defineSymbol(symbols_math, main, symbols_textord, "#", "\\#"); -defineSymbol(symbols_text, main, symbols_textord, "#", "\\#"); -defineSymbol(symbols_math, main, symbols_textord, "&", "\\&"); -defineSymbol(symbols_text, main, symbols_textord, "&", "\\&"); -defineSymbol(symbols_math, main, symbols_textord, "\u2135", "\\aleph", true); -defineSymbol(symbols_math, main, symbols_textord, "\u2200", "\\forall", true); -defineSymbol(symbols_math, main, symbols_textord, "\u210F", "\\hbar", true); -defineSymbol(symbols_math, main, symbols_textord, "\u2203", "\\exists", true); -defineSymbol(symbols_math, main, symbols_textord, "\u2207", "\\nabla", true); -defineSymbol(symbols_math, main, symbols_textord, "\u266D", "\\flat", true); -defineSymbol(symbols_math, main, symbols_textord, "\u2113", "\\ell", true); -defineSymbol(symbols_math, main, symbols_textord, "\u266E", "\\natural", true); -defineSymbol(symbols_math, main, symbols_textord, "\u2663", "\\clubsuit", true); -defineSymbol(symbols_math, main, symbols_textord, "\u2118", "\\wp", true); -defineSymbol(symbols_math, main, symbols_textord, "\u266F", "\\sharp", true); -defineSymbol(symbols_math, main, symbols_textord, "\u2662", "\\diamondsuit", true); -defineSymbol(symbols_math, main, symbols_textord, "\u211C", "\\Re", true); -defineSymbol(symbols_math, main, symbols_textord, "\u2661", "\\heartsuit", true); -defineSymbol(symbols_math, main, symbols_textord, "\u2111", "\\Im", true); -defineSymbol(symbols_math, main, symbols_textord, "\u2660", "\\spadesuit", true); -defineSymbol(symbols_text, main, symbols_textord, "\xA7", "\\S", true); -defineSymbol(symbols_text, main, symbols_textord, "\xB6", "\\P", true); // Math and Text - -defineSymbol(symbols_math, main, symbols_textord, "\u2020", "\\dag"); -defineSymbol(symbols_text, main, symbols_textord, "\u2020", "\\dag"); -defineSymbol(symbols_text, main, symbols_textord, "\u2020", "\\textdagger"); -defineSymbol(symbols_math, main, symbols_textord, "\u2021", "\\ddag"); -defineSymbol(symbols_text, main, symbols_textord, "\u2021", "\\ddag"); -defineSymbol(symbols_text, main, symbols_textord, "\u2021", "\\textdaggerdbl"); // Large Delimiters - -defineSymbol(symbols_math, main, symbols_close, "\u23B1", "\\rmoustache", true); -defineSymbol(symbols_math, main, symbols_open, "\u23B0", "\\lmoustache", true); -defineSymbol(symbols_math, main, symbols_close, "\u27EF", "\\rgroup", true); -defineSymbol(symbols_math, main, symbols_open, "\u27EE", "\\lgroup", true); // Binary Operators - -defineSymbol(symbols_math, main, bin, "\u2213", "\\mp", true); -defineSymbol(symbols_math, main, bin, "\u2296", "\\ominus", true); -defineSymbol(symbols_math, main, bin, "\u228E", "\\uplus", true); -defineSymbol(symbols_math, main, bin, "\u2293", "\\sqcap", true); -defineSymbol(symbols_math, main, bin, "\u2217", "\\ast"); -defineSymbol(symbols_math, main, bin, "\u2294", "\\sqcup", true); -defineSymbol(symbols_math, main, bin, "\u25EF", "\\bigcirc"); -defineSymbol(symbols_math, main, bin, "\u2219", "\\bullet"); -defineSymbol(symbols_math, main, bin, "\u2021", "\\ddagger"); -defineSymbol(symbols_math, main, bin, "\u2240", "\\wr", true); -defineSymbol(symbols_math, main, bin, "\u2A3F", "\\amalg"); -defineSymbol(symbols_math, main, bin, "&", "\\And"); // from amsmath -// Arrow Symbols - -defineSymbol(symbols_math, main, rel, "\u27F5", "\\longleftarrow", true); -defineSymbol(symbols_math, main, rel, "\u21D0", "\\Leftarrow", true); -defineSymbol(symbols_math, main, rel, "\u27F8", "\\Longleftarrow", true); -defineSymbol(symbols_math, main, rel, "\u27F6", "\\longrightarrow", true); -defineSymbol(symbols_math, main, rel, "\u21D2", "\\Rightarrow", true); -defineSymbol(symbols_math, main, rel, "\u27F9", "\\Longrightarrow", true); -defineSymbol(symbols_math, main, rel, "\u2194", "\\leftrightarrow", true); -defineSymbol(symbols_math, main, rel, "\u27F7", "\\longleftrightarrow", true); -defineSymbol(symbols_math, main, rel, "\u21D4", "\\Leftrightarrow", true); -defineSymbol(symbols_math, main, rel, "\u27FA", "\\Longleftrightarrow", true); -defineSymbol(symbols_math, main, rel, "\u21A6", "\\mapsto", true); -defineSymbol(symbols_math, main, rel, "\u27FC", "\\longmapsto", true); -defineSymbol(symbols_math, main, rel, "\u2197", "\\nearrow", true); -defineSymbol(symbols_math, main, rel, "\u21A9", "\\hookleftarrow", true); -defineSymbol(symbols_math, main, rel, "\u21AA", "\\hookrightarrow", true); -defineSymbol(symbols_math, main, rel, "\u2198", "\\searrow", true); -defineSymbol(symbols_math, main, rel, "\u21BC", "\\leftharpoonup", true); -defineSymbol(symbols_math, main, rel, "\u21C0", "\\rightharpoonup", true); -defineSymbol(symbols_math, main, rel, "\u2199", "\\swarrow", true); -defineSymbol(symbols_math, main, rel, "\u21BD", "\\leftharpoondown", true); -defineSymbol(symbols_math, main, rel, "\u21C1", "\\rightharpoondown", true); -defineSymbol(symbols_math, main, rel, "\u2196", "\\nwarrow", true); -defineSymbol(symbols_math, main, rel, "\u21CC", "\\rightleftharpoons", true); // AMS Negated Binary Relations - -defineSymbol(symbols_math, ams, rel, "\u226E", "\\nless", true); // Symbol names preceeded by "@" each have a corresponding macro. - -defineSymbol(symbols_math, ams, rel, "\uE010", "\\@nleqslant"); -defineSymbol(symbols_math, ams, rel, "\uE011", "\\@nleqq"); -defineSymbol(symbols_math, ams, rel, "\u2A87", "\\lneq", true); -defineSymbol(symbols_math, ams, rel, "\u2268", "\\lneqq", true); -defineSymbol(symbols_math, ams, rel, "\uE00C", "\\@lvertneqq"); -defineSymbol(symbols_math, ams, rel, "\u22E6", "\\lnsim", true); -defineSymbol(symbols_math, ams, rel, "\u2A89", "\\lnapprox", true); -defineSymbol(symbols_math, ams, rel, "\u2280", "\\nprec", true); // unicode-math maps \u22e0 to \npreccurlyeq. We'll use the AMS synonym. - -defineSymbol(symbols_math, ams, rel, "\u22E0", "\\npreceq", true); -defineSymbol(symbols_math, ams, rel, "\u22E8", "\\precnsim", true); -defineSymbol(symbols_math, ams, rel, "\u2AB9", "\\precnapprox", true); -defineSymbol(symbols_math, ams, rel, "\u2241", "\\nsim", true); -defineSymbol(symbols_math, ams, rel, "\uE006", "\\@nshortmid"); -defineSymbol(symbols_math, ams, rel, "\u2224", "\\nmid", true); -defineSymbol(symbols_math, ams, rel, "\u22AC", "\\nvdash", true); -defineSymbol(symbols_math, ams, rel, "\u22AD", "\\nvDash", true); -defineSymbol(symbols_math, ams, rel, "\u22EA", "\\ntriangleleft"); -defineSymbol(symbols_math, ams, rel, "\u22EC", "\\ntrianglelefteq", true); -defineSymbol(symbols_math, ams, rel, "\u228A", "\\subsetneq", true); -defineSymbol(symbols_math, ams, rel, "\uE01A", "\\@varsubsetneq"); -defineSymbol(symbols_math, ams, rel, "\u2ACB", "\\subsetneqq", true); -defineSymbol(symbols_math, ams, rel, "\uE017", "\\@varsubsetneqq"); -defineSymbol(symbols_math, ams, rel, "\u226F", "\\ngtr", true); -defineSymbol(symbols_math, ams, rel, "\uE00F", "\\@ngeqslant"); -defineSymbol(symbols_math, ams, rel, "\uE00E", "\\@ngeqq"); -defineSymbol(symbols_math, ams, rel, "\u2A88", "\\gneq", true); -defineSymbol(symbols_math, ams, rel, "\u2269", "\\gneqq", true); -defineSymbol(symbols_math, ams, rel, "\uE00D", "\\@gvertneqq"); -defineSymbol(symbols_math, ams, rel, "\u22E7", "\\gnsim", true); -defineSymbol(symbols_math, ams, rel, "\u2A8A", "\\gnapprox", true); -defineSymbol(symbols_math, ams, rel, "\u2281", "\\nsucc", true); // unicode-math maps \u22e1 to \nsucccurlyeq. We'll use the AMS synonym. - -defineSymbol(symbols_math, ams, rel, "\u22E1", "\\nsucceq", true); -defineSymbol(symbols_math, ams, rel, "\u22E9", "\\succnsim", true); -defineSymbol(symbols_math, ams, rel, "\u2ABA", "\\succnapprox", true); // unicode-math maps \u2246 to \simneqq. We'll use the AMS synonym. - -defineSymbol(symbols_math, ams, rel, "\u2246", "\\ncong", true); -defineSymbol(symbols_math, ams, rel, "\uE007", "\\@nshortparallel"); -defineSymbol(symbols_math, ams, rel, "\u2226", "\\nparallel", true); -defineSymbol(symbols_math, ams, rel, "\u22AF", "\\nVDash", true); -defineSymbol(symbols_math, ams, rel, "\u22EB", "\\ntriangleright"); -defineSymbol(symbols_math, ams, rel, "\u22ED", "\\ntrianglerighteq", true); -defineSymbol(symbols_math, ams, rel, "\uE018", "\\@nsupseteqq"); -defineSymbol(symbols_math, ams, rel, "\u228B", "\\supsetneq", true); -defineSymbol(symbols_math, ams, rel, "\uE01B", "\\@varsupsetneq"); -defineSymbol(symbols_math, ams, rel, "\u2ACC", "\\supsetneqq", true); -defineSymbol(symbols_math, ams, rel, "\uE019", "\\@varsupsetneqq"); -defineSymbol(symbols_math, ams, rel, "\u22AE", "\\nVdash", true); -defineSymbol(symbols_math, ams, rel, "\u2AB5", "\\precneqq", true); -defineSymbol(symbols_math, ams, rel, "\u2AB6", "\\succneqq", true); -defineSymbol(symbols_math, ams, rel, "\uE016", "\\@nsubseteqq"); -defineSymbol(symbols_math, ams, bin, "\u22B4", "\\unlhd"); -defineSymbol(symbols_math, ams, bin, "\u22B5", "\\unrhd"); // AMS Negated Arrows - -defineSymbol(symbols_math, ams, rel, "\u219A", "\\nleftarrow", true); -defineSymbol(symbols_math, ams, rel, "\u219B", "\\nrightarrow", true); -defineSymbol(symbols_math, ams, rel, "\u21CD", "\\nLeftarrow", true); -defineSymbol(symbols_math, ams, rel, "\u21CF", "\\nRightarrow", true); -defineSymbol(symbols_math, ams, rel, "\u21AE", "\\nleftrightarrow", true); -defineSymbol(symbols_math, ams, rel, "\u21CE", "\\nLeftrightarrow", true); // AMS Misc - -defineSymbol(symbols_math, ams, rel, "\u25B3", "\\vartriangle"); -defineSymbol(symbols_math, ams, symbols_textord, "\u210F", "\\hslash"); -defineSymbol(symbols_math, ams, symbols_textord, "\u25BD", "\\triangledown"); -defineSymbol(symbols_math, ams, symbols_textord, "\u25CA", "\\lozenge"); -defineSymbol(symbols_math, ams, symbols_textord, "\u24C8", "\\circledS"); -defineSymbol(symbols_math, ams, symbols_textord, "\xAE", "\\circledR"); -defineSymbol(symbols_text, ams, symbols_textord, "\xAE", "\\circledR"); -defineSymbol(symbols_math, ams, symbols_textord, "\u2221", "\\measuredangle", true); -defineSymbol(symbols_math, ams, symbols_textord, "\u2204", "\\nexists"); -defineSymbol(symbols_math, ams, symbols_textord, "\u2127", "\\mho"); -defineSymbol(symbols_math, ams, symbols_textord, "\u2132", "\\Finv", true); -defineSymbol(symbols_math, ams, symbols_textord, "\u2141", "\\Game", true); -defineSymbol(symbols_math, ams, symbols_textord, "\u2035", "\\backprime"); -defineSymbol(symbols_math, ams, symbols_textord, "\u25B2", "\\blacktriangle"); -defineSymbol(symbols_math, ams, symbols_textord, "\u25BC", "\\blacktriangledown"); -defineSymbol(symbols_math, ams, symbols_textord, "\u25A0", "\\blacksquare"); -defineSymbol(symbols_math, ams, symbols_textord, "\u29EB", "\\blacklozenge"); -defineSymbol(symbols_math, ams, symbols_textord, "\u2605", "\\bigstar"); -defineSymbol(symbols_math, ams, symbols_textord, "\u2222", "\\sphericalangle", true); -defineSymbol(symbols_math, ams, symbols_textord, "\u2201", "\\complement", true); // unicode-math maps U+F0 to \matheth. We map to AMS function \eth - -defineSymbol(symbols_math, ams, symbols_textord, "\xF0", "\\eth", true); -defineSymbol(symbols_text, main, symbols_textord, "\xF0", "\xF0"); -defineSymbol(symbols_math, ams, symbols_textord, "\u2571", "\\diagup"); -defineSymbol(symbols_math, ams, symbols_textord, "\u2572", "\\diagdown"); -defineSymbol(symbols_math, ams, symbols_textord, "\u25A1", "\\square"); -defineSymbol(symbols_math, ams, symbols_textord, "\u25A1", "\\Box"); -defineSymbol(symbols_math, ams, symbols_textord, "\u25CA", "\\Diamond"); // unicode-math maps U+A5 to \mathyen. We map to AMS function \yen - -defineSymbol(symbols_math, ams, symbols_textord, "\xA5", "\\yen", true); -defineSymbol(symbols_text, ams, symbols_textord, "\xA5", "\\yen", true); -defineSymbol(symbols_math, ams, symbols_textord, "\u2713", "\\checkmark", true); -defineSymbol(symbols_text, ams, symbols_textord, "\u2713", "\\checkmark"); // AMS Hebrew - -defineSymbol(symbols_math, ams, symbols_textord, "\u2136", "\\beth", true); -defineSymbol(symbols_math, ams, symbols_textord, "\u2138", "\\daleth", true); -defineSymbol(symbols_math, ams, symbols_textord, "\u2137", "\\gimel", true); // AMS Greek - -defineSymbol(symbols_math, ams, symbols_textord, "\u03DD", "\\digamma", true); -defineSymbol(symbols_math, ams, symbols_textord, "\u03F0", "\\varkappa"); // AMS Delimiters - -defineSymbol(symbols_math, ams, symbols_open, "\u250C", "\\@ulcorner", true); -defineSymbol(symbols_math, ams, symbols_close, "\u2510", "\\@urcorner", true); -defineSymbol(symbols_math, ams, symbols_open, "\u2514", "\\@llcorner", true); -defineSymbol(symbols_math, ams, symbols_close, "\u2518", "\\@lrcorner", true); // AMS Binary Relations - -defineSymbol(symbols_math, ams, rel, "\u2266", "\\leqq", true); -defineSymbol(symbols_math, ams, rel, "\u2A7D", "\\leqslant", true); -defineSymbol(symbols_math, ams, rel, "\u2A95", "\\eqslantless", true); -defineSymbol(symbols_math, ams, rel, "\u2272", "\\lesssim", true); -defineSymbol(symbols_math, ams, rel, "\u2A85", "\\lessapprox", true); -defineSymbol(symbols_math, ams, rel, "\u224A", "\\approxeq", true); -defineSymbol(symbols_math, ams, bin, "\u22D6", "\\lessdot"); -defineSymbol(symbols_math, ams, rel, "\u22D8", "\\lll", true); -defineSymbol(symbols_math, ams, rel, "\u2276", "\\lessgtr", true); -defineSymbol(symbols_math, ams, rel, "\u22DA", "\\lesseqgtr", true); -defineSymbol(symbols_math, ams, rel, "\u2A8B", "\\lesseqqgtr", true); -defineSymbol(symbols_math, ams, rel, "\u2251", "\\doteqdot"); -defineSymbol(symbols_math, ams, rel, "\u2253", "\\risingdotseq", true); -defineSymbol(symbols_math, ams, rel, "\u2252", "\\fallingdotseq", true); -defineSymbol(symbols_math, ams, rel, "\u223D", "\\backsim", true); -defineSymbol(symbols_math, ams, rel, "\u22CD", "\\backsimeq", true); -defineSymbol(symbols_math, ams, rel, "\u2AC5", "\\subseteqq", true); -defineSymbol(symbols_math, ams, rel, "\u22D0", "\\Subset", true); -defineSymbol(symbols_math, ams, rel, "\u228F", "\\sqsubset", true); -defineSymbol(symbols_math, ams, rel, "\u227C", "\\preccurlyeq", true); -defineSymbol(symbols_math, ams, rel, "\u22DE", "\\curlyeqprec", true); -defineSymbol(symbols_math, ams, rel, "\u227E", "\\precsim", true); -defineSymbol(symbols_math, ams, rel, "\u2AB7", "\\precapprox", true); -defineSymbol(symbols_math, ams, rel, "\u22B2", "\\vartriangleleft"); -defineSymbol(symbols_math, ams, rel, "\u22B4", "\\trianglelefteq"); -defineSymbol(symbols_math, ams, rel, "\u22A8", "\\vDash", true); -defineSymbol(symbols_math, ams, rel, "\u22AA", "\\Vvdash", true); -defineSymbol(symbols_math, ams, rel, "\u2323", "\\smallsmile"); -defineSymbol(symbols_math, ams, rel, "\u2322", "\\smallfrown"); -defineSymbol(symbols_math, ams, rel, "\u224F", "\\bumpeq", true); -defineSymbol(symbols_math, ams, rel, "\u224E", "\\Bumpeq", true); -defineSymbol(symbols_math, ams, rel, "\u2267", "\\geqq", true); -defineSymbol(symbols_math, ams, rel, "\u2A7E", "\\geqslant", true); -defineSymbol(symbols_math, ams, rel, "\u2A96", "\\eqslantgtr", true); -defineSymbol(symbols_math, ams, rel, "\u2273", "\\gtrsim", true); -defineSymbol(symbols_math, ams, rel, "\u2A86", "\\gtrapprox", true); -defineSymbol(symbols_math, ams, bin, "\u22D7", "\\gtrdot"); -defineSymbol(symbols_math, ams, rel, "\u22D9", "\\ggg", true); -defineSymbol(symbols_math, ams, rel, "\u2277", "\\gtrless", true); -defineSymbol(symbols_math, ams, rel, "\u22DB", "\\gtreqless", true); -defineSymbol(symbols_math, ams, rel, "\u2A8C", "\\gtreqqless", true); -defineSymbol(symbols_math, ams, rel, "\u2256", "\\eqcirc", true); -defineSymbol(symbols_math, ams, rel, "\u2257", "\\circeq", true); -defineSymbol(symbols_math, ams, rel, "\u225C", "\\triangleq", true); -defineSymbol(symbols_math, ams, rel, "\u223C", "\\thicksim"); -defineSymbol(symbols_math, ams, rel, "\u2248", "\\thickapprox"); -defineSymbol(symbols_math, ams, rel, "\u2AC6", "\\supseteqq", true); -defineSymbol(symbols_math, ams, rel, "\u22D1", "\\Supset", true); -defineSymbol(symbols_math, ams, rel, "\u2290", "\\sqsupset", true); -defineSymbol(symbols_math, ams, rel, "\u227D", "\\succcurlyeq", true); -defineSymbol(symbols_math, ams, rel, "\u22DF", "\\curlyeqsucc", true); -defineSymbol(symbols_math, ams, rel, "\u227F", "\\succsim", true); -defineSymbol(symbols_math, ams, rel, "\u2AB8", "\\succapprox", true); -defineSymbol(symbols_math, ams, rel, "\u22B3", "\\vartriangleright"); -defineSymbol(symbols_math, ams, rel, "\u22B5", "\\trianglerighteq"); -defineSymbol(symbols_math, ams, rel, "\u22A9", "\\Vdash", true); -defineSymbol(symbols_math, ams, rel, "\u2223", "\\shortmid"); -defineSymbol(symbols_math, ams, rel, "\u2225", "\\shortparallel"); -defineSymbol(symbols_math, ams, rel, "\u226C", "\\between", true); -defineSymbol(symbols_math, ams, rel, "\u22D4", "\\pitchfork", true); -defineSymbol(symbols_math, ams, rel, "\u221D", "\\varpropto"); -defineSymbol(symbols_math, ams, rel, "\u25C0", "\\blacktriangleleft"); // unicode-math says that \therefore is a mathord atom. -// We kept the amssymb atom type, which is rel. - -defineSymbol(symbols_math, ams, rel, "\u2234", "\\therefore", true); -defineSymbol(symbols_math, ams, rel, "\u220D", "\\backepsilon"); -defineSymbol(symbols_math, ams, rel, "\u25B6", "\\blacktriangleright"); // unicode-math says that \because is a mathord atom. -// We kept the amssymb atom type, which is rel. - -defineSymbol(symbols_math, ams, rel, "\u2235", "\\because", true); -defineSymbol(symbols_math, ams, rel, "\u22D8", "\\llless"); -defineSymbol(symbols_math, ams, rel, "\u22D9", "\\gggtr"); -defineSymbol(symbols_math, ams, bin, "\u22B2", "\\lhd"); -defineSymbol(symbols_math, ams, bin, "\u22B3", "\\rhd"); -defineSymbol(symbols_math, ams, rel, "\u2242", "\\eqsim", true); -defineSymbol(symbols_math, main, rel, "\u22C8", "\\Join"); -defineSymbol(symbols_math, ams, rel, "\u2251", "\\Doteq", true); // AMS Binary Operators - -defineSymbol(symbols_math, ams, bin, "\u2214", "\\dotplus", true); -defineSymbol(symbols_math, ams, bin, "\u2216", "\\smallsetminus"); -defineSymbol(symbols_math, ams, bin, "\u22D2", "\\Cap", true); -defineSymbol(symbols_math, ams, bin, "\u22D3", "\\Cup", true); -defineSymbol(symbols_math, ams, bin, "\u2A5E", "\\doublebarwedge", true); -defineSymbol(symbols_math, ams, bin, "\u229F", "\\boxminus", true); -defineSymbol(symbols_math, ams, bin, "\u229E", "\\boxplus", true); -defineSymbol(symbols_math, ams, bin, "\u22C7", "\\divideontimes", true); -defineSymbol(symbols_math, ams, bin, "\u22C9", "\\ltimes", true); -defineSymbol(symbols_math, ams, bin, "\u22CA", "\\rtimes", true); -defineSymbol(symbols_math, ams, bin, "\u22CB", "\\leftthreetimes", true); -defineSymbol(symbols_math, ams, bin, "\u22CC", "\\rightthreetimes", true); -defineSymbol(symbols_math, ams, bin, "\u22CF", "\\curlywedge", true); -defineSymbol(symbols_math, ams, bin, "\u22CE", "\\curlyvee", true); -defineSymbol(symbols_math, ams, bin, "\u229D", "\\circleddash", true); -defineSymbol(symbols_math, ams, bin, "\u229B", "\\circledast", true); -defineSymbol(symbols_math, ams, bin, "\u22C5", "\\centerdot"); -defineSymbol(symbols_math, ams, bin, "\u22BA", "\\intercal", true); -defineSymbol(symbols_math, ams, bin, "\u22D2", "\\doublecap"); -defineSymbol(symbols_math, ams, bin, "\u22D3", "\\doublecup"); -defineSymbol(symbols_math, ams, bin, "\u22A0", "\\boxtimes", true); // AMS Arrows -// Note: unicode-math maps \u21e2 to their own function \rightdasharrow. -// We'll map it to AMS function \dashrightarrow. It produces the same atom. - -defineSymbol(symbols_math, ams, rel, "\u21E2", "\\dashrightarrow", true); // unicode-math maps \u21e0 to \leftdasharrow. We'll use the AMS synonym. - -defineSymbol(symbols_math, ams, rel, "\u21E0", "\\dashleftarrow", true); -defineSymbol(symbols_math, ams, rel, "\u21C7", "\\leftleftarrows", true); -defineSymbol(symbols_math, ams, rel, "\u21C6", "\\leftrightarrows", true); -defineSymbol(symbols_math, ams, rel, "\u21DA", "\\Lleftarrow", true); -defineSymbol(symbols_math, ams, rel, "\u219E", "\\twoheadleftarrow", true); -defineSymbol(symbols_math, ams, rel, "\u21A2", "\\leftarrowtail", true); -defineSymbol(symbols_math, ams, rel, "\u21AB", "\\looparrowleft", true); -defineSymbol(symbols_math, ams, rel, "\u21CB", "\\leftrightharpoons", true); -defineSymbol(symbols_math, ams, rel, "\u21B6", "\\curvearrowleft", true); // unicode-math maps \u21ba to \acwopencirclearrow. We'll use the AMS synonym. - -defineSymbol(symbols_math, ams, rel, "\u21BA", "\\circlearrowleft", true); -defineSymbol(symbols_math, ams, rel, "\u21B0", "\\Lsh", true); -defineSymbol(symbols_math, ams, rel, "\u21C8", "\\upuparrows", true); -defineSymbol(symbols_math, ams, rel, "\u21BF", "\\upharpoonleft", true); -defineSymbol(symbols_math, ams, rel, "\u21C3", "\\downharpoonleft", true); -defineSymbol(symbols_math, ams, rel, "\u22B8", "\\multimap", true); -defineSymbol(symbols_math, ams, rel, "\u21AD", "\\leftrightsquigarrow", true); -defineSymbol(symbols_math, ams, rel, "\u21C9", "\\rightrightarrows", true); -defineSymbol(symbols_math, ams, rel, "\u21C4", "\\rightleftarrows", true); -defineSymbol(symbols_math, ams, rel, "\u21A0", "\\twoheadrightarrow", true); -defineSymbol(symbols_math, ams, rel, "\u21A3", "\\rightarrowtail", true); -defineSymbol(symbols_math, ams, rel, "\u21AC", "\\looparrowright", true); -defineSymbol(symbols_math, ams, rel, "\u21B7", "\\curvearrowright", true); // unicode-math maps \u21bb to \cwopencirclearrow. We'll use the AMS synonym. - -defineSymbol(symbols_math, ams, rel, "\u21BB", "\\circlearrowright", true); -defineSymbol(symbols_math, ams, rel, "\u21B1", "\\Rsh", true); -defineSymbol(symbols_math, ams, rel, "\u21CA", "\\downdownarrows", true); -defineSymbol(symbols_math, ams, rel, "\u21BE", "\\upharpoonright", true); -defineSymbol(symbols_math, ams, rel, "\u21C2", "\\downharpoonright", true); -defineSymbol(symbols_math, ams, rel, "\u21DD", "\\rightsquigarrow", true); -defineSymbol(symbols_math, ams, rel, "\u21DD", "\\leadsto"); -defineSymbol(symbols_math, ams, rel, "\u21DB", "\\Rrightarrow", true); -defineSymbol(symbols_math, ams, rel, "\u21BE", "\\restriction"); -defineSymbol(symbols_math, main, symbols_textord, "\u2018", "`"); -defineSymbol(symbols_math, main, symbols_textord, "$", "\\$"); -defineSymbol(symbols_text, main, symbols_textord, "$", "\\$"); -defineSymbol(symbols_text, main, symbols_textord, "$", "\\textdollar"); -defineSymbol(symbols_math, main, symbols_textord, "%", "\\%"); -defineSymbol(symbols_text, main, symbols_textord, "%", "\\%"); -defineSymbol(symbols_math, main, symbols_textord, "_", "\\_"); -defineSymbol(symbols_text, main, symbols_textord, "_", "\\_"); -defineSymbol(symbols_text, main, symbols_textord, "_", "\\textunderscore"); -defineSymbol(symbols_math, main, symbols_textord, "\u2220", "\\angle", true); -defineSymbol(symbols_math, main, symbols_textord, "\u221E", "\\infty", true); -defineSymbol(symbols_math, main, symbols_textord, "\u2032", "\\prime"); -defineSymbol(symbols_math, main, symbols_textord, "\u25B3", "\\triangle"); -defineSymbol(symbols_math, main, symbols_textord, "\u0393", "\\Gamma", true); -defineSymbol(symbols_math, main, symbols_textord, "\u0394", "\\Delta", true); -defineSymbol(symbols_math, main, symbols_textord, "\u0398", "\\Theta", true); -defineSymbol(symbols_math, main, symbols_textord, "\u039B", "\\Lambda", true); -defineSymbol(symbols_math, main, symbols_textord, "\u039E", "\\Xi", true); -defineSymbol(symbols_math, main, symbols_textord, "\u03A0", "\\Pi", true); -defineSymbol(symbols_math, main, symbols_textord, "\u03A3", "\\Sigma", true); -defineSymbol(symbols_math, main, symbols_textord, "\u03A5", "\\Upsilon", true); -defineSymbol(symbols_math, main, symbols_textord, "\u03A6", "\\Phi", true); -defineSymbol(symbols_math, main, symbols_textord, "\u03A8", "\\Psi", true); -defineSymbol(symbols_math, main, symbols_textord, "\u03A9", "\\Omega", true); -defineSymbol(symbols_math, main, symbols_textord, "A", "\u0391"); -defineSymbol(symbols_math, main, symbols_textord, "B", "\u0392"); -defineSymbol(symbols_math, main, symbols_textord, "E", "\u0395"); -defineSymbol(symbols_math, main, symbols_textord, "Z", "\u0396"); -defineSymbol(symbols_math, main, symbols_textord, "H", "\u0397"); -defineSymbol(symbols_math, main, symbols_textord, "I", "\u0399"); -defineSymbol(symbols_math, main, symbols_textord, "K", "\u039A"); -defineSymbol(symbols_math, main, symbols_textord, "M", "\u039C"); -defineSymbol(symbols_math, main, symbols_textord, "N", "\u039D"); -defineSymbol(symbols_math, main, symbols_textord, "O", "\u039F"); -defineSymbol(symbols_math, main, symbols_textord, "P", "\u03A1"); -defineSymbol(symbols_math, main, symbols_textord, "T", "\u03A4"); -defineSymbol(symbols_math, main, symbols_textord, "X", "\u03A7"); -defineSymbol(symbols_math, main, symbols_textord, "\xAC", "\\neg", true); -defineSymbol(symbols_math, main, symbols_textord, "\xAC", "\\lnot"); -defineSymbol(symbols_math, main, symbols_textord, "\u22A4", "\\top"); -defineSymbol(symbols_math, main, symbols_textord, "\u22A5", "\\bot"); -defineSymbol(symbols_math, main, symbols_textord, "\u2205", "\\emptyset"); -defineSymbol(symbols_math, ams, symbols_textord, "\u2205", "\\varnothing"); -defineSymbol(symbols_math, main, mathord, "\u03B1", "\\alpha", true); -defineSymbol(symbols_math, main, mathord, "\u03B2", "\\beta", true); -defineSymbol(symbols_math, main, mathord, "\u03B3", "\\gamma", true); -defineSymbol(symbols_math, main, mathord, "\u03B4", "\\delta", true); -defineSymbol(symbols_math, main, mathord, "\u03F5", "\\epsilon", true); -defineSymbol(symbols_math, main, mathord, "\u03B6", "\\zeta", true); -defineSymbol(symbols_math, main, mathord, "\u03B7", "\\eta", true); -defineSymbol(symbols_math, main, mathord, "\u03B8", "\\theta", true); -defineSymbol(symbols_math, main, mathord, "\u03B9", "\\iota", true); -defineSymbol(symbols_math, main, mathord, "\u03BA", "\\kappa", true); -defineSymbol(symbols_math, main, mathord, "\u03BB", "\\lambda", true); -defineSymbol(symbols_math, main, mathord, "\u03BC", "\\mu", true); -defineSymbol(symbols_math, main, mathord, "\u03BD", "\\nu", true); -defineSymbol(symbols_math, main, mathord, "\u03BE", "\\xi", true); -defineSymbol(symbols_math, main, mathord, "\u03BF", "\\omicron", true); -defineSymbol(symbols_math, main, mathord, "\u03C0", "\\pi", true); -defineSymbol(symbols_math, main, mathord, "\u03C1", "\\rho", true); -defineSymbol(symbols_math, main, mathord, "\u03C3", "\\sigma", true); -defineSymbol(symbols_math, main, mathord, "\u03C4", "\\tau", true); -defineSymbol(symbols_math, main, mathord, "\u03C5", "\\upsilon", true); -defineSymbol(symbols_math, main, mathord, "\u03D5", "\\phi", true); -defineSymbol(symbols_math, main, mathord, "\u03C7", "\\chi", true); -defineSymbol(symbols_math, main, mathord, "\u03C8", "\\psi", true); -defineSymbol(symbols_math, main, mathord, "\u03C9", "\\omega", true); -defineSymbol(symbols_math, main, mathord, "\u03B5", "\\varepsilon", true); -defineSymbol(symbols_math, main, mathord, "\u03D1", "\\vartheta", true); -defineSymbol(symbols_math, main, mathord, "\u03D6", "\\varpi", true); -defineSymbol(symbols_math, main, mathord, "\u03F1", "\\varrho", true); -defineSymbol(symbols_math, main, mathord, "\u03C2", "\\varsigma", true); -defineSymbol(symbols_math, main, mathord, "\u03C6", "\\varphi", true); -defineSymbol(symbols_math, main, bin, "\u2217", "*"); -defineSymbol(symbols_math, main, bin, "+", "+"); -defineSymbol(symbols_math, main, bin, "\u2212", "-"); -defineSymbol(symbols_math, main, bin, "\u22C5", "\\cdot", true); -defineSymbol(symbols_math, main, bin, "\u2218", "\\circ"); -defineSymbol(symbols_math, main, bin, "\xF7", "\\div", true); -defineSymbol(symbols_math, main, bin, "\xB1", "\\pm", true); -defineSymbol(symbols_math, main, bin, "\xD7", "\\times", true); -defineSymbol(symbols_math, main, bin, "\u2229", "\\cap", true); -defineSymbol(symbols_math, main, bin, "\u222A", "\\cup", true); -defineSymbol(symbols_math, main, bin, "\u2216", "\\setminus"); -defineSymbol(symbols_math, main, bin, "\u2227", "\\land"); -defineSymbol(symbols_math, main, bin, "\u2228", "\\lor"); -defineSymbol(symbols_math, main, bin, "\u2227", "\\wedge", true); -defineSymbol(symbols_math, main, bin, "\u2228", "\\vee", true); -defineSymbol(symbols_math, main, symbols_textord, "\u221A", "\\surd"); -defineSymbol(symbols_math, main, symbols_open, "\u27E8", "\\langle", true); -defineSymbol(symbols_math, main, symbols_open, "\u2223", "\\lvert"); -defineSymbol(symbols_math, main, symbols_open, "\u2225", "\\lVert"); -defineSymbol(symbols_math, main, symbols_close, "?", "?"); -defineSymbol(symbols_math, main, symbols_close, "!", "!"); -defineSymbol(symbols_math, main, symbols_close, "\u27E9", "\\rangle", true); -defineSymbol(symbols_math, main, symbols_close, "\u2223", "\\rvert"); -defineSymbol(symbols_math, main, symbols_close, "\u2225", "\\rVert"); -defineSymbol(symbols_math, main, rel, "=", "="); -defineSymbol(symbols_math, main, rel, ":", ":"); -defineSymbol(symbols_math, main, rel, "\u2248", "\\approx", true); -defineSymbol(symbols_math, main, rel, "\u2245", "\\cong", true); -defineSymbol(symbols_math, main, rel, "\u2265", "\\ge"); -defineSymbol(symbols_math, main, rel, "\u2265", "\\geq", true); -defineSymbol(symbols_math, main, rel, "\u2190", "\\gets"); -defineSymbol(symbols_math, main, rel, ">", "\\gt", true); -defineSymbol(symbols_math, main, rel, "\u2208", "\\in", true); -defineSymbol(symbols_math, main, rel, "\uE020", "\\@not"); -defineSymbol(symbols_math, main, rel, "\u2282", "\\subset", true); -defineSymbol(symbols_math, main, rel, "\u2283", "\\supset", true); -defineSymbol(symbols_math, main, rel, "\u2286", "\\subseteq", true); -defineSymbol(symbols_math, main, rel, "\u2287", "\\supseteq", true); -defineSymbol(symbols_math, ams, rel, "\u2288", "\\nsubseteq", true); -defineSymbol(symbols_math, ams, rel, "\u2289", "\\nsupseteq", true); -defineSymbol(symbols_math, main, rel, "\u22A8", "\\models"); -defineSymbol(symbols_math, main, rel, "\u2190", "\\leftarrow", true); -defineSymbol(symbols_math, main, rel, "\u2264", "\\le"); -defineSymbol(symbols_math, main, rel, "\u2264", "\\leq", true); -defineSymbol(symbols_math, main, rel, "<", "\\lt", true); -defineSymbol(symbols_math, main, rel, "\u2192", "\\rightarrow", true); -defineSymbol(symbols_math, main, rel, "\u2192", "\\to"); -defineSymbol(symbols_math, ams, rel, "\u2271", "\\ngeq", true); -defineSymbol(symbols_math, ams, rel, "\u2270", "\\nleq", true); -defineSymbol(symbols_math, main, symbols_spacing, "\xA0", "\\ "); -defineSymbol(symbols_math, main, symbols_spacing, "\xA0", "~"); -defineSymbol(symbols_math, main, symbols_spacing, "\xA0", "\\space"); // Ref: LaTeX Source 2e: \DeclareRobustCommand{\nobreakspace}{% - -defineSymbol(symbols_math, main, symbols_spacing, "\xA0", "\\nobreakspace"); -defineSymbol(symbols_text, main, symbols_spacing, "\xA0", "\\ "); -defineSymbol(symbols_text, main, symbols_spacing, "\xA0", " "); -defineSymbol(symbols_text, main, symbols_spacing, "\xA0", "~"); -defineSymbol(symbols_text, main, symbols_spacing, "\xA0", "\\space"); -defineSymbol(symbols_text, main, symbols_spacing, "\xA0", "\\nobreakspace"); -defineSymbol(symbols_math, main, symbols_spacing, null, "\\nobreak"); -defineSymbol(symbols_math, main, symbols_spacing, null, "\\allowbreak"); -defineSymbol(symbols_math, main, punct, ",", ","); -defineSymbol(symbols_math, main, punct, ";", ";"); -defineSymbol(symbols_math, ams, bin, "\u22BC", "\\barwedge", true); -defineSymbol(symbols_math, ams, bin, "\u22BB", "\\veebar", true); -defineSymbol(symbols_math, main, bin, "\u2299", "\\odot", true); -defineSymbol(symbols_math, main, bin, "\u2295", "\\oplus", true); -defineSymbol(symbols_math, main, bin, "\u2297", "\\otimes", true); -defineSymbol(symbols_math, main, symbols_textord, "\u2202", "\\partial", true); -defineSymbol(symbols_math, main, bin, "\u2298", "\\oslash", true); -defineSymbol(symbols_math, ams, bin, "\u229A", "\\circledcirc", true); -defineSymbol(symbols_math, ams, bin, "\u22A1", "\\boxdot", true); -defineSymbol(symbols_math, main, bin, "\u25B3", "\\bigtriangleup"); -defineSymbol(symbols_math, main, bin, "\u25BD", "\\bigtriangledown"); -defineSymbol(symbols_math, main, bin, "\u2020", "\\dagger"); -defineSymbol(symbols_math, main, bin, "\u22C4", "\\diamond"); -defineSymbol(symbols_math, main, bin, "\u22C6", "\\star"); -defineSymbol(symbols_math, main, bin, "\u25C3", "\\triangleleft"); -defineSymbol(symbols_math, main, bin, "\u25B9", "\\triangleright"); -defineSymbol(symbols_math, main, symbols_open, "{", "\\{"); -defineSymbol(symbols_text, main, symbols_textord, "{", "\\{"); -defineSymbol(symbols_text, main, symbols_textord, "{", "\\textbraceleft"); -defineSymbol(symbols_math, main, symbols_close, "}", "\\}"); -defineSymbol(symbols_text, main, symbols_textord, "}", "\\}"); -defineSymbol(symbols_text, main, symbols_textord, "}", "\\textbraceright"); -defineSymbol(symbols_math, main, symbols_open, "{", "\\lbrace"); -defineSymbol(symbols_math, main, symbols_close, "}", "\\rbrace"); -defineSymbol(symbols_math, main, symbols_open, "[", "\\lbrack", true); -defineSymbol(symbols_text, main, symbols_textord, "[", "\\lbrack", true); -defineSymbol(symbols_math, main, symbols_close, "]", "\\rbrack", true); -defineSymbol(symbols_text, main, symbols_textord, "]", "\\rbrack", true); -defineSymbol(symbols_math, main, symbols_open, "(", "\\lparen", true); -defineSymbol(symbols_math, main, symbols_close, ")", "\\rparen", true); -defineSymbol(symbols_text, main, symbols_textord, "<", "\\textless", true); // in T1 fontenc - -defineSymbol(symbols_text, main, symbols_textord, ">", "\\textgreater", true); // in T1 fontenc - -defineSymbol(symbols_math, main, symbols_open, "\u230A", "\\lfloor", true); -defineSymbol(symbols_math, main, symbols_close, "\u230B", "\\rfloor", true); -defineSymbol(symbols_math, main, symbols_open, "\u2308", "\\lceil", true); -defineSymbol(symbols_math, main, symbols_close, "\u2309", "\\rceil", true); -defineSymbol(symbols_math, main, symbols_textord, "\\", "\\backslash"); -defineSymbol(symbols_math, main, symbols_textord, "\u2223", "|"); -defineSymbol(symbols_math, main, symbols_textord, "\u2223", "\\vert"); -defineSymbol(symbols_text, main, symbols_textord, "|", "\\textbar", true); // in T1 fontenc - -defineSymbol(symbols_math, main, symbols_textord, "\u2225", "\\|"); -defineSymbol(symbols_math, main, symbols_textord, "\u2225", "\\Vert"); -defineSymbol(symbols_text, main, symbols_textord, "\u2225", "\\textbardbl"); -defineSymbol(symbols_text, main, symbols_textord, "~", "\\textasciitilde"); -defineSymbol(symbols_text, main, symbols_textord, "\\", "\\textbackslash"); -defineSymbol(symbols_text, main, symbols_textord, "^", "\\textasciicircum"); -defineSymbol(symbols_math, main, rel, "\u2191", "\\uparrow", true); -defineSymbol(symbols_math, main, rel, "\u21D1", "\\Uparrow", true); -defineSymbol(symbols_math, main, rel, "\u2193", "\\downarrow", true); -defineSymbol(symbols_math, main, rel, "\u21D3", "\\Downarrow", true); -defineSymbol(symbols_math, main, rel, "\u2195", "\\updownarrow", true); -defineSymbol(symbols_math, main, rel, "\u21D5", "\\Updownarrow", true); -defineSymbol(symbols_math, main, op, "\u2210", "\\coprod"); -defineSymbol(symbols_math, main, op, "\u22C1", "\\bigvee"); -defineSymbol(symbols_math, main, op, "\u22C0", "\\bigwedge"); -defineSymbol(symbols_math, main, op, "\u2A04", "\\biguplus"); -defineSymbol(symbols_math, main, op, "\u22C2", "\\bigcap"); -defineSymbol(symbols_math, main, op, "\u22C3", "\\bigcup"); -defineSymbol(symbols_math, main, op, "\u222B", "\\int"); -defineSymbol(symbols_math, main, op, "\u222B", "\\intop"); -defineSymbol(symbols_math, main, op, "\u222C", "\\iint"); -defineSymbol(symbols_math, main, op, "\u222D", "\\iiint"); -defineSymbol(symbols_math, main, op, "\u220F", "\\prod"); -defineSymbol(symbols_math, main, op, "\u2211", "\\sum"); -defineSymbol(symbols_math, main, op, "\u2A02", "\\bigotimes"); -defineSymbol(symbols_math, main, op, "\u2A01", "\\bigoplus"); -defineSymbol(symbols_math, main, op, "\u2A00", "\\bigodot"); -defineSymbol(symbols_math, main, op, "\u222E", "\\oint"); -defineSymbol(symbols_math, main, op, "\u2A06", "\\bigsqcup"); -defineSymbol(symbols_math, main, op, "\u222B", "\\smallint"); -defineSymbol(symbols_text, main, symbols_inner, "\u2026", "\\textellipsis"); -defineSymbol(symbols_math, main, symbols_inner, "\u2026", "\\mathellipsis"); -defineSymbol(symbols_text, main, symbols_inner, "\u2026", "\\ldots", true); -defineSymbol(symbols_math, main, symbols_inner, "\u2026", "\\ldots", true); -defineSymbol(symbols_math, main, symbols_inner, "\u22EF", "\\@cdots", true); -defineSymbol(symbols_math, main, symbols_inner, "\u22F1", "\\ddots", true); -defineSymbol(symbols_math, main, symbols_textord, "\u22EE", "\\varvdots"); // \vdots is a macro - -defineSymbol(symbols_math, main, symbols_accent, "\u02CA", "\\acute"); -defineSymbol(symbols_math, main, symbols_accent, "\u02CB", "\\grave"); -defineSymbol(symbols_math, main, symbols_accent, "\xA8", "\\ddot"); -defineSymbol(symbols_math, main, symbols_accent, "~", "\\tilde"); -defineSymbol(symbols_math, main, symbols_accent, "\u02C9", "\\bar"); -defineSymbol(symbols_math, main, symbols_accent, "\u02D8", "\\breve"); -defineSymbol(symbols_math, main, symbols_accent, "\u02C7", "\\check"); -defineSymbol(symbols_math, main, symbols_accent, "^", "\\hat"); -defineSymbol(symbols_math, main, symbols_accent, "\u20D7", "\\vec"); -defineSymbol(symbols_math, main, symbols_accent, "\u02D9", "\\dot"); -defineSymbol(symbols_math, main, symbols_accent, "\u02DA", "\\mathring"); // \imath and \jmath should be invariant to \mathrm, \mathbf, etc., so use PUA - -defineSymbol(symbols_math, main, mathord, "\uE131", "\\@imath"); -defineSymbol(symbols_math, main, mathord, "\uE237", "\\@jmath"); -defineSymbol(symbols_math, main, symbols_textord, "\u0131", "\u0131"); -defineSymbol(symbols_math, main, symbols_textord, "\u0237", "\u0237"); -defineSymbol(symbols_text, main, symbols_textord, "\u0131", "\\i", true); -defineSymbol(symbols_text, main, symbols_textord, "\u0237", "\\j", true); -defineSymbol(symbols_text, main, symbols_textord, "\xDF", "\\ss", true); -defineSymbol(symbols_text, main, symbols_textord, "\xE6", "\\ae", true); -defineSymbol(symbols_text, main, symbols_textord, "\u0153", "\\oe", true); -defineSymbol(symbols_text, main, symbols_textord, "\xF8", "\\o", true); -defineSymbol(symbols_text, main, symbols_textord, "\xC6", "\\AE", true); -defineSymbol(symbols_text, main, symbols_textord, "\u0152", "\\OE", true); -defineSymbol(symbols_text, main, symbols_textord, "\xD8", "\\O", true); -defineSymbol(symbols_text, main, symbols_accent, "\u02CA", "\\'"); // acute - -defineSymbol(symbols_text, main, symbols_accent, "\u02CB", "\\`"); // grave - -defineSymbol(symbols_text, main, symbols_accent, "\u02C6", "\\^"); // circumflex - -defineSymbol(symbols_text, main, symbols_accent, "\u02DC", "\\~"); // tilde - -defineSymbol(symbols_text, main, symbols_accent, "\u02C9", "\\="); // macron - -defineSymbol(symbols_text, main, symbols_accent, "\u02D8", "\\u"); // breve - -defineSymbol(symbols_text, main, symbols_accent, "\u02D9", "\\."); // dot above - -defineSymbol(symbols_text, main, symbols_accent, "\u02DA", "\\r"); // ring above - -defineSymbol(symbols_text, main, symbols_accent, "\u02C7", "\\v"); // caron - -defineSymbol(symbols_text, main, symbols_accent, "\xA8", '\\"'); // diaresis - -defineSymbol(symbols_text, main, symbols_accent, "\u02DD", "\\H"); // double acute - -defineSymbol(symbols_text, main, symbols_accent, "\u25EF", "\\textcircled"); // \bigcirc glyph -// These ligatures are detected and created in Parser.js's `formLigatures`. - -var ligatures = { - "--": true, - "---": true, - "``": true, - "''": true -}; -defineSymbol(symbols_text, main, symbols_textord, "\u2013", "--", true); -defineSymbol(symbols_text, main, symbols_textord, "\u2013", "\\textendash"); -defineSymbol(symbols_text, main, symbols_textord, "\u2014", "---", true); -defineSymbol(symbols_text, main, symbols_textord, "\u2014", "\\textemdash"); -defineSymbol(symbols_text, main, symbols_textord, "\u2018", "`", true); -defineSymbol(symbols_text, main, symbols_textord, "\u2018", "\\textquoteleft"); -defineSymbol(symbols_text, main, symbols_textord, "\u2019", "'", true); -defineSymbol(symbols_text, main, symbols_textord, "\u2019", "\\textquoteright"); -defineSymbol(symbols_text, main, symbols_textord, "\u201C", "``", true); -defineSymbol(symbols_text, main, symbols_textord, "\u201C", "\\textquotedblleft"); -defineSymbol(symbols_text, main, symbols_textord, "\u201D", "''", true); -defineSymbol(symbols_text, main, symbols_textord, "\u201D", "\\textquotedblright"); // \degree from gensymb package - -defineSymbol(symbols_math, main, symbols_textord, "\xB0", "\\degree", true); -defineSymbol(symbols_text, main, symbols_textord, "\xB0", "\\degree"); // \textdegree from inputenc package - -defineSymbol(symbols_text, main, symbols_textord, "\xB0", "\\textdegree", true); // TODO: In LaTeX, \pounds can generate a different character in text and math -// mode, but among our fonts, only Main-Regular defines this character "163". - -defineSymbol(symbols_math, main, symbols_textord, "\xA3", "\\pounds"); -defineSymbol(symbols_math, main, symbols_textord, "\xA3", "\\mathsterling", true); -defineSymbol(symbols_text, main, symbols_textord, "\xA3", "\\pounds"); -defineSymbol(symbols_text, main, symbols_textord, "\xA3", "\\textsterling", true); -defineSymbol(symbols_math, ams, symbols_textord, "\u2720", "\\maltese"); -defineSymbol(symbols_text, ams, symbols_textord, "\u2720", "\\maltese"); // There are lots of symbols which are the same, so we add them in afterwards. -// All of these are textords in math mode - -var mathTextSymbols = "0123456789/@.\""; - -for (var symbols_i = 0; symbols_i < mathTextSymbols.length; symbols_i++) { - var symbols_ch = mathTextSymbols.charAt(symbols_i); - defineSymbol(symbols_math, main, symbols_textord, symbols_ch, symbols_ch); -} // All of these are textords in text mode - - -var textSymbols = "0123456789!@*()-=+\";:?/.,"; - -for (var src_symbols_i = 0; src_symbols_i < textSymbols.length; src_symbols_i++) { - var _ch = textSymbols.charAt(src_symbols_i); - - defineSymbol(symbols_text, main, symbols_textord, _ch, _ch); -} // All of these are textords in text mode, and mathords in math mode - - -var letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - -for (var symbols_i2 = 0; symbols_i2 < letters.length; symbols_i2++) { - var _ch2 = letters.charAt(symbols_i2); - - defineSymbol(symbols_math, main, mathord, _ch2, _ch2); - defineSymbol(symbols_text, main, symbols_textord, _ch2, _ch2); -} // Blackboard bold and script letters in Unicode range - - -defineSymbol(symbols_math, ams, symbols_textord, "C", "\u2102"); // blackboard bold - -defineSymbol(symbols_text, ams, symbols_textord, "C", "\u2102"); -defineSymbol(symbols_math, ams, symbols_textord, "H", "\u210D"); -defineSymbol(symbols_text, ams, symbols_textord, "H", "\u210D"); -defineSymbol(symbols_math, ams, symbols_textord, "N", "\u2115"); -defineSymbol(symbols_text, ams, symbols_textord, "N", "\u2115"); -defineSymbol(symbols_math, ams, symbols_textord, "P", "\u2119"); -defineSymbol(symbols_text, ams, symbols_textord, "P", "\u2119"); -defineSymbol(symbols_math, ams, symbols_textord, "Q", "\u211A"); -defineSymbol(symbols_text, ams, symbols_textord, "Q", "\u211A"); -defineSymbol(symbols_math, ams, symbols_textord, "R", "\u211D"); -defineSymbol(symbols_text, ams, symbols_textord, "R", "\u211D"); -defineSymbol(symbols_math, ams, symbols_textord, "Z", "\u2124"); -defineSymbol(symbols_text, ams, symbols_textord, "Z", "\u2124"); -defineSymbol(symbols_math, main, mathord, "h", "\u210E"); // italic h, Planck constant - -defineSymbol(symbols_text, main, mathord, "h", "\u210E"); // The next loop loads wide (surrogate pair) characters. -// We support some letters in the Unicode range U+1D400 to U+1D7FF, -// Mathematical Alphanumeric Symbols. -// Some editors do not deal well with wide characters. So don't write the -// string into this file. Instead, create the string from the surrogate pair. - -var symbols_wideChar = ""; - -for (var symbols_i3 = 0; symbols_i3 < letters.length; symbols_i3++) { - var _ch3 = letters.charAt(symbols_i3); // The hex numbers in the next line are a surrogate pair. - // 0xD835 is the high surrogate for all letters in the range we support. - // 0xDC00 is the low surrogate for bold A. - - - symbols_wideChar = String.fromCharCode(0xD835, 0xDC00 + symbols_i3); // A-Z a-z bold - - defineSymbol(symbols_math, main, mathord, _ch3, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch3, symbols_wideChar); - symbols_wideChar = String.fromCharCode(0xD835, 0xDC34 + symbols_i3); // A-Z a-z italic - - defineSymbol(symbols_math, main, mathord, _ch3, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch3, symbols_wideChar); - symbols_wideChar = String.fromCharCode(0xD835, 0xDC68 + symbols_i3); // A-Z a-z bold italic - - defineSymbol(symbols_math, main, mathord, _ch3, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch3, symbols_wideChar); - symbols_wideChar = String.fromCharCode(0xD835, 0xDD04 + symbols_i3); // A-Z a-z Fractur - - defineSymbol(symbols_math, main, mathord, _ch3, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch3, symbols_wideChar); - symbols_wideChar = String.fromCharCode(0xD835, 0xDDA0 + symbols_i3); // A-Z a-z sans-serif - - defineSymbol(symbols_math, main, mathord, _ch3, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch3, symbols_wideChar); - symbols_wideChar = String.fromCharCode(0xD835, 0xDDD4 + symbols_i3); // A-Z a-z sans bold - - defineSymbol(symbols_math, main, mathord, _ch3, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch3, symbols_wideChar); - symbols_wideChar = String.fromCharCode(0xD835, 0xDE08 + symbols_i3); // A-Z a-z sans italic - - defineSymbol(symbols_math, main, mathord, _ch3, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch3, symbols_wideChar); - symbols_wideChar = String.fromCharCode(0xD835, 0xDE70 + symbols_i3); // A-Z a-z monospace - - defineSymbol(symbols_math, main, mathord, _ch3, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch3, symbols_wideChar); - - if (symbols_i3 < 26) { - // KaTeX fonts have only capital letters for blackboard bold and script. - // See exception for k below. - symbols_wideChar = String.fromCharCode(0xD835, 0xDD38 + symbols_i3); // A-Z double struck - - defineSymbol(symbols_math, main, mathord, _ch3, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch3, symbols_wideChar); - symbols_wideChar = String.fromCharCode(0xD835, 0xDC9C + symbols_i3); // A-Z script - - defineSymbol(symbols_math, main, mathord, _ch3, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch3, symbols_wideChar); - } // TODO: Add bold script when it is supported by a KaTeX font. - -} // "k" is the only double struck lower case letter in the KaTeX fonts. - - -symbols_wideChar = String.fromCharCode(0xD835, 0xDD5C); // k double struck - -defineSymbol(symbols_math, main, mathord, "k", symbols_wideChar); -defineSymbol(symbols_text, main, symbols_textord, "k", symbols_wideChar); // Next, some wide character numerals - -for (var symbols_i4 = 0; symbols_i4 < 10; symbols_i4++) { - var _ch4 = symbols_i4.toString(); - - symbols_wideChar = String.fromCharCode(0xD835, 0xDFCE + symbols_i4); // 0-9 bold - - defineSymbol(symbols_math, main, mathord, _ch4, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch4, symbols_wideChar); - symbols_wideChar = String.fromCharCode(0xD835, 0xDFE2 + symbols_i4); // 0-9 sans serif - - defineSymbol(symbols_math, main, mathord, _ch4, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch4, symbols_wideChar); - symbols_wideChar = String.fromCharCode(0xD835, 0xDFEC + symbols_i4); // 0-9 bold sans - - defineSymbol(symbols_math, main, mathord, _ch4, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch4, symbols_wideChar); - symbols_wideChar = String.fromCharCode(0xD835, 0xDFF6 + symbols_i4); // 0-9 monospace - - defineSymbol(symbols_math, main, mathord, _ch4, symbols_wideChar); - defineSymbol(symbols_text, main, symbols_textord, _ch4, symbols_wideChar); -} // We add these Latin-1 letters as symbols for backwards-compatibility, -// but they are not actually in the font, nor are they supported by the -// Unicode accent mechanism, so they fall back to Times font and look ugly. -// TODO(edemaine): Fix this. - - -var extraLatin = "\xC7\xD0\xDE\xE7\xFE"; - -for (var _i5 = 0; _i5 < extraLatin.length; _i5++) { - var _ch5 = extraLatin.charAt(_i5); - - defineSymbol(symbols_math, main, mathord, _ch5, _ch5); - defineSymbol(symbols_text, main, symbols_textord, _ch5, _ch5); -} -// CONCATENATED MODULE: ./src/wide-character.js -/** - * This file provides support for Unicode range U+1D400 to U+1D7FF, - * Mathematical Alphanumeric Symbols. - * - * Function wideCharacterFont takes a wide character as input and returns - * the font information necessary to render it properly. - */ - -/** - * Data below is from https://www.unicode.org/charts/PDF/U1D400.pdf - * That document sorts characters into groups by font type, say bold or italic. - * - * In the arrays below, each subarray consists three elements: - * * The CSS class of that group when in math mode. - * * The CSS class of that group when in text mode. - * * The font name, so that KaTeX can get font metrics. - */ - -var wideLatinLetterData = [["mathbf", "textbf", "Main-Bold"], // A-Z bold upright -["mathbf", "textbf", "Main-Bold"], // a-z bold upright -["mathnormal", "textit", "Math-Italic"], // A-Z italic -["mathnormal", "textit", "Math-Italic"], // a-z italic -["boldsymbol", "boldsymbol", "Main-BoldItalic"], // A-Z bold italic -["boldsymbol", "boldsymbol", "Main-BoldItalic"], // a-z bold italic -// Map fancy A-Z letters to script, not calligraphic. -// This aligns with unicode-math and math fonts (except Cambria Math). -["mathscr", "textscr", "Script-Regular"], // A-Z script -["", "", ""], // a-z script. No font -["", "", ""], // A-Z bold script. No font -["", "", ""], // a-z bold script. No font -["mathfrak", "textfrak", "Fraktur-Regular"], // A-Z Fraktur -["mathfrak", "textfrak", "Fraktur-Regular"], // a-z Fraktur -["mathbb", "textbb", "AMS-Regular"], // A-Z double-struck -["mathbb", "textbb", "AMS-Regular"], // k double-struck -["", "", ""], // A-Z bold Fraktur No font metrics -["", "", ""], // a-z bold Fraktur. No font. -["mathsf", "textsf", "SansSerif-Regular"], // A-Z sans-serif -["mathsf", "textsf", "SansSerif-Regular"], // a-z sans-serif -["mathboldsf", "textboldsf", "SansSerif-Bold"], // A-Z bold sans-serif -["mathboldsf", "textboldsf", "SansSerif-Bold"], // a-z bold sans-serif -["mathitsf", "textitsf", "SansSerif-Italic"], // A-Z italic sans-serif -["mathitsf", "textitsf", "SansSerif-Italic"], // a-z italic sans-serif -["", "", ""], // A-Z bold italic sans. No font -["", "", ""], // a-z bold italic sans. No font -["mathtt", "texttt", "Typewriter-Regular"], // A-Z monospace -["mathtt", "texttt", "Typewriter-Regular"]]; -var wideNumeralData = [["mathbf", "textbf", "Main-Bold"], // 0-9 bold -["", "", ""], // 0-9 double-struck. No KaTeX font. -["mathsf", "textsf", "SansSerif-Regular"], // 0-9 sans-serif -["mathboldsf", "textboldsf", "SansSerif-Bold"], // 0-9 bold sans-serif -["mathtt", "texttt", "Typewriter-Regular"]]; -var wide_character_wideCharacterFont = function wideCharacterFont(wideChar, mode) { - // IE doesn't support codePointAt(). So work with the surrogate pair. - var H = wideChar.charCodeAt(0); // high surrogate - - var L = wideChar.charCodeAt(1); // low surrogate - - var codePoint = (H - 0xD800) * 0x400 + (L - 0xDC00) + 0x10000; - var j = mode === "math" ? 0 : 1; // column index for CSS class. - - if (0x1D400 <= codePoint && codePoint < 0x1D6A4) { - // wideLatinLetterData contains exactly 26 chars on each row. - // So we can calculate the relevant row. No traverse necessary. - var i = Math.floor((codePoint - 0x1D400) / 26); - return [wideLatinLetterData[i][2], wideLatinLetterData[i][j]]; - } else if (0x1D7CE <= codePoint && codePoint <= 0x1D7FF) { - // Numerals, ten per row. - var _i = Math.floor((codePoint - 0x1D7CE) / 10); - - return [wideNumeralData[_i][2], wideNumeralData[_i][j]]; - } else if (codePoint === 0x1D6A5 || codePoint === 0x1D6A6) { - // dotless i or j - return [wideLatinLetterData[0][2], wideLatinLetterData[0][j]]; - } else if (0x1D6A6 < codePoint && codePoint < 0x1D7CE) { - // Greek letters. Not supported, yet. - return ["", ""]; - } else { - // We don't support any wide characters outside 1D400–1D7FF. - throw new src_ParseError("Unsupported character: " + wideChar); - } -}; -// CONCATENATED MODULE: ./src/Options.js -/** - * This file contains information about the options that the Parser carries - * around with it while parsing. Data is held in an `Options` object, and when - * recursing, a new `Options` object can be created with the `.with*` and - * `.reset` functions. - */ - -var sizeStyleMap = [// Each element contains [textsize, scriptsize, scriptscriptsize]. -// The size mappings are taken from TeX with \normalsize=10pt. -[1, 1, 1], // size1: [5, 5, 5] \tiny -[2, 1, 1], // size2: [6, 5, 5] -[3, 1, 1], // size3: [7, 5, 5] \scriptsize -[4, 2, 1], // size4: [8, 6, 5] \footnotesize -[5, 2, 1], // size5: [9, 6, 5] \small -[6, 3, 1], // size6: [10, 7, 5] \normalsize -[7, 4, 2], // size7: [12, 8, 6] \large -[8, 6, 3], // size8: [14.4, 10, 7] \Large -[9, 7, 6], // size9: [17.28, 12, 10] \LARGE -[10, 8, 7], // size10: [20.74, 14.4, 12] \huge -[11, 10, 9]]; -var sizeMultipliers = [// fontMetrics.js:getGlobalMetrics also uses size indexes, so if -// you change size indexes, change that function. -0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2, 1.44, 1.728, 2.074, 2.488]; - -var sizeAtStyle = function sizeAtStyle(size, style) { - return style.size < 2 ? size : sizeStyleMap[size - 1][style.size - 1]; -}; // In these types, "" (empty string) means "no change". - - -/** - * This is the main options class. It contains the current style, size, color, - * and font. - * - * Options objects should not be modified. To create a new Options with - * different properties, call a `.having*` method. - */ -var Options_Options = -/*#__PURE__*/ -function () { - // A font family applies to a group of fonts (i.e. SansSerif), while a font - // represents a specific font (i.e. SansSerif Bold). - // See: https://tex.stackexchange.com/questions/22350/difference-between-textrm-and-mathrm - - /** - * The base size index. - */ - function Options(data) { - this.style = void 0; - this.color = void 0; - this.size = void 0; - this.textSize = void 0; - this.phantom = void 0; - this.font = void 0; - this.fontFamily = void 0; - this.fontWeight = void 0; - this.fontShape = void 0; - this.sizeMultiplier = void 0; - this.maxSize = void 0; - this.minRuleThickness = void 0; - this._fontMetrics = void 0; - this.style = data.style; - this.color = data.color; - this.size = data.size || Options.BASESIZE; - this.textSize = data.textSize || this.size; - this.phantom = !!data.phantom; - this.font = data.font || ""; - this.fontFamily = data.fontFamily || ""; - this.fontWeight = data.fontWeight || ''; - this.fontShape = data.fontShape || ''; - this.sizeMultiplier = sizeMultipliers[this.size - 1]; - this.maxSize = data.maxSize; - this.minRuleThickness = data.minRuleThickness; - this._fontMetrics = undefined; - } - /** - * Returns a new options object with the same properties as "this". Properties - * from "extension" will be copied to the new options object. - */ - - - var _proto = Options.prototype; - - _proto.extend = function extend(extension) { - var data = { - style: this.style, - size: this.size, - textSize: this.textSize, - color: this.color, - phantom: this.phantom, - font: this.font, - fontFamily: this.fontFamily, - fontWeight: this.fontWeight, - fontShape: this.fontShape, - maxSize: this.maxSize, - minRuleThickness: this.minRuleThickness - }; - - for (var key in extension) { - if (extension.hasOwnProperty(key)) { - data[key] = extension[key]; - } - } - - return new Options(data); - } - /** - * Return an options object with the given style. If `this.style === style`, - * returns `this`. - */ - ; - - _proto.havingStyle = function havingStyle(style) { - if (this.style === style) { - return this; - } else { - return this.extend({ - style: style, - size: sizeAtStyle(this.textSize, style) - }); - } - } - /** - * Return an options object with a cramped version of the current style. If - * the current style is cramped, returns `this`. - */ - ; - - _proto.havingCrampedStyle = function havingCrampedStyle() { - return this.havingStyle(this.style.cramp()); - } - /** - * Return an options object with the given size and in at least `\textstyle`. - * Returns `this` if appropriate. - */ - ; - - _proto.havingSize = function havingSize(size) { - if (this.size === size && this.textSize === size) { - return this; - } else { - return this.extend({ - style: this.style.text(), - size: size, - textSize: size, - sizeMultiplier: sizeMultipliers[size - 1] - }); - } - } - /** - * Like `this.havingSize(BASESIZE).havingStyle(style)`. If `style` is omitted, - * changes to at least `\textstyle`. - */ - ; - - _proto.havingBaseStyle = function havingBaseStyle(style) { - style = style || this.style.text(); - var wantSize = sizeAtStyle(Options.BASESIZE, style); - - if (this.size === wantSize && this.textSize === Options.BASESIZE && this.style === style) { - return this; - } else { - return this.extend({ - style: style, - size: wantSize - }); - } - } - /** - * Remove the effect of sizing changes such as \Huge. - * Keep the effect of the current style, such as \scriptstyle. - */ - ; - - _proto.havingBaseSizing = function havingBaseSizing() { - var size; - - switch (this.style.id) { - case 4: - case 5: - size = 3; // normalsize in scriptstyle - - break; - - case 6: - case 7: - size = 1; // normalsize in scriptscriptstyle - - break; - - default: - size = 6; - // normalsize in textstyle or displaystyle - } - - return this.extend({ - style: this.style.text(), - size: size - }); - } - /** - * Create a new options object with the given color. - */ - ; - - _proto.withColor = function withColor(color) { - return this.extend({ - color: color - }); - } - /** - * Create a new options object with "phantom" set to true. - */ - ; - - _proto.withPhantom = function withPhantom() { - return this.extend({ - phantom: true - }); - } - /** - * Creates a new options object with the given math font or old text font. - * @type {[type]} - */ - ; - - _proto.withFont = function withFont(font) { - return this.extend({ - font: font - }); - } - /** - * Create a new options objects with the given fontFamily. - */ - ; - - _proto.withTextFontFamily = function withTextFontFamily(fontFamily) { - return this.extend({ - fontFamily: fontFamily, - font: "" - }); - } - /** - * Creates a new options object with the given font weight - */ - ; - - _proto.withTextFontWeight = function withTextFontWeight(fontWeight) { - return this.extend({ - fontWeight: fontWeight, - font: "" - }); - } - /** - * Creates a new options object with the given font weight - */ - ; - - _proto.withTextFontShape = function withTextFontShape(fontShape) { - return this.extend({ - fontShape: fontShape, - font: "" - }); - } - /** - * Return the CSS sizing classes required to switch from enclosing options - * `oldOptions` to `this`. Returns an array of classes. - */ - ; - - _proto.sizingClasses = function sizingClasses(oldOptions) { - if (oldOptions.size !== this.size) { - return ["sizing", "reset-size" + oldOptions.size, "size" + this.size]; - } else { - return []; - } - } - /** - * Return the CSS sizing classes required to switch to the base size. Like - * `this.havingSize(BASESIZE).sizingClasses(this)`. - */ - ; - - _proto.baseSizingClasses = function baseSizingClasses() { - if (this.size !== Options.BASESIZE) { - return ["sizing", "reset-size" + this.size, "size" + Options.BASESIZE]; - } else { - return []; - } - } - /** - * Return the font metrics for this size. - */ - ; - - _proto.fontMetrics = function fontMetrics() { - if (!this._fontMetrics) { - this._fontMetrics = getGlobalMetrics(this.size); - } - - return this._fontMetrics; - } - /** - * Gets the CSS color of the current options object - */ - ; - - _proto.getColor = function getColor() { - if (this.phantom) { - return "transparent"; - } else { - return this.color; - } - }; - - return Options; -}(); - -Options_Options.BASESIZE = 6; -/* harmony default export */ var src_Options = (Options_Options); -// CONCATENATED MODULE: ./src/units.js -/** - * This file does conversion between units. In particular, it provides - * calculateSize to convert other units into ems. - */ - - // This table gives the number of TeX pts in one of each *absolute* TeX unit. -// Thus, multiplying a length by this number converts the length from units -// into pts. Dividing the result by ptPerEm gives the number of ems -// *assuming* a font size of ptPerEm (normal size, normal style). - -var ptPerUnit = { - // https://en.wikibooks.org/wiki/LaTeX/Lengths and - // https://tex.stackexchange.com/a/8263 - "pt": 1, - // TeX point - "mm": 7227 / 2540, - // millimeter - "cm": 7227 / 254, - // centimeter - "in": 72.27, - // inch - "bp": 803 / 800, - // big (PostScript) points - "pc": 12, - // pica - "dd": 1238 / 1157, - // didot - "cc": 14856 / 1157, - // cicero (12 didot) - "nd": 685 / 642, - // new didot - "nc": 1370 / 107, - // new cicero (12 new didot) - "sp": 1 / 65536, - // scaled point (TeX's internal smallest unit) - // https://tex.stackexchange.com/a/41371 - "px": 803 / 800 // \pdfpxdimen defaults to 1 bp in pdfTeX and LuaTeX - -}; // Dictionary of relative units, for fast validity testing. - -var relativeUnit = { - "ex": true, - "em": true, - "mu": true -}; - -/** - * Determine whether the specified unit (either a string defining the unit - * or a "size" parse node containing a unit field) is valid. - */ -var validUnit = function validUnit(unit) { - if (typeof unit !== "string") { - unit = unit.unit; - } - - return unit in ptPerUnit || unit in relativeUnit || unit === "ex"; -}; -/* - * Convert a "size" parse node (with numeric "number" and string "unit" fields, - * as parsed by functions.js argType "size") into a CSS em value for the - * current style/scale. `options` gives the current options. - */ - -var units_calculateSize = function calculateSize(sizeValue, options) { - var scale; - - if (sizeValue.unit in ptPerUnit) { - // Absolute units - scale = ptPerUnit[sizeValue.unit] // Convert unit to pt - / options.fontMetrics().ptPerEm // Convert pt to CSS em - / options.sizeMultiplier; // Unscale to make absolute units - } else if (sizeValue.unit === "mu") { - // `mu` units scale with scriptstyle/scriptscriptstyle. - scale = options.fontMetrics().cssEmPerMu; - } else { - // Other relative units always refer to the *textstyle* font - // in the current size. - var unitOptions; - - if (options.style.isTight()) { - // isTight() means current style is script/scriptscript. - unitOptions = options.havingStyle(options.style.text()); - } else { - unitOptions = options; - } // TODO: In TeX these units are relative to the quad of the current - // *text* font, e.g. cmr10. KaTeX instead uses values from the - // comparably-sized *Computer Modern symbol* font. At 10pt, these - // match. At 7pt and 5pt, they differ: cmr7=1.138894, cmsy7=1.170641; - // cmr5=1.361133, cmsy5=1.472241. Consider $\scriptsize a\kern1emb$. - // TeX \showlists shows a kern of 1.13889 * fontsize; - // KaTeX shows a kern of 1.171 * fontsize. - - - if (sizeValue.unit === "ex") { - scale = unitOptions.fontMetrics().xHeight; - } else if (sizeValue.unit === "em") { - scale = unitOptions.fontMetrics().quad; - } else { - throw new src_ParseError("Invalid unit: '" + sizeValue.unit + "'"); - } - - if (unitOptions !== options) { - scale *= unitOptions.sizeMultiplier / options.sizeMultiplier; - } - } - - return Math.min(sizeValue.number * scale, options.maxSize); -}; -// CONCATENATED MODULE: ./src/buildCommon.js -/* eslint no-console:0 */ - -/** - * This module contains general functions that can be used for building - * different kinds of domTree nodes in a consistent manner. - */ - - - - - - - -/** - * Looks up the given symbol in fontMetrics, after applying any symbol - * replacements defined in symbol.js - */ -var buildCommon_lookupSymbol = function lookupSymbol(value, // TODO(#963): Use a union type for this. -fontName, mode) { - // Replace the value with its replaced value from symbol.js - if (src_symbols[mode][value] && src_symbols[mode][value].replace) { - value = src_symbols[mode][value].replace; - } - - return { - value: value, - metrics: getCharacterMetrics(value, fontName, mode) - }; -}; -/** - * Makes a symbolNode after translation via the list of symbols in symbols.js. - * Correctly pulls out metrics for the character, and optionally takes a list of - * classes to be attached to the node. - * - * TODO: make argument order closer to makeSpan - * TODO: add a separate argument for math class (e.g. `mop`, `mbin`), which - * should if present come first in `classes`. - * TODO(#953): Make `options` mandatory and always pass it in. - */ - - -var buildCommon_makeSymbol = function makeSymbol(value, fontName, mode, options, classes) { - var lookup = buildCommon_lookupSymbol(value, fontName, mode); - var metrics = lookup.metrics; - value = lookup.value; - var symbolNode; - - if (metrics) { - var italic = metrics.italic; - - if (mode === "text" || options && options.font === "mathit") { - italic = 0; - } - - symbolNode = new domTree_SymbolNode(value, metrics.height, metrics.depth, italic, metrics.skew, metrics.width, classes); - } else { - // TODO(emily): Figure out a good way to only print this in development - typeof console !== "undefined" && console.warn("No character metrics " + ("for '" + value + "' in style '" + fontName + "' and mode '" + mode + "'")); - symbolNode = new domTree_SymbolNode(value, 0, 0, 0, 0, 0, classes); - } - - if (options) { - symbolNode.maxFontSize = options.sizeMultiplier; - - if (options.style.isTight()) { - symbolNode.classes.push("mtight"); - } - - var color = options.getColor(); - - if (color) { - symbolNode.style.color = color; - } - } - - return symbolNode; -}; -/** - * Makes a symbol in Main-Regular or AMS-Regular. - * Used for rel, bin, open, close, inner, and punct. - */ - - -var buildCommon_mathsym = function mathsym(value, mode, options, classes) { - if (classes === void 0) { - classes = []; - } - - // Decide what font to render the symbol in by its entry in the symbols - // table. - // Have a special case for when the value = \ because the \ is used as a - // textord in unsupported command errors but cannot be parsed as a regular - // text ordinal and is therefore not present as a symbol in the symbols - // table for text, as well as a special case for boldsymbol because it - // can be used for bold + and - - if (options.font === "boldsymbol" && buildCommon_lookupSymbol(value, "Main-Bold", mode).metrics) { - return buildCommon_makeSymbol(value, "Main-Bold", mode, options, classes.concat(["mathbf"])); - } else if (value === "\\" || src_symbols[mode][value].font === "main") { - return buildCommon_makeSymbol(value, "Main-Regular", mode, options, classes); - } else { - return buildCommon_makeSymbol(value, "AMS-Regular", mode, options, classes.concat(["amsrm"])); - } -}; -/** - * Determines which of the two font names (Main-Bold and Math-BoldItalic) and - * corresponding style tags (mathbf or boldsymbol) to use for font "boldsymbol", - * depending on the symbol. Use this function instead of fontMap for font - * "boldsymbol". - */ - - -var boldsymbol = function boldsymbol(value, mode, options, classes, type) { - if (type !== "textord" && buildCommon_lookupSymbol(value, "Math-BoldItalic", mode).metrics) { - return { - fontName: "Math-BoldItalic", - fontClass: "boldsymbol" - }; - } else { - // Some glyphs do not exist in Math-BoldItalic so we need to use - // Main-Bold instead. - return { - fontName: "Main-Bold", - fontClass: "mathbf" - }; - } -}; -/** - * Makes either a mathord or textord in the correct font and color. - */ - - -var buildCommon_makeOrd = function makeOrd(group, options, type) { - var mode = group.mode; - var text = group.text; - var classes = ["mord"]; // Math mode or Old font (i.e. \rm) - - var isFont = mode === "math" || mode === "text" && options.font; - var fontOrFamily = isFont ? options.font : options.fontFamily; - - if (text.charCodeAt(0) === 0xD835) { - // surrogate pairs get special treatment - var _wideCharacterFont = wide_character_wideCharacterFont(text, mode), - wideFontName = _wideCharacterFont[0], - wideFontClass = _wideCharacterFont[1]; - - return buildCommon_makeSymbol(text, wideFontName, mode, options, classes.concat(wideFontClass)); - } else if (fontOrFamily) { - var fontName; - var fontClasses; - - if (fontOrFamily === "boldsymbol") { - var fontData = boldsymbol(text, mode, options, classes, type); - fontName = fontData.fontName; - fontClasses = [fontData.fontClass]; - } else if (isFont) { - fontName = fontMap[fontOrFamily].fontName; - fontClasses = [fontOrFamily]; - } else { - fontName = retrieveTextFontName(fontOrFamily, options.fontWeight, options.fontShape); - fontClasses = [fontOrFamily, options.fontWeight, options.fontShape]; - } - - if (buildCommon_lookupSymbol(text, fontName, mode).metrics) { - return buildCommon_makeSymbol(text, fontName, mode, options, classes.concat(fontClasses)); - } else if (ligatures.hasOwnProperty(text) && fontName.substr(0, 10) === "Typewriter") { - // Deconstruct ligatures in monospace fonts (\texttt, \tt). - var parts = []; - - for (var i = 0; i < text.length; i++) { - parts.push(buildCommon_makeSymbol(text[i], fontName, mode, options, classes.concat(fontClasses))); - } - - return buildCommon_makeFragment(parts); - } - } // Makes a symbol in the default font for mathords and textords. - - - if (type === "mathord") { - return buildCommon_makeSymbol(text, "Math-Italic", mode, options, classes.concat(["mathnormal"])); - } else if (type === "textord") { - var font = src_symbols[mode][text] && src_symbols[mode][text].font; - - if (font === "ams") { - var _fontName = retrieveTextFontName("amsrm", options.fontWeight, options.fontShape); - - return buildCommon_makeSymbol(text, _fontName, mode, options, classes.concat("amsrm", options.fontWeight, options.fontShape)); - } else if (font === "main" || !font) { - var _fontName2 = retrieveTextFontName("textrm", options.fontWeight, options.fontShape); - - return buildCommon_makeSymbol(text, _fontName2, mode, options, classes.concat(options.fontWeight, options.fontShape)); - } else { - // fonts added by plugins - var _fontName3 = retrieveTextFontName(font, options.fontWeight, options.fontShape); // We add font name as a css class - - - return buildCommon_makeSymbol(text, _fontName3, mode, options, classes.concat(_fontName3, options.fontWeight, options.fontShape)); - } - } else { - throw new Error("unexpected type: " + type + " in makeOrd"); - } -}; -/** - * Returns true if subsequent symbolNodes have the same classes, skew, maxFont, - * and styles. - */ - - -var buildCommon_canCombine = function canCombine(prev, next) { - if (createClass(prev.classes) !== createClass(next.classes) || prev.skew !== next.skew || prev.maxFontSize !== next.maxFontSize) { - return false; - } - - for (var style in prev.style) { - if (prev.style.hasOwnProperty(style) && prev.style[style] !== next.style[style]) { - return false; - } - } - - for (var _style in next.style) { - if (next.style.hasOwnProperty(_style) && prev.style[_style] !== next.style[_style]) { - return false; - } - } - - return true; -}; -/** - * Combine consequetive domTree.symbolNodes into a single symbolNode. - * Note: this function mutates the argument. - */ - - -var buildCommon_tryCombineChars = function tryCombineChars(chars) { - for (var i = 0; i < chars.length - 1; i++) { - var prev = chars[i]; - var next = chars[i + 1]; - - if (prev instanceof domTree_SymbolNode && next instanceof domTree_SymbolNode && buildCommon_canCombine(prev, next)) { - prev.text += next.text; - prev.height = Math.max(prev.height, next.height); - prev.depth = Math.max(prev.depth, next.depth); // Use the last character's italic correction since we use - // it to add padding to the right of the span created from - // the combined characters. - - prev.italic = next.italic; - chars.splice(i + 1, 1); - i--; - } - } - - return chars; -}; -/** - * Calculate the height, depth, and maxFontSize of an element based on its - * children. - */ - - -var sizeElementFromChildren = function sizeElementFromChildren(elem) { - var height = 0; - var depth = 0; - var maxFontSize = 0; - - for (var i = 0; i < elem.children.length; i++) { - var child = elem.children[i]; - - if (child.height > height) { - height = child.height; - } - - if (child.depth > depth) { - depth = child.depth; - } - - if (child.maxFontSize > maxFontSize) { - maxFontSize = child.maxFontSize; - } - } - - elem.height = height; - elem.depth = depth; - elem.maxFontSize = maxFontSize; -}; -/** - * Makes a span with the given list of classes, list of children, and options. - * - * TODO(#953): Ensure that `options` is always provided (currently some call - * sites don't pass it) and make the type below mandatory. - * TODO: add a separate argument for math class (e.g. `mop`, `mbin`), which - * should if present come first in `classes`. - */ - - -var buildCommon_makeSpan = function makeSpan(classes, children, options, style) { - var span = new domTree_Span(classes, children, options, style); - sizeElementFromChildren(span); - return span; -}; // SVG one is simpler -- doesn't require height, depth, max-font setting. -// This is also a separate method for typesafety. - - -var buildCommon_makeSvgSpan = function makeSvgSpan(classes, children, options, style) { - return new domTree_Span(classes, children, options, style); -}; - -var makeLineSpan = function makeLineSpan(className, options, thickness) { - var line = buildCommon_makeSpan([className], [], options); - line.height = Math.max(thickness || options.fontMetrics().defaultRuleThickness, options.minRuleThickness); - line.style.borderBottomWidth = line.height + "em"; - line.maxFontSize = 1.0; - return line; -}; -/** - * Makes an anchor with the given href, list of classes, list of children, - * and options. - */ - - -var buildCommon_makeAnchor = function makeAnchor(href, classes, children, options) { - var anchor = new domTree_Anchor(href, classes, children, options); - sizeElementFromChildren(anchor); - return anchor; -}; -/** - * Makes a document fragment with the given list of children. - */ - - -var buildCommon_makeFragment = function makeFragment(children) { - var fragment = new tree_DocumentFragment(children); - sizeElementFromChildren(fragment); - return fragment; -}; -/** - * Wraps group in a span if it's a document fragment, allowing to apply classes - * and styles - */ - - -var buildCommon_wrapFragment = function wrapFragment(group, options) { - if (group instanceof tree_DocumentFragment) { - return buildCommon_makeSpan([], [group], options); - } - - return group; -}; // These are exact object types to catch typos in the names of the optional fields. - - -// Computes the updated `children` list and the overall depth. -// -// This helper function for makeVList makes it easier to enforce type safety by -// allowing early exits (returns) in the logic. -var getVListChildrenAndDepth = function getVListChildrenAndDepth(params) { - if (params.positionType === "individualShift") { - var oldChildren = params.children; - var children = [oldChildren[0]]; // Add in kerns to the list of params.children to get each element to be - // shifted to the correct specified shift - - var _depth = -oldChildren[0].shift - oldChildren[0].elem.depth; - - var currPos = _depth; - - for (var i = 1; i < oldChildren.length; i++) { - var diff = -oldChildren[i].shift - currPos - oldChildren[i].elem.depth; - var size = diff - (oldChildren[i - 1].elem.height + oldChildren[i - 1].elem.depth); - currPos = currPos + diff; - children.push({ - type: "kern", - size: size - }); - children.push(oldChildren[i]); - } - - return { - children: children, - depth: _depth - }; - } - - var depth; - - if (params.positionType === "top") { - // We always start at the bottom, so calculate the bottom by adding up - // all the sizes - var bottom = params.positionData; - - for (var _i = 0; _i < params.children.length; _i++) { - var child = params.children[_i]; - bottom -= child.type === "kern" ? child.size : child.elem.height + child.elem.depth; - } - - depth = bottom; - } else if (params.positionType === "bottom") { - depth = -params.positionData; - } else { - var firstChild = params.children[0]; - - if (firstChild.type !== "elem") { - throw new Error('First child must have type "elem".'); - } - - if (params.positionType === "shift") { - depth = -firstChild.elem.depth - params.positionData; - } else if (params.positionType === "firstBaseline") { - depth = -firstChild.elem.depth; - } else { - throw new Error("Invalid positionType " + params.positionType + "."); - } - } - - return { - children: params.children, - depth: depth - }; -}; -/** - * Makes a vertical list by stacking elements and kerns on top of each other. - * Allows for many different ways of specifying the positioning method. - * - * See VListParam documentation above. - */ - - -var buildCommon_makeVList = function makeVList(params, options) { - var _getVListChildrenAndD = getVListChildrenAndDepth(params), - children = _getVListChildrenAndD.children, - depth = _getVListChildrenAndD.depth; // Create a strut that is taller than any list item. The strut is added to - // each item, where it will determine the item's baseline. Since it has - // `overflow:hidden`, the strut's top edge will sit on the item's line box's - // top edge and the strut's bottom edge will sit on the item's baseline, - // with no additional line-height spacing. This allows the item baseline to - // be positioned precisely without worrying about font ascent and - // line-height. - - - var pstrutSize = 0; - - for (var i = 0; i < children.length; i++) { - var child = children[i]; - - if (child.type === "elem") { - var elem = child.elem; - pstrutSize = Math.max(pstrutSize, elem.maxFontSize, elem.height); - } - } - - pstrutSize += 2; - var pstrut = buildCommon_makeSpan(["pstrut"], []); - pstrut.style.height = pstrutSize + "em"; // Create a new list of actual children at the correct offsets - - var realChildren = []; - var minPos = depth; - var maxPos = depth; - var currPos = depth; - - for (var _i2 = 0; _i2 < children.length; _i2++) { - var _child = children[_i2]; - - if (_child.type === "kern") { - currPos += _child.size; - } else { - var _elem = _child.elem; - var classes = _child.wrapperClasses || []; - var style = _child.wrapperStyle || {}; - var childWrap = buildCommon_makeSpan(classes, [pstrut, _elem], undefined, style); - childWrap.style.top = -pstrutSize - currPos - _elem.depth + "em"; - - if (_child.marginLeft) { - childWrap.style.marginLeft = _child.marginLeft; - } - - if (_child.marginRight) { - childWrap.style.marginRight = _child.marginRight; - } - - realChildren.push(childWrap); - currPos += _elem.height + _elem.depth; - } - - minPos = Math.min(minPos, currPos); - maxPos = Math.max(maxPos, currPos); - } // The vlist contents go in a table-cell with `vertical-align:bottom`. - // This cell's bottom edge will determine the containing table's baseline - // without overly expanding the containing line-box. - - - var vlist = buildCommon_makeSpan(["vlist"], realChildren); - vlist.style.height = maxPos + "em"; // A second row is used if necessary to represent the vlist's depth. - - var rows; - - if (minPos < 0) { - // We will define depth in an empty span with display: table-cell. - // It should render with the height that we define. But Chrome, in - // contenteditable mode only, treats that span as if it contains some - // text content. And that min-height over-rides our desired height. - // So we put another empty span inside the depth strut span. - var emptySpan = buildCommon_makeSpan([], []); - var depthStrut = buildCommon_makeSpan(["vlist"], [emptySpan]); - depthStrut.style.height = -minPos + "em"; // Safari wants the first row to have inline content; otherwise it - // puts the bottom of the *second* row on the baseline. - - var topStrut = buildCommon_makeSpan(["vlist-s"], [new domTree_SymbolNode("\u200B")]); - rows = [buildCommon_makeSpan(["vlist-r"], [vlist, topStrut]), buildCommon_makeSpan(["vlist-r"], [depthStrut])]; - } else { - rows = [buildCommon_makeSpan(["vlist-r"], [vlist])]; - } - - var vtable = buildCommon_makeSpan(["vlist-t"], rows); - - if (rows.length === 2) { - vtable.classes.push("vlist-t2"); - } - - vtable.height = maxPos; - vtable.depth = -minPos; - return vtable; -}; // Glue is a concept from TeX which is a flexible space between elements in -// either a vertical or horizontal list. In KaTeX, at least for now, it's -// static space between elements in a horizontal layout. - - -var buildCommon_makeGlue = function makeGlue(measurement, options) { - // Make an empty span for the space - var rule = buildCommon_makeSpan(["mspace"], [], options); - var size = units_calculateSize(measurement, options); - rule.style.marginRight = size + "em"; - return rule; -}; // Takes font options, and returns the appropriate fontLookup name - - -var retrieveTextFontName = function retrieveTextFontName(fontFamily, fontWeight, fontShape) { - var baseFontName = ""; - - switch (fontFamily) { - case "amsrm": - baseFontName = "AMS"; - break; - - case "textrm": - baseFontName = "Main"; - break; - - case "textsf": - baseFontName = "SansSerif"; - break; - - case "texttt": - baseFontName = "Typewriter"; - break; - - default: - baseFontName = fontFamily; - // use fonts added by a plugin - } - - var fontStylesName; - - if (fontWeight === "textbf" && fontShape === "textit") { - fontStylesName = "BoldItalic"; - } else if (fontWeight === "textbf") { - fontStylesName = "Bold"; - } else if (fontWeight === "textit") { - fontStylesName = "Italic"; - } else { - fontStylesName = "Regular"; - } - - return baseFontName + "-" + fontStylesName; -}; -/** - * Maps TeX font commands to objects containing: - * - variant: string used for "mathvariant" attribute in buildMathML.js - * - fontName: the "style" parameter to fontMetrics.getCharacterMetrics - */ -// A map between tex font commands an MathML mathvariant attribute values - - -var fontMap = { - // styles - "mathbf": { - variant: "bold", - fontName: "Main-Bold" - }, - "mathrm": { - variant: "normal", - fontName: "Main-Regular" - }, - "textit": { - variant: "italic", - fontName: "Main-Italic" - }, - "mathit": { - variant: "italic", - fontName: "Main-Italic" - }, - "mathnormal": { - variant: "italic", - fontName: "Math-Italic" - }, - // "boldsymbol" is missing because they require the use of multiple fonts: - // Math-BoldItalic and Main-Bold. This is handled by a special case in - // makeOrd which ends up calling boldsymbol. - // families - "mathbb": { - variant: "double-struck", - fontName: "AMS-Regular" - }, - "mathcal": { - variant: "script", - fontName: "Caligraphic-Regular" - }, - "mathfrak": { - variant: "fraktur", - fontName: "Fraktur-Regular" - }, - "mathscr": { - variant: "script", - fontName: "Script-Regular" - }, - "mathsf": { - variant: "sans-serif", - fontName: "SansSerif-Regular" - }, - "mathtt": { - variant: "monospace", - fontName: "Typewriter-Regular" - } -}; -var svgData = { - // path, width, height - vec: ["vec", 0.471, 0.714], - // values from the font glyph - oiintSize1: ["oiintSize1", 0.957, 0.499], - // oval to overlay the integrand - oiintSize2: ["oiintSize2", 1.472, 0.659], - oiiintSize1: ["oiiintSize1", 1.304, 0.499], - oiiintSize2: ["oiiintSize2", 1.98, 0.659], - leftParenInner: ["leftParenInner", 0.875, 0.3], - rightParenInner: ["rightParenInner", 0.875, 0.3] -}; - -var buildCommon_staticSvg = function staticSvg(value, options) { - // Create a span with inline SVG for the element. - var _svgData$value = svgData[value], - pathName = _svgData$value[0], - width = _svgData$value[1], - height = _svgData$value[2]; - var path = new domTree_PathNode(pathName); - var svgNode = new SvgNode([path], { - "width": width + "em", - "height": height + "em", - // Override CSS rule `.katex svg { width: 100% }` - "style": "width:" + width + "em", - "viewBox": "0 0 " + 1000 * width + " " + 1000 * height, - "preserveAspectRatio": "xMinYMin" - }); - var span = buildCommon_makeSvgSpan(["overlay"], [svgNode], options); - span.height = height; - span.style.height = height + "em"; - span.style.width = width + "em"; - return span; -}; - -/* harmony default export */ var buildCommon = ({ - fontMap: fontMap, - makeSymbol: buildCommon_makeSymbol, - mathsym: buildCommon_mathsym, - makeSpan: buildCommon_makeSpan, - makeSvgSpan: buildCommon_makeSvgSpan, - makeLineSpan: makeLineSpan, - makeAnchor: buildCommon_makeAnchor, - makeFragment: buildCommon_makeFragment, - wrapFragment: buildCommon_wrapFragment, - makeVList: buildCommon_makeVList, - makeOrd: buildCommon_makeOrd, - makeGlue: buildCommon_makeGlue, - staticSvg: buildCommon_staticSvg, - svgData: svgData, - tryCombineChars: buildCommon_tryCombineChars -}); -// CONCATENATED MODULE: ./src/spacingData.js -/** - * Describes spaces between different classes of atoms. - */ -var thinspace = { - number: 3, - unit: "mu" -}; -var mediumspace = { - number: 4, - unit: "mu" -}; -var thickspace = { - number: 5, - unit: "mu" -}; // Making the type below exact with all optional fields doesn't work due to -// - https://github.com/facebook/flow/issues/4582 -// - https://github.com/facebook/flow/issues/5688 -// However, since *all* fields are optional, $Shape<> works as suggested in 5688 -// above. - -// Spacing relationships for display and text styles -var spacings = { - mord: { - mop: thinspace, - mbin: mediumspace, - mrel: thickspace, - minner: thinspace - }, - mop: { - mord: thinspace, - mop: thinspace, - mrel: thickspace, - minner: thinspace - }, - mbin: { - mord: mediumspace, - mop: mediumspace, - mopen: mediumspace, - minner: mediumspace - }, - mrel: { - mord: thickspace, - mop: thickspace, - mopen: thickspace, - minner: thickspace - }, - mopen: {}, - mclose: { - mop: thinspace, - mbin: mediumspace, - mrel: thickspace, - minner: thinspace - }, - mpunct: { - mord: thinspace, - mop: thinspace, - mrel: thickspace, - mopen: thinspace, - mclose: thinspace, - mpunct: thinspace, - minner: thinspace - }, - minner: { - mord: thinspace, - mop: thinspace, - mbin: mediumspace, - mrel: thickspace, - mopen: thinspace, - mpunct: thinspace, - minner: thinspace - } -}; // Spacing relationships for script and scriptscript styles - -var tightSpacings = { - mord: { - mop: thinspace - }, - mop: { - mord: thinspace, - mop: thinspace - }, - mbin: {}, - mrel: {}, - mopen: {}, - mclose: { - mop: thinspace - }, - mpunct: {}, - minner: { - mop: thinspace - } -}; -// CONCATENATED MODULE: ./src/defineFunction.js -/** Context provided to function handlers for error messages. */ -// Note: reverse the order of the return type union will cause a flow error. -// See https://github.com/facebook/flow/issues/3663. -// More general version of `HtmlBuilder` for nodes (e.g. \sum, accent types) -// whose presence impacts super/subscripting. In this case, ParseNode<"supsub"> -// delegates its HTML building to the HtmlBuilder corresponding to these nodes. - -/** - * Final function spec for use at parse time. - * This is almost identical to `FunctionPropSpec`, except it - * 1. includes the function handler, and - * 2. requires all arguments except argTypes. - * It is generated by `defineFunction()` below. - */ - -/** - * All registered functions. - * `functions.js` just exports this same dictionary again and makes it public. - * `Parser.js` requires this dictionary. - */ -var _functions = {}; -/** - * All HTML builders. Should be only used in the `define*` and the `build*ML` - * functions. - */ - -var _htmlGroupBuilders = {}; -/** - * All MathML builders. Should be only used in the `define*` and the `build*ML` - * functions. - */ - -var _mathmlGroupBuilders = {}; -function defineFunction(_ref) { - var type = _ref.type, - names = _ref.names, - props = _ref.props, - handler = _ref.handler, - htmlBuilder = _ref.htmlBuilder, - mathmlBuilder = _ref.mathmlBuilder; - // Set default values of functions - var data = { - type: type, - numArgs: props.numArgs, - argTypes: props.argTypes, - greediness: props.greediness === undefined ? 1 : props.greediness, - allowedInText: !!props.allowedInText, - allowedInMath: props.allowedInMath === undefined ? true : props.allowedInMath, - numOptionalArgs: props.numOptionalArgs || 0, - infix: !!props.infix, - handler: handler - }; - - for (var i = 0; i < names.length; ++i) { - _functions[names[i]] = data; - } - - if (type) { - if (htmlBuilder) { - _htmlGroupBuilders[type] = htmlBuilder; - } - - if (mathmlBuilder) { - _mathmlGroupBuilders[type] = mathmlBuilder; - } - } -} -/** - * Use this to register only the HTML and MathML builders for a function (e.g. - * if the function's ParseNode is generated in Parser.js rather than via a - * stand-alone handler provided to `defineFunction`). - */ - -function defineFunctionBuilders(_ref2) { - var type = _ref2.type, - htmlBuilder = _ref2.htmlBuilder, - mathmlBuilder = _ref2.mathmlBuilder; - defineFunction({ - type: type, - names: [], - props: { - numArgs: 0 - }, - handler: function handler() { - throw new Error('Should never be called.'); - }, - htmlBuilder: htmlBuilder, - mathmlBuilder: mathmlBuilder - }); -} // Since the corresponding buildHTML/buildMathML function expects a -// list of elements, we normalize for different kinds of arguments - -var ordargument = function ordargument(arg) { - return arg.type === "ordgroup" ? arg.body : [arg]; -}; -// CONCATENATED MODULE: ./src/buildHTML.js -/** - * This file does the main work of building a domTree structure from a parse - * tree. The entry point is the `buildHTML` function, which takes a parse tree. - * Then, the buildExpression, buildGroup, and various groupBuilders functions - * are called, to produce a final HTML tree. - */ - - - - - - - - -var buildHTML_makeSpan = buildCommon.makeSpan; // Binary atoms (first class `mbin`) change into ordinary atoms (`mord`) -// depending on their surroundings. See TeXbook pg. 442-446, Rules 5 and 6, -// and the text before Rule 19. - -var binLeftCanceller = ["leftmost", "mbin", "mopen", "mrel", "mop", "mpunct"]; -var binRightCanceller = ["rightmost", "mrel", "mclose", "mpunct"]; -var styleMap = { - "display": src_Style.DISPLAY, - "text": src_Style.TEXT, - "script": src_Style.SCRIPT, - "scriptscript": src_Style.SCRIPTSCRIPT -}; -var DomEnum = { - mord: "mord", - mop: "mop", - mbin: "mbin", - mrel: "mrel", - mopen: "mopen", - mclose: "mclose", - mpunct: "mpunct", - minner: "minner" -}; - -/** - * Take a list of nodes, build them in order, and return a list of the built - * nodes. documentFragments are flattened into their contents, so the - * returned list contains no fragments. `isRealGroup` is true if `expression` - * is a real group (no atoms will be added on either side), as opposed to - * a partial group (e.g. one created by \color). `surrounding` is an array - * consisting type of nodes that will be added to the left and right. - */ -var buildHTML_buildExpression = function buildExpression(expression, options, isRealGroup, surrounding) { - if (surrounding === void 0) { - surrounding = [null, null]; - } - - // Parse expressions into `groups`. - var groups = []; - - for (var i = 0; i < expression.length; i++) { - var output = buildHTML_buildGroup(expression[i], options); - - if (output instanceof tree_DocumentFragment) { - var children = output.children; - groups.push.apply(groups, children); - } else { - groups.push(output); - } - } // If `expression` is a partial group, let the parent handle spacings - // to avoid processing groups multiple times. - - - if (!isRealGroup) { - return groups; - } - - var glueOptions = options; - - if (expression.length === 1) { - var node = expression[0]; - - if (node.type === "sizing") { - glueOptions = options.havingSize(node.size); - } else if (node.type === "styling") { - glueOptions = options.havingStyle(styleMap[node.style]); - } - } // Dummy spans for determining spacings between surrounding atoms. - // If `expression` has no atoms on the left or right, class "leftmost" - // or "rightmost", respectively, is used to indicate it. - - - var dummyPrev = buildHTML_makeSpan([surrounding[0] || "leftmost"], [], options); - var dummyNext = buildHTML_makeSpan([surrounding[1] || "rightmost"], [], options); // TODO: These code assumes that a node's math class is the first element - // of its `classes` array. A later cleanup should ensure this, for - // instance by changing the signature of `makeSpan`. - // Before determining what spaces to insert, perform bin cancellation. - // Binary operators change to ordinary symbols in some contexts. - - var isRoot = isRealGroup === "root"; - traverseNonSpaceNodes(groups, function (node, prev) { - var prevType = prev.classes[0]; - var type = node.classes[0]; - - if (prevType === "mbin" && utils.contains(binRightCanceller, type)) { - prev.classes[0] = "mord"; - } else if (type === "mbin" && utils.contains(binLeftCanceller, prevType)) { - node.classes[0] = "mord"; - } - }, { - node: dummyPrev - }, dummyNext, isRoot); - traverseNonSpaceNodes(groups, function (node, prev) { - var prevType = getTypeOfDomTree(prev); - var type = getTypeOfDomTree(node); // 'mtight' indicates that the node is script or scriptscript style. - - var space = prevType && type ? node.hasClass("mtight") ? tightSpacings[prevType][type] : spacings[prevType][type] : null; - - if (space) { - // Insert glue (spacing) after the `prev`. - return buildCommon.makeGlue(space, glueOptions); - } - }, { - node: dummyPrev - }, dummyNext, isRoot); - return groups; -}; // Depth-first traverse non-space `nodes`, calling `callback` with the current and -// previous node as arguments, optionally returning a node to insert after the -// previous node. `prev` is an object with the previous node and `insertAfter` -// function to insert after it. `next` is a node that will be added to the right. -// Used for bin cancellation and inserting spacings. - -var traverseNonSpaceNodes = function traverseNonSpaceNodes(nodes, callback, prev, next, isRoot) { - if (next) { - // temporarily append the right node, if exists - nodes.push(next); - } - - var i = 0; - - for (; i < nodes.length; i++) { - var node = nodes[i]; - var partialGroup = buildHTML_checkPartialGroup(node); - - if (partialGroup) { - // Recursive DFS - // $FlowFixMe: make nodes a $ReadOnlyArray by returning a new array - traverseNonSpaceNodes(partialGroup.children, callback, prev, null, isRoot); - continue; - } // Ignore explicit spaces (e.g., \;, \,) when determining what implicit - // spacing should go between atoms of different classes - - - var nonspace = !node.hasClass("mspace"); - - if (nonspace) { - var result = callback(node, prev.node); - - if (result) { - if (prev.insertAfter) { - prev.insertAfter(result); - } else { - // insert at front - nodes.unshift(result); - i++; - } - } - } - - if (nonspace) { - prev.node = node; - } else if (isRoot && node.hasClass("newline")) { - prev.node = buildHTML_makeSpan(["leftmost"]); // treat like beginning of line - } - - prev.insertAfter = function (index) { - return function (n) { - nodes.splice(index + 1, 0, n); - i++; - }; - }(i); - } - - if (next) { - nodes.pop(); - } -}; // Check if given node is a partial group, i.e., does not affect spacing around. - - -var buildHTML_checkPartialGroup = function checkPartialGroup(node) { - if (node instanceof tree_DocumentFragment || node instanceof domTree_Anchor || node instanceof domTree_Span && node.hasClass("enclosing")) { - return node; - } - - return null; -}; // Return the outermost node of a domTree. - - -var getOutermostNode = function getOutermostNode(node, side) { - var partialGroup = buildHTML_checkPartialGroup(node); - - if (partialGroup) { - var children = partialGroup.children; - - if (children.length) { - if (side === "right") { - return getOutermostNode(children[children.length - 1], "right"); - } else if (side === "left") { - return getOutermostNode(children[0], "left"); - } - } - } - - return node; -}; // Return math atom class (mclass) of a domTree. -// If `side` is given, it will get the type of the outermost node at given side. - - -var getTypeOfDomTree = function getTypeOfDomTree(node, side) { - if (!node) { - return null; - } - - if (side) { - node = getOutermostNode(node, side); - } // This makes a lot of assumptions as to where the type of atom - // appears. We should do a better job of enforcing this. - - - return DomEnum[node.classes[0]] || null; -}; -var makeNullDelimiter = function makeNullDelimiter(options, classes) { - var moreClasses = ["nulldelimiter"].concat(options.baseSizingClasses()); - return buildHTML_makeSpan(classes.concat(moreClasses)); -}; -/** - * buildGroup is the function that takes a group and calls the correct groupType - * function for it. It also handles the interaction of size and style changes - * between parents and children. - */ - -var buildHTML_buildGroup = function buildGroup(group, options, baseOptions) { - if (!group) { - return buildHTML_makeSpan(); - } - - if (_htmlGroupBuilders[group.type]) { - // Call the groupBuilders function - var groupNode = _htmlGroupBuilders[group.type](group, options); // If the size changed between the parent and the current group, account - // for that size difference. - - if (baseOptions && options.size !== baseOptions.size) { - groupNode = buildHTML_makeSpan(options.sizingClasses(baseOptions), [groupNode], options); - var multiplier = options.sizeMultiplier / baseOptions.sizeMultiplier; - groupNode.height *= multiplier; - groupNode.depth *= multiplier; - } - - return groupNode; - } else { - throw new src_ParseError("Got group of unknown type: '" + group.type + "'"); - } -}; -/** - * Combine an array of HTML DOM nodes (e.g., the output of `buildExpression`) - * into an unbreakable HTML node of class .base, with proper struts to - * guarantee correct vertical extent. `buildHTML` calls this repeatedly to - * make up the entire expression as a sequence of unbreakable units. - */ - -function buildHTMLUnbreakable(children, options) { - // Compute height and depth of this chunk. - var body = buildHTML_makeSpan(["base"], children, options); // Add strut, which ensures that the top of the HTML element falls at - // the height of the expression, and the bottom of the HTML element - // falls at the depth of the expression. - - var strut = buildHTML_makeSpan(["strut"]); - strut.style.height = body.height + body.depth + "em"; - strut.style.verticalAlign = -body.depth + "em"; - body.children.unshift(strut); - return body; -} -/** - * Take an entire parse tree, and build it into an appropriate set of HTML - * nodes. - */ - - -function buildHTML(tree, options) { - // Strip off outer tag wrapper for processing below. - var tag = null; - - if (tree.length === 1 && tree[0].type === "tag") { - tag = tree[0].tag; - tree = tree[0].body; - } // Build the expression contained in the tree - - - var expression = buildHTML_buildExpression(tree, options, "root"); - var children = []; // Create one base node for each chunk between potential line breaks. - // The TeXBook [p.173] says "A formula will be broken only after a - // relation symbol like $=$ or $<$ or $\rightarrow$, or after a binary - // operation symbol like $+$ or $-$ or $\times$, where the relation or - // binary operation is on the ``outer level'' of the formula (i.e., not - // enclosed in {...} and not part of an \over construction)." - - var parts = []; - - for (var i = 0; i < expression.length; i++) { - parts.push(expression[i]); - - if (expression[i].hasClass("mbin") || expression[i].hasClass("mrel") || expression[i].hasClass("allowbreak")) { - // Put any post-operator glue on same line as operator. - // Watch for \nobreak along the way, and stop at \newline. - var nobreak = false; - - while (i < expression.length - 1 && expression[i + 1].hasClass("mspace") && !expression[i + 1].hasClass("newline")) { - i++; - parts.push(expression[i]); - - if (expression[i].hasClass("nobreak")) { - nobreak = true; - } - } // Don't allow break if \nobreak among the post-operator glue. - - - if (!nobreak) { - children.push(buildHTMLUnbreakable(parts, options)); - parts = []; - } - } else if (expression[i].hasClass("newline")) { - // Write the line except the newline - parts.pop(); - - if (parts.length > 0) { - children.push(buildHTMLUnbreakable(parts, options)); - parts = []; - } // Put the newline at the top level - - - children.push(expression[i]); - } - } - - if (parts.length > 0) { - children.push(buildHTMLUnbreakable(parts, options)); - } // Now, if there was a tag, build it too and append it as a final child. - - - var tagChild; - - if (tag) { - tagChild = buildHTMLUnbreakable(buildHTML_buildExpression(tag, options, true)); - tagChild.classes = ["tag"]; - children.push(tagChild); - } - - var htmlNode = buildHTML_makeSpan(["katex-html"], children); - htmlNode.setAttribute("aria-hidden", "true"); // Adjust the strut of the tag to be the maximum height of all children - // (the height of the enclosing htmlNode) for proper vertical alignment. - - if (tagChild) { - var strut = tagChild.children[0]; - strut.style.height = htmlNode.height + htmlNode.depth + "em"; - strut.style.verticalAlign = -htmlNode.depth + "em"; - } - - return htmlNode; -} -// CONCATENATED MODULE: ./src/mathMLTree.js -/** - * These objects store data about MathML nodes. This is the MathML equivalent - * of the types in domTree.js. Since MathML handles its own rendering, and - * since we're mainly using MathML to improve accessibility, we don't manage - * any of the styling state that the plain DOM nodes do. - * - * The `toNode` and `toMarkup` functions work simlarly to how they do in - * domTree.js, creating namespaced DOM nodes and HTML text markup respectively. - */ - - -function newDocumentFragment(children) { - return new tree_DocumentFragment(children); -} -/** - * This node represents a general purpose MathML node of any type. The - * constructor requires the type of node to create (for example, `"mo"` or - * `"mspace"`, corresponding to `` and `` tags). - */ - -var mathMLTree_MathNode = -/*#__PURE__*/ -function () { - function MathNode(type, children) { - this.type = void 0; - this.attributes = void 0; - this.children = void 0; - this.type = type; - this.attributes = {}; - this.children = children || []; - } - /** - * Sets an attribute on a MathML node. MathML depends on attributes to convey a - * semantic content, so this is used heavily. - */ - - - var _proto = MathNode.prototype; - - _proto.setAttribute = function setAttribute(name, value) { - this.attributes[name] = value; - } - /** - * Gets an attribute on a MathML node. - */ - ; - - _proto.getAttribute = function getAttribute(name) { - return this.attributes[name]; - } - /** - * Converts the math node into a MathML-namespaced DOM element. - */ - ; - - _proto.toNode = function toNode() { - var node = document.createElementNS("http://www.w3.org/1998/Math/MathML", this.type); - - for (var attr in this.attributes) { - if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { - node.setAttribute(attr, this.attributes[attr]); - } - } - - for (var i = 0; i < this.children.length; i++) { - node.appendChild(this.children[i].toNode()); - } - - return node; - } - /** - * Converts the math node into an HTML markup string. - */ - ; - - _proto.toMarkup = function toMarkup() { - var markup = "<" + this.type; // Add the attributes - - for (var attr in this.attributes) { - if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { - markup += " " + attr + "=\""; - markup += utils.escape(this.attributes[attr]); - markup += "\""; - } - } - - markup += ">"; - - for (var i = 0; i < this.children.length; i++) { - markup += this.children[i].toMarkup(); - } - - markup += ""; - return markup; - } - /** - * Converts the math node into a string, similar to innerText, but escaped. - */ - ; - - _proto.toText = function toText() { - return this.children.map(function (child) { - return child.toText(); - }).join(""); - }; - - return MathNode; -}(); -/** - * This node represents a piece of text. - */ - -var mathMLTree_TextNode = -/*#__PURE__*/ -function () { - function TextNode(text) { - this.text = void 0; - this.text = text; - } - /** - * Converts the text node into a DOM text node. - */ - - - var _proto2 = TextNode.prototype; - - _proto2.toNode = function toNode() { - return document.createTextNode(this.text); - } - /** - * Converts the text node into escaped HTML markup - * (representing the text itself). - */ - ; - - _proto2.toMarkup = function toMarkup() { - return utils.escape(this.toText()); - } - /** - * Converts the text node into a string - * (representing the text iteself). - */ - ; - - _proto2.toText = function toText() { - return this.text; - }; - - return TextNode; -}(); -/** - * This node represents a space, but may render as or as text, - * depending on the width. - */ - -var SpaceNode = -/*#__PURE__*/ -function () { - /** - * Create a Space node with width given in CSS ems. - */ - function SpaceNode(width) { - this.width = void 0; - this.character = void 0; - this.width = width; // See https://www.w3.org/TR/2000/WD-MathML2-20000328/chapter6.html - // for a table of space-like characters. We use Unicode - // representations instead of &LongNames; as it's not clear how to - // make the latter via document.createTextNode. - - if (width >= 0.05555 && width <= 0.05556) { - this.character = "\u200A"; //   - } else if (width >= 0.1666 && width <= 0.1667) { - this.character = "\u2009"; //   - } else if (width >= 0.2222 && width <= 0.2223) { - this.character = "\u2005"; //   - } else if (width >= 0.2777 && width <= 0.2778) { - this.character = "\u2005\u200A"; //    - } else if (width >= -0.05556 && width <= -0.05555) { - this.character = "\u200A\u2063"; // ​ - } else if (width >= -0.1667 && width <= -0.1666) { - this.character = "\u2009\u2063"; // ​ - } else if (width >= -0.2223 && width <= -0.2222) { - this.character = "\u205F\u2063"; // ​ - } else if (width >= -0.2778 && width <= -0.2777) { - this.character = "\u2005\u2063"; // ​ - } else { - this.character = null; - } - } - /** - * Converts the math node into a MathML-namespaced DOM element. - */ - - - var _proto3 = SpaceNode.prototype; - - _proto3.toNode = function toNode() { - if (this.character) { - return document.createTextNode(this.character); - } else { - var node = document.createElementNS("http://www.w3.org/1998/Math/MathML", "mspace"); - node.setAttribute("width", this.width + "em"); - return node; - } - } - /** - * Converts the math node into an HTML markup string. - */ - ; - - _proto3.toMarkup = function toMarkup() { - if (this.character) { - return "" + this.character + ""; - } else { - return ""; - } - } - /** - * Converts the math node into a string, similar to innerText. - */ - ; - - _proto3.toText = function toText() { - if (this.character) { - return this.character; - } else { - return " "; - } - }; - - return SpaceNode; -}(); - -/* harmony default export */ var mathMLTree = ({ - MathNode: mathMLTree_MathNode, - TextNode: mathMLTree_TextNode, - SpaceNode: SpaceNode, - newDocumentFragment: newDocumentFragment -}); -// CONCATENATED MODULE: ./src/buildMathML.js -/** - * This file converts a parse tree into a cooresponding MathML tree. The main - * entry point is the `buildMathML` function, which takes a parse tree from the - * parser. - */ - - - - - - - - - -/** - * Takes a symbol and converts it into a MathML text node after performing - * optional replacement from symbols.js. - */ -var buildMathML_makeText = function makeText(text, mode, options) { - if (src_symbols[mode][text] && src_symbols[mode][text].replace && text.charCodeAt(0) !== 0xD835 && !(ligatures.hasOwnProperty(text) && options && (options.fontFamily && options.fontFamily.substr(4, 2) === "tt" || options.font && options.font.substr(4, 2) === "tt"))) { - text = src_symbols[mode][text].replace; - } - - return new mathMLTree.TextNode(text); -}; -/** - * Wrap the given array of nodes in an node if needed, i.e., - * unless the array has length 1. Always returns a single node. - */ - -var buildMathML_makeRow = function makeRow(body) { - if (body.length === 1) { - return body[0]; - } else { - return new mathMLTree.MathNode("mrow", body); - } -}; -/** - * Returns the math variant as a string or null if none is required. - */ - -var buildMathML_getVariant = function getVariant(group, options) { - // Handle \text... font specifiers as best we can. - // MathML has a limited list of allowable mathvariant specifiers; see - // https://www.w3.org/TR/MathML3/chapter3.html#presm.commatt - if (options.fontFamily === "texttt") { - return "monospace"; - } else if (options.fontFamily === "textsf") { - if (options.fontShape === "textit" && options.fontWeight === "textbf") { - return "sans-serif-bold-italic"; - } else if (options.fontShape === "textit") { - return "sans-serif-italic"; - } else if (options.fontWeight === "textbf") { - return "bold-sans-serif"; - } else { - return "sans-serif"; - } - } else if (options.fontShape === "textit" && options.fontWeight === "textbf") { - return "bold-italic"; - } else if (options.fontShape === "textit") { - return "italic"; - } else if (options.fontWeight === "textbf") { - return "bold"; - } - - var font = options.font; - - if (!font || font === "mathnormal") { - return null; - } - - var mode = group.mode; - - if (font === "mathit") { - return "italic"; - } else if (font === "boldsymbol") { - return group.type === "textord" ? "bold" : "bold-italic"; - } else if (font === "mathbf") { - return "bold"; - } else if (font === "mathbb") { - return "double-struck"; - } else if (font === "mathfrak") { - return "fraktur"; - } else if (font === "mathscr" || font === "mathcal") { - // MathML makes no distinction between script and caligrahpic - return "script"; - } else if (font === "mathsf") { - return "sans-serif"; - } else if (font === "mathtt") { - return "monospace"; - } - - var text = group.text; - - if (utils.contains(["\\imath", "\\jmath"], text)) { - return null; - } - - if (src_symbols[mode][text] && src_symbols[mode][text].replace) { - text = src_symbols[mode][text].replace; - } - - var fontName = buildCommon.fontMap[font].fontName; - - if (getCharacterMetrics(text, fontName, mode)) { - return buildCommon.fontMap[font].variant; - } - - return null; -}; -/** - * Takes a list of nodes, builds them, and returns a list of the generated - * MathML nodes. Also combine consecutive outputs into a single - * tag. - */ - -var buildMathML_buildExpression = function buildExpression(expression, options, isOrdgroup) { - if (expression.length === 1) { - var group = buildMathML_buildGroup(expression[0], options); - - if (isOrdgroup && group instanceof mathMLTree_MathNode && group.type === "mo") { - // When TeX writers want to suppress spacing on an operator, - // they often put the operator by itself inside braces. - group.setAttribute("lspace", "0em"); - group.setAttribute("rspace", "0em"); - } - - return [group]; - } - - var groups = []; - var lastGroup; - - for (var i = 0; i < expression.length; i++) { - var _group = buildMathML_buildGroup(expression[i], options); - - if (_group instanceof mathMLTree_MathNode && lastGroup instanceof mathMLTree_MathNode) { - // Concatenate adjacent s - if (_group.type === 'mtext' && lastGroup.type === 'mtext' && _group.getAttribute('mathvariant') === lastGroup.getAttribute('mathvariant')) { - var _lastGroup$children; - - (_lastGroup$children = lastGroup.children).push.apply(_lastGroup$children, _group.children); - - continue; // Concatenate adjacent s - } else if (_group.type === 'mn' && lastGroup.type === 'mn') { - var _lastGroup$children2; - - (_lastGroup$children2 = lastGroup.children).push.apply(_lastGroup$children2, _group.children); - - continue; // Concatenate ... followed by . - } else if (_group.type === 'mi' && _group.children.length === 1 && lastGroup.type === 'mn') { - var child = _group.children[0]; - - if (child instanceof mathMLTree_TextNode && child.text === '.') { - var _lastGroup$children3; - - (_lastGroup$children3 = lastGroup.children).push.apply(_lastGroup$children3, _group.children); - - continue; - } - } else if (lastGroup.type === 'mi' && lastGroup.children.length === 1) { - var lastChild = lastGroup.children[0]; - - if (lastChild instanceof mathMLTree_TextNode && lastChild.text === "\u0338" && (_group.type === 'mo' || _group.type === 'mi' || _group.type === 'mn')) { - var _child = _group.children[0]; - - if (_child instanceof mathMLTree_TextNode && _child.text.length > 0) { - // Overlay with combining character long solidus - _child.text = _child.text.slice(0, 1) + "\u0338" + _child.text.slice(1); - groups.pop(); - } - } - } - } - - groups.push(_group); - lastGroup = _group; - } - - return groups; -}; -/** - * Equivalent to buildExpression, but wraps the elements in an - * if there's more than one. Returns a single node instead of an array. - */ - -var buildExpressionRow = function buildExpressionRow(expression, options, isOrdgroup) { - return buildMathML_makeRow(buildMathML_buildExpression(expression, options, isOrdgroup)); -}; -/** - * Takes a group from the parser and calls the appropriate groupBuilders function - * on it to produce a MathML node. - */ - -var buildMathML_buildGroup = function buildGroup(group, options) { - if (!group) { - return new mathMLTree.MathNode("mrow"); - } - - if (_mathmlGroupBuilders[group.type]) { - // Call the groupBuilders function - var result = _mathmlGroupBuilders[group.type](group, options); - return result; - } else { - throw new src_ParseError("Got group of unknown type: '" + group.type + "'"); - } -}; -/** - * Takes a full parse tree and settings and builds a MathML representation of - * it. In particular, we put the elements from building the parse tree into a - * tag so we can also include that TeX source as an annotation. - * - * Note that we actually return a domTree element with a `` inside it so - * we can do appropriate styling. - */ - -function buildMathML(tree, texExpression, options, isDisplayMode, forMathmlOnly) { - var expression = buildMathML_buildExpression(tree, options); // Wrap up the expression in an mrow so it is presented in the semantics - // tag correctly, unless it's a single or . - - var wrapper; - - if (expression.length === 1 && expression[0] instanceof mathMLTree_MathNode && utils.contains(["mrow", "mtable"], expression[0].type)) { - wrapper = expression[0]; - } else { - wrapper = new mathMLTree.MathNode("mrow", expression); - } // Build a TeX annotation of the source - - - var annotation = new mathMLTree.MathNode("annotation", [new mathMLTree.TextNode(texExpression)]); - annotation.setAttribute("encoding", "application/x-tex"); - var semantics = new mathMLTree.MathNode("semantics", [wrapper, annotation]); - var math = new mathMLTree.MathNode("math", [semantics]); - math.setAttribute("xmlns", "http://www.w3.org/1998/Math/MathML"); - - if (isDisplayMode) { - math.setAttribute("display", "block"); - } // You can't style nodes, so we wrap the node in a span. - // NOTE: The span class is not typed to have nodes as children, and - // we don't want to make the children type more generic since the children - // of span are expected to have more fields in `buildHtml` contexts. - - - var wrapperClass = forMathmlOnly ? "katex" : "katex-mathml"; // $FlowFixMe - - return buildCommon.makeSpan([wrapperClass], [math]); -} -// CONCATENATED MODULE: ./src/buildTree.js - - - - - - - -var buildTree_optionsFromSettings = function optionsFromSettings(settings) { - return new src_Options({ - style: settings.displayMode ? src_Style.DISPLAY : src_Style.TEXT, - maxSize: settings.maxSize, - minRuleThickness: settings.minRuleThickness - }); -}; - -var buildTree_displayWrap = function displayWrap(node, settings) { - if (settings.displayMode) { - var classes = ["katex-display"]; - - if (settings.leqno) { - classes.push("leqno"); - } - - if (settings.fleqn) { - classes.push("fleqn"); - } - - node = buildCommon.makeSpan(classes, [node]); - } - - return node; -}; - -var buildTree_buildTree = function buildTree(tree, expression, settings) { - var options = buildTree_optionsFromSettings(settings); - var katexNode; - - if (settings.output === "mathml") { - return buildMathML(tree, expression, options, settings.displayMode, true); - } else if (settings.output === "html") { - var htmlNode = buildHTML(tree, options); - katexNode = buildCommon.makeSpan(["katex"], [htmlNode]); - } else { - var mathMLNode = buildMathML(tree, expression, options, settings.displayMode, false); - - var _htmlNode = buildHTML(tree, options); - - katexNode = buildCommon.makeSpan(["katex"], [mathMLNode, _htmlNode]); - } - - return buildTree_displayWrap(katexNode, settings); -}; -var buildTree_buildHTMLTree = function buildHTMLTree(tree, expression, settings) { - var options = buildTree_optionsFromSettings(settings); - var htmlNode = buildHTML(tree, options); - var katexNode = buildCommon.makeSpan(["katex"], [htmlNode]); - return buildTree_displayWrap(katexNode, settings); -}; -/* harmony default export */ var src_buildTree = (buildTree_buildTree); -// CONCATENATED MODULE: ./src/stretchy.js -/** - * This file provides support to buildMathML.js and buildHTML.js - * for stretchy wide elements rendered from SVG files - * and other CSS trickery. - */ - - - - -var stretchyCodePoint = { - widehat: "^", - widecheck: "ˇ", - widetilde: "~", - utilde: "~", - overleftarrow: "\u2190", - underleftarrow: "\u2190", - xleftarrow: "\u2190", - overrightarrow: "\u2192", - underrightarrow: "\u2192", - xrightarrow: "\u2192", - underbrace: "\u23DF", - overbrace: "\u23DE", - overgroup: "\u23E0", - undergroup: "\u23E1", - overleftrightarrow: "\u2194", - underleftrightarrow: "\u2194", - xleftrightarrow: "\u2194", - Overrightarrow: "\u21D2", - xRightarrow: "\u21D2", - overleftharpoon: "\u21BC", - xleftharpoonup: "\u21BC", - overrightharpoon: "\u21C0", - xrightharpoonup: "\u21C0", - xLeftarrow: "\u21D0", - xLeftrightarrow: "\u21D4", - xhookleftarrow: "\u21A9", - xhookrightarrow: "\u21AA", - xmapsto: "\u21A6", - xrightharpoondown: "\u21C1", - xleftharpoondown: "\u21BD", - xrightleftharpoons: "\u21CC", - xleftrightharpoons: "\u21CB", - xtwoheadleftarrow: "\u219E", - xtwoheadrightarrow: "\u21A0", - xlongequal: "=", - xtofrom: "\u21C4", - xrightleftarrows: "\u21C4", - xrightequilibrium: "\u21CC", - // Not a perfect match. - xleftequilibrium: "\u21CB" // None better available. - -}; - -var stretchy_mathMLnode = function mathMLnode(label) { - var node = new mathMLTree.MathNode("mo", [new mathMLTree.TextNode(stretchyCodePoint[label.substr(1)])]); - node.setAttribute("stretchy", "true"); - return node; -}; // Many of the KaTeX SVG images have been adapted from glyphs in KaTeX fonts. -// Copyright (c) 2009-2010, Design Science, Inc. () -// Copyright (c) 2014-2017 Khan Academy () -// Licensed under the SIL Open Font License, Version 1.1. -// See \nhttp://scripts.sil.org/OFL -// Very Long SVGs -// Many of the KaTeX stretchy wide elements use a long SVG image and an -// overflow: hidden tactic to achieve a stretchy image while avoiding -// distortion of arrowheads or brace corners. -// The SVG typically contains a very long (400 em) arrow. -// The SVG is in a container span that has overflow: hidden, so the span -// acts like a window that exposes only part of the SVG. -// The SVG always has a longer, thinner aspect ratio than the container span. -// After the SVG fills 100% of the height of the container span, -// there is a long arrow shaft left over. That left-over shaft is not shown. -// Instead, it is sliced off because the span's CSS has overflow: hidden. -// Thus, the reader sees an arrow that matches the subject matter width -// without distortion. -// Some functions, such as \cancel, need to vary their aspect ratio. These -// functions do not get the overflow SVG treatment. -// Second Brush Stroke -// Low resolution monitors struggle to display images in fine detail. -// So browsers apply anti-aliasing. A long straight arrow shaft therefore -// will sometimes appear as if it has a blurred edge. -// To mitigate this, these SVG files contain a second "brush-stroke" on the -// arrow shafts. That is, a second long thin rectangular SVG path has been -// written directly on top of each arrow shaft. This reinforcement causes -// some of the screen pixels to display as black instead of the anti-aliased -// gray pixel that a single path would generate. So we get arrow shafts -// whose edges appear to be sharper. -// In the katexImagesData object just below, the dimensions all -// correspond to path geometry inside the relevant SVG. -// For example, \overrightarrow uses the same arrowhead as glyph U+2192 -// from the KaTeX Main font. The scaling factor is 1000. -// That is, inside the font, that arrowhead is 522 units tall, which -// corresponds to 0.522 em inside the document. - - -var katexImagesData = { - // path(s), minWidth, height, align - overrightarrow: [["rightarrow"], 0.888, 522, "xMaxYMin"], - overleftarrow: [["leftarrow"], 0.888, 522, "xMinYMin"], - underrightarrow: [["rightarrow"], 0.888, 522, "xMaxYMin"], - underleftarrow: [["leftarrow"], 0.888, 522, "xMinYMin"], - xrightarrow: [["rightarrow"], 1.469, 522, "xMaxYMin"], - xleftarrow: [["leftarrow"], 1.469, 522, "xMinYMin"], - Overrightarrow: [["doublerightarrow"], 0.888, 560, "xMaxYMin"], - xRightarrow: [["doublerightarrow"], 1.526, 560, "xMaxYMin"], - xLeftarrow: [["doubleleftarrow"], 1.526, 560, "xMinYMin"], - overleftharpoon: [["leftharpoon"], 0.888, 522, "xMinYMin"], - xleftharpoonup: [["leftharpoon"], 0.888, 522, "xMinYMin"], - xleftharpoondown: [["leftharpoondown"], 0.888, 522, "xMinYMin"], - overrightharpoon: [["rightharpoon"], 0.888, 522, "xMaxYMin"], - xrightharpoonup: [["rightharpoon"], 0.888, 522, "xMaxYMin"], - xrightharpoondown: [["rightharpoondown"], 0.888, 522, "xMaxYMin"], - xlongequal: [["longequal"], 0.888, 334, "xMinYMin"], - xtwoheadleftarrow: [["twoheadleftarrow"], 0.888, 334, "xMinYMin"], - xtwoheadrightarrow: [["twoheadrightarrow"], 0.888, 334, "xMaxYMin"], - overleftrightarrow: [["leftarrow", "rightarrow"], 0.888, 522], - overbrace: [["leftbrace", "midbrace", "rightbrace"], 1.6, 548], - underbrace: [["leftbraceunder", "midbraceunder", "rightbraceunder"], 1.6, 548], - underleftrightarrow: [["leftarrow", "rightarrow"], 0.888, 522], - xleftrightarrow: [["leftarrow", "rightarrow"], 1.75, 522], - xLeftrightarrow: [["doubleleftarrow", "doublerightarrow"], 1.75, 560], - xrightleftharpoons: [["leftharpoondownplus", "rightharpoonplus"], 1.75, 716], - xleftrightharpoons: [["leftharpoonplus", "rightharpoondownplus"], 1.75, 716], - xhookleftarrow: [["leftarrow", "righthook"], 1.08, 522], - xhookrightarrow: [["lefthook", "rightarrow"], 1.08, 522], - overlinesegment: [["leftlinesegment", "rightlinesegment"], 0.888, 522], - underlinesegment: [["leftlinesegment", "rightlinesegment"], 0.888, 522], - overgroup: [["leftgroup", "rightgroup"], 0.888, 342], - undergroup: [["leftgroupunder", "rightgroupunder"], 0.888, 342], - xmapsto: [["leftmapsto", "rightarrow"], 1.5, 522], - xtofrom: [["leftToFrom", "rightToFrom"], 1.75, 528], - // The next three arrows are from the mhchem package. - // In mhchem.sty, min-length is 2.0em. But these arrows might appear in the - // document as \xrightarrow or \xrightleftharpoons. Those have - // min-length = 1.75em, so we set min-length on these next three to match. - xrightleftarrows: [["baraboveleftarrow", "rightarrowabovebar"], 1.75, 901], - xrightequilibrium: [["baraboveshortleftharpoon", "rightharpoonaboveshortbar"], 1.75, 716], - xleftequilibrium: [["shortbaraboveleftharpoon", "shortrightharpoonabovebar"], 1.75, 716] -}; - -var groupLength = function groupLength(arg) { - if (arg.type === "ordgroup") { - return arg.body.length; - } else { - return 1; - } -}; - -var stretchy_svgSpan = function svgSpan(group, options) { - // Create a span with inline SVG for the element. - function buildSvgSpan_() { - var viewBoxWidth = 400000; // default - - var label = group.label.substr(1); - - if (utils.contains(["widehat", "widecheck", "widetilde", "utilde"], label)) { - // Each type in the `if` statement corresponds to one of the ParseNode - // types below. This narrowing is required to access `grp.base`. - var grp = group; // There are four SVG images available for each function. - // Choose a taller image when there are more characters. - - var numChars = groupLength(grp.base); - var viewBoxHeight; - var pathName; - - var _height; - - if (numChars > 5) { - if (label === "widehat" || label === "widecheck") { - viewBoxHeight = 420; - viewBoxWidth = 2364; - _height = 0.42; - pathName = label + "4"; - } else { - viewBoxHeight = 312; - viewBoxWidth = 2340; - _height = 0.34; - pathName = "tilde4"; - } - } else { - var imgIndex = [1, 1, 2, 2, 3, 3][numChars]; - - if (label === "widehat" || label === "widecheck") { - viewBoxWidth = [0, 1062, 2364, 2364, 2364][imgIndex]; - viewBoxHeight = [0, 239, 300, 360, 420][imgIndex]; - _height = [0, 0.24, 0.3, 0.3, 0.36, 0.42][imgIndex]; - pathName = label + imgIndex; - } else { - viewBoxWidth = [0, 600, 1033, 2339, 2340][imgIndex]; - viewBoxHeight = [0, 260, 286, 306, 312][imgIndex]; - _height = [0, 0.26, 0.286, 0.3, 0.306, 0.34][imgIndex]; - pathName = "tilde" + imgIndex; - } - } - - var path = new domTree_PathNode(pathName); - var svgNode = new SvgNode([path], { - "width": "100%", - "height": _height + "em", - "viewBox": "0 0 " + viewBoxWidth + " " + viewBoxHeight, - "preserveAspectRatio": "none" - }); - return { - span: buildCommon.makeSvgSpan([], [svgNode], options), - minWidth: 0, - height: _height - }; - } else { - var spans = []; - var data = katexImagesData[label]; - var paths = data[0], - _minWidth = data[1], - _viewBoxHeight = data[2]; - - var _height2 = _viewBoxHeight / 1000; - - var numSvgChildren = paths.length; - var widthClasses; - var aligns; - - if (numSvgChildren === 1) { - // $FlowFixMe: All these cases must be of the 4-tuple type. - var align1 = data[3]; - widthClasses = ["hide-tail"]; - aligns = [align1]; - } else if (numSvgChildren === 2) { - widthClasses = ["halfarrow-left", "halfarrow-right"]; - aligns = ["xMinYMin", "xMaxYMin"]; - } else if (numSvgChildren === 3) { - widthClasses = ["brace-left", "brace-center", "brace-right"]; - aligns = ["xMinYMin", "xMidYMin", "xMaxYMin"]; - } else { - throw new Error("Correct katexImagesData or update code here to support\n " + numSvgChildren + " children."); - } - - for (var i = 0; i < numSvgChildren; i++) { - var _path = new domTree_PathNode(paths[i]); - - var _svgNode = new SvgNode([_path], { - "width": "400em", - "height": _height2 + "em", - "viewBox": "0 0 " + viewBoxWidth + " " + _viewBoxHeight, - "preserveAspectRatio": aligns[i] + " slice" - }); - - var _span = buildCommon.makeSvgSpan([widthClasses[i]], [_svgNode], options); - - if (numSvgChildren === 1) { - return { - span: _span, - minWidth: _minWidth, - height: _height2 - }; - } else { - _span.style.height = _height2 + "em"; - spans.push(_span); - } - } - - return { - span: buildCommon.makeSpan(["stretchy"], spans, options), - minWidth: _minWidth, - height: _height2 - }; - } - } // buildSvgSpan_() - - - var _buildSvgSpan_ = buildSvgSpan_(), - span = _buildSvgSpan_.span, - minWidth = _buildSvgSpan_.minWidth, - height = _buildSvgSpan_.height; // Note that we are returning span.depth = 0. - // Any adjustments relative to the baseline must be done in buildHTML. - - - span.height = height; - span.style.height = height + "em"; - - if (minWidth > 0) { - span.style.minWidth = minWidth + "em"; - } - - return span; -}; - -var stretchy_encloseSpan = function encloseSpan(inner, label, pad, options) { - // Return an image span for \cancel, \bcancel, \xcancel, or \fbox - var img; - var totalHeight = inner.height + inner.depth + 2 * pad; - - if (/fbox|color/.test(label)) { - img = buildCommon.makeSpan(["stretchy", label], [], options); - - if (label === "fbox") { - var color = options.color && options.getColor(); - - if (color) { - img.style.borderColor = color; - } - } - } else { - // \cancel, \bcancel, or \xcancel - // Since \cancel's SVG is inline and it omits the viewBox attribute, - // its stroke-width will not vary with span area. - var lines = []; - - if (/^[bx]cancel$/.test(label)) { - lines.push(new LineNode({ - "x1": "0", - "y1": "0", - "x2": "100%", - "y2": "100%", - "stroke-width": "0.046em" - })); - } - - if (/^x?cancel$/.test(label)) { - lines.push(new LineNode({ - "x1": "0", - "y1": "100%", - "x2": "100%", - "y2": "0", - "stroke-width": "0.046em" - })); - } - - var svgNode = new SvgNode(lines, { - "width": "100%", - "height": totalHeight + "em" - }); - img = buildCommon.makeSvgSpan([], [svgNode], options); - } - - img.height = totalHeight; - img.style.height = totalHeight + "em"; - return img; -}; - -/* harmony default export */ var stretchy = ({ - encloseSpan: stretchy_encloseSpan, - mathMLnode: stretchy_mathMLnode, - svgSpan: stretchy_svgSpan -}); -// CONCATENATED MODULE: ./src/parseNode.js - - -/** - * Asserts that the node is of the given type and returns it with stricter - * typing. Throws if the node's type does not match. - */ -function assertNodeType(node, type) { - if (!node || node.type !== type) { - throw new Error("Expected node of type " + type + ", but got " + (node ? "node of type " + node.type : String(node))); - } - - return node; -} -/** - * Returns the node more strictly typed iff it is of the given type. Otherwise, - * returns null. - */ - -function assertSymbolNodeType(node) { - var typedNode = checkSymbolNodeType(node); - - if (!typedNode) { - throw new Error("Expected node of symbol group type, but got " + (node ? "node of type " + node.type : String(node))); - } - - return typedNode; -} -/** - * Returns the node more strictly typed iff it is of the given type. Otherwise, - * returns null. - */ - -function checkSymbolNodeType(node) { - if (node && (node.type === "atom" || NON_ATOMS.hasOwnProperty(node.type))) { - // $FlowFixMe - return node; - } - - return null; -} -// CONCATENATED MODULE: ./src/functions/accent.js - - - - - - - - - -// NOTE: Unlike most `htmlBuilder`s, this one handles not only "accent", but -var accent_htmlBuilder = function htmlBuilder(grp, options) { - // Accents are handled in the TeXbook pg. 443, rule 12. - var base; - var group; - var supSubGroup; - - if (grp && grp.type === "supsub") { - // If our base is a character box, and we have superscripts and - // subscripts, the supsub will defer to us. In particular, we want - // to attach the superscripts and subscripts to the inner body (so - // that the position of the superscripts and subscripts won't be - // affected by the height of the accent). We accomplish this by - // sticking the base of the accent into the base of the supsub, and - // rendering that, while keeping track of where the accent is. - // The real accent group is the base of the supsub group - group = assertNodeType(grp.base, "accent"); // The character box is the base of the accent group - - base = group.base; // Stick the character box into the base of the supsub group - - grp.base = base; // Rerender the supsub group with its new base, and store that - // result. - - supSubGroup = assertSpan(buildHTML_buildGroup(grp, options)); // reset original base - - grp.base = group; - } else { - group = assertNodeType(grp, "accent"); - base = group.base; - } // Build the base group - - - var body = buildHTML_buildGroup(base, options.havingCrampedStyle()); // Does the accent need to shift for the skew of a character? - - var mustShift = group.isShifty && utils.isCharacterBox(base); // Calculate the skew of the accent. This is based on the line "If the - // nucleus is not a single character, let s = 0; otherwise set s to the - // kern amount for the nucleus followed by the \skewchar of its font." - // Note that our skew metrics are just the kern between each character - // and the skewchar. - - var skew = 0; - - if (mustShift) { - // If the base is a character box, then we want the skew of the - // innermost character. To do that, we find the innermost character: - var baseChar = utils.getBaseElem(base); // Then, we render its group to get the symbol inside it - - var baseGroup = buildHTML_buildGroup(baseChar, options.havingCrampedStyle()); // Finally, we pull the skew off of the symbol. - - skew = assertSymbolDomNode(baseGroup).skew; // Note that we now throw away baseGroup, because the layers we - // removed with getBaseElem might contain things like \color which - // we can't get rid of. - // TODO(emily): Find a better way to get the skew - } // calculate the amount of space between the body and the accent - - - var clearance = Math.min(body.height, options.fontMetrics().xHeight); // Build the accent - - var accentBody; - - if (!group.isStretchy) { - var accent; - var width; - - if (group.label === "\\vec") { - // Before version 0.9, \vec used the combining font glyph U+20D7. - // But browsers, especially Safari, are not consistent in how they - // render combining characters when not preceded by a character. - // So now we use an SVG. - // If Safari reforms, we should consider reverting to the glyph. - accent = buildCommon.staticSvg("vec", options); - width = buildCommon.svgData.vec[1]; - } else { - accent = buildCommon.makeOrd({ - mode: group.mode, - text: group.label - }, options, "textord"); - accent = assertSymbolDomNode(accent); // Remove the italic correction of the accent, because it only serves to - // shift the accent over to a place we don't want. - - accent.italic = 0; - width = accent.width; - } - - accentBody = buildCommon.makeSpan(["accent-body"], [accent]); // "Full" accents expand the width of the resulting symbol to be - // at least the width of the accent, and overlap directly onto the - // character without any vertical offset. - - var accentFull = group.label === "\\textcircled"; - - if (accentFull) { - accentBody.classes.push('accent-full'); - clearance = body.height; - } // Shift the accent over by the skew. - - - var left = skew; // CSS defines `.katex .accent .accent-body:not(.accent-full) { width: 0 }` - // so that the accent doesn't contribute to the bounding box. - // We need to shift the character by its width (effectively half - // its width) to compensate. - - if (!accentFull) { - left -= width / 2; - } - - accentBody.style.left = left + "em"; // \textcircled uses the \bigcirc glyph, so it needs some - // vertical adjustment to match LaTeX. - - if (group.label === "\\textcircled") { - accentBody.style.top = ".2em"; - } - - accentBody = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: body - }, { - type: "kern", - size: -clearance - }, { - type: "elem", - elem: accentBody - }] - }, options); - } else { - accentBody = stretchy.svgSpan(group, options); - accentBody = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: body - }, { - type: "elem", - elem: accentBody, - wrapperClasses: ["svg-align"], - wrapperStyle: skew > 0 ? { - width: "calc(100% - " + 2 * skew + "em)", - marginLeft: 2 * skew + "em" - } : undefined - }] - }, options); - } - - var accentWrap = buildCommon.makeSpan(["mord", "accent"], [accentBody], options); - - if (supSubGroup) { - // Here, we replace the "base" child of the supsub with our newly - // generated accent. - supSubGroup.children[0] = accentWrap; // Since we don't rerun the height calculation after replacing the - // accent, we manually recalculate height. - - supSubGroup.height = Math.max(accentWrap.height, supSubGroup.height); // Accents should always be ords, even when their innards are not. - - supSubGroup.classes[0] = "mord"; - return supSubGroup; - } else { - return accentWrap; - } -}; - -var accent_mathmlBuilder = function mathmlBuilder(group, options) { - var accentNode = group.isStretchy ? stretchy.mathMLnode(group.label) : new mathMLTree.MathNode("mo", [buildMathML_makeText(group.label, group.mode)]); - var node = new mathMLTree.MathNode("mover", [buildMathML_buildGroup(group.base, options), accentNode]); - node.setAttribute("accent", "true"); - return node; -}; - -var NON_STRETCHY_ACCENT_REGEX = new RegExp(["\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve", "\\check", "\\hat", "\\vec", "\\dot", "\\mathring"].map(function (accent) { - return "\\" + accent; -}).join("|")); // Accents - -defineFunction({ - type: "accent", - names: ["\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve", "\\check", "\\hat", "\\vec", "\\dot", "\\mathring", "\\widecheck", "\\widehat", "\\widetilde", "\\overrightarrow", "\\overleftarrow", "\\Overrightarrow", "\\overleftrightarrow", "\\overgroup", "\\overlinesegment", "\\overleftharpoon", "\\overrightharpoon"], - props: { - numArgs: 1 - }, - handler: function handler(context, args) { - var base = args[0]; - var isStretchy = !NON_STRETCHY_ACCENT_REGEX.test(context.funcName); - var isShifty = !isStretchy || context.funcName === "\\widehat" || context.funcName === "\\widetilde" || context.funcName === "\\widecheck"; - return { - type: "accent", - mode: context.parser.mode, - label: context.funcName, - isStretchy: isStretchy, - isShifty: isShifty, - base: base - }; - }, - htmlBuilder: accent_htmlBuilder, - mathmlBuilder: accent_mathmlBuilder -}); // Text-mode accents - -defineFunction({ - type: "accent", - names: ["\\'", "\\`", "\\^", "\\~", "\\=", "\\u", "\\.", '\\"', "\\r", "\\H", "\\v", "\\textcircled"], - props: { - numArgs: 1, - allowedInText: true, - allowedInMath: false - }, - handler: function handler(context, args) { - var base = args[0]; - return { - type: "accent", - mode: context.parser.mode, - label: context.funcName, - isStretchy: false, - isShifty: true, - base: base - }; - }, - htmlBuilder: accent_htmlBuilder, - mathmlBuilder: accent_mathmlBuilder -}); -// CONCATENATED MODULE: ./src/functions/accentunder.js -// Horizontal overlap functions - - - - - - -defineFunction({ - type: "accentUnder", - names: ["\\underleftarrow", "\\underrightarrow", "\\underleftrightarrow", "\\undergroup", "\\underlinesegment", "\\utilde"], - props: { - numArgs: 1 - }, - handler: function handler(_ref, args) { - var parser = _ref.parser, - funcName = _ref.funcName; - var base = args[0]; - return { - type: "accentUnder", - mode: parser.mode, - label: funcName, - base: base - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - // Treat under accents much like underlines. - var innerGroup = buildHTML_buildGroup(group.base, options); - var accentBody = stretchy.svgSpan(group, options); - var kern = group.label === "\\utilde" ? 0.12 : 0; // Generate the vlist, with the appropriate kerns - - var vlist = buildCommon.makeVList({ - positionType: "top", - positionData: innerGroup.height, - children: [{ - type: "elem", - elem: accentBody, - wrapperClasses: ["svg-align"] - }, { - type: "kern", - size: kern - }, { - type: "elem", - elem: innerGroup - }] - }, options); - return buildCommon.makeSpan(["mord", "accentunder"], [vlist], options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var accentNode = stretchy.mathMLnode(group.label); - var node = new mathMLTree.MathNode("munder", [buildMathML_buildGroup(group.base, options), accentNode]); - node.setAttribute("accentunder", "true"); - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/arrow.js - - - - - - - -// Helper function -var arrow_paddedNode = function paddedNode(group) { - var node = new mathMLTree.MathNode("mpadded", group ? [group] : []); - node.setAttribute("width", "+0.6em"); - node.setAttribute("lspace", "0.3em"); - return node; -}; // Stretchy arrows with an optional argument - - -defineFunction({ - type: "xArrow", - names: ["\\xleftarrow", "\\xrightarrow", "\\xLeftarrow", "\\xRightarrow", "\\xleftrightarrow", "\\xLeftrightarrow", "\\xhookleftarrow", "\\xhookrightarrow", "\\xmapsto", "\\xrightharpoondown", "\\xrightharpoonup", "\\xleftharpoondown", "\\xleftharpoonup", "\\xrightleftharpoons", "\\xleftrightharpoons", "\\xlongequal", "\\xtwoheadrightarrow", "\\xtwoheadleftarrow", "\\xtofrom", // The next 3 functions are here to support the mhchem extension. - // Direct use of these functions is discouraged and may break someday. - "\\xrightleftarrows", "\\xrightequilibrium", "\\xleftequilibrium"], - props: { - numArgs: 1, - numOptionalArgs: 1 - }, - handler: function handler(_ref, args, optArgs) { - var parser = _ref.parser, - funcName = _ref.funcName; - return { - type: "xArrow", - mode: parser.mode, - label: funcName, - body: args[0], - below: optArgs[0] - }; - }, - // Flow is unable to correctly infer the type of `group`, even though it's - // unamibiguously determined from the passed-in `type` above. - htmlBuilder: function htmlBuilder(group, options) { - var style = options.style; // Build the argument groups in the appropriate style. - // Ref: amsmath.dtx: \hbox{$\scriptstyle\mkern#3mu{#6}\mkern#4mu$}% - // Some groups can return document fragments. Handle those by wrapping - // them in a span. - - var newOptions = options.havingStyle(style.sup()); - var upperGroup = buildCommon.wrapFragment(buildHTML_buildGroup(group.body, newOptions, options), options); - upperGroup.classes.push("x-arrow-pad"); - var lowerGroup; - - if (group.below) { - // Build the lower group - newOptions = options.havingStyle(style.sub()); - lowerGroup = buildCommon.wrapFragment(buildHTML_buildGroup(group.below, newOptions, options), options); - lowerGroup.classes.push("x-arrow-pad"); - } - - var arrowBody = stretchy.svgSpan(group, options); // Re shift: Note that stretchy.svgSpan returned arrowBody.depth = 0. - // The point we want on the math axis is at 0.5 * arrowBody.height. - - var arrowShift = -options.fontMetrics().axisHeight + 0.5 * arrowBody.height; // 2 mu kern. Ref: amsmath.dtx: #7\if0#2\else\mkern#2mu\fi - - var upperShift = -options.fontMetrics().axisHeight - 0.5 * arrowBody.height - 0.111; // 0.111 em = 2 mu - - if (upperGroup.depth > 0.25 || group.label === "\\xleftequilibrium") { - upperShift -= upperGroup.depth; // shift up if depth encroaches - } // Generate the vlist - - - var vlist; - - if (lowerGroup) { - var lowerShift = -options.fontMetrics().axisHeight + lowerGroup.height + 0.5 * arrowBody.height + 0.111; - vlist = buildCommon.makeVList({ - positionType: "individualShift", - children: [{ - type: "elem", - elem: upperGroup, - shift: upperShift - }, { - type: "elem", - elem: arrowBody, - shift: arrowShift - }, { - type: "elem", - elem: lowerGroup, - shift: lowerShift - }] - }, options); - } else { - vlist = buildCommon.makeVList({ - positionType: "individualShift", - children: [{ - type: "elem", - elem: upperGroup, - shift: upperShift - }, { - type: "elem", - elem: arrowBody, - shift: arrowShift - }] - }, options); - } // $FlowFixMe: Replace this with passing "svg-align" into makeVList. - - - vlist.children[0].children[0].children[1].classes.push("svg-align"); - return buildCommon.makeSpan(["mrel", "x-arrow"], [vlist], options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var arrowNode = stretchy.mathMLnode(group.label); - var node; - - if (group.body) { - var upperNode = arrow_paddedNode(buildMathML_buildGroup(group.body, options)); - - if (group.below) { - var lowerNode = arrow_paddedNode(buildMathML_buildGroup(group.below, options)); - node = new mathMLTree.MathNode("munderover", [arrowNode, lowerNode, upperNode]); - } else { - node = new mathMLTree.MathNode("mover", [arrowNode, upperNode]); - } - } else if (group.below) { - var _lowerNode = arrow_paddedNode(buildMathML_buildGroup(group.below, options)); - - node = new mathMLTree.MathNode("munder", [arrowNode, _lowerNode]); - } else { - // This should never happen. - // Parser.js throws an error if there is no argument. - node = arrow_paddedNode(); - node = new mathMLTree.MathNode("mover", [arrowNode, node]); - } - - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/char.js - - - // \@char is an internal function that takes a grouped decimal argument like -// {123} and converts into symbol with code 123. It is used by the *macro* -// \char defined in macros.js. - -defineFunction({ - type: "textord", - names: ["\\@char"], - props: { - numArgs: 1, - allowedInText: true - }, - handler: function handler(_ref, args) { - var parser = _ref.parser; - var arg = assertNodeType(args[0], "ordgroup"); - var group = arg.body; - var number = ""; - - for (var i = 0; i < group.length; i++) { - var node = assertNodeType(group[i], "textord"); - number += node.text; - } - - var code = parseInt(number); - - if (isNaN(code)) { - throw new src_ParseError("\\@char has non-numeric argument " + number); - } - - return { - type: "textord", - mode: parser.mode, - text: String.fromCharCode(code) - }; - } -}); -// CONCATENATED MODULE: ./src/functions/color.js - - - - - - - -var color_htmlBuilder = function htmlBuilder(group, options) { - var elements = buildHTML_buildExpression(group.body, options.withColor(group.color), false); // \color isn't supposed to affect the type of the elements it contains. - // To accomplish this, we wrap the results in a fragment, so the inner - // elements will be able to directly interact with their neighbors. For - // example, `\color{red}{2 +} 3` has the same spacing as `2 + 3` - - return buildCommon.makeFragment(elements); -}; - -var color_mathmlBuilder = function mathmlBuilder(group, options) { - var inner = buildMathML_buildExpression(group.body, options.withColor(group.color)); - var node = new mathMLTree.MathNode("mstyle", inner); - node.setAttribute("mathcolor", group.color); - return node; -}; - -defineFunction({ - type: "color", - names: ["\\textcolor"], - props: { - numArgs: 2, - allowedInText: true, - greediness: 3, - argTypes: ["color", "original"] - }, - handler: function handler(_ref, args) { - var parser = _ref.parser; - var color = assertNodeType(args[0], "color-token").color; - var body = args[1]; - return { - type: "color", - mode: parser.mode, - color: color, - body: ordargument(body) - }; - }, - htmlBuilder: color_htmlBuilder, - mathmlBuilder: color_mathmlBuilder -}); -defineFunction({ - type: "color", - names: ["\\color"], - props: { - numArgs: 1, - allowedInText: true, - greediness: 3, - argTypes: ["color"] - }, - handler: function handler(_ref2, args) { - var parser = _ref2.parser, - breakOnTokenText = _ref2.breakOnTokenText; - var color = assertNodeType(args[0], "color-token").color; // Set macro \current@color in current namespace to store the current - // color, mimicking the behavior of color.sty. - // This is currently used just to correctly color a \right - // that follows a \color command. - - parser.gullet.macros.set("\\current@color", color); // Parse out the implicit body that should be colored. - - var body = parser.parseExpression(true, breakOnTokenText); - return { - type: "color", - mode: parser.mode, - color: color, - body: body - }; - }, - htmlBuilder: color_htmlBuilder, - mathmlBuilder: color_mathmlBuilder -}); -// CONCATENATED MODULE: ./src/functions/cr.js -// Row breaks within tabular environments, and line breaks at top level - - - - - - // \\ is a macro mapping to either \cr or \newline. Because they have the -// same signature, we implement them as one megafunction, with newRow -// indicating whether we're in the \cr case, and newLine indicating whether -// to break the line in the \newline case. - -defineFunction({ - type: "cr", - names: ["\\cr", "\\newline"], - props: { - numArgs: 0, - numOptionalArgs: 1, - argTypes: ["size"], - allowedInText: true - }, - handler: function handler(_ref, args, optArgs) { - var parser = _ref.parser, - funcName = _ref.funcName; - var size = optArgs[0]; - var newRow = funcName === "\\cr"; - var newLine = false; - - if (!newRow) { - if (parser.settings.displayMode && parser.settings.useStrictBehavior("newLineInDisplayMode", "In LaTeX, \\\\ or \\newline " + "does nothing in display mode")) { - newLine = false; - } else { - newLine = true; - } - } - - return { - type: "cr", - mode: parser.mode, - newLine: newLine, - newRow: newRow, - size: size && assertNodeType(size, "size").value - }; - }, - // The following builders are called only at the top level, - // not within tabular/array environments. - htmlBuilder: function htmlBuilder(group, options) { - if (group.newRow) { - throw new src_ParseError("\\cr valid only within a tabular/array environment"); - } - - var span = buildCommon.makeSpan(["mspace"], [], options); - - if (group.newLine) { - span.classes.push("newline"); - - if (group.size) { - span.style.marginTop = units_calculateSize(group.size, options) + "em"; - } - } - - return span; - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var node = new mathMLTree.MathNode("mspace"); - - if (group.newLine) { - node.setAttribute("linebreak", "newline"); - - if (group.size) { - node.setAttribute("height", units_calculateSize(group.size, options) + "em"); - } - } - - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/def.js - - - -var globalMap = { - "\\global": "\\global", - "\\long": "\\\\globallong", - "\\\\globallong": "\\\\globallong", - "\\def": "\\gdef", - "\\gdef": "\\gdef", - "\\edef": "\\xdef", - "\\xdef": "\\xdef", - "\\let": "\\\\globallet", - "\\futurelet": "\\\\globalfuture" -}; - -var def_checkControlSequence = function checkControlSequence(tok) { - var name = tok.text; - - if (/^(?:[\\{}$&#^_]|EOF)$/.test(name)) { - throw new src_ParseError("Expected a control sequence", tok); - } - - return name; -}; - -var getRHS = function getRHS(parser) { - var tok = parser.gullet.popToken(); - - if (tok.text === "=") { - // consume optional equals - tok = parser.gullet.popToken(); - - if (tok.text === " ") { - // consume one optional space - tok = parser.gullet.popToken(); - } - } - - return tok; -}; - -var letCommand = function letCommand(parser, name, tok, global) { - var macro = parser.gullet.macros.get(tok.text); - - if (macro == null) { - // don't expand it later even if a macro with the same name is defined - // e.g., \let\foo=\frac \def\frac{\relax} \frac12 - tok.noexpand = true; - macro = { - tokens: [tok], - numArgs: 0, - // reproduce the same behavior in expansion - unexpandable: !parser.gullet.isExpandable(tok.text) - }; - } - - parser.gullet.macros.set(name, macro, global); -}; // -> | -// -> |\global -// -> | -// -> \global|\long|\outer - - -defineFunction({ - type: "internal", - names: ["\\global", "\\long", "\\\\globallong"], - props: { - numArgs: 0, - allowedInText: true - }, - handler: function handler(_ref) { - var parser = _ref.parser, - funcName = _ref.funcName; - parser.consumeSpaces(); - var token = parser.fetch(); - - if (globalMap[token.text]) { - // KaTeX doesn't have \par, so ignore \long - if (funcName === "\\global" || funcName === "\\\\globallong") { - token.text = globalMap[token.text]; - } - - return assertNodeType(parser.parseFunction(), "internal"); - } - - throw new src_ParseError("Invalid token after macro prefix", token); - } -}); // Basic support for macro definitions: \def, \gdef, \edef, \xdef -// -> -// -> \def|\gdef|\edef|\xdef -// -> - -defineFunction({ - type: "internal", - names: ["\\def", "\\gdef", "\\edef", "\\xdef"], - props: { - numArgs: 0, - allowedInText: true - }, - handler: function handler(_ref2) { - var parser = _ref2.parser, - funcName = _ref2.funcName; - var arg = parser.gullet.consumeArgs(1)[0]; - - if (arg.length !== 1) { - throw new src_ParseError("\\gdef's first argument must be a macro name"); - } - - var name = arg[0].text; // Count argument specifiers, and check they are in the order #1 #2 ... - - var numArgs = 0; - arg = parser.gullet.consumeArgs(1)[0]; - - while (arg.length === 1 && arg[0].text === "#") { - arg = parser.gullet.consumeArgs(1)[0]; - - if (arg.length !== 1) { - throw new src_ParseError("Invalid argument number length \"" + arg.length + "\""); - } - - if (!/^[1-9]$/.test(arg[0].text)) { - throw new src_ParseError("Invalid argument number \"" + arg[0].text + "\""); - } - - numArgs++; - - if (parseInt(arg[0].text) !== numArgs) { - throw new src_ParseError("Argument number \"" + arg[0].text + "\" out of order"); - } - - arg = parser.gullet.consumeArgs(1)[0]; - } - - if (funcName === "\\edef" || funcName === "\\xdef") { - arg = parser.gullet.expandTokens(arg); - arg.reverse(); // to fit in with stack order - } // Final arg is the expansion of the macro - - - parser.gullet.macros.set(name, { - tokens: arg, - numArgs: numArgs - }, funcName === globalMap[funcName]); - return { - type: "internal", - mode: parser.mode - }; - } -}); // -> -// -> \futurelet -// | \let -// -> |= - -defineFunction({ - type: "internal", - names: ["\\let", "\\\\globallet"], - props: { - numArgs: 0, - allowedInText: true - }, - handler: function handler(_ref3) { - var parser = _ref3.parser, - funcName = _ref3.funcName; - var name = def_checkControlSequence(parser.gullet.popToken()); - parser.gullet.consumeSpaces(); - var tok = getRHS(parser); - letCommand(parser, name, tok, funcName === "\\\\globallet"); - return { - type: "internal", - mode: parser.mode - }; - } -}); // ref: https://www.tug.org/TUGboat/tb09-3/tb22bechtolsheim.pdf - -defineFunction({ - type: "internal", - names: ["\\futurelet", "\\\\globalfuture"], - props: { - numArgs: 0, - allowedInText: true - }, - handler: function handler(_ref4) { - var parser = _ref4.parser, - funcName = _ref4.funcName; - var name = def_checkControlSequence(parser.gullet.popToken()); - var middle = parser.gullet.popToken(); - var tok = parser.gullet.popToken(); - letCommand(parser, name, tok, funcName === "\\\\globalfuture"); - parser.gullet.pushToken(tok); - parser.gullet.pushToken(middle); - return { - type: "internal", - mode: parser.mode - }; - } -}); -// CONCATENATED MODULE: ./src/delimiter.js -/** - * This file deals with creating delimiters of various sizes. The TeXbook - * discusses these routines on page 441-442, in the "Another subroutine sets box - * x to a specified variable delimiter" paragraph. - * - * There are three main routines here. `makeSmallDelim` makes a delimiter in the - * normal font, but in either text, script, or scriptscript style. - * `makeLargeDelim` makes a delimiter in textstyle, but in one of the Size1, - * Size2, Size3, or Size4 fonts. `makeStackedDelim` makes a delimiter out of - * smaller pieces that are stacked on top of one another. - * - * The functions take a parameter `center`, which determines if the delimiter - * should be centered around the axis. - * - * Then, there are three exposed functions. `sizedDelim` makes a delimiter in - * one of the given sizes. This is used for things like `\bigl`. - * `customSizedDelim` makes a delimiter with a given total height+depth. It is - * called in places like `\sqrt`. `leftRightDelim` makes an appropriate - * delimiter which surrounds an expression of a given height an depth. It is - * used in `\left` and `\right`. - */ - - - - - - - - - -/** - * Get the metrics for a given symbol and font, after transformation (i.e. - * after following replacement from symbols.js) - */ -var delimiter_getMetrics = function getMetrics(symbol, font, mode) { - var replace = src_symbols.math[symbol] && src_symbols.math[symbol].replace; - var metrics = getCharacterMetrics(replace || symbol, font, mode); - - if (!metrics) { - throw new Error("Unsupported symbol " + symbol + " and font size " + font + "."); - } - - return metrics; -}; -/** - * Puts a delimiter span in a given style, and adds appropriate height, depth, - * and maxFontSizes. - */ - - -var delimiter_styleWrap = function styleWrap(delim, toStyle, options, classes) { - var newOptions = options.havingBaseStyle(toStyle); - var span = buildCommon.makeSpan(classes.concat(newOptions.sizingClasses(options)), [delim], options); - var delimSizeMultiplier = newOptions.sizeMultiplier / options.sizeMultiplier; - span.height *= delimSizeMultiplier; - span.depth *= delimSizeMultiplier; - span.maxFontSize = newOptions.sizeMultiplier; - return span; -}; - -var centerSpan = function centerSpan(span, options, style) { - var newOptions = options.havingBaseStyle(style); - var shift = (1 - options.sizeMultiplier / newOptions.sizeMultiplier) * options.fontMetrics().axisHeight; - span.classes.push("delimcenter"); - span.style.top = shift + "em"; - span.height -= shift; - span.depth += shift; -}; -/** - * Makes a small delimiter. This is a delimiter that comes in the Main-Regular - * font, but is restyled to either be in textstyle, scriptstyle, or - * scriptscriptstyle. - */ - - -var delimiter_makeSmallDelim = function makeSmallDelim(delim, style, center, options, mode, classes) { - var text = buildCommon.makeSymbol(delim, "Main-Regular", mode, options); - var span = delimiter_styleWrap(text, style, options, classes); - - if (center) { - centerSpan(span, options, style); - } - - return span; -}; -/** - * Builds a symbol in the given font size (note size is an integer) - */ - - -var delimiter_mathrmSize = function mathrmSize(value, size, mode, options) { - return buildCommon.makeSymbol(value, "Size" + size + "-Regular", mode, options); -}; -/** - * Makes a large delimiter. This is a delimiter that comes in the Size1, Size2, - * Size3, or Size4 fonts. It is always rendered in textstyle. - */ - - -var delimiter_makeLargeDelim = function makeLargeDelim(delim, size, center, options, mode, classes) { - var inner = delimiter_mathrmSize(delim, size, mode, options); - var span = delimiter_styleWrap(buildCommon.makeSpan(["delimsizing", "size" + size], [inner], options), src_Style.TEXT, options, classes); - - if (center) { - centerSpan(span, options, src_Style.TEXT); - } - - return span; -}; -/** - * Make an inner span with the given offset and in the given font. This is used - * in `makeStackedDelim` to make the stacking pieces for the delimiter. - */ - - -var delimiter_makeInner = function makeInner(symbol, font, mode) { - var sizeClass; // Apply the correct CSS class to choose the right font. - - if (font === "Size1-Regular") { - sizeClass = "delim-size1"; - } else - /* if (font === "Size4-Regular") */ - { - sizeClass = "delim-size4"; - } - - var inner = buildCommon.makeSpan(["delimsizinginner", sizeClass], [buildCommon.makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]); // Since this will be passed into `makeVList` in the end, wrap the element - // in the appropriate tag that VList uses. - - return { - type: "elem", - elem: inner - }; -}; // Helper for makeStackedDelim - - -var lap = { - type: "kern", - size: -0.005 -}; -/** - * Make a stacked delimiter out of a given delimiter, with the total height at - * least `heightTotal`. This routine is mentioned on page 442 of the TeXbook. - */ - -var delimiter_makeStackedDelim = function makeStackedDelim(delim, heightTotal, center, options, mode, classes) { - // There are four parts, the top, an optional middle, a repeated part, and a - // bottom. - var top; - var middle; - var repeat; - var bottom; - top = repeat = bottom = delim; - middle = null; // Also keep track of what font the delimiters are in - - var font = "Size1-Regular"; // We set the parts and font based on the symbol. Note that we use - // '\u23d0' instead of '|' and '\u2016' instead of '\\|' for the - // repeats of the arrows - - if (delim === "\\uparrow") { - repeat = bottom = "\u23D0"; - } else if (delim === "\\Uparrow") { - repeat = bottom = "\u2016"; - } else if (delim === "\\downarrow") { - top = repeat = "\u23D0"; - } else if (delim === "\\Downarrow") { - top = repeat = "\u2016"; - } else if (delim === "\\updownarrow") { - top = "\\uparrow"; - repeat = "\u23D0"; - bottom = "\\downarrow"; - } else if (delim === "\\Updownarrow") { - top = "\\Uparrow"; - repeat = "\u2016"; - bottom = "\\Downarrow"; - } else if (delim === "[" || delim === "\\lbrack") { - top = "\u23A1"; - repeat = "\u23A2"; - bottom = "\u23A3"; - font = "Size4-Regular"; - } else if (delim === "]" || delim === "\\rbrack") { - top = "\u23A4"; - repeat = "\u23A5"; - bottom = "\u23A6"; - font = "Size4-Regular"; - } else if (delim === "\\lfloor" || delim === "\u230A") { - repeat = top = "\u23A2"; - bottom = "\u23A3"; - font = "Size4-Regular"; - } else if (delim === "\\lceil" || delim === "\u2308") { - top = "\u23A1"; - repeat = bottom = "\u23A2"; - font = "Size4-Regular"; - } else if (delim === "\\rfloor" || delim === "\u230B") { - repeat = top = "\u23A5"; - bottom = "\u23A6"; - font = "Size4-Regular"; - } else if (delim === "\\rceil" || delim === "\u2309") { - top = "\u23A4"; - repeat = bottom = "\u23A5"; - font = "Size4-Regular"; - } else if (delim === "(" || delim === "\\lparen") { - top = "\u239B"; - repeat = "\u239C"; - bottom = "\u239D"; - font = "Size4-Regular"; - } else if (delim === ")" || delim === "\\rparen") { - top = "\u239E"; - repeat = "\u239F"; - bottom = "\u23A0"; - font = "Size4-Regular"; - } else if (delim === "\\{" || delim === "\\lbrace") { - top = "\u23A7"; - middle = "\u23A8"; - bottom = "\u23A9"; - repeat = "\u23AA"; - font = "Size4-Regular"; - } else if (delim === "\\}" || delim === "\\rbrace") { - top = "\u23AB"; - middle = "\u23AC"; - bottom = "\u23AD"; - repeat = "\u23AA"; - font = "Size4-Regular"; - } else if (delim === "\\lgroup" || delim === "\u27EE") { - top = "\u23A7"; - bottom = "\u23A9"; - repeat = "\u23AA"; - font = "Size4-Regular"; - } else if (delim === "\\rgroup" || delim === "\u27EF") { - top = "\u23AB"; - bottom = "\u23AD"; - repeat = "\u23AA"; - font = "Size4-Regular"; - } else if (delim === "\\lmoustache" || delim === "\u23B0") { - top = "\u23A7"; - bottom = "\u23AD"; - repeat = "\u23AA"; - font = "Size4-Regular"; - } else if (delim === "\\rmoustache" || delim === "\u23B1") { - top = "\u23AB"; - bottom = "\u23A9"; - repeat = "\u23AA"; - font = "Size4-Regular"; - } // Get the metrics of the four sections - - - var topMetrics = delimiter_getMetrics(top, font, mode); - var topHeightTotal = topMetrics.height + topMetrics.depth; - var repeatMetrics = delimiter_getMetrics(repeat, font, mode); - var repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth; - var bottomMetrics = delimiter_getMetrics(bottom, font, mode); - var bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth; - var middleHeightTotal = 0; - var middleFactor = 1; - - if (middle !== null) { - var middleMetrics = delimiter_getMetrics(middle, font, mode); - middleHeightTotal = middleMetrics.height + middleMetrics.depth; - middleFactor = 2; // repeat symmetrically above and below middle - } // Calcuate the minimal height that the delimiter can have. - // It is at least the size of the top, bottom, and optional middle combined. - - - var minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal; // Compute the number of copies of the repeat symbol we will need - - var repeatCount = Math.max(0, Math.ceil((heightTotal - minHeight) / (middleFactor * repeatHeightTotal))); // Compute the total height of the delimiter including all the symbols - - var realHeightTotal = minHeight + repeatCount * middleFactor * repeatHeightTotal; // The center of the delimiter is placed at the center of the axis. Note - // that in this context, "center" means that the delimiter should be - // centered around the axis in the current style, while normally it is - // centered around the axis in textstyle. - - var axisHeight = options.fontMetrics().axisHeight; - - if (center) { - axisHeight *= options.sizeMultiplier; - } // Calculate the depth - - - var depth = realHeightTotal / 2 - axisHeight; // This function differs from the TeX procedure in one way. - // We shift each repeat element downwards by 0.005em, to prevent a gap - // due to browser floating point rounding error. - // Then, at the last element-to element joint, we add one extra repeat - // element to cover the gap created by the shifts. - // Find the shift needed to align the upper end of the extra element at a point - // 0.005em above the lower end of the top element. - - var shiftOfExtraElement = (repeatCount + 1) * 0.005 - repeatHeightTotal; // Now, we start building the pieces that will go into the vlist - // Keep a list of the inner pieces - - var inners = []; // Add the bottom symbol - - inners.push(delimiter_makeInner(bottom, font, mode)); - - if (middle === null) { - // Add that many symbols - for (var i = 0; i < repeatCount; i++) { - inners.push(lap); // overlap - - inners.push(delimiter_makeInner(repeat, font, mode)); - } - } else { - // When there is a middle bit, we need the middle part and two repeated - // sections - for (var _i = 0; _i < repeatCount; _i++) { - inners.push(lap); - inners.push(delimiter_makeInner(repeat, font, mode)); - } // Insert one extra repeat element. - - - inners.push({ - type: "kern", - size: shiftOfExtraElement - }); - inners.push(delimiter_makeInner(repeat, font, mode)); - inners.push(lap); // Now insert the middle of the brace. - - inners.push(delimiter_makeInner(middle, font, mode)); - - for (var _i2 = 0; _i2 < repeatCount; _i2++) { - inners.push(lap); - inners.push(delimiter_makeInner(repeat, font, mode)); - } - } // To cover the gap create by the overlaps, insert one more repeat element, - // at a position that juts 0.005 above the bottom of the top element. - - - if ((repeat === "\u239C" || repeat === "\u239F") && repeatCount === 0) { - // Parentheses need a short repeat element in order to avoid an overrun. - // We'll make a 0.3em tall element from a SVG. - var overlap = buildCommon.svgData.leftParenInner[2] / 2; - inners.push({ - type: "kern", - size: -overlap - }); - var pathName = repeat === "\u239C" ? "leftParenInner" : "rightParenInner"; - var innerSpan = buildCommon.staticSvg(pathName, options); - inners.push({ - type: "elem", - elem: innerSpan - }); - inners.push({ - type: "kern", - size: -overlap - }); - } else { - inners.push({ - type: "kern", - size: shiftOfExtraElement - }); - inners.push(delimiter_makeInner(repeat, font, mode)); - inners.push(lap); - } // Add the top symbol - - - inners.push(delimiter_makeInner(top, font, mode)); // Finally, build the vlist - - var newOptions = options.havingBaseStyle(src_Style.TEXT); - var inner = buildCommon.makeVList({ - positionType: "bottom", - positionData: depth, - children: inners - }, newOptions); - return delimiter_styleWrap(buildCommon.makeSpan(["delimsizing", "mult"], [inner], newOptions), src_Style.TEXT, options, classes); -}; // All surds have 0.08em padding above the viniculum inside the SVG. -// That keeps browser span height rounding error from pinching the line. - - -var vbPad = 80; // padding above the surd, measured inside the viewBox. - -var emPad = 0.08; // padding, in ems, measured in the document. - -var delimiter_sqrtSvg = function sqrtSvg(sqrtName, height, viewBoxHeight, extraViniculum, options) { - var path = sqrtPath(sqrtName, extraViniculum, viewBoxHeight); - var pathNode = new domTree_PathNode(sqrtName, path); - var svg = new SvgNode([pathNode], { - // Note: 1000:1 ratio of viewBox to document em width. - "width": "400em", - "height": height + "em", - "viewBox": "0 0 400000 " + viewBoxHeight, - "preserveAspectRatio": "xMinYMin slice" - }); - return buildCommon.makeSvgSpan(["hide-tail"], [svg], options); -}; -/** - * Make a sqrt image of the given height, - */ - - -var makeSqrtImage = function makeSqrtImage(height, options) { - // Define a newOptions that removes the effect of size changes such as \Huge. - // We don't pick different a height surd for \Huge. For it, we scale up. - var newOptions = options.havingBaseSizing(); // Pick the desired surd glyph from a sequence of surds. - - var delim = traverseSequence("\\surd", height * newOptions.sizeMultiplier, stackLargeDelimiterSequence, newOptions); - var sizeMultiplier = newOptions.sizeMultiplier; // default - // The standard sqrt SVGs each have a 0.04em thick viniculum. - // If Settings.minRuleThickness is larger than that, we add extraViniculum. - - var extraViniculum = Math.max(0, options.minRuleThickness - options.fontMetrics().sqrtRuleThickness); // Create a span containing an SVG image of a sqrt symbol. - - var span; - var spanHeight = 0; - var texHeight = 0; - var viewBoxHeight = 0; - var advanceWidth; // We create viewBoxes with 80 units of "padding" above each surd. - // Then browser rounding error on the parent span height will not - // encroach on the ink of the viniculum. But that padding is not - // included in the TeX-like `height` used for calculation of - // vertical alignment. So texHeight = span.height < span.style.height. - - if (delim.type === "small") { - // Get an SVG that is derived from glyph U+221A in font KaTeX-Main. - // 1000 unit normal glyph height. - viewBoxHeight = 1000 + 1000 * extraViniculum + vbPad; - - if (height < 1.0) { - sizeMultiplier = 1.0; // mimic a \textfont radical - } else if (height < 1.4) { - sizeMultiplier = 0.7; // mimic a \scriptfont radical - } - - spanHeight = (1.0 + extraViniculum + emPad) / sizeMultiplier; - texHeight = (1.00 + extraViniculum) / sizeMultiplier; - span = delimiter_sqrtSvg("sqrtMain", spanHeight, viewBoxHeight, extraViniculum, options); - span.style.minWidth = "0.853em"; - advanceWidth = 0.833 / sizeMultiplier; // from the font. - } else if (delim.type === "large") { - // These SVGs come from fonts: KaTeX_Size1, _Size2, etc. - viewBoxHeight = (1000 + vbPad) * sizeToMaxHeight[delim.size]; - texHeight = (sizeToMaxHeight[delim.size] + extraViniculum) / sizeMultiplier; - spanHeight = (sizeToMaxHeight[delim.size] + extraViniculum + emPad) / sizeMultiplier; - span = delimiter_sqrtSvg("sqrtSize" + delim.size, spanHeight, viewBoxHeight, extraViniculum, options); - span.style.minWidth = "1.02em"; - advanceWidth = 1.0 / sizeMultiplier; // 1.0 from the font. - } else { - // Tall sqrt. In TeX, this would be stacked using multiple glyphs. - // We'll use a single SVG to accomplish the same thing. - spanHeight = height + extraViniculum + emPad; - texHeight = height + extraViniculum; - viewBoxHeight = Math.floor(1000 * height + extraViniculum) + vbPad; - span = delimiter_sqrtSvg("sqrtTall", spanHeight, viewBoxHeight, extraViniculum, options); - span.style.minWidth = "0.742em"; - advanceWidth = 1.056; - } - - span.height = texHeight; - span.style.height = spanHeight + "em"; - return { - span: span, - advanceWidth: advanceWidth, - // Calculate the actual line width. - // This actually should depend on the chosen font -- e.g. \boldmath - // should use the thicker surd symbols from e.g. KaTeX_Main-Bold, and - // have thicker rules. - ruleWidth: (options.fontMetrics().sqrtRuleThickness + extraViniculum) * sizeMultiplier - }; -}; // There are three kinds of delimiters, delimiters that stack when they become -// too large - - -var stackLargeDelimiters = ["(", "\\lparen", ")", "\\rparen", "[", "\\lbrack", "]", "\\rbrack", "\\{", "\\lbrace", "\\}", "\\rbrace", "\\lfloor", "\\rfloor", "\u230A", "\u230B", "\\lceil", "\\rceil", "\u2308", "\u2309", "\\surd"]; // delimiters that always stack - -var stackAlwaysDelimiters = ["\\uparrow", "\\downarrow", "\\updownarrow", "\\Uparrow", "\\Downarrow", "\\Updownarrow", "|", "\\|", "\\vert", "\\Vert", "\\lvert", "\\rvert", "\\lVert", "\\rVert", "\\lgroup", "\\rgroup", "\u27EE", "\u27EF", "\\lmoustache", "\\rmoustache", "\u23B0", "\u23B1"]; // and delimiters that never stack - -var stackNeverDelimiters = ["<", ">", "\\langle", "\\rangle", "/", "\\backslash", "\\lt", "\\gt"]; // Metrics of the different sizes. Found by looking at TeX's output of -// $\bigl| // \Bigl| \biggl| \Biggl| \showlists$ -// Used to create stacked delimiters of appropriate sizes in makeSizedDelim. - -var sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0]; -/** - * Used to create a delimiter of a specific size, where `size` is 1, 2, 3, or 4. - */ - -var delimiter_makeSizedDelim = function makeSizedDelim(delim, size, options, mode, classes) { - // < and > turn into \langle and \rangle in delimiters - if (delim === "<" || delim === "\\lt" || delim === "\u27E8") { - delim = "\\langle"; - } else if (delim === ">" || delim === "\\gt" || delim === "\u27E9") { - delim = "\\rangle"; - } // Sized delimiters are never centered. - - - if (utils.contains(stackLargeDelimiters, delim) || utils.contains(stackNeverDelimiters, delim)) { - return delimiter_makeLargeDelim(delim, size, false, options, mode, classes); - } else if (utils.contains(stackAlwaysDelimiters, delim)) { - return delimiter_makeStackedDelim(delim, sizeToMaxHeight[size], false, options, mode, classes); - } else { - throw new src_ParseError("Illegal delimiter: '" + delim + "'"); - } -}; -/** - * There are three different sequences of delimiter sizes that the delimiters - * follow depending on the kind of delimiter. This is used when creating custom - * sized delimiters to decide whether to create a small, large, or stacked - * delimiter. - * - * In real TeX, these sequences aren't explicitly defined, but are instead - * defined inside the font metrics. Since there are only three sequences that - * are possible for the delimiters that TeX defines, it is easier to just encode - * them explicitly here. - */ - - -// Delimiters that never stack try small delimiters and large delimiters only -var stackNeverDelimiterSequence = [{ - type: "small", - style: src_Style.SCRIPTSCRIPT -}, { - type: "small", - style: src_Style.SCRIPT -}, { - type: "small", - style: src_Style.TEXT -}, { - type: "large", - size: 1 -}, { - type: "large", - size: 2 -}, { - type: "large", - size: 3 -}, { - type: "large", - size: 4 -}]; // Delimiters that always stack try the small delimiters first, then stack - -var stackAlwaysDelimiterSequence = [{ - type: "small", - style: src_Style.SCRIPTSCRIPT -}, { - type: "small", - style: src_Style.SCRIPT -}, { - type: "small", - style: src_Style.TEXT -}, { - type: "stack" -}]; // Delimiters that stack when large try the small and then large delimiters, and -// stack afterwards - -var stackLargeDelimiterSequence = [{ - type: "small", - style: src_Style.SCRIPTSCRIPT -}, { - type: "small", - style: src_Style.SCRIPT -}, { - type: "small", - style: src_Style.TEXT -}, { - type: "large", - size: 1 -}, { - type: "large", - size: 2 -}, { - type: "large", - size: 3 -}, { - type: "large", - size: 4 -}, { - type: "stack" -}]; -/** - * Get the font used in a delimiter based on what kind of delimiter it is. - * TODO(#963) Use more specific font family return type once that is introduced. - */ - -var delimTypeToFont = function delimTypeToFont(type) { - if (type.type === "small") { - return "Main-Regular"; - } else if (type.type === "large") { - return "Size" + type.size + "-Regular"; - } else if (type.type === "stack") { - return "Size4-Regular"; - } else { - throw new Error("Add support for delim type '" + type.type + "' here."); - } -}; -/** - * Traverse a sequence of types of delimiters to decide what kind of delimiter - * should be used to create a delimiter of the given height+depth. - */ - - -var traverseSequence = function traverseSequence(delim, height, sequence, options) { - // Here, we choose the index we should start at in the sequences. In smaller - // sizes (which correspond to larger numbers in style.size) we start earlier - // in the sequence. Thus, scriptscript starts at index 3-3=0, script starts - // at index 3-2=1, text starts at 3-1=2, and display starts at min(2,3-0)=2 - var start = Math.min(2, 3 - options.style.size); - - for (var i = start; i < sequence.length; i++) { - if (sequence[i].type === "stack") { - // This is always the last delimiter, so we just break the loop now. - break; - } - - var metrics = delimiter_getMetrics(delim, delimTypeToFont(sequence[i]), "math"); - var heightDepth = metrics.height + metrics.depth; // Small delimiters are scaled down versions of the same font, so we - // account for the style change size. - - if (sequence[i].type === "small") { - var newOptions = options.havingBaseStyle(sequence[i].style); - heightDepth *= newOptions.sizeMultiplier; - } // Check if the delimiter at this size works for the given height. - - - if (heightDepth > height) { - return sequence[i]; - } - } // If we reached the end of the sequence, return the last sequence element. - - - return sequence[sequence.length - 1]; -}; -/** - * Make a delimiter of a given height+depth, with optional centering. Here, we - * traverse the sequences, and create a delimiter that the sequence tells us to. - */ - - -var delimiter_makeCustomSizedDelim = function makeCustomSizedDelim(delim, height, center, options, mode, classes) { - if (delim === "<" || delim === "\\lt" || delim === "\u27E8") { - delim = "\\langle"; - } else if (delim === ">" || delim === "\\gt" || delim === "\u27E9") { - delim = "\\rangle"; - } // Decide what sequence to use - - - var sequence; - - if (utils.contains(stackNeverDelimiters, delim)) { - sequence = stackNeverDelimiterSequence; - } else if (utils.contains(stackLargeDelimiters, delim)) { - sequence = stackLargeDelimiterSequence; - } else { - sequence = stackAlwaysDelimiterSequence; - } // Look through the sequence - - - var delimType = traverseSequence(delim, height, sequence, options); // Get the delimiter from font glyphs. - // Depending on the sequence element we decided on, call the - // appropriate function. - - if (delimType.type === "small") { - return delimiter_makeSmallDelim(delim, delimType.style, center, options, mode, classes); - } else if (delimType.type === "large") { - return delimiter_makeLargeDelim(delim, delimType.size, center, options, mode, classes); - } else - /* if (delimType.type === "stack") */ - { - return delimiter_makeStackedDelim(delim, height, center, options, mode, classes); - } -}; -/** - * Make a delimiter for use with `\left` and `\right`, given a height and depth - * of an expression that the delimiters surround. - */ - - -var makeLeftRightDelim = function makeLeftRightDelim(delim, height, depth, options, mode, classes) { - // We always center \left/\right delimiters, so the axis is always shifted - var axisHeight = options.fontMetrics().axisHeight * options.sizeMultiplier; // Taken from TeX source, tex.web, function make_left_right - - var delimiterFactor = 901; - var delimiterExtend = 5.0 / options.fontMetrics().ptPerEm; - var maxDistFromAxis = Math.max(height - axisHeight, depth + axisHeight); - var totalHeight = Math.max( // In real TeX, calculations are done using integral values which are - // 65536 per pt, or 655360 per em. So, the division here truncates in - // TeX but doesn't here, producing different results. If we wanted to - // exactly match TeX's calculation, we could do - // Math.floor(655360 * maxDistFromAxis / 500) * - // delimiterFactor / 655360 - // (To see the difference, compare - // x^{x^{\left(\rule{0.1em}{0.68em}\right)}} - // in TeX and KaTeX) - maxDistFromAxis / 500 * delimiterFactor, 2 * maxDistFromAxis - delimiterExtend); // Finally, we defer to `makeCustomSizedDelim` with our calculated total - // height - - return delimiter_makeCustomSizedDelim(delim, totalHeight, true, options, mode, classes); -}; - -/* harmony default export */ var delimiter = ({ - sqrtImage: makeSqrtImage, - sizedDelim: delimiter_makeSizedDelim, - customSizedDelim: delimiter_makeCustomSizedDelim, - leftRightDelim: makeLeftRightDelim -}); -// CONCATENATED MODULE: ./src/functions/delimsizing.js - - - - - - - - - -// Extra data needed for the delimiter handler down below -var delimiterSizes = { - "\\bigl": { - mclass: "mopen", - size: 1 - }, - "\\Bigl": { - mclass: "mopen", - size: 2 - }, - "\\biggl": { - mclass: "mopen", - size: 3 - }, - "\\Biggl": { - mclass: "mopen", - size: 4 - }, - "\\bigr": { - mclass: "mclose", - size: 1 - }, - "\\Bigr": { - mclass: "mclose", - size: 2 - }, - "\\biggr": { - mclass: "mclose", - size: 3 - }, - "\\Biggr": { - mclass: "mclose", - size: 4 - }, - "\\bigm": { - mclass: "mrel", - size: 1 - }, - "\\Bigm": { - mclass: "mrel", - size: 2 - }, - "\\biggm": { - mclass: "mrel", - size: 3 - }, - "\\Biggm": { - mclass: "mrel", - size: 4 - }, - "\\big": { - mclass: "mord", - size: 1 - }, - "\\Big": { - mclass: "mord", - size: 2 - }, - "\\bigg": { - mclass: "mord", - size: 3 - }, - "\\Bigg": { - mclass: "mord", - size: 4 - } -}; -var delimiters = ["(", "\\lparen", ")", "\\rparen", "[", "\\lbrack", "]", "\\rbrack", "\\{", "\\lbrace", "\\}", "\\rbrace", "\\lfloor", "\\rfloor", "\u230A", "\u230B", "\\lceil", "\\rceil", "\u2308", "\u2309", "<", ">", "\\langle", "\u27E8", "\\rangle", "\u27E9", "\\lt", "\\gt", "\\lvert", "\\rvert", "\\lVert", "\\rVert", "\\lgroup", "\\rgroup", "\u27EE", "\u27EF", "\\lmoustache", "\\rmoustache", "\u23B0", "\u23B1", "/", "\\backslash", "|", "\\vert", "\\|", "\\Vert", "\\uparrow", "\\Uparrow", "\\downarrow", "\\Downarrow", "\\updownarrow", "\\Updownarrow", "."]; - -// Delimiter functions -function checkDelimiter(delim, context) { - var symDelim = checkSymbolNodeType(delim); - - if (symDelim && utils.contains(delimiters, symDelim.text)) { - return symDelim; - } else if (symDelim) { - throw new src_ParseError("Invalid delimiter '" + symDelim.text + "' after '" + context.funcName + "'", delim); - } else { - throw new src_ParseError("Invalid delimiter type '" + delim.type + "'", delim); - } -} - -defineFunction({ - type: "delimsizing", - names: ["\\bigl", "\\Bigl", "\\biggl", "\\Biggl", "\\bigr", "\\Bigr", "\\biggr", "\\Biggr", "\\bigm", "\\Bigm", "\\biggm", "\\Biggm", "\\big", "\\Big", "\\bigg", "\\Bigg"], - props: { - numArgs: 1 - }, - handler: function handler(context, args) { - var delim = checkDelimiter(args[0], context); - return { - type: "delimsizing", - mode: context.parser.mode, - size: delimiterSizes[context.funcName].size, - mclass: delimiterSizes[context.funcName].mclass, - delim: delim.text - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - if (group.delim === ".") { - // Empty delimiters still count as elements, even though they don't - // show anything. - return buildCommon.makeSpan([group.mclass]); - } // Use delimiter.sizedDelim to generate the delimiter. - - - return delimiter.sizedDelim(group.delim, group.size, options, group.mode, [group.mclass]); - }, - mathmlBuilder: function mathmlBuilder(group) { - var children = []; - - if (group.delim !== ".") { - children.push(buildMathML_makeText(group.delim, group.mode)); - } - - var node = new mathMLTree.MathNode("mo", children); - - if (group.mclass === "mopen" || group.mclass === "mclose") { - // Only some of the delimsizing functions act as fences, and they - // return "mopen" or "mclose" mclass. - node.setAttribute("fence", "true"); - } else { - // Explicitly disable fencing if it's not a fence, to override the - // defaults. - node.setAttribute("fence", "false"); - } - - return node; - } -}); - -function assertParsed(group) { - if (!group.body) { - throw new Error("Bug: The leftright ParseNode wasn't fully parsed."); - } -} - -defineFunction({ - type: "leftright-right", - names: ["\\right"], - props: { - numArgs: 1 - }, - handler: function handler(context, args) { - // \left case below triggers parsing of \right in - // `const right = parser.parseFunction();` - // uses this return value. - var color = context.parser.gullet.macros.get("\\current@color"); - - if (color && typeof color !== "string") { - throw new src_ParseError("\\current@color set to non-string in \\right"); - } - - return { - type: "leftright-right", - mode: context.parser.mode, - delim: checkDelimiter(args[0], context).text, - color: color // undefined if not set via \color - - }; - } -}); -defineFunction({ - type: "leftright", - names: ["\\left"], - props: { - numArgs: 1 - }, - handler: function handler(context, args) { - var delim = checkDelimiter(args[0], context); - var parser = context.parser; // Parse out the implicit body - - ++parser.leftrightDepth; // parseExpression stops before '\\right' - - var body = parser.parseExpression(false); - --parser.leftrightDepth; // Check the next token - - parser.expect("\\right", false); - var right = assertNodeType(parser.parseFunction(), "leftright-right"); - return { - type: "leftright", - mode: parser.mode, - body: body, - left: delim.text, - right: right.delim, - rightColor: right.color - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - assertParsed(group); // Build the inner expression - - var inner = buildHTML_buildExpression(group.body, options, true, ["mopen", "mclose"]); - var innerHeight = 0; - var innerDepth = 0; - var hadMiddle = false; // Calculate its height and depth - - for (var i = 0; i < inner.length; i++) { - // Property `isMiddle` not defined on `span`. See comment in - // "middle"'s htmlBuilder. - // $FlowFixMe - if (inner[i].isMiddle) { - hadMiddle = true; - } else { - innerHeight = Math.max(inner[i].height, innerHeight); - innerDepth = Math.max(inner[i].depth, innerDepth); - } - } // The size of delimiters is the same, regardless of what style we are - // in. Thus, to correctly calculate the size of delimiter we need around - // a group, we scale down the inner size based on the size. - - - innerHeight *= options.sizeMultiplier; - innerDepth *= options.sizeMultiplier; - var leftDelim; - - if (group.left === ".") { - // Empty delimiters in \left and \right make null delimiter spaces. - leftDelim = makeNullDelimiter(options, ["mopen"]); - } else { - // Otherwise, use leftRightDelim to generate the correct sized - // delimiter. - leftDelim = delimiter.leftRightDelim(group.left, innerHeight, innerDepth, options, group.mode, ["mopen"]); - } // Add it to the beginning of the expression - - - inner.unshift(leftDelim); // Handle middle delimiters - - if (hadMiddle) { - for (var _i = 1; _i < inner.length; _i++) { - var middleDelim = inner[_i]; // Property `isMiddle` not defined on `span`. See comment in - // "middle"'s htmlBuilder. - // $FlowFixMe - - var isMiddle = middleDelim.isMiddle; - - if (isMiddle) { - // Apply the options that were active when \middle was called - inner[_i] = delimiter.leftRightDelim(isMiddle.delim, innerHeight, innerDepth, isMiddle.options, group.mode, []); - } - } - } - - var rightDelim; // Same for the right delimiter, but using color specified by \color - - if (group.right === ".") { - rightDelim = makeNullDelimiter(options, ["mclose"]); - } else { - var colorOptions = group.rightColor ? options.withColor(group.rightColor) : options; - rightDelim = delimiter.leftRightDelim(group.right, innerHeight, innerDepth, colorOptions, group.mode, ["mclose"]); - } // Add it to the end of the expression. - - - inner.push(rightDelim); - return buildCommon.makeSpan(["minner"], inner, options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - assertParsed(group); - var inner = buildMathML_buildExpression(group.body, options); - - if (group.left !== ".") { - var leftNode = new mathMLTree.MathNode("mo", [buildMathML_makeText(group.left, group.mode)]); - leftNode.setAttribute("fence", "true"); - inner.unshift(leftNode); - } - - if (group.right !== ".") { - var rightNode = new mathMLTree.MathNode("mo", [buildMathML_makeText(group.right, group.mode)]); - rightNode.setAttribute("fence", "true"); - - if (group.rightColor) { - rightNode.setAttribute("mathcolor", group.rightColor); - } - - inner.push(rightNode); - } - - return buildMathML_makeRow(inner); - } -}); -defineFunction({ - type: "middle", - names: ["\\middle"], - props: { - numArgs: 1 - }, - handler: function handler(context, args) { - var delim = checkDelimiter(args[0], context); - - if (!context.parser.leftrightDepth) { - throw new src_ParseError("\\middle without preceding \\left", delim); - } - - return { - type: "middle", - mode: context.parser.mode, - delim: delim.text - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - var middleDelim; - - if (group.delim === ".") { - middleDelim = makeNullDelimiter(options, []); - } else { - middleDelim = delimiter.sizedDelim(group.delim, 1, options, group.mode, []); - var isMiddle = { - delim: group.delim, - options: options - }; // Property `isMiddle` not defined on `span`. It is only used in - // this file above. - // TODO: Fix this violation of the `span` type and possibly rename - // things since `isMiddle` sounds like a boolean, but is a struct. - // $FlowFixMe - - middleDelim.isMiddle = isMiddle; - } - - return middleDelim; - }, - mathmlBuilder: function mathmlBuilder(group, options) { - // A Firefox \middle will strech a character vertically only if it - // is in the fence part of the operator dictionary at: - // https://www.w3.org/TR/MathML3/appendixc.html. - // So we need to avoid U+2223 and use plain "|" instead. - var textNode = group.delim === "\\vert" || group.delim === "|" ? buildMathML_makeText("|", "text") : buildMathML_makeText(group.delim, group.mode); - var middleNode = new mathMLTree.MathNode("mo", [textNode]); - middleNode.setAttribute("fence", "true"); // MathML gives 5/18em spacing to each element. - // \middle should get delimiter spacing instead. - - middleNode.setAttribute("lspace", "0.05em"); - middleNode.setAttribute("rspace", "0.05em"); - return middleNode; - } -}); -// CONCATENATED MODULE: ./src/functions/enclose.js - - - - - - - - - -var enclose_htmlBuilder = function htmlBuilder(group, options) { - // \cancel, \bcancel, \xcancel, \sout, \fbox, \colorbox, \fcolorbox - // Some groups can return document fragments. Handle those by wrapping - // them in a span. - var inner = buildCommon.wrapFragment(buildHTML_buildGroup(group.body, options), options); - var label = group.label.substr(1); - var scale = options.sizeMultiplier; - var img; - var imgShift = 0; // In the LaTeX cancel package, line geometry is slightly different - // depending on whether the subject is wider than it is tall, or vice versa. - // We don't know the width of a group, so as a proxy, we test if - // the subject is a single character. This captures most of the - // subjects that should get the "tall" treatment. - - var isSingleChar = utils.isCharacterBox(group.body); - - if (label === "sout") { - img = buildCommon.makeSpan(["stretchy", "sout"]); - img.height = options.fontMetrics().defaultRuleThickness / scale; - imgShift = -0.5 * options.fontMetrics().xHeight; - } else { - // Add horizontal padding - if (/cancel/.test(label)) { - if (!isSingleChar) { - inner.classes.push("cancel-pad"); - } - } else { - inner.classes.push("boxpad"); - } // Add vertical padding - - - var vertPad = 0; - var ruleThickness = 0; // ref: cancel package: \advance\totalheight2\p@ % "+2" - - if (/box/.test(label)) { - ruleThickness = Math.max(options.fontMetrics().fboxrule, // default - options.minRuleThickness // User override. - ); - vertPad = options.fontMetrics().fboxsep + (label === "colorbox" ? 0 : ruleThickness); - } else { - vertPad = isSingleChar ? 0.2 : 0; - } - - img = stretchy.encloseSpan(inner, label, vertPad, options); - - if (/fbox|boxed|fcolorbox/.test(label)) { - img.style.borderStyle = "solid"; - img.style.borderWidth = ruleThickness + "em"; - } - - imgShift = inner.depth + vertPad; - - if (group.backgroundColor) { - img.style.backgroundColor = group.backgroundColor; - - if (group.borderColor) { - img.style.borderColor = group.borderColor; - } - } - } - - var vlist; - - if (group.backgroundColor) { - vlist = buildCommon.makeVList({ - positionType: "individualShift", - children: [// Put the color background behind inner; - { - type: "elem", - elem: img, - shift: imgShift - }, { - type: "elem", - elem: inner, - shift: 0 - }] - }, options); - } else { - vlist = buildCommon.makeVList({ - positionType: "individualShift", - children: [// Write the \cancel stroke on top of inner. - { - type: "elem", - elem: inner, - shift: 0 - }, { - type: "elem", - elem: img, - shift: imgShift, - wrapperClasses: /cancel/.test(label) ? ["svg-align"] : [] - }] - }, options); - } - - if (/cancel/.test(label)) { - // The cancel package documentation says that cancel lines add their height - // to the expression, but tests show that isn't how it actually works. - vlist.height = inner.height; - vlist.depth = inner.depth; - } - - if (/cancel/.test(label) && !isSingleChar) { - // cancel does not create horiz space for its line extension. - return buildCommon.makeSpan(["mord", "cancel-lap"], [vlist], options); - } else { - return buildCommon.makeSpan(["mord"], [vlist], options); - } -}; - -var enclose_mathmlBuilder = function mathmlBuilder(group, options) { - var fboxsep = 0; - var node = new mathMLTree.MathNode(group.label.indexOf("colorbox") > -1 ? "mpadded" : "menclose", [buildMathML_buildGroup(group.body, options)]); - - switch (group.label) { - case "\\cancel": - node.setAttribute("notation", "updiagonalstrike"); - break; - - case "\\bcancel": - node.setAttribute("notation", "downdiagonalstrike"); - break; - - case "\\sout": - node.setAttribute("notation", "horizontalstrike"); - break; - - case "\\fbox": - node.setAttribute("notation", "box"); - break; - - case "\\fcolorbox": - case "\\colorbox": - // doesn't have a good notation option. So use - // instead. Set some attributes that come included with . - fboxsep = options.fontMetrics().fboxsep * options.fontMetrics().ptPerEm; - node.setAttribute("width", "+" + 2 * fboxsep + "pt"); - node.setAttribute("height", "+" + 2 * fboxsep + "pt"); - node.setAttribute("lspace", fboxsep + "pt"); // - - node.setAttribute("voffset", fboxsep + "pt"); - - if (group.label === "\\fcolorbox") { - var thk = Math.max(options.fontMetrics().fboxrule, // default - options.minRuleThickness // user override - ); - node.setAttribute("style", "border: " + thk + "em solid " + String(group.borderColor)); - } - - break; - - case "\\xcancel": - node.setAttribute("notation", "updiagonalstrike downdiagonalstrike"); - break; - } - - if (group.backgroundColor) { - node.setAttribute("mathbackground", group.backgroundColor); - } - - return node; -}; - -defineFunction({ - type: "enclose", - names: ["\\colorbox"], - props: { - numArgs: 2, - allowedInText: true, - greediness: 3, - argTypes: ["color", "text"] - }, - handler: function handler(_ref, args, optArgs) { - var parser = _ref.parser, - funcName = _ref.funcName; - var color = assertNodeType(args[0], "color-token").color; - var body = args[1]; - return { - type: "enclose", - mode: parser.mode, - label: funcName, - backgroundColor: color, - body: body - }; - }, - htmlBuilder: enclose_htmlBuilder, - mathmlBuilder: enclose_mathmlBuilder -}); -defineFunction({ - type: "enclose", - names: ["\\fcolorbox"], - props: { - numArgs: 3, - allowedInText: true, - greediness: 3, - argTypes: ["color", "color", "text"] - }, - handler: function handler(_ref2, args, optArgs) { - var parser = _ref2.parser, - funcName = _ref2.funcName; - var borderColor = assertNodeType(args[0], "color-token").color; - var backgroundColor = assertNodeType(args[1], "color-token").color; - var body = args[2]; - return { - type: "enclose", - mode: parser.mode, - label: funcName, - backgroundColor: backgroundColor, - borderColor: borderColor, - body: body - }; - }, - htmlBuilder: enclose_htmlBuilder, - mathmlBuilder: enclose_mathmlBuilder -}); -defineFunction({ - type: "enclose", - names: ["\\fbox"], - props: { - numArgs: 1, - argTypes: ["hbox"], - allowedInText: true - }, - handler: function handler(_ref3, args) { - var parser = _ref3.parser; - return { - type: "enclose", - mode: parser.mode, - label: "\\fbox", - body: args[0] - }; - } -}); -defineFunction({ - type: "enclose", - names: ["\\cancel", "\\bcancel", "\\xcancel", "\\sout"], - props: { - numArgs: 1 - }, - handler: function handler(_ref4, args, optArgs) { - var parser = _ref4.parser, - funcName = _ref4.funcName; - var body = args[0]; - return { - type: "enclose", - mode: parser.mode, - label: funcName, - body: body - }; - }, - htmlBuilder: enclose_htmlBuilder, - mathmlBuilder: enclose_mathmlBuilder -}); -// CONCATENATED MODULE: ./src/defineEnvironment.js - - -/** - * All registered environments. - * `environments.js` exports this same dictionary again and makes it public. - * `Parser.js` requires this dictionary via `environments.js`. - */ -var _environments = {}; -function defineEnvironment(_ref) { - var type = _ref.type, - names = _ref.names, - props = _ref.props, - handler = _ref.handler, - htmlBuilder = _ref.htmlBuilder, - mathmlBuilder = _ref.mathmlBuilder; - // Set default values of environments. - var data = { - type: type, - numArgs: props.numArgs || 0, - greediness: 1, - allowedInText: false, - numOptionalArgs: 0, - handler: handler - }; - - for (var i = 0; i < names.length; ++i) { - // TODO: The value type of _environments should be a type union of all - // possible `EnvSpec<>` possibilities instead of `EnvSpec<*>`, which is - // an existential type. - // $FlowFixMe - _environments[names[i]] = data; - } - - if (htmlBuilder) { - _htmlGroupBuilders[type] = htmlBuilder; - } - - if (mathmlBuilder) { - _mathmlGroupBuilders[type] = mathmlBuilder; - } -} -// CONCATENATED MODULE: ./src/environments/array.js - - - - - - - - - - - - - -function getHLines(parser) { - // Return an array. The array length = number of hlines. - // Each element in the array tells if the line is dashed. - var hlineInfo = []; - parser.consumeSpaces(); - var nxt = parser.fetch().text; - - while (nxt === "\\hline" || nxt === "\\hdashline") { - parser.consume(); - hlineInfo.push(nxt === "\\hdashline"); - parser.consumeSpaces(); - nxt = parser.fetch().text; - } - - return hlineInfo; -} -/** - * Parse the body of the environment, with rows delimited by \\ and - * columns delimited by &, and create a nested list in row-major order - * with one group per cell. If given an optional argument style - * ("text", "display", etc.), then each cell is cast into that style. - */ - - -function parseArray(parser, _ref, style) { - var hskipBeforeAndAfter = _ref.hskipBeforeAndAfter, - addJot = _ref.addJot, - cols = _ref.cols, - arraystretch = _ref.arraystretch, - colSeparationType = _ref.colSeparationType; - // Parse body of array with \\ temporarily mapped to \cr - parser.gullet.beginGroup(); - parser.gullet.macros.set("\\\\", "\\cr"); // Get current arraystretch if it's not set by the environment - - if (!arraystretch) { - var stretch = parser.gullet.expandMacroAsText("\\arraystretch"); - - if (stretch == null) { - // Default \arraystretch from lttab.dtx - arraystretch = 1; - } else { - arraystretch = parseFloat(stretch); - - if (!arraystretch || arraystretch < 0) { - throw new src_ParseError("Invalid \\arraystretch: " + stretch); - } - } - } // Start group for first cell - - - parser.gullet.beginGroup(); - var row = []; - var body = [row]; - var rowGaps = []; - var hLinesBeforeRow = []; // Test for \hline at the top of the array. - - hLinesBeforeRow.push(getHLines(parser)); - - while (true) { - // eslint-disable-line no-constant-condition - // Parse each cell in its own group (namespace) - var cell = parser.parseExpression(false, "\\cr"); - parser.gullet.endGroup(); - parser.gullet.beginGroup(); - cell = { - type: "ordgroup", - mode: parser.mode, - body: cell - }; - - if (style) { - cell = { - type: "styling", - mode: parser.mode, - style: style, - body: [cell] - }; - } - - row.push(cell); - var next = parser.fetch().text; - - if (next === "&") { - parser.consume(); - } else if (next === "\\end") { - // Arrays terminate newlines with `\crcr` which consumes a `\cr` if - // the last line is empty. - // NOTE: Currently, `cell` is the last item added into `row`. - if (row.length === 1 && cell.type === "styling" && cell.body[0].body.length === 0) { - body.pop(); - } - - if (hLinesBeforeRow.length < body.length + 1) { - hLinesBeforeRow.push([]); - } - - break; - } else if (next === "\\cr") { - var cr = assertNodeType(parser.parseFunction(), "cr"); - rowGaps.push(cr.size); // check for \hline(s) following the row separator - - hLinesBeforeRow.push(getHLines(parser)); - row = []; - body.push(row); - } else { - throw new src_ParseError("Expected & or \\\\ or \\cr or \\end", parser.nextToken); - } - } // End cell group - - - parser.gullet.endGroup(); // End array group defining \\ - - parser.gullet.endGroup(); - return { - type: "array", - mode: parser.mode, - addJot: addJot, - arraystretch: arraystretch, - body: body, - cols: cols, - rowGaps: rowGaps, - hskipBeforeAndAfter: hskipBeforeAndAfter, - hLinesBeforeRow: hLinesBeforeRow, - colSeparationType: colSeparationType - }; -} // Decides on a style for cells in an array according to whether the given -// environment name starts with the letter 'd'. - - -function dCellStyle(envName) { - if (envName.substr(0, 1) === "d") { - return "display"; - } else { - return "text"; - } -} - -var array_htmlBuilder = function htmlBuilder(group, options) { - var r; - var c; - var nr = group.body.length; - var hLinesBeforeRow = group.hLinesBeforeRow; - var nc = 0; - var body = new Array(nr); - var hlines = []; - var ruleThickness = Math.max( // From LaTeX \showthe\arrayrulewidth. Equals 0.04 em. - options.fontMetrics().arrayRuleWidth, options.minRuleThickness // User override. - ); // Horizontal spacing - - var pt = 1 / options.fontMetrics().ptPerEm; - var arraycolsep = 5 * pt; // default value, i.e. \arraycolsep in article.cls - - if (group.colSeparationType && group.colSeparationType === "small") { - // We're in a {smallmatrix}. Default column space is \thickspace, - // i.e. 5/18em = 0.2778em, per amsmath.dtx for {smallmatrix}. - // But that needs adjustment because LaTeX applies \scriptstyle to the - // entire array, including the colspace, but this function applies - // \scriptstyle only inside each element. - var localMultiplier = options.havingStyle(src_Style.SCRIPT).sizeMultiplier; - arraycolsep = 0.2778 * (localMultiplier / options.sizeMultiplier); - } // Vertical spacing - - - var baselineskip = 12 * pt; // see size10.clo - // Default \jot from ltmath.dtx - // TODO(edemaine): allow overriding \jot via \setlength (#687) - - var jot = 3 * pt; - var arrayskip = group.arraystretch * baselineskip; - var arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and - - var arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx - - var totalHeight = 0; // Set a position for \hline(s) at the top of the array, if any. - - function setHLinePos(hlinesInGap) { - for (var i = 0; i < hlinesInGap.length; ++i) { - if (i > 0) { - totalHeight += 0.25; - } - - hlines.push({ - pos: totalHeight, - isDashed: hlinesInGap[i] - }); - } - } - - setHLinePos(hLinesBeforeRow[0]); - - for (r = 0; r < group.body.length; ++r) { - var inrow = group.body[r]; - var height = arstrutHeight; // \@array adds an \@arstrut - - var depth = arstrutDepth; // to each tow (via the template) - - if (nc < inrow.length) { - nc = inrow.length; - } - - var outrow = new Array(inrow.length); - - for (c = 0; c < inrow.length; ++c) { - var elt = buildHTML_buildGroup(inrow[c], options); - - if (depth < elt.depth) { - depth = elt.depth; - } - - if (height < elt.height) { - height = elt.height; - } - - outrow[c] = elt; - } - - var rowGap = group.rowGaps[r]; - var gap = 0; - - if (rowGap) { - gap = units_calculateSize(rowGap, options); - - if (gap > 0) { - // \@argarraycr - gap += arstrutDepth; - - if (depth < gap) { - depth = gap; // \@xargarraycr - } - - gap = 0; - } - } // In AMS multiline environments such as aligned and gathered, rows - // correspond to lines that have additional \jot added to the - // \baselineskip via \openup. - - - if (group.addJot) { - depth += jot; - } - - outrow.height = height; - outrow.depth = depth; - totalHeight += height; - outrow.pos = totalHeight; - totalHeight += depth + gap; // \@yargarraycr - - body[r] = outrow; // Set a position for \hline(s), if any. - - setHLinePos(hLinesBeforeRow[r + 1]); - } - - var offset = totalHeight / 2 + options.fontMetrics().axisHeight; - var colDescriptions = group.cols || []; - var cols = []; - var colSep; - var colDescrNum; - - for (c = 0, colDescrNum = 0; // Continue while either there are more columns or more column - // descriptions, so trailing separators don't get lost. - c < nc || colDescrNum < colDescriptions.length; ++c, ++colDescrNum) { - var colDescr = colDescriptions[colDescrNum] || {}; - var firstSeparator = true; - - while (colDescr.type === "separator") { - // If there is more than one separator in a row, add a space - // between them. - if (!firstSeparator) { - colSep = buildCommon.makeSpan(["arraycolsep"], []); - colSep.style.width = options.fontMetrics().doubleRuleSep + "em"; - cols.push(colSep); - } - - if (colDescr.separator === "|" || colDescr.separator === ":") { - var lineType = colDescr.separator === "|" ? "solid" : "dashed"; - var separator = buildCommon.makeSpan(["vertical-separator"], [], options); - separator.style.height = totalHeight + "em"; - separator.style.borderRightWidth = ruleThickness + "em"; - separator.style.borderRightStyle = lineType; - separator.style.margin = "0 -" + ruleThickness / 2 + "em"; - separator.style.verticalAlign = -(totalHeight - offset) + "em"; - cols.push(separator); - } else { - throw new src_ParseError("Invalid separator type: " + colDescr.separator); - } - - colDescrNum++; - colDescr = colDescriptions[colDescrNum] || {}; - firstSeparator = false; - } - - if (c >= nc) { - continue; - } - - var sepwidth = void 0; - - if (c > 0 || group.hskipBeforeAndAfter) { - sepwidth = utils.deflt(colDescr.pregap, arraycolsep); - - if (sepwidth !== 0) { - colSep = buildCommon.makeSpan(["arraycolsep"], []); - colSep.style.width = sepwidth + "em"; - cols.push(colSep); - } - } - - var col = []; - - for (r = 0; r < nr; ++r) { - var row = body[r]; - var elem = row[c]; - - if (!elem) { - continue; - } - - var shift = row.pos - offset; - elem.depth = row.depth; - elem.height = row.height; - col.push({ - type: "elem", - elem: elem, - shift: shift - }); - } - - col = buildCommon.makeVList({ - positionType: "individualShift", - children: col - }, options); - col = buildCommon.makeSpan(["col-align-" + (colDescr.align || "c")], [col]); - cols.push(col); - - if (c < nc - 1 || group.hskipBeforeAndAfter) { - sepwidth = utils.deflt(colDescr.postgap, arraycolsep); - - if (sepwidth !== 0) { - colSep = buildCommon.makeSpan(["arraycolsep"], []); - colSep.style.width = sepwidth + "em"; - cols.push(colSep); - } - } - } - - body = buildCommon.makeSpan(["mtable"], cols); // Add \hline(s), if any. - - if (hlines.length > 0) { - var line = buildCommon.makeLineSpan("hline", options, ruleThickness); - var dashes = buildCommon.makeLineSpan("hdashline", options, ruleThickness); - var vListElems = [{ - type: "elem", - elem: body, - shift: 0 - }]; - - while (hlines.length > 0) { - var hline = hlines.pop(); - var lineShift = hline.pos - offset; - - if (hline.isDashed) { - vListElems.push({ - type: "elem", - elem: dashes, - shift: lineShift - }); - } else { - vListElems.push({ - type: "elem", - elem: line, - shift: lineShift - }); - } - } - - body = buildCommon.makeVList({ - positionType: "individualShift", - children: vListElems - }, options); - } - - return buildCommon.makeSpan(["mord"], [body], options); -}; - -var alignMap = { - c: "center ", - l: "left ", - r: "right " -}; - -var array_mathmlBuilder = function mathmlBuilder(group, options) { - var table = new mathMLTree.MathNode("mtable", group.body.map(function (row) { - return new mathMLTree.MathNode("mtr", row.map(function (cell) { - return new mathMLTree.MathNode("mtd", [buildMathML_buildGroup(cell, options)]); - })); - })); // Set column alignment, row spacing, column spacing, and - // array lines by setting attributes on the table element. - // Set the row spacing. In MathML, we specify a gap distance. - // We do not use rowGap[] because MathML automatically increases - // cell height with the height/depth of the element content. - // LaTeX \arraystretch multiplies the row baseline-to-baseline distance. - // We simulate this by adding (arraystretch - 1)em to the gap. This - // does a reasonable job of adjusting arrays containing 1 em tall content. - // The 0.16 and 0.09 values are found emprically. They produce an array - // similar to LaTeX and in which content does not interfere with \hines. - - var gap = group.arraystretch === 0.5 ? 0.1 // {smallmatrix}, {subarray} - : 0.16 + group.arraystretch - 1 + (group.addJot ? 0.09 : 0); - table.setAttribute("rowspacing", gap + "em"); // MathML table lines go only between cells. - // To place a line on an edge we'll use , if necessary. - - var menclose = ""; - var align = ""; - - if (group.cols && group.cols.length > 0) { - // Find column alignment, column spacing, and vertical lines. - var cols = group.cols; - var columnLines = ""; - var prevTypeWasAlign = false; - var iStart = 0; - var iEnd = cols.length; - - if (cols[0].type === "separator") { - menclose += "top "; - iStart = 1; - } - - if (cols[cols.length - 1].type === "separator") { - menclose += "bottom "; - iEnd -= 1; - } - - for (var i = iStart; i < iEnd; i++) { - if (cols[i].type === "align") { - align += alignMap[cols[i].align]; - - if (prevTypeWasAlign) { - columnLines += "none "; - } - - prevTypeWasAlign = true; - } else if (cols[i].type === "separator") { - // MathML accepts only single lines between cells. - // So we read only the first of consecutive separators. - if (prevTypeWasAlign) { - columnLines += cols[i].separator === "|" ? "solid " : "dashed "; - prevTypeWasAlign = false; - } - } - } - - table.setAttribute("columnalign", align.trim()); - - if (/[sd]/.test(columnLines)) { - table.setAttribute("columnlines", columnLines.trim()); - } - } // Set column spacing. - - - if (group.colSeparationType === "align") { - var _cols = group.cols || []; - - var spacing = ""; - - for (var _i = 1; _i < _cols.length; _i++) { - spacing += _i % 2 ? "0em " : "1em "; - } - - table.setAttribute("columnspacing", spacing.trim()); - } else if (group.colSeparationType === "alignat") { - table.setAttribute("columnspacing", "0em"); - } else if (group.colSeparationType === "small") { - table.setAttribute("columnspacing", "0.2778em"); - } else { - table.setAttribute("columnspacing", "1em"); - } // Address \hline and \hdashline - - - var rowLines = ""; - var hlines = group.hLinesBeforeRow; - menclose += hlines[0].length > 0 ? "left " : ""; - menclose += hlines[hlines.length - 1].length > 0 ? "right " : ""; - - for (var _i2 = 1; _i2 < hlines.length - 1; _i2++) { - rowLines += hlines[_i2].length === 0 ? "none " // MathML accepts only a single line between rows. Read one element. - : hlines[_i2][0] ? "dashed " : "solid "; - } - - if (/[sd]/.test(rowLines)) { - table.setAttribute("rowlines", rowLines.trim()); - } - - if (menclose !== "") { - table = new mathMLTree.MathNode("menclose", [table]); - table.setAttribute("notation", menclose.trim()); - } - - if (group.arraystretch && group.arraystretch < 1) { - // A small array. Wrap in scriptstyle so row gap is not too large. - table = new mathMLTree.MathNode("mstyle", [table]); - table.setAttribute("scriptlevel", "1"); - } - - return table; -}; // Convenience function for aligned and alignedat environments. - - -var array_alignedHandler = function alignedHandler(context, args) { - var cols = []; - var res = parseArray(context.parser, { - cols: cols, - addJot: true - }, "display"); // Determining number of columns. - // 1. If the first argument is given, we use it as a number of columns, - // and makes sure that each row doesn't exceed that number. - // 2. Otherwise, just count number of columns = maximum number - // of cells in each row ("aligned" mode -- isAligned will be true). - // - // At the same time, prepend empty group {} at beginning of every second - // cell in each row (starting with second cell) so that operators become - // binary. This behavior is implemented in amsmath's \start@aligned. - - var numMaths; - var numCols = 0; - var emptyGroup = { - type: "ordgroup", - mode: context.mode, - body: [] - }; - - if (args[0] && args[0].type === "ordgroup") { - var arg0 = ""; - - for (var i = 0; i < args[0].body.length; i++) { - var textord = assertNodeType(args[0].body[i], "textord"); - arg0 += textord.text; - } - - numMaths = Number(arg0); - numCols = numMaths * 2; - } - - var isAligned = !numCols; - res.body.forEach(function (row) { - for (var _i3 = 1; _i3 < row.length; _i3 += 2) { - // Modify ordgroup node within styling node - var styling = assertNodeType(row[_i3], "styling"); - var ordgroup = assertNodeType(styling.body[0], "ordgroup"); - ordgroup.body.unshift(emptyGroup); - } - - if (!isAligned) { - // Case 1 - var curMaths = row.length / 2; - - if (numMaths < curMaths) { - throw new src_ParseError("Too many math in a row: " + ("expected " + numMaths + ", but got " + curMaths), row[0]); - } - } else if (numCols < row.length) { - // Case 2 - numCols = row.length; - } - }); // Adjusting alignment. - // In aligned mode, we add one \qquad between columns; - // otherwise we add nothing. - - for (var _i4 = 0; _i4 < numCols; ++_i4) { - var align = "r"; - var pregap = 0; - - if (_i4 % 2 === 1) { - align = "l"; - } else if (_i4 > 0 && isAligned) { - // "aligned" mode. - pregap = 1; // add one \quad - } - - cols[_i4] = { - type: "align", - align: align, - pregap: pregap, - postgap: 0 - }; - } - - res.colSeparationType = isAligned ? "align" : "alignat"; - return res; -}; // Arrays are part of LaTeX, defined in lttab.dtx so its documentation -// is part of the source2e.pdf file of LaTeX2e source documentation. -// {darray} is an {array} environment where cells are set in \displaystyle, -// as defined in nccmath.sty. - - -defineEnvironment({ - type: "array", - names: ["array", "darray"], - props: { - numArgs: 1 - }, - handler: function handler(context, args) { - // Since no types are specified above, the two possibilities are - // - The argument is wrapped in {} or [], in which case Parser's - // parseGroup() returns an "ordgroup" wrapping some symbol node. - // - The argument is a bare symbol node. - var symNode = checkSymbolNodeType(args[0]); - var colalign = symNode ? [args[0]] : assertNodeType(args[0], "ordgroup").body; - var cols = colalign.map(function (nde) { - var node = assertSymbolNodeType(nde); - var ca = node.text; - - if ("lcr".indexOf(ca) !== -1) { - return { - type: "align", - align: ca - }; - } else if (ca === "|") { - return { - type: "separator", - separator: "|" - }; - } else if (ca === ":") { - return { - type: "separator", - separator: ":" - }; - } - - throw new src_ParseError("Unknown column alignment: " + ca, nde); - }); - var res = { - cols: cols, - hskipBeforeAndAfter: true // \@preamble in lttab.dtx - - }; - return parseArray(context.parser, res, dCellStyle(context.envName)); - }, - htmlBuilder: array_htmlBuilder, - mathmlBuilder: array_mathmlBuilder -}); // The matrix environments of amsmath builds on the array environment -// of LaTeX, which is discussed above. - -defineEnvironment({ - type: "array", - names: ["matrix", "pmatrix", "bmatrix", "Bmatrix", "vmatrix", "Vmatrix"], - props: { - numArgs: 0 - }, - handler: function handler(context) { - var delimiters = { - "matrix": null, - "pmatrix": ["(", ")"], - "bmatrix": ["[", "]"], - "Bmatrix": ["\\{", "\\}"], - "vmatrix": ["|", "|"], - "Vmatrix": ["\\Vert", "\\Vert"] - }[context.envName]; // \hskip -\arraycolsep in amsmath - - var payload = { - hskipBeforeAndAfter: false - }; - var res = parseArray(context.parser, payload, dCellStyle(context.envName)); - return delimiters ? { - type: "leftright", - mode: context.mode, - body: [res], - left: delimiters[0], - right: delimiters[1], - rightColor: undefined // \right uninfluenced by \color in array - - } : res; - }, - htmlBuilder: array_htmlBuilder, - mathmlBuilder: array_mathmlBuilder -}); -defineEnvironment({ - type: "array", - names: ["smallmatrix"], - props: { - numArgs: 0 - }, - handler: function handler(context) { - var payload = { - arraystretch: 0.5 - }; - var res = parseArray(context.parser, payload, "script"); - res.colSeparationType = "small"; - return res; - }, - htmlBuilder: array_htmlBuilder, - mathmlBuilder: array_mathmlBuilder -}); -defineEnvironment({ - type: "array", - names: ["subarray"], - props: { - numArgs: 1 - }, - handler: function handler(context, args) { - // Parsing of {subarray} is similar to {array} - var symNode = checkSymbolNodeType(args[0]); - var colalign = symNode ? [args[0]] : assertNodeType(args[0], "ordgroup").body; - var cols = colalign.map(function (nde) { - var node = assertSymbolNodeType(nde); - var ca = node.text; // {subarray} only recognizes "l" & "c" - - if ("lc".indexOf(ca) !== -1) { - return { - type: "align", - align: ca - }; - } - - throw new src_ParseError("Unknown column alignment: " + ca, nde); - }); - - if (cols.length > 1) { - throw new src_ParseError("{subarray} can contain only one column"); - } - - var res = { - cols: cols, - hskipBeforeAndAfter: false, - arraystretch: 0.5 - }; - res = parseArray(context.parser, res, "script"); - - if (res.body.length > 0 && res.body[0].length > 1) { - throw new src_ParseError("{subarray} can contain only one column"); - } - - return res; - }, - htmlBuilder: array_htmlBuilder, - mathmlBuilder: array_mathmlBuilder -}); // A cases environment (in amsmath.sty) is almost equivalent to -// \def\arraystretch{1.2}% -// \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right. -// {dcases} is a {cases} environment where cells are set in \displaystyle, -// as defined in mathtools.sty. -// {rcases} is another mathtools environment. It's brace is on the right side. - -defineEnvironment({ - type: "array", - names: ["cases", "dcases", "rcases", "drcases"], - props: { - numArgs: 0 - }, - handler: function handler(context) { - var payload = { - arraystretch: 1.2, - cols: [{ - type: "align", - align: "l", - pregap: 0, - // TODO(kevinb) get the current style. - // For now we use the metrics for TEXT style which is what we were - // doing before. Before attempting to get the current style we - // should look at TeX's behavior especially for \over and matrices. - postgap: 1.0 - /* 1em quad */ - - }, { - type: "align", - align: "l", - pregap: 0, - postgap: 0 - }] - }; - var res = parseArray(context.parser, payload, dCellStyle(context.envName)); - return { - type: "leftright", - mode: context.mode, - body: [res], - left: context.envName.indexOf("r") > -1 ? "." : "\\{", - right: context.envName.indexOf("r") > -1 ? "\\}" : ".", - rightColor: undefined - }; - }, - htmlBuilder: array_htmlBuilder, - mathmlBuilder: array_mathmlBuilder -}); // An aligned environment is like the align* environment -// except it operates within math mode. -// Note that we assume \nomallineskiplimit to be zero, -// so that \strut@ is the same as \strut. - -defineEnvironment({ - type: "array", - names: ["aligned"], - props: { - numArgs: 0 - }, - handler: array_alignedHandler, - htmlBuilder: array_htmlBuilder, - mathmlBuilder: array_mathmlBuilder -}); // A gathered environment is like an array environment with one centered -// column, but where rows are considered lines so get \jot line spacing -// and contents are set in \displaystyle. - -defineEnvironment({ - type: "array", - names: ["gathered"], - props: { - numArgs: 0 - }, - handler: function handler(context) { - var res = { - cols: [{ - type: "align", - align: "c" - }], - addJot: true - }; - return parseArray(context.parser, res, "display"); - }, - htmlBuilder: array_htmlBuilder, - mathmlBuilder: array_mathmlBuilder -}); // alignat environment is like an align environment, but one must explicitly -// specify maximum number of columns in each row, and can adjust spacing between -// each columns. - -defineEnvironment({ - type: "array", - names: ["alignedat"], - // One for numbered and for unnumbered; - // but, KaTeX doesn't supports math numbering yet, - // they make no difference for now. - props: { - numArgs: 1 - }, - handler: array_alignedHandler, - htmlBuilder: array_htmlBuilder, - mathmlBuilder: array_mathmlBuilder -}); // Catch \hline outside array environment - -defineFunction({ - type: "text", - // Doesn't matter what this is. - names: ["\\hline", "\\hdashline"], - props: { - numArgs: 0, - allowedInText: true, - allowedInMath: true - }, - handler: function handler(context, args) { - throw new src_ParseError(context.funcName + " valid only within array environment"); - } -}); -// CONCATENATED MODULE: ./src/environments.js - -var environments = _environments; -/* harmony default export */ var src_environments = (environments); // All environment definitions should be imported below - - -// CONCATENATED MODULE: ./src/functions/environment.js - - - - // Environment delimiters. HTML/MathML rendering is defined in the corresponding -// defineEnvironment definitions. -// $FlowFixMe, "environment" handler returns an environment ParseNode - -defineFunction({ - type: "environment", - names: ["\\begin", "\\end"], - props: { - numArgs: 1, - argTypes: ["text"] - }, - handler: function handler(_ref, args) { - var parser = _ref.parser, - funcName = _ref.funcName; - var nameGroup = args[0]; - - if (nameGroup.type !== "ordgroup") { - throw new src_ParseError("Invalid environment name", nameGroup); - } - - var envName = ""; - - for (var i = 0; i < nameGroup.body.length; ++i) { - envName += assertNodeType(nameGroup.body[i], "textord").text; - } - - if (funcName === "\\begin") { - // begin...end is similar to left...right - if (!src_environments.hasOwnProperty(envName)) { - throw new src_ParseError("No such environment: " + envName, nameGroup); - } // Build the environment object. Arguments and other information will - // be made available to the begin and end methods using properties. - - - var env = src_environments[envName]; - - var _parser$parseArgument = parser.parseArguments("\\begin{" + envName + "}", env), - _args = _parser$parseArgument.args, - optArgs = _parser$parseArgument.optArgs; - - var context = { - mode: parser.mode, - envName: envName, - parser: parser - }; - var result = env.handler(context, _args, optArgs); - parser.expect("\\end", false); - var endNameToken = parser.nextToken; - var end = assertNodeType(parser.parseFunction(), "environment"); - - if (end.name !== envName) { - throw new src_ParseError("Mismatch: \\begin{" + envName + "} matched by \\end{" + end.name + "}", endNameToken); - } - - return result; - } - - return { - type: "environment", - mode: parser.mode, - name: envName, - nameGroup: nameGroup - }; - } -}); -// CONCATENATED MODULE: ./src/functions/mclass.js - - - - - - -var mclass_makeSpan = buildCommon.makeSpan; - -function mclass_htmlBuilder(group, options) { - var elements = buildHTML_buildExpression(group.body, options, true); - return mclass_makeSpan([group.mclass], elements, options); -} - -function mclass_mathmlBuilder(group, options) { - var node; - var inner = buildMathML_buildExpression(group.body, options); - - if (group.mclass === "minner") { - return mathMLTree.newDocumentFragment(inner); - } else if (group.mclass === "mord") { - if (group.isCharacterBox) { - node = inner[0]; - node.type = "mi"; - } else { - node = new mathMLTree.MathNode("mi", inner); - } - } else { - if (group.isCharacterBox) { - node = inner[0]; - node.type = "mo"; - } else { - node = new mathMLTree.MathNode("mo", inner); - } // Set spacing based on what is the most likely adjacent atom type. - // See TeXbook p170. - - - if (group.mclass === "mbin") { - node.attributes.lspace = "0.22em"; // medium space - - node.attributes.rspace = "0.22em"; - } else if (group.mclass === "mpunct") { - node.attributes.lspace = "0em"; - node.attributes.rspace = "0.17em"; // thinspace - } else if (group.mclass === "mopen" || group.mclass === "mclose") { - node.attributes.lspace = "0em"; - node.attributes.rspace = "0em"; - } // MathML default space is 5/18 em, so needs no action. - // Ref: https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mo - - } - - return node; -} // Math class commands except \mathop - - -defineFunction({ - type: "mclass", - names: ["\\mathord", "\\mathbin", "\\mathrel", "\\mathopen", "\\mathclose", "\\mathpunct", "\\mathinner"], - props: { - numArgs: 1 - }, - handler: function handler(_ref, args) { - var parser = _ref.parser, - funcName = _ref.funcName; - var body = args[0]; - return { - type: "mclass", - mode: parser.mode, - mclass: "m" + funcName.substr(5), - // TODO(kevinb): don't prefix with 'm' - body: ordargument(body), - isCharacterBox: utils.isCharacterBox(body) - }; - }, - htmlBuilder: mclass_htmlBuilder, - mathmlBuilder: mclass_mathmlBuilder -}); -var binrelClass = function binrelClass(arg) { - // \binrel@ spacing varies with (bin|rel|ord) of the atom in the argument. - // (by rendering separately and with {}s before and after, and measuring - // the change in spacing). We'll do roughly the same by detecting the - // atom type directly. - var atom = arg.type === "ordgroup" && arg.body.length ? arg.body[0] : arg; - - if (atom.type === "atom" && (atom.family === "bin" || atom.family === "rel")) { - return "m" + atom.family; - } else { - return "mord"; - } -}; // \@binrel{x}{y} renders like y but as mbin/mrel/mord if x is mbin/mrel/mord. -// This is equivalent to \binrel@{x}\binrel@@{y} in AMSTeX. - -defineFunction({ - type: "mclass", - names: ["\\@binrel"], - props: { - numArgs: 2 - }, - handler: function handler(_ref2, args) { - var parser = _ref2.parser; - return { - type: "mclass", - mode: parser.mode, - mclass: binrelClass(args[0]), - body: [args[1]], - isCharacterBox: utils.isCharacterBox(args[1]) - }; - } -}); // Build a relation or stacked op by placing one symbol on top of another - -defineFunction({ - type: "mclass", - names: ["\\stackrel", "\\overset", "\\underset"], - props: { - numArgs: 2 - }, - handler: function handler(_ref3, args) { - var parser = _ref3.parser, - funcName = _ref3.funcName; - var baseArg = args[1]; - var shiftedArg = args[0]; - var mclass; - - if (funcName !== "\\stackrel") { - // LaTeX applies \binrel spacing to \overset and \underset. - mclass = binrelClass(baseArg); - } else { - mclass = "mrel"; // for \stackrel - } - - var baseOp = { - type: "op", - mode: baseArg.mode, - limits: true, - alwaysHandleSupSub: true, - parentIsSupSub: false, - symbol: false, - suppressBaseShift: funcName !== "\\stackrel", - body: ordargument(baseArg) - }; - var supsub = { - type: "supsub", - mode: shiftedArg.mode, - base: baseOp, - sup: funcName === "\\underset" ? null : shiftedArg, - sub: funcName === "\\underset" ? shiftedArg : null - }; - return { - type: "mclass", - mode: parser.mode, - mclass: mclass, - body: [supsub], - isCharacterBox: utils.isCharacterBox(supsub) - }; - }, - htmlBuilder: mclass_htmlBuilder, - mathmlBuilder: mclass_mathmlBuilder -}); -// CONCATENATED MODULE: ./src/functions/font.js -// TODO(kevinb): implement \\sl and \\sc - - - - - - -var font_htmlBuilder = function htmlBuilder(group, options) { - var font = group.font; - var newOptions = options.withFont(font); - return buildHTML_buildGroup(group.body, newOptions); -}; - -var font_mathmlBuilder = function mathmlBuilder(group, options) { - var font = group.font; - var newOptions = options.withFont(font); - return buildMathML_buildGroup(group.body, newOptions); -}; - -var fontAliases = { - "\\Bbb": "\\mathbb", - "\\bold": "\\mathbf", - "\\frak": "\\mathfrak", - "\\bm": "\\boldsymbol" -}; -defineFunction({ - type: "font", - names: [// styles, except \boldsymbol defined below - "\\mathrm", "\\mathit", "\\mathbf", "\\mathnormal", // families - "\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf", "\\mathtt", // aliases, except \bm defined below - "\\Bbb", "\\bold", "\\frak"], - props: { - numArgs: 1, - greediness: 2 - }, - handler: function handler(_ref, args) { - var parser = _ref.parser, - funcName = _ref.funcName; - var body = args[0]; - var func = funcName; - - if (func in fontAliases) { - func = fontAliases[func]; - } - - return { - type: "font", - mode: parser.mode, - font: func.slice(1), - body: body - }; - }, - htmlBuilder: font_htmlBuilder, - mathmlBuilder: font_mathmlBuilder -}); -defineFunction({ - type: "mclass", - names: ["\\boldsymbol", "\\bm"], - props: { - numArgs: 1, - greediness: 2 - }, - handler: function handler(_ref2, args) { - var parser = _ref2.parser; - var body = args[0]; - var isCharacterBox = utils.isCharacterBox(body); // amsbsy.sty's \boldsymbol uses \binrel spacing to inherit the - // argument's bin|rel|ord status - - return { - type: "mclass", - mode: parser.mode, - mclass: binrelClass(body), - body: [{ - type: "font", - mode: parser.mode, - font: "boldsymbol", - body: body - }], - isCharacterBox: isCharacterBox - }; - } -}); // Old font changing functions - -defineFunction({ - type: "font", - names: ["\\rm", "\\sf", "\\tt", "\\bf", "\\it", "\\cal"], - props: { - numArgs: 0, - allowedInText: true - }, - handler: function handler(_ref3, args) { - var parser = _ref3.parser, - funcName = _ref3.funcName, - breakOnTokenText = _ref3.breakOnTokenText; - var mode = parser.mode; - var body = parser.parseExpression(true, breakOnTokenText); - var style = "math" + funcName.slice(1); - return { - type: "font", - mode: mode, - font: style, - body: { - type: "ordgroup", - mode: parser.mode, - body: body - } - }; - }, - htmlBuilder: font_htmlBuilder, - mathmlBuilder: font_mathmlBuilder -}); -// CONCATENATED MODULE: ./src/functions/genfrac.js - - - - - - - - - - - -var genfrac_adjustStyle = function adjustStyle(size, originalStyle) { - // Figure out what style this fraction should be in based on the - // function used - var style = originalStyle; - - if (size === "display") { - // Get display style as a default. - // If incoming style is sub/sup, use style.text() to get correct size. - style = style.id >= src_Style.SCRIPT.id ? style.text() : src_Style.DISPLAY; - } else if (size === "text" && style.size === src_Style.DISPLAY.size) { - // We're in a \tfrac but incoming style is displaystyle, so: - style = src_Style.TEXT; - } else if (size === "script") { - style = src_Style.SCRIPT; - } else if (size === "scriptscript") { - style = src_Style.SCRIPTSCRIPT; - } - - return style; -}; - -var genfrac_htmlBuilder = function htmlBuilder(group, options) { - // Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e). - var style = genfrac_adjustStyle(group.size, options.style); - var nstyle = style.fracNum(); - var dstyle = style.fracDen(); - var newOptions; - newOptions = options.havingStyle(nstyle); - var numerm = buildHTML_buildGroup(group.numer, newOptions, options); - - if (group.continued) { - // \cfrac inserts a \strut into the numerator. - // Get \strut dimensions from TeXbook page 353. - var hStrut = 8.5 / options.fontMetrics().ptPerEm; - var dStrut = 3.5 / options.fontMetrics().ptPerEm; - numerm.height = numerm.height < hStrut ? hStrut : numerm.height; - numerm.depth = numerm.depth < dStrut ? dStrut : numerm.depth; - } - - newOptions = options.havingStyle(dstyle); - var denomm = buildHTML_buildGroup(group.denom, newOptions, options); - var rule; - var ruleWidth; - var ruleSpacing; - - if (group.hasBarLine) { - if (group.barSize) { - ruleWidth = units_calculateSize(group.barSize, options); - rule = buildCommon.makeLineSpan("frac-line", options, ruleWidth); - } else { - rule = buildCommon.makeLineSpan("frac-line", options); - } - - ruleWidth = rule.height; - ruleSpacing = rule.height; - } else { - rule = null; - ruleWidth = 0; - ruleSpacing = options.fontMetrics().defaultRuleThickness; - } // Rule 15b - - - var numShift; - var clearance; - var denomShift; - - if (style.size === src_Style.DISPLAY.size || group.size === "display") { - numShift = options.fontMetrics().num1; - - if (ruleWidth > 0) { - clearance = 3 * ruleSpacing; - } else { - clearance = 7 * ruleSpacing; - } - - denomShift = options.fontMetrics().denom1; - } else { - if (ruleWidth > 0) { - numShift = options.fontMetrics().num2; - clearance = ruleSpacing; - } else { - numShift = options.fontMetrics().num3; - clearance = 3 * ruleSpacing; - } - - denomShift = options.fontMetrics().denom2; - } - - var frac; - - if (!rule) { - // Rule 15c - var candidateClearance = numShift - numerm.depth - (denomm.height - denomShift); - - if (candidateClearance < clearance) { - numShift += 0.5 * (clearance - candidateClearance); - denomShift += 0.5 * (clearance - candidateClearance); - } - - frac = buildCommon.makeVList({ - positionType: "individualShift", - children: [{ - type: "elem", - elem: denomm, - shift: denomShift - }, { - type: "elem", - elem: numerm, - shift: -numShift - }] - }, options); - } else { - // Rule 15d - var axisHeight = options.fontMetrics().axisHeight; - - if (numShift - numerm.depth - (axisHeight + 0.5 * ruleWidth) < clearance) { - numShift += clearance - (numShift - numerm.depth - (axisHeight + 0.5 * ruleWidth)); - } - - if (axisHeight - 0.5 * ruleWidth - (denomm.height - denomShift) < clearance) { - denomShift += clearance - (axisHeight - 0.5 * ruleWidth - (denomm.height - denomShift)); - } - - var midShift = -(axisHeight - 0.5 * ruleWidth); - frac = buildCommon.makeVList({ - positionType: "individualShift", - children: [{ - type: "elem", - elem: denomm, - shift: denomShift - }, { - type: "elem", - elem: rule, - shift: midShift - }, { - type: "elem", - elem: numerm, - shift: -numShift - }] - }, options); - } // Since we manually change the style sometimes (with \dfrac or \tfrac), - // account for the possible size change here. - - - newOptions = options.havingStyle(style); - frac.height *= newOptions.sizeMultiplier / options.sizeMultiplier; - frac.depth *= newOptions.sizeMultiplier / options.sizeMultiplier; // Rule 15e - - var delimSize; - - if (style.size === src_Style.DISPLAY.size) { - delimSize = options.fontMetrics().delim1; - } else { - delimSize = options.fontMetrics().delim2; - } - - var leftDelim; - var rightDelim; - - if (group.leftDelim == null) { - leftDelim = makeNullDelimiter(options, ["mopen"]); - } else { - leftDelim = delimiter.customSizedDelim(group.leftDelim, delimSize, true, options.havingStyle(style), group.mode, ["mopen"]); - } - - if (group.continued) { - rightDelim = buildCommon.makeSpan([]); // zero width for \cfrac - } else if (group.rightDelim == null) { - rightDelim = makeNullDelimiter(options, ["mclose"]); - } else { - rightDelim = delimiter.customSizedDelim(group.rightDelim, delimSize, true, options.havingStyle(style), group.mode, ["mclose"]); - } - - return buildCommon.makeSpan(["mord"].concat(newOptions.sizingClasses(options)), [leftDelim, buildCommon.makeSpan(["mfrac"], [frac]), rightDelim], options); -}; - -var genfrac_mathmlBuilder = function mathmlBuilder(group, options) { - var node = new mathMLTree.MathNode("mfrac", [buildMathML_buildGroup(group.numer, options), buildMathML_buildGroup(group.denom, options)]); - - if (!group.hasBarLine) { - node.setAttribute("linethickness", "0px"); - } else if (group.barSize) { - var ruleWidth = units_calculateSize(group.barSize, options); - node.setAttribute("linethickness", ruleWidth + "em"); - } - - var style = genfrac_adjustStyle(group.size, options.style); - - if (style.size !== options.style.size) { - node = new mathMLTree.MathNode("mstyle", [node]); - var isDisplay = style.size === src_Style.DISPLAY.size ? "true" : "false"; - node.setAttribute("displaystyle", isDisplay); - node.setAttribute("scriptlevel", "0"); - } - - if (group.leftDelim != null || group.rightDelim != null) { - var withDelims = []; - - if (group.leftDelim != null) { - var leftOp = new mathMLTree.MathNode("mo", [new mathMLTree.TextNode(group.leftDelim.replace("\\", ""))]); - leftOp.setAttribute("fence", "true"); - withDelims.push(leftOp); - } - - withDelims.push(node); - - if (group.rightDelim != null) { - var rightOp = new mathMLTree.MathNode("mo", [new mathMLTree.TextNode(group.rightDelim.replace("\\", ""))]); - rightOp.setAttribute("fence", "true"); - withDelims.push(rightOp); - } - - return buildMathML_makeRow(withDelims); - } - - return node; -}; - -defineFunction({ - type: "genfrac", - names: ["\\cfrac", "\\dfrac", "\\frac", "\\tfrac", "\\dbinom", "\\binom", "\\tbinom", "\\\\atopfrac", // can’t be entered directly - "\\\\bracefrac", "\\\\brackfrac"], - props: { - numArgs: 2, - greediness: 2 - }, - handler: function handler(_ref, args) { - var parser = _ref.parser, - funcName = _ref.funcName; - var numer = args[0]; - var denom = args[1]; - var hasBarLine; - var leftDelim = null; - var rightDelim = null; - var size = "auto"; - - switch (funcName) { - case "\\cfrac": - case "\\dfrac": - case "\\frac": - case "\\tfrac": - hasBarLine = true; - break; - - case "\\\\atopfrac": - hasBarLine = false; - break; - - case "\\dbinom": - case "\\binom": - case "\\tbinom": - hasBarLine = false; - leftDelim = "("; - rightDelim = ")"; - break; - - case "\\\\bracefrac": - hasBarLine = false; - leftDelim = "\\{"; - rightDelim = "\\}"; - break; - - case "\\\\brackfrac": - hasBarLine = false; - leftDelim = "["; - rightDelim = "]"; - break; - - default: - throw new Error("Unrecognized genfrac command"); - } - - switch (funcName) { - case "\\cfrac": - case "\\dfrac": - case "\\dbinom": - size = "display"; - break; - - case "\\tfrac": - case "\\tbinom": - size = "text"; - break; - } - - return { - type: "genfrac", - mode: parser.mode, - continued: funcName === "\\cfrac", - numer: numer, - denom: denom, - hasBarLine: hasBarLine, - leftDelim: leftDelim, - rightDelim: rightDelim, - size: size, - barSize: null - }; - }, - htmlBuilder: genfrac_htmlBuilder, - mathmlBuilder: genfrac_mathmlBuilder -}); // Infix generalized fractions -- these are not rendered directly, but replaced -// immediately by one of the variants above. - -defineFunction({ - type: "infix", - names: ["\\over", "\\choose", "\\atop", "\\brace", "\\brack"], - props: { - numArgs: 0, - infix: true - }, - handler: function handler(_ref2) { - var parser = _ref2.parser, - funcName = _ref2.funcName, - token = _ref2.token; - var replaceWith; - - switch (funcName) { - case "\\over": - replaceWith = "\\frac"; - break; - - case "\\choose": - replaceWith = "\\binom"; - break; - - case "\\atop": - replaceWith = "\\\\atopfrac"; - break; - - case "\\brace": - replaceWith = "\\\\bracefrac"; - break; - - case "\\brack": - replaceWith = "\\\\brackfrac"; - break; - - default: - throw new Error("Unrecognized infix genfrac command"); - } - - return { - type: "infix", - mode: parser.mode, - replaceWith: replaceWith, - token: token - }; - } -}); -var stylArray = ["display", "text", "script", "scriptscript"]; - -var delimFromValue = function delimFromValue(delimString) { - var delim = null; - - if (delimString.length > 0) { - delim = delimString; - delim = delim === "." ? null : delim; - } - - return delim; -}; - -defineFunction({ - type: "genfrac", - names: ["\\genfrac"], - props: { - numArgs: 6, - greediness: 6, - argTypes: ["math", "math", "size", "text", "math", "math"] - }, - handler: function handler(_ref3, args) { - var parser = _ref3.parser; - var numer = args[4]; - var denom = args[5]; // Look into the parse nodes to get the desired delimiters. - - var leftDelim = args[0].type === "atom" && args[0].family === "open" ? delimFromValue(args[0].text) : null; - var rightDelim = args[1].type === "atom" && args[1].family === "close" ? delimFromValue(args[1].text) : null; - var barNode = assertNodeType(args[2], "size"); - var hasBarLine; - var barSize = null; - - if (barNode.isBlank) { - // \genfrac acts differently than \above. - // \genfrac treats an empty size group as a signal to use a - // standard bar size. \above would see size = 0 and omit the bar. - hasBarLine = true; - } else { - barSize = barNode.value; - hasBarLine = barSize.number > 0; - } // Find out if we want displaystyle, textstyle, etc. - - - var size = "auto"; - var styl = args[3]; - - if (styl.type === "ordgroup") { - if (styl.body.length > 0) { - var textOrd = assertNodeType(styl.body[0], "textord"); - size = stylArray[Number(textOrd.text)]; - } - } else { - styl = assertNodeType(styl, "textord"); - size = stylArray[Number(styl.text)]; - } - - return { - type: "genfrac", - mode: parser.mode, - numer: numer, - denom: denom, - continued: false, - hasBarLine: hasBarLine, - barSize: barSize, - leftDelim: leftDelim, - rightDelim: rightDelim, - size: size - }; - }, - htmlBuilder: genfrac_htmlBuilder, - mathmlBuilder: genfrac_mathmlBuilder -}); // \above is an infix fraction that also defines a fraction bar size. - -defineFunction({ - type: "infix", - names: ["\\above"], - props: { - numArgs: 1, - argTypes: ["size"], - infix: true - }, - handler: function handler(_ref4, args) { - var parser = _ref4.parser, - funcName = _ref4.funcName, - token = _ref4.token; - return { - type: "infix", - mode: parser.mode, - replaceWith: "\\\\abovefrac", - size: assertNodeType(args[0], "size").value, - token: token - }; - } -}); -defineFunction({ - type: "genfrac", - names: ["\\\\abovefrac"], - props: { - numArgs: 3, - argTypes: ["math", "size", "math"] - }, - handler: function handler(_ref5, args) { - var parser = _ref5.parser, - funcName = _ref5.funcName; - var numer = args[0]; - var barSize = assert(assertNodeType(args[1], "infix").size); - var denom = args[2]; - var hasBarLine = barSize.number > 0; - return { - type: "genfrac", - mode: parser.mode, - numer: numer, - denom: denom, - continued: false, - hasBarLine: hasBarLine, - barSize: barSize, - leftDelim: null, - rightDelim: null, - size: "auto" - }; - }, - htmlBuilder: genfrac_htmlBuilder, - mathmlBuilder: genfrac_mathmlBuilder -}); -// CONCATENATED MODULE: ./src/functions/horizBrace.js - - - - - - - - -// NOTE: Unlike most `htmlBuilder`s, this one handles not only "horizBrace", but -var horizBrace_htmlBuilder = function htmlBuilder(grp, options) { - var style = options.style; // Pull out the `ParseNode<"horizBrace">` if `grp` is a "supsub" node. - - var supSubGroup; - var group; - - if (grp.type === "supsub") { - // Ref: LaTeX source2e: }}}}\limits} - // i.e. LaTeX treats the brace similar to an op and passes it - // with \limits, so we need to assign supsub style. - supSubGroup = grp.sup ? buildHTML_buildGroup(grp.sup, options.havingStyle(style.sup()), options) : buildHTML_buildGroup(grp.sub, options.havingStyle(style.sub()), options); - group = assertNodeType(grp.base, "horizBrace"); - } else { - group = assertNodeType(grp, "horizBrace"); - } // Build the base group - - - var body = buildHTML_buildGroup(group.base, options.havingBaseStyle(src_Style.DISPLAY)); // Create the stretchy element - - var braceBody = stretchy.svgSpan(group, options); // Generate the vlist, with the appropriate kerns ┏━━━━━━━━┓ - // This first vlist contains the content and the brace: equation - - var vlist; - - if (group.isOver) { - vlist = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: body - }, { - type: "kern", - size: 0.1 - }, { - type: "elem", - elem: braceBody - }] - }, options); // $FlowFixMe: Replace this with passing "svg-align" into makeVList. - - vlist.children[0].children[0].children[1].classes.push("svg-align"); - } else { - vlist = buildCommon.makeVList({ - positionType: "bottom", - positionData: body.depth + 0.1 + braceBody.height, - children: [{ - type: "elem", - elem: braceBody - }, { - type: "kern", - size: 0.1 - }, { - type: "elem", - elem: body - }] - }, options); // $FlowFixMe: Replace this with passing "svg-align" into makeVList. - - vlist.children[0].children[0].children[0].classes.push("svg-align"); - } - - if (supSubGroup) { - // To write the supsub, wrap the first vlist in another vlist: - // They can't all go in the same vlist, because the note might be - // wider than the equation. We want the equation to control the - // brace width. - // note long note long note - // ┏━━━━━━━━┓ or ┏━━━┓ not ┏━━━━━━━━━┓ - // equation eqn eqn - var vSpan = buildCommon.makeSpan(["mord", group.isOver ? "mover" : "munder"], [vlist], options); - - if (group.isOver) { - vlist = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: vSpan - }, { - type: "kern", - size: 0.2 - }, { - type: "elem", - elem: supSubGroup - }] - }, options); - } else { - vlist = buildCommon.makeVList({ - positionType: "bottom", - positionData: vSpan.depth + 0.2 + supSubGroup.height + supSubGroup.depth, - children: [{ - type: "elem", - elem: supSubGroup - }, { - type: "kern", - size: 0.2 - }, { - type: "elem", - elem: vSpan - }] - }, options); - } - } - - return buildCommon.makeSpan(["mord", group.isOver ? "mover" : "munder"], [vlist], options); -}; - -var horizBrace_mathmlBuilder = function mathmlBuilder(group, options) { - var accentNode = stretchy.mathMLnode(group.label); - return new mathMLTree.MathNode(group.isOver ? "mover" : "munder", [buildMathML_buildGroup(group.base, options), accentNode]); -}; // Horizontal stretchy braces - - -defineFunction({ - type: "horizBrace", - names: ["\\overbrace", "\\underbrace"], - props: { - numArgs: 1 - }, - handler: function handler(_ref, args) { - var parser = _ref.parser, - funcName = _ref.funcName; - return { - type: "horizBrace", - mode: parser.mode, - label: funcName, - isOver: /^\\over/.test(funcName), - base: args[0] - }; - }, - htmlBuilder: horizBrace_htmlBuilder, - mathmlBuilder: horizBrace_mathmlBuilder -}); -// CONCATENATED MODULE: ./src/functions/href.js - - - - - - -defineFunction({ - type: "href", - names: ["\\href"], - props: { - numArgs: 2, - argTypes: ["url", "original"], - allowedInText: true - }, - handler: function handler(_ref, args) { - var parser = _ref.parser; - var body = args[1]; - var href = assertNodeType(args[0], "url").url; - - if (!parser.settings.isTrusted({ - command: "\\href", - url: href - })) { - return parser.formatUnsupportedCmd("\\href"); - } - - return { - type: "href", - mode: parser.mode, - href: href, - body: ordargument(body) - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - var elements = buildHTML_buildExpression(group.body, options, false); - return buildCommon.makeAnchor(group.href, [], elements, options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var math = buildExpressionRow(group.body, options); - - if (!(math instanceof mathMLTree_MathNode)) { - math = new mathMLTree_MathNode("mrow", [math]); - } - - math.setAttribute("href", group.href); - return math; - } -}); -defineFunction({ - type: "href", - names: ["\\url"], - props: { - numArgs: 1, - argTypes: ["url"], - allowedInText: true - }, - handler: function handler(_ref2, args) { - var parser = _ref2.parser; - var href = assertNodeType(args[0], "url").url; - - if (!parser.settings.isTrusted({ - command: "\\url", - url: href - })) { - return parser.formatUnsupportedCmd("\\url"); - } - - var chars = []; - - for (var i = 0; i < href.length; i++) { - var c = href[i]; - - if (c === "~") { - c = "\\textasciitilde"; - } - - chars.push({ - type: "textord", - mode: "text", - text: c - }); - } - - var body = { - type: "text", - mode: parser.mode, - font: "\\texttt", - body: chars - }; - return { - type: "href", - mode: parser.mode, - href: href, - body: ordargument(body) - }; - } -}); -// CONCATENATED MODULE: ./src/functions/html.js - - - - - - -defineFunction({ - type: "html", - names: ["\\htmlClass", "\\htmlId", "\\htmlStyle", "\\htmlData"], - props: { - numArgs: 2, - argTypes: ["raw", "original"], - allowedInText: true - }, - handler: function handler(_ref, args) { - var parser = _ref.parser, - funcName = _ref.funcName, - token = _ref.token; - var value = assertNodeType(args[0], "raw").string; - var body = args[1]; - - if (parser.settings.strict) { - parser.settings.reportNonstrict("htmlExtension", "HTML extension is disabled on strict mode"); - } - - var trustContext; - var attributes = {}; - - switch (funcName) { - case "\\htmlClass": - attributes.class = value; - trustContext = { - command: "\\htmlClass", - class: value - }; - break; - - case "\\htmlId": - attributes.id = value; - trustContext = { - command: "\\htmlId", - id: value - }; - break; - - case "\\htmlStyle": - attributes.style = value; - trustContext = { - command: "\\htmlStyle", - style: value - }; - break; - - case "\\htmlData": - { - var data = value.split(","); - - for (var i = 0; i < data.length; i++) { - var keyVal = data[i].split("="); - - if (keyVal.length !== 2) { - throw new src_ParseError("Error parsing key-value for \\htmlData"); - } - - attributes["data-" + keyVal[0].trim()] = keyVal[1].trim(); - } - - trustContext = { - command: "\\htmlData", - attributes: attributes - }; - break; - } - - default: - throw new Error("Unrecognized html command"); - } - - if (!parser.settings.isTrusted(trustContext)) { - return parser.formatUnsupportedCmd(funcName); - } - - return { - type: "html", - mode: parser.mode, - attributes: attributes, - body: ordargument(body) - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - var elements = buildHTML_buildExpression(group.body, options, false); - var classes = ["enclosing"]; - - if (group.attributes.class) { - classes.push.apply(classes, group.attributes.class.trim().split(/\s+/)); - } - - var span = buildCommon.makeSpan(classes, elements, options); - - for (var attr in group.attributes) { - if (attr !== "class" && group.attributes.hasOwnProperty(attr)) { - span.setAttribute(attr, group.attributes[attr]); - } - } - - return span; - }, - mathmlBuilder: function mathmlBuilder(group, options) { - return buildExpressionRow(group.body, options); - } -}); -// CONCATENATED MODULE: ./src/functions/htmlmathml.js - - - - -defineFunction({ - type: "htmlmathml", - names: ["\\html@mathml"], - props: { - numArgs: 2, - allowedInText: true - }, - handler: function handler(_ref, args) { - var parser = _ref.parser; - return { - type: "htmlmathml", - mode: parser.mode, - html: ordargument(args[0]), - mathml: ordargument(args[1]) - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - var elements = buildHTML_buildExpression(group.html, options, false); - return buildCommon.makeFragment(elements); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - return buildExpressionRow(group.mathml, options); - } -}); -// CONCATENATED MODULE: ./src/functions/includegraphics.js - - - - - - - -var includegraphics_sizeData = function sizeData(str) { - if (/^[-+]? *(\d+(\.\d*)?|\.\d+)$/.test(str)) { - // str is a number with no unit specified. - // default unit is bp, per graphix package. - return { - number: +str, - unit: "bp" - }; - } else { - var match = /([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/.exec(str); - - if (!match) { - throw new src_ParseError("Invalid size: '" + str + "' in \\includegraphics"); - } - - var data = { - number: +(match[1] + match[2]), - // sign + magnitude, cast to number - unit: match[3] - }; - - if (!validUnit(data)) { - throw new src_ParseError("Invalid unit: '" + data.unit + "' in \\includegraphics."); - } - - return data; - } -}; - -defineFunction({ - type: "includegraphics", - names: ["\\includegraphics"], - props: { - numArgs: 1, - numOptionalArgs: 1, - argTypes: ["raw", "url"], - allowedInText: false - }, - handler: function handler(_ref, args, optArgs) { - var parser = _ref.parser; - var width = { - number: 0, - unit: "em" - }; - var height = { - number: 0.9, - unit: "em" - }; // sorta character sized. - - var totalheight = { - number: 0, - unit: "em" - }; - var alt = ""; - - if (optArgs[0]) { - var attributeStr = assertNodeType(optArgs[0], "raw").string; // Parser.js does not parse key/value pairs. We get a string. - - var attributes = attributeStr.split(","); - - for (var i = 0; i < attributes.length; i++) { - var keyVal = attributes[i].split("="); - - if (keyVal.length === 2) { - var str = keyVal[1].trim(); - - switch (keyVal[0].trim()) { - case "alt": - alt = str; - break; - - case "width": - width = includegraphics_sizeData(str); - break; - - case "height": - height = includegraphics_sizeData(str); - break; - - case "totalheight": - totalheight = includegraphics_sizeData(str); - break; - - default: - throw new src_ParseError("Invalid key: '" + keyVal[0] + "' in \\includegraphics."); - } - } - } - } - - var src = assertNodeType(args[0], "url").url; - - if (alt === "") { - // No alt given. Use the file name. Strip away the path. - alt = src; - alt = alt.replace(/^.*[\\/]/, ''); - alt = alt.substring(0, alt.lastIndexOf('.')); - } - - if (!parser.settings.isTrusted({ - command: "\\includegraphics", - url: src - })) { - return parser.formatUnsupportedCmd("\\includegraphics"); - } - - return { - type: "includegraphics", - mode: parser.mode, - alt: alt, - width: width, - height: height, - totalheight: totalheight, - src: src - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - var height = units_calculateSize(group.height, options); - var depth = 0; - - if (group.totalheight.number > 0) { - depth = units_calculateSize(group.totalheight, options) - height; - depth = Number(depth.toFixed(2)); - } - - var width = 0; - - if (group.width.number > 0) { - width = units_calculateSize(group.width, options); - } - - var style = { - height: height + depth + "em" - }; - - if (width > 0) { - style.width = width + "em"; - } - - if (depth > 0) { - style.verticalAlign = -depth + "em"; - } - - var node = new domTree_Img(group.src, group.alt, style); - node.height = height; - node.depth = depth; - return node; - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var node = new mathMLTree.MathNode("mglyph", []); - node.setAttribute("alt", group.alt); - var height = units_calculateSize(group.height, options); - var depth = 0; - - if (group.totalheight.number > 0) { - depth = units_calculateSize(group.totalheight, options) - height; - depth = depth.toFixed(2); - node.setAttribute("valign", "-" + depth + "em"); - } - - node.setAttribute("height", height + depth + "em"); - - if (group.width.number > 0) { - var width = units_calculateSize(group.width, options); - node.setAttribute("width", width + "em"); - } - - node.setAttribute("src", group.src); - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/kern.js -// Horizontal spacing commands - - - - - // TODO: \hskip and \mskip should support plus and minus in lengths - -defineFunction({ - type: "kern", - names: ["\\kern", "\\mkern", "\\hskip", "\\mskip"], - props: { - numArgs: 1, - argTypes: ["size"], - allowedInText: true - }, - handler: function handler(_ref, args) { - var parser = _ref.parser, - funcName = _ref.funcName; - var size = assertNodeType(args[0], "size"); - - if (parser.settings.strict) { - var mathFunction = funcName[1] === 'm'; // \mkern, \mskip - - var muUnit = size.value.unit === 'mu'; - - if (mathFunction) { - if (!muUnit) { - parser.settings.reportNonstrict("mathVsTextUnits", "LaTeX's " + funcName + " supports only mu units, " + ("not " + size.value.unit + " units")); - } - - if (parser.mode !== "math") { - parser.settings.reportNonstrict("mathVsTextUnits", "LaTeX's " + funcName + " works only in math mode"); - } - } else { - // !mathFunction - if (muUnit) { - parser.settings.reportNonstrict("mathVsTextUnits", "LaTeX's " + funcName + " doesn't support mu units"); - } - } - } - - return { - type: "kern", - mode: parser.mode, - dimension: size.value - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - return buildCommon.makeGlue(group.dimension, options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var dimension = units_calculateSize(group.dimension, options); - return new mathMLTree.SpaceNode(dimension); - } -}); -// CONCATENATED MODULE: ./src/functions/lap.js -// Horizontal overlap functions - - - - - -defineFunction({ - type: "lap", - names: ["\\mathllap", "\\mathrlap", "\\mathclap"], - props: { - numArgs: 1, - allowedInText: true - }, - handler: function handler(_ref, args) { - var parser = _ref.parser, - funcName = _ref.funcName; - var body = args[0]; - return { - type: "lap", - mode: parser.mode, - alignment: funcName.slice(5), - body: body - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - // mathllap, mathrlap, mathclap - var inner; - - if (group.alignment === "clap") { - // ref: https://www.math.lsu.edu/~aperlis/publications/mathclap/ - inner = buildCommon.makeSpan([], [buildHTML_buildGroup(group.body, options)]); // wrap, since CSS will center a .clap > .inner > span - - inner = buildCommon.makeSpan(["inner"], [inner], options); - } else { - inner = buildCommon.makeSpan(["inner"], [buildHTML_buildGroup(group.body, options)]); - } - - var fix = buildCommon.makeSpan(["fix"], []); - var node = buildCommon.makeSpan([group.alignment], [inner, fix], options); // At this point, we have correctly set horizontal alignment of the - // two items involved in the lap. - // Next, use a strut to set the height of the HTML bounding box. - // Otherwise, a tall argument may be misplaced. - // This code resolved issue #1153 - - var strut = buildCommon.makeSpan(["strut"]); - strut.style.height = node.height + node.depth + "em"; - strut.style.verticalAlign = -node.depth + "em"; - node.children.unshift(strut); // Next, prevent vertical misplacement when next to something tall. - // This code resolves issue #1234 - - node = buildCommon.makeSpan(["thinbox"], [node], options); - return buildCommon.makeSpan(["mord", "vbox"], [node], options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - // mathllap, mathrlap, mathclap - var node = new mathMLTree.MathNode("mpadded", [buildMathML_buildGroup(group.body, options)]); - - if (group.alignment !== "rlap") { - var offset = group.alignment === "llap" ? "-1" : "-0.5"; - node.setAttribute("lspace", offset + "width"); - } - - node.setAttribute("width", "0px"); - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/math.js - - // Switching from text mode back to math mode - -defineFunction({ - type: "styling", - names: ["\\(", "$"], - props: { - numArgs: 0, - allowedInText: true, - allowedInMath: false - }, - handler: function handler(_ref, args) { - var funcName = _ref.funcName, - parser = _ref.parser; - var outerMode = parser.mode; - parser.switchMode("math"); - var close = funcName === "\\(" ? "\\)" : "$"; - var body = parser.parseExpression(false, close); - parser.expect(close); - parser.switchMode(outerMode); - return { - type: "styling", - mode: parser.mode, - style: "text", - body: body - }; - } -}); // Check for extra closing math delimiters - -defineFunction({ - type: "text", - // Doesn't matter what this is. - names: ["\\)", "\\]"], - props: { - numArgs: 0, - allowedInText: true, - allowedInMath: false - }, - handler: function handler(context, args) { - throw new src_ParseError("Mismatched " + context.funcName); - } -}); -// CONCATENATED MODULE: ./src/functions/mathchoice.js - - - - - - -var mathchoice_chooseMathStyle = function chooseMathStyle(group, options) { - switch (options.style.size) { - case src_Style.DISPLAY.size: - return group.display; - - case src_Style.TEXT.size: - return group.text; - - case src_Style.SCRIPT.size: - return group.script; - - case src_Style.SCRIPTSCRIPT.size: - return group.scriptscript; - - default: - return group.text; - } -}; - -defineFunction({ - type: "mathchoice", - names: ["\\mathchoice"], - props: { - numArgs: 4 - }, - handler: function handler(_ref, args) { - var parser = _ref.parser; - return { - type: "mathchoice", - mode: parser.mode, - display: ordargument(args[0]), - text: ordargument(args[1]), - script: ordargument(args[2]), - scriptscript: ordargument(args[3]) - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - var body = mathchoice_chooseMathStyle(group, options); - var elements = buildHTML_buildExpression(body, options, false); - return buildCommon.makeFragment(elements); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var body = mathchoice_chooseMathStyle(group, options); - return buildExpressionRow(body, options); - } -}); -// CONCATENATED MODULE: ./src/functions/utils/assembleSupSub.js - - -// For an operator with limits, assemble the base, sup, and sub into a span. -var assembleSupSub_assembleSupSub = function assembleSupSub(base, supGroup, subGroup, options, style, slant, baseShift) { - base = buildCommon.makeSpan([], [base]); - var sub; - var sup; // We manually have to handle the superscripts and subscripts. This, - // aside from the kern calculations, is copied from supsub. - - if (supGroup) { - var elem = buildHTML_buildGroup(supGroup, options.havingStyle(style.sup()), options); - sup = { - elem: elem, - kern: Math.max(options.fontMetrics().bigOpSpacing1, options.fontMetrics().bigOpSpacing3 - elem.depth) - }; - } - - if (subGroup) { - var _elem = buildHTML_buildGroup(subGroup, options.havingStyle(style.sub()), options); - - sub = { - elem: _elem, - kern: Math.max(options.fontMetrics().bigOpSpacing2, options.fontMetrics().bigOpSpacing4 - _elem.height) - }; - } // Build the final group as a vlist of the possible subscript, base, - // and possible superscript. - - - var finalGroup; - - if (sup && sub) { - var bottom = options.fontMetrics().bigOpSpacing5 + sub.elem.height + sub.elem.depth + sub.kern + base.depth + baseShift; - finalGroup = buildCommon.makeVList({ - positionType: "bottom", - positionData: bottom, - children: [{ - type: "kern", - size: options.fontMetrics().bigOpSpacing5 - }, { - type: "elem", - elem: sub.elem, - marginLeft: -slant + "em" - }, { - type: "kern", - size: sub.kern - }, { - type: "elem", - elem: base - }, { - type: "kern", - size: sup.kern - }, { - type: "elem", - elem: sup.elem, - marginLeft: slant + "em" - }, { - type: "kern", - size: options.fontMetrics().bigOpSpacing5 - }] - }, options); - } else if (sub) { - var top = base.height - baseShift; // Shift the limits by the slant of the symbol. Note - // that we are supposed to shift the limits by 1/2 of the slant, - // but since we are centering the limits adding a full slant of - // margin will shift by 1/2 that. - - finalGroup = buildCommon.makeVList({ - positionType: "top", - positionData: top, - children: [{ - type: "kern", - size: options.fontMetrics().bigOpSpacing5 - }, { - type: "elem", - elem: sub.elem, - marginLeft: -slant + "em" - }, { - type: "kern", - size: sub.kern - }, { - type: "elem", - elem: base - }] - }, options); - } else if (sup) { - var _bottom = base.depth + baseShift; - - finalGroup = buildCommon.makeVList({ - positionType: "bottom", - positionData: _bottom, - children: [{ - type: "elem", - elem: base - }, { - type: "kern", - size: sup.kern - }, { - type: "elem", - elem: sup.elem, - marginLeft: slant + "em" - }, { - type: "kern", - size: options.fontMetrics().bigOpSpacing5 - }] - }, options); - } else { - // This case probably shouldn't occur (this would mean the - // supsub was sending us a group with no superscript or - // subscript) but be safe. - return base; - } - - return buildCommon.makeSpan(["mop", "op-limits"], [finalGroup], options); -}; -// CONCATENATED MODULE: ./src/functions/op.js -// Limits, symbols - - - - - - - - - - -// Most operators have a large successor symbol, but these don't. -var noSuccessor = ["\\smallint"]; // NOTE: Unlike most `htmlBuilder`s, this one handles not only "op", but also -// "supsub" since some of them (like \int) can affect super/subscripting. - -var op_htmlBuilder = function htmlBuilder(grp, options) { - // Operators are handled in the TeXbook pg. 443-444, rule 13(a). - var supGroup; - var subGroup; - var hasLimits = false; - var group; - - if (grp.type === "supsub") { - // If we have limits, supsub will pass us its group to handle. Pull - // out the superscript and subscript and set the group to the op in - // its base. - supGroup = grp.sup; - subGroup = grp.sub; - group = assertNodeType(grp.base, "op"); - hasLimits = true; - } else { - group = assertNodeType(grp, "op"); - } - - var style = options.style; - var large = false; - - if (style.size === src_Style.DISPLAY.size && group.symbol && !utils.contains(noSuccessor, group.name)) { - // Most symbol operators get larger in displaystyle (rule 13) - large = true; - } - - var base; - - if (group.symbol) { - // If this is a symbol, create the symbol. - var fontName = large ? "Size2-Regular" : "Size1-Regular"; - var stash = ""; - - if (group.name === "\\oiint" || group.name === "\\oiiint") { - // No font glyphs yet, so use a glyph w/o the oval. - // TODO: When font glyphs are available, delete this code. - stash = group.name.substr(1); // $FlowFixMe - - group.name = stash === "oiint" ? "\\iint" : "\\iiint"; - } - - base = buildCommon.makeSymbol(group.name, fontName, "math", options, ["mop", "op-symbol", large ? "large-op" : "small-op"]); - - if (stash.length > 0) { - // We're in \oiint or \oiiint. Overlay the oval. - // TODO: When font glyphs are available, delete this code. - var italic = base.italic; - var oval = buildCommon.staticSvg(stash + "Size" + (large ? "2" : "1"), options); - base = buildCommon.makeVList({ - positionType: "individualShift", - children: [{ - type: "elem", - elem: base, - shift: 0 - }, { - type: "elem", - elem: oval, - shift: large ? 0.08 : 0 - }] - }, options); // $FlowFixMe - - group.name = "\\" + stash; - base.classes.unshift("mop"); // $FlowFixMe - - base.italic = italic; - } - } else if (group.body) { - // If this is a list, compose that list. - var inner = buildHTML_buildExpression(group.body, options, true); - - if (inner.length === 1 && inner[0] instanceof domTree_SymbolNode) { - base = inner[0]; - base.classes[0] = "mop"; // replace old mclass - } else { - base = buildCommon.makeSpan(["mop"], buildCommon.tryCombineChars(inner), options); - } - } else { - // Otherwise, this is a text operator. Build the text from the - // operator's name. - // TODO(emily): Add a space in the middle of some of these - // operators, like \limsup - var output = []; - - for (var i = 1; i < group.name.length; i++) { - output.push(buildCommon.mathsym(group.name[i], group.mode, options)); - } - - base = buildCommon.makeSpan(["mop"], output, options); - } // If content of op is a single symbol, shift it vertically. - - - var baseShift = 0; - var slant = 0; - - if ((base instanceof domTree_SymbolNode || group.name === "\\oiint" || group.name === "\\oiiint") && !group.suppressBaseShift) { - // We suppress the shift of the base of \overset and \underset. Otherwise, - // shift the symbol so its center lies on the axis (rule 13). It - // appears that our fonts have the centers of the symbols already - // almost on the axis, so these numbers are very small. Note we - // don't actually apply this here, but instead it is used either in - // the vlist creation or separately when there are no limits. - baseShift = (base.height - base.depth) / 2 - options.fontMetrics().axisHeight; // The slant of the symbol is just its italic correction. - // $FlowFixMe - - slant = base.italic; - } - - if (hasLimits) { - return assembleSupSub_assembleSupSub(base, supGroup, subGroup, options, style, slant, baseShift); - } else { - if (baseShift) { - base.style.position = "relative"; - base.style.top = baseShift + "em"; - } - - return base; - } -}; - -var op_mathmlBuilder = function mathmlBuilder(group, options) { - var node; - - if (group.symbol) { - // This is a symbol. Just add the symbol. - node = new mathMLTree_MathNode("mo", [buildMathML_makeText(group.name, group.mode)]); - - if (utils.contains(noSuccessor, group.name)) { - node.setAttribute("largeop", "false"); - } - } else if (group.body) { - // This is an operator with children. Add them. - node = new mathMLTree_MathNode("mo", buildMathML_buildExpression(group.body, options)); - } else { - // This is a text operator. Add all of the characters from the - // operator's name. - node = new mathMLTree_MathNode("mi", [new mathMLTree_TextNode(group.name.slice(1))]); // Append an . - // ref: https://www.w3.org/TR/REC-MathML/chap3_2.html#sec3.2.4 - - var operator = new mathMLTree_MathNode("mo", [buildMathML_makeText("\u2061", "text")]); - - if (group.parentIsSupSub) { - node = new mathMLTree_MathNode("mo", [node, operator]); - } else { - node = newDocumentFragment([node, operator]); - } - } - - return node; -}; - -var singleCharBigOps = { - "\u220F": "\\prod", - "\u2210": "\\coprod", - "\u2211": "\\sum", - "\u22C0": "\\bigwedge", - "\u22C1": "\\bigvee", - "\u22C2": "\\bigcap", - "\u22C3": "\\bigcup", - "\u2A00": "\\bigodot", - "\u2A01": "\\bigoplus", - "\u2A02": "\\bigotimes", - "\u2A04": "\\biguplus", - "\u2A06": "\\bigsqcup" -}; -defineFunction({ - type: "op", - names: ["\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap", "\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes", "\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint", "\u220F", "\u2210", "\u2211", "\u22C0", "\u22C1", "\u22C2", "\u22C3", "\u2A00", "\u2A01", "\u2A02", "\u2A04", "\u2A06"], - props: { - numArgs: 0 - }, - handler: function handler(_ref, args) { - var parser = _ref.parser, - funcName = _ref.funcName; - var fName = funcName; - - if (fName.length === 1) { - fName = singleCharBigOps[fName]; - } - - return { - type: "op", - mode: parser.mode, - limits: true, - parentIsSupSub: false, - symbol: true, - name: fName - }; - }, - htmlBuilder: op_htmlBuilder, - mathmlBuilder: op_mathmlBuilder -}); // Note: calling defineFunction with a type that's already been defined only -// works because the same htmlBuilder and mathmlBuilder are being used. - -defineFunction({ - type: "op", - names: ["\\mathop"], - props: { - numArgs: 1 - }, - handler: function handler(_ref2, args) { - var parser = _ref2.parser; - var body = args[0]; - return { - type: "op", - mode: parser.mode, - limits: false, - parentIsSupSub: false, - symbol: false, - body: ordargument(body) - }; - }, - htmlBuilder: op_htmlBuilder, - mathmlBuilder: op_mathmlBuilder -}); // There are 2 flags for operators; whether they produce limits in -// displaystyle, and whether they are symbols and should grow in -// displaystyle. These four groups cover the four possible choices. - -var singleCharIntegrals = { - "\u222B": "\\int", - "\u222C": "\\iint", - "\u222D": "\\iiint", - "\u222E": "\\oint", - "\u222F": "\\oiint", - "\u2230": "\\oiiint" -}; // No limits, not symbols - -defineFunction({ - type: "op", - names: ["\\arcsin", "\\arccos", "\\arctan", "\\arctg", "\\arcctg", "\\arg", "\\ch", "\\cos", "\\cosec", "\\cosh", "\\cot", "\\cotg", "\\coth", "\\csc", "\\ctg", "\\cth", "\\deg", "\\dim", "\\exp", "\\hom", "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh", "\\sh", "\\tan", "\\tanh", "\\tg", "\\th"], - props: { - numArgs: 0 - }, - handler: function handler(_ref3) { - var parser = _ref3.parser, - funcName = _ref3.funcName; - return { - type: "op", - mode: parser.mode, - limits: false, - parentIsSupSub: false, - symbol: false, - name: funcName - }; - }, - htmlBuilder: op_htmlBuilder, - mathmlBuilder: op_mathmlBuilder -}); // Limits, not symbols - -defineFunction({ - type: "op", - names: ["\\det", "\\gcd", "\\inf", "\\lim", "\\max", "\\min", "\\Pr", "\\sup"], - props: { - numArgs: 0 - }, - handler: function handler(_ref4) { - var parser = _ref4.parser, - funcName = _ref4.funcName; - return { - type: "op", - mode: parser.mode, - limits: true, - parentIsSupSub: false, - symbol: false, - name: funcName - }; - }, - htmlBuilder: op_htmlBuilder, - mathmlBuilder: op_mathmlBuilder -}); // No limits, symbols - -defineFunction({ - type: "op", - names: ["\\int", "\\iint", "\\iiint", "\\oint", "\\oiint", "\\oiiint", "\u222B", "\u222C", "\u222D", "\u222E", "\u222F", "\u2230"], - props: { - numArgs: 0 - }, - handler: function handler(_ref5) { - var parser = _ref5.parser, - funcName = _ref5.funcName; - var fName = funcName; - - if (fName.length === 1) { - fName = singleCharIntegrals[fName]; - } - - return { - type: "op", - mode: parser.mode, - limits: false, - parentIsSupSub: false, - symbol: true, - name: fName - }; - }, - htmlBuilder: op_htmlBuilder, - mathmlBuilder: op_mathmlBuilder -}); -// CONCATENATED MODULE: ./src/functions/operatorname.js - - - - - - - - -// NOTE: Unlike most `htmlBuilder`s, this one handles not only -// "operatorname", but also "supsub" since \operatorname* can -var operatorname_htmlBuilder = function htmlBuilder(grp, options) { - // Operators are handled in the TeXbook pg. 443-444, rule 13(a). - var supGroup; - var subGroup; - var hasLimits = false; - var group; - - if (grp.type === "supsub") { - // If we have limits, supsub will pass us its group to handle. Pull - // out the superscript and subscript and set the group to the op in - // its base. - supGroup = grp.sup; - subGroup = grp.sub; - group = assertNodeType(grp.base, "operatorname"); - hasLimits = true; - } else { - group = assertNodeType(grp, "operatorname"); - } - - var base; - - if (group.body.length > 0) { - var body = group.body.map(function (child) { - // $FlowFixMe: Check if the node has a string `text` property. - var childText = child.text; - - if (typeof childText === "string") { - return { - type: "textord", - mode: child.mode, - text: childText - }; - } else { - return child; - } - }); // Consolidate function names into symbol characters. - - var expression = buildHTML_buildExpression(body, options.withFont("mathrm"), true); - - for (var i = 0; i < expression.length; i++) { - var child = expression[i]; - - if (child instanceof domTree_SymbolNode) { - // Per amsopn package, - // change minus to hyphen and \ast to asterisk - child.text = child.text.replace(/\u2212/, "-").replace(/\u2217/, "*"); - } - } - - base = buildCommon.makeSpan(["mop"], expression, options); - } else { - base = buildCommon.makeSpan(["mop"], [], options); - } - - if (hasLimits) { - return assembleSupSub_assembleSupSub(base, supGroup, subGroup, options, options.style, 0, 0); - } else { - return base; - } -}; - -var operatorname_mathmlBuilder = function mathmlBuilder(group, options) { - // The steps taken here are similar to the html version. - var expression = buildMathML_buildExpression(group.body, options.withFont("mathrm")); // Is expression a string or has it something like a fraction? - - var isAllString = true; // default - - for (var i = 0; i < expression.length; i++) { - var node = expression[i]; - - if (node instanceof mathMLTree.SpaceNode) {// Do nothing - } else if (node instanceof mathMLTree.MathNode) { - switch (node.type) { - case "mi": - case "mn": - case "ms": - case "mspace": - case "mtext": - break; - // Do nothing yet. - - case "mo": - { - var child = node.children[0]; - - if (node.children.length === 1 && child instanceof mathMLTree.TextNode) { - child.text = child.text.replace(/\u2212/, "-").replace(/\u2217/, "*"); - } else { - isAllString = false; - } - - break; - } - - default: - isAllString = false; - } - } else { - isAllString = false; - } - } - - if (isAllString) { - // Write a single TextNode instead of multiple nested tags. - var word = expression.map(function (node) { - return node.toText(); - }).join(""); - expression = [new mathMLTree.TextNode(word)]; - } - - var identifier = new mathMLTree.MathNode("mi", expression); - identifier.setAttribute("mathvariant", "normal"); // \u2061 is the same as ⁡ - // ref: https://www.w3schools.com/charsets/ref_html_entities_a.asp - - var operator = new mathMLTree.MathNode("mo", [buildMathML_makeText("\u2061", "text")]); - - if (group.parentIsSupSub) { - return new mathMLTree.MathNode("mo", [identifier, operator]); - } else { - return mathMLTree.newDocumentFragment([identifier, operator]); - } -}; // \operatorname -// amsopn.dtx: \mathop{#1\kern\z@\operator@font#3}\newmcodes@ - - -defineFunction({ - type: "operatorname", - names: ["\\operatorname", "\\operatorname*"], - props: { - numArgs: 1 - }, - handler: function handler(_ref, args) { - var parser = _ref.parser, - funcName = _ref.funcName; - var body = args[0]; - return { - type: "operatorname", - mode: parser.mode, - body: ordargument(body), - alwaysHandleSupSub: funcName === "\\operatorname*", - limits: false, - parentIsSupSub: false - }; - }, - htmlBuilder: operatorname_htmlBuilder, - mathmlBuilder: operatorname_mathmlBuilder -}); -// CONCATENATED MODULE: ./src/functions/ordgroup.js - - - - -defineFunctionBuilders({ - type: "ordgroup", - htmlBuilder: function htmlBuilder(group, options) { - if (group.semisimple) { - return buildCommon.makeFragment(buildHTML_buildExpression(group.body, options, false)); - } - - return buildCommon.makeSpan(["mord"], buildHTML_buildExpression(group.body, options, true), options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - return buildExpressionRow(group.body, options, true); - } -}); -// CONCATENATED MODULE: ./src/functions/overline.js - - - - - -defineFunction({ - type: "overline", - names: ["\\overline"], - props: { - numArgs: 1 - }, - handler: function handler(_ref, args) { - var parser = _ref.parser; - var body = args[0]; - return { - type: "overline", - mode: parser.mode, - body: body - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - // Overlines are handled in the TeXbook pg 443, Rule 9. - // Build the inner group in the cramped style. - var innerGroup = buildHTML_buildGroup(group.body, options.havingCrampedStyle()); // Create the line above the body - - var line = buildCommon.makeLineSpan("overline-line", options); // Generate the vlist, with the appropriate kerns - - var defaultRuleThickness = options.fontMetrics().defaultRuleThickness; - var vlist = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: innerGroup - }, { - type: "kern", - size: 3 * defaultRuleThickness - }, { - type: "elem", - elem: line - }, { - type: "kern", - size: defaultRuleThickness - }] - }, options); - return buildCommon.makeSpan(["mord", "overline"], [vlist], options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var operator = new mathMLTree.MathNode("mo", [new mathMLTree.TextNode("\u203E")]); - operator.setAttribute("stretchy", "true"); - var node = new mathMLTree.MathNode("mover", [buildMathML_buildGroup(group.body, options), operator]); - node.setAttribute("accent", "true"); - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/phantom.js - - - - - -defineFunction({ - type: "phantom", - names: ["\\phantom"], - props: { - numArgs: 1, - allowedInText: true - }, - handler: function handler(_ref, args) { - var parser = _ref.parser; - var body = args[0]; - return { - type: "phantom", - mode: parser.mode, - body: ordargument(body) - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - var elements = buildHTML_buildExpression(group.body, options.withPhantom(), false); // \phantom isn't supposed to affect the elements it contains. - // See "color" for more details. - - return buildCommon.makeFragment(elements); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var inner = buildMathML_buildExpression(group.body, options); - return new mathMLTree.MathNode("mphantom", inner); - } -}); -defineFunction({ - type: "hphantom", - names: ["\\hphantom"], - props: { - numArgs: 1, - allowedInText: true - }, - handler: function handler(_ref2, args) { - var parser = _ref2.parser; - var body = args[0]; - return { - type: "hphantom", - mode: parser.mode, - body: body - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - var node = buildCommon.makeSpan([], [buildHTML_buildGroup(group.body, options.withPhantom())]); - node.height = 0; - node.depth = 0; - - if (node.children) { - for (var i = 0; i < node.children.length; i++) { - node.children[i].height = 0; - node.children[i].depth = 0; - } - } // See smash for comment re: use of makeVList - - - node = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: node - }] - }, options); // For spacing, TeX treats \smash as a math group (same spacing as ord). - - return buildCommon.makeSpan(["mord"], [node], options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var inner = buildMathML_buildExpression(ordargument(group.body), options); - var phantom = new mathMLTree.MathNode("mphantom", inner); - var node = new mathMLTree.MathNode("mpadded", [phantom]); - node.setAttribute("height", "0px"); - node.setAttribute("depth", "0px"); - return node; - } -}); -defineFunction({ - type: "vphantom", - names: ["\\vphantom"], - props: { - numArgs: 1, - allowedInText: true - }, - handler: function handler(_ref3, args) { - var parser = _ref3.parser; - var body = args[0]; - return { - type: "vphantom", - mode: parser.mode, - body: body - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - var inner = buildCommon.makeSpan(["inner"], [buildHTML_buildGroup(group.body, options.withPhantom())]); - var fix = buildCommon.makeSpan(["fix"], []); - return buildCommon.makeSpan(["mord", "rlap"], [inner, fix], options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var inner = buildMathML_buildExpression(ordargument(group.body), options); - var phantom = new mathMLTree.MathNode("mphantom", inner); - var node = new mathMLTree.MathNode("mpadded", [phantom]); - node.setAttribute("width", "0px"); - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/raisebox.js - - - - - - - // Box manipulation - -defineFunction({ - type: "raisebox", - names: ["\\raisebox"], - props: { - numArgs: 2, - argTypes: ["size", "hbox"], - allowedInText: true - }, - handler: function handler(_ref, args) { - var parser = _ref.parser; - var amount = assertNodeType(args[0], "size").value; - var body = args[1]; - return { - type: "raisebox", - mode: parser.mode, - dy: amount, - body: body - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - var body = buildHTML_buildGroup(group.body, options); - var dy = units_calculateSize(group.dy, options); - return buildCommon.makeVList({ - positionType: "shift", - positionData: -dy, - children: [{ - type: "elem", - elem: body - }] - }, options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var node = new mathMLTree.MathNode("mpadded", [buildMathML_buildGroup(group.body, options)]); - var dy = group.dy.number + group.dy.unit; - node.setAttribute("voffset", dy); - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/rule.js - - - - - -defineFunction({ - type: "rule", - names: ["\\rule"], - props: { - numArgs: 2, - numOptionalArgs: 1, - argTypes: ["size", "size", "size"] - }, - handler: function handler(_ref, args, optArgs) { - var parser = _ref.parser; - var shift = optArgs[0]; - var width = assertNodeType(args[0], "size"); - var height = assertNodeType(args[1], "size"); - return { - type: "rule", - mode: parser.mode, - shift: shift && assertNodeType(shift, "size").value, - width: width.value, - height: height.value - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - // Make an empty span for the rule - var rule = buildCommon.makeSpan(["mord", "rule"], [], options); // Calculate the shift, width, and height of the rule, and account for units - - var width = units_calculateSize(group.width, options); - var height = units_calculateSize(group.height, options); - var shift = group.shift ? units_calculateSize(group.shift, options) : 0; // Style the rule to the right size - - rule.style.borderRightWidth = width + "em"; - rule.style.borderTopWidth = height + "em"; - rule.style.bottom = shift + "em"; // Record the height and width - - rule.width = width; - rule.height = height + shift; - rule.depth = -shift; // Font size is the number large enough that the browser will - // reserve at least `absHeight` space above the baseline. - // The 1.125 factor was empirically determined - - rule.maxFontSize = height * 1.125 * options.sizeMultiplier; - return rule; - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var width = units_calculateSize(group.width, options); - var height = units_calculateSize(group.height, options); - var shift = group.shift ? units_calculateSize(group.shift, options) : 0; - var color = options.color && options.getColor() || "black"; - var rule = new mathMLTree.MathNode("mspace"); - rule.setAttribute("mathbackground", color); - rule.setAttribute("width", width + "em"); - rule.setAttribute("height", height + "em"); - var wrapper = new mathMLTree.MathNode("mpadded", [rule]); - - if (shift >= 0) { - wrapper.setAttribute("height", "+" + shift + "em"); - } else { - wrapper.setAttribute("height", shift + "em"); - wrapper.setAttribute("depth", "+" + -shift + "em"); - } - - wrapper.setAttribute("voffset", shift + "em"); - return wrapper; - } -}); -// CONCATENATED MODULE: ./src/functions/sizing.js - - - - - -function sizingGroup(value, options, baseOptions) { - var inner = buildHTML_buildExpression(value, options, false); - var multiplier = options.sizeMultiplier / baseOptions.sizeMultiplier; // Add size-resetting classes to the inner list and set maxFontSize - // manually. Handle nested size changes. - - for (var i = 0; i < inner.length; i++) { - var pos = inner[i].classes.indexOf("sizing"); - - if (pos < 0) { - Array.prototype.push.apply(inner[i].classes, options.sizingClasses(baseOptions)); - } else if (inner[i].classes[pos + 1] === "reset-size" + options.size) { - // This is a nested size change: e.g., inner[i] is the "b" in - // `\Huge a \small b`. Override the old size (the `reset-` class) - // but not the new size. - inner[i].classes[pos + 1] = "reset-size" + baseOptions.size; - } - - inner[i].height *= multiplier; - inner[i].depth *= multiplier; - } - - return buildCommon.makeFragment(inner); -} -var sizeFuncs = ["\\tiny", "\\sixptsize", "\\scriptsize", "\\footnotesize", "\\small", "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge"]; -var sizing_htmlBuilder = function htmlBuilder(group, options) { - // Handle sizing operators like \Huge. Real TeX doesn't actually allow - // these functions inside of math expressions, so we do some special - // handling. - var newOptions = options.havingSize(group.size); - return sizingGroup(group.body, newOptions, options); -}; -defineFunction({ - type: "sizing", - names: sizeFuncs, - props: { - numArgs: 0, - allowedInText: true - }, - handler: function handler(_ref, args) { - var breakOnTokenText = _ref.breakOnTokenText, - funcName = _ref.funcName, - parser = _ref.parser; - var body = parser.parseExpression(false, breakOnTokenText); - return { - type: "sizing", - mode: parser.mode, - // Figure out what size to use based on the list of functions above - size: sizeFuncs.indexOf(funcName) + 1, - body: body - }; - }, - htmlBuilder: sizing_htmlBuilder, - mathmlBuilder: function mathmlBuilder(group, options) { - var newOptions = options.havingSize(group.size); - var inner = buildMathML_buildExpression(group.body, newOptions); - var node = new mathMLTree.MathNode("mstyle", inner); // TODO(emily): This doesn't produce the correct size for nested size - // changes, because we don't keep state of what style we're currently - // in, so we can't reset the size to normal before changing it. Now - // that we're passing an options parameter we should be able to fix - // this. - - node.setAttribute("mathsize", newOptions.sizeMultiplier + "em"); - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/smash.js -// smash, with optional [tb], as in AMS - - - - - - -defineFunction({ - type: "smash", - names: ["\\smash"], - props: { - numArgs: 1, - numOptionalArgs: 1, - allowedInText: true - }, - handler: function handler(_ref, args, optArgs) { - var parser = _ref.parser; - var smashHeight = false; - var smashDepth = false; - var tbArg = optArgs[0] && assertNodeType(optArgs[0], "ordgroup"); - - if (tbArg) { - // Optional [tb] argument is engaged. - // ref: amsmath: \renewcommand{\smash}[1][tb]{% - // def\mb@t{\ht}\def\mb@b{\dp}\def\mb@tb{\ht\z@\z@\dp}% - var letter = ""; - - for (var i = 0; i < tbArg.body.length; ++i) { - var node = tbArg.body[i]; // $FlowFixMe: Not every node type has a `text` property. - - letter = node.text; - - if (letter === "t") { - smashHeight = true; - } else if (letter === "b") { - smashDepth = true; - } else { - smashHeight = false; - smashDepth = false; - break; - } - } - } else { - smashHeight = true; - smashDepth = true; - } - - var body = args[0]; - return { - type: "smash", - mode: parser.mode, - body: body, - smashHeight: smashHeight, - smashDepth: smashDepth - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - var node = buildCommon.makeSpan([], [buildHTML_buildGroup(group.body, options)]); - - if (!group.smashHeight && !group.smashDepth) { - return node; - } - - if (group.smashHeight) { - node.height = 0; // In order to influence makeVList, we have to reset the children. - - if (node.children) { - for (var i = 0; i < node.children.length; i++) { - node.children[i].height = 0; - } - } - } - - if (group.smashDepth) { - node.depth = 0; - - if (node.children) { - for (var _i = 0; _i < node.children.length; _i++) { - node.children[_i].depth = 0; - } - } - } // At this point, we've reset the TeX-like height and depth values. - // But the span still has an HTML line height. - // makeVList applies "display: table-cell", which prevents the browser - // from acting on that line height. So we'll call makeVList now. - - - var smashedNode = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: node - }] - }, options); // For spacing, TeX treats \hphantom as a math group (same spacing as ord). - - return buildCommon.makeSpan(["mord"], [smashedNode], options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var node = new mathMLTree.MathNode("mpadded", [buildMathML_buildGroup(group.body, options)]); - - if (group.smashHeight) { - node.setAttribute("height", "0px"); - } - - if (group.smashDepth) { - node.setAttribute("depth", "0px"); - } - - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/sqrt.js - - - - - - - -defineFunction({ - type: "sqrt", - names: ["\\sqrt"], - props: { - numArgs: 1, - numOptionalArgs: 1 - }, - handler: function handler(_ref, args, optArgs) { - var parser = _ref.parser; - var index = optArgs[0]; - var body = args[0]; - return { - type: "sqrt", - mode: parser.mode, - body: body, - index: index - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - // Square roots are handled in the TeXbook pg. 443, Rule 11. - // First, we do the same steps as in overline to build the inner group - // and line - var inner = buildHTML_buildGroup(group.body, options.havingCrampedStyle()); - - if (inner.height === 0) { - // Render a small surd. - inner.height = options.fontMetrics().xHeight; - } // Some groups can return document fragments. Handle those by wrapping - // them in a span. - - - inner = buildCommon.wrapFragment(inner, options); // Calculate the minimum size for the \surd delimiter - - var metrics = options.fontMetrics(); - var theta = metrics.defaultRuleThickness; - var phi = theta; - - if (options.style.id < src_Style.TEXT.id) { - phi = options.fontMetrics().xHeight; - } // Calculate the clearance between the body and line - - - var lineClearance = theta + phi / 4; - var minDelimiterHeight = inner.height + inner.depth + lineClearance + theta; // Create a sqrt SVG of the required minimum size - - var _delimiter$sqrtImage = delimiter.sqrtImage(minDelimiterHeight, options), - img = _delimiter$sqrtImage.span, - ruleWidth = _delimiter$sqrtImage.ruleWidth, - advanceWidth = _delimiter$sqrtImage.advanceWidth; - - var delimDepth = img.height - ruleWidth; // Adjust the clearance based on the delimiter size - - if (delimDepth > inner.height + inner.depth + lineClearance) { - lineClearance = (lineClearance + delimDepth - inner.height - inner.depth) / 2; - } // Shift the sqrt image - - - var imgShift = img.height - inner.height - lineClearance - ruleWidth; - inner.style.paddingLeft = advanceWidth + "em"; // Overlay the image and the argument. - - var body = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: inner, - wrapperClasses: ["svg-align"] - }, { - type: "kern", - size: -(inner.height + imgShift) - }, { - type: "elem", - elem: img - }, { - type: "kern", - size: ruleWidth - }] - }, options); - - if (!group.index) { - return buildCommon.makeSpan(["mord", "sqrt"], [body], options); - } else { - // Handle the optional root index - // The index is always in scriptscript style - var newOptions = options.havingStyle(src_Style.SCRIPTSCRIPT); - var rootm = buildHTML_buildGroup(group.index, newOptions, options); // The amount the index is shifted by. This is taken from the TeX - // source, in the definition of `\r@@t`. - - var toShift = 0.6 * (body.height - body.depth); // Build a VList with the superscript shifted up correctly - - var rootVList = buildCommon.makeVList({ - positionType: "shift", - positionData: -toShift, - children: [{ - type: "elem", - elem: rootm - }] - }, options); // Add a class surrounding it so we can add on the appropriate - // kerning - - var rootVListWrap = buildCommon.makeSpan(["root"], [rootVList]); - return buildCommon.makeSpan(["mord", "sqrt"], [rootVListWrap, body], options); - } - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var body = group.body, - index = group.index; - return index ? new mathMLTree.MathNode("mroot", [buildMathML_buildGroup(body, options), buildMathML_buildGroup(index, options)]) : new mathMLTree.MathNode("msqrt", [buildMathML_buildGroup(body, options)]); - } -}); -// CONCATENATED MODULE: ./src/functions/styling.js - - - - - -var styling_styleMap = { - "display": src_Style.DISPLAY, - "text": src_Style.TEXT, - "script": src_Style.SCRIPT, - "scriptscript": src_Style.SCRIPTSCRIPT -}; -defineFunction({ - type: "styling", - names: ["\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle"], - props: { - numArgs: 0, - allowedInText: true - }, - handler: function handler(_ref, args) { - var breakOnTokenText = _ref.breakOnTokenText, - funcName = _ref.funcName, - parser = _ref.parser; - // parse out the implicit body - var body = parser.parseExpression(true, breakOnTokenText); // TODO: Refactor to avoid duplicating styleMap in multiple places (e.g. - // here and in buildHTML and de-dupe the enumeration of all the styles). - // $FlowFixMe: The names above exactly match the styles. - - var style = funcName.slice(1, funcName.length - 5); - return { - type: "styling", - mode: parser.mode, - // Figure out what style to use by pulling out the style from - // the function name - style: style, - body: body - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - // Style changes are handled in the TeXbook on pg. 442, Rule 3. - var newStyle = styling_styleMap[group.style]; - var newOptions = options.havingStyle(newStyle).withFont(''); - return sizingGroup(group.body, newOptions, options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - // Figure out what style we're changing to. - var newStyle = styling_styleMap[group.style]; - var newOptions = options.havingStyle(newStyle); - var inner = buildMathML_buildExpression(group.body, newOptions); - var node = new mathMLTree.MathNode("mstyle", inner); - var styleAttributes = { - "display": ["0", "true"], - "text": ["0", "false"], - "script": ["1", "false"], - "scriptscript": ["2", "false"] - }; - var attr = styleAttributes[group.style]; - node.setAttribute("scriptlevel", attr[0]); - node.setAttribute("displaystyle", attr[1]); - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/supsub.js - - - - - - - - - - - - - -/** - * Sometimes, groups perform special rules when they have superscripts or - * subscripts attached to them. This function lets the `supsub` group know that - * Sometimes, groups perform special rules when they have superscripts or - * its inner element should handle the superscripts and subscripts instead of - * handling them itself. - */ -var supsub_htmlBuilderDelegate = function htmlBuilderDelegate(group, options) { - var base = group.base; - - if (!base) { - return null; - } else if (base.type === "op") { - // Operators handle supsubs differently when they have limits - // (e.g. `\displaystyle\sum_2^3`) - var delegate = base.limits && (options.style.size === src_Style.DISPLAY.size || base.alwaysHandleSupSub); - return delegate ? op_htmlBuilder : null; - } else if (base.type === "operatorname") { - var _delegate = base.alwaysHandleSupSub && (options.style.size === src_Style.DISPLAY.size || base.limits); - - return _delegate ? operatorname_htmlBuilder : null; - } else if (base.type === "accent") { - return utils.isCharacterBox(base.base) ? accent_htmlBuilder : null; - } else if (base.type === "horizBrace") { - var isSup = !group.sub; - return isSup === base.isOver ? horizBrace_htmlBuilder : null; - } else { - return null; - } -}; // Super scripts and subscripts, whose precise placement can depend on other -// functions that precede them. - - -defineFunctionBuilders({ - type: "supsub", - htmlBuilder: function htmlBuilder(group, options) { - // Superscript and subscripts are handled in the TeXbook on page - // 445-446, rules 18(a-f). - // Here is where we defer to the inner group if it should handle - // superscripts and subscripts itself. - var builderDelegate = supsub_htmlBuilderDelegate(group, options); - - if (builderDelegate) { - return builderDelegate(group, options); - } - - var valueBase = group.base, - valueSup = group.sup, - valueSub = group.sub; - var base = buildHTML_buildGroup(valueBase, options); - var supm; - var subm; - var metrics = options.fontMetrics(); // Rule 18a - - var supShift = 0; - var subShift = 0; - var isCharacterBox = valueBase && utils.isCharacterBox(valueBase); - - if (valueSup) { - var newOptions = options.havingStyle(options.style.sup()); - supm = buildHTML_buildGroup(valueSup, newOptions, options); - - if (!isCharacterBox) { - supShift = base.height - newOptions.fontMetrics().supDrop * newOptions.sizeMultiplier / options.sizeMultiplier; - } - } - - if (valueSub) { - var _newOptions = options.havingStyle(options.style.sub()); - - subm = buildHTML_buildGroup(valueSub, _newOptions, options); - - if (!isCharacterBox) { - subShift = base.depth + _newOptions.fontMetrics().subDrop * _newOptions.sizeMultiplier / options.sizeMultiplier; - } - } // Rule 18c - - - var minSupShift; - - if (options.style === src_Style.DISPLAY) { - minSupShift = metrics.sup1; - } else if (options.style.cramped) { - minSupShift = metrics.sup3; - } else { - minSupShift = metrics.sup2; - } // scriptspace is a font-size-independent size, so scale it - // appropriately for use as the marginRight. - - - var multiplier = options.sizeMultiplier; - var marginRight = 0.5 / metrics.ptPerEm / multiplier + "em"; - var marginLeft = null; - - if (subm) { - // Subscripts shouldn't be shifted by the base's italic correction. - // Account for that by shifting the subscript back the appropriate - // amount. Note we only do this when the base is a single symbol. - var isOiint = group.base && group.base.type === "op" && group.base.name && (group.base.name === "\\oiint" || group.base.name === "\\oiiint"); - - if (base instanceof domTree_SymbolNode || isOiint) { - // $FlowFixMe - marginLeft = -base.italic + "em"; - } - } - - var supsub; - - if (supm && subm) { - supShift = Math.max(supShift, minSupShift, supm.depth + 0.25 * metrics.xHeight); - subShift = Math.max(subShift, metrics.sub2); - var ruleWidth = metrics.defaultRuleThickness; // Rule 18e - - var maxWidth = 4 * ruleWidth; - - if (supShift - supm.depth - (subm.height - subShift) < maxWidth) { - subShift = maxWidth - (supShift - supm.depth) + subm.height; - var psi = 0.8 * metrics.xHeight - (supShift - supm.depth); - - if (psi > 0) { - supShift += psi; - subShift -= psi; - } - } - - var vlistElem = [{ - type: "elem", - elem: subm, - shift: subShift, - marginRight: marginRight, - marginLeft: marginLeft - }, { - type: "elem", - elem: supm, - shift: -supShift, - marginRight: marginRight - }]; - supsub = buildCommon.makeVList({ - positionType: "individualShift", - children: vlistElem - }, options); - } else if (subm) { - // Rule 18b - subShift = Math.max(subShift, metrics.sub1, subm.height - 0.8 * metrics.xHeight); - var _vlistElem = [{ - type: "elem", - elem: subm, - marginLeft: marginLeft, - marginRight: marginRight - }]; - supsub = buildCommon.makeVList({ - positionType: "shift", - positionData: subShift, - children: _vlistElem - }, options); - } else if (supm) { - // Rule 18c, d - supShift = Math.max(supShift, minSupShift, supm.depth + 0.25 * metrics.xHeight); - supsub = buildCommon.makeVList({ - positionType: "shift", - positionData: -supShift, - children: [{ - type: "elem", - elem: supm, - marginRight: marginRight - }] - }, options); - } else { - throw new Error("supsub must have either sup or sub."); - } // Wrap the supsub vlist in a span.msupsub to reset text-align. - - - var mclass = getTypeOfDomTree(base, "right") || "mord"; - return buildCommon.makeSpan([mclass], [base, buildCommon.makeSpan(["msupsub"], [supsub])], options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - // Is the inner group a relevant horizonal brace? - var isBrace = false; - var isOver; - var isSup; - - if (group.base && group.base.type === "horizBrace") { - isSup = !!group.sup; - - if (isSup === group.base.isOver) { - isBrace = true; - isOver = group.base.isOver; - } - } - - if (group.base && (group.base.type === "op" || group.base.type === "operatorname")) { - group.base.parentIsSupSub = true; - } - - var children = [buildMathML_buildGroup(group.base, options)]; - - if (group.sub) { - children.push(buildMathML_buildGroup(group.sub, options)); - } - - if (group.sup) { - children.push(buildMathML_buildGroup(group.sup, options)); - } - - var nodeType; - - if (isBrace) { - nodeType = isOver ? "mover" : "munder"; - } else if (!group.sub) { - var base = group.base; - - if (base && base.type === "op" && base.limits && (options.style === src_Style.DISPLAY || base.alwaysHandleSupSub)) { - nodeType = "mover"; - } else if (base && base.type === "operatorname" && base.alwaysHandleSupSub && (base.limits || options.style === src_Style.DISPLAY)) { - nodeType = "mover"; - } else { - nodeType = "msup"; - } - } else if (!group.sup) { - var _base = group.base; - - if (_base && _base.type === "op" && _base.limits && (options.style === src_Style.DISPLAY || _base.alwaysHandleSupSub)) { - nodeType = "munder"; - } else if (_base && _base.type === "operatorname" && _base.alwaysHandleSupSub && (_base.limits || options.style === src_Style.DISPLAY)) { - nodeType = "munder"; - } else { - nodeType = "msub"; - } - } else { - var _base2 = group.base; - - if (_base2 && _base2.type === "op" && _base2.limits && options.style === src_Style.DISPLAY) { - nodeType = "munderover"; - } else if (_base2 && _base2.type === "operatorname" && _base2.alwaysHandleSupSub && (options.style === src_Style.DISPLAY || _base2.limits)) { - nodeType = "munderover"; - } else { - nodeType = "msubsup"; - } - } - - var node = new mathMLTree.MathNode(nodeType, children); - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/symbolsOp.js - - - - // Operator ParseNodes created in Parser.js from symbol Groups in src/symbols.js. - -defineFunctionBuilders({ - type: "atom", - htmlBuilder: function htmlBuilder(group, options) { - return buildCommon.mathsym(group.text, group.mode, options, ["m" + group.family]); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var node = new mathMLTree.MathNode("mo", [buildMathML_makeText(group.text, group.mode)]); - - if (group.family === "bin") { - var variant = buildMathML_getVariant(group, options); - - if (variant === "bold-italic") { - node.setAttribute("mathvariant", variant); - } - } else if (group.family === "punct") { - node.setAttribute("separator", "true"); - } else if (group.family === "open" || group.family === "close") { - // Delims built here should not stretch vertically. - // See delimsizing.js for stretchy delims. - node.setAttribute("stretchy", "false"); - } - - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/symbolsOrd.js - - - - -// "mathord" and "textord" ParseNodes created in Parser.js from symbol Groups in -var defaultVariant = { - "mi": "italic", - "mn": "normal", - "mtext": "normal" -}; -defineFunctionBuilders({ - type: "mathord", - htmlBuilder: function htmlBuilder(group, options) { - return buildCommon.makeOrd(group, options, "mathord"); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var node = new mathMLTree.MathNode("mi", [buildMathML_makeText(group.text, group.mode, options)]); - var variant = buildMathML_getVariant(group, options) || "italic"; - - if (variant !== defaultVariant[node.type]) { - node.setAttribute("mathvariant", variant); - } - - return node; - } -}); -defineFunctionBuilders({ - type: "textord", - htmlBuilder: function htmlBuilder(group, options) { - return buildCommon.makeOrd(group, options, "textord"); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var text = buildMathML_makeText(group.text, group.mode, options); - var variant = buildMathML_getVariant(group, options) || "normal"; - var node; - - if (group.mode === 'text') { - node = new mathMLTree.MathNode("mtext", [text]); - } else if (/[0-9]/.test(group.text)) { - // TODO(kevinb) merge adjacent nodes - // do it as a post processing step - node = new mathMLTree.MathNode("mn", [text]); - } else if (group.text === "\\prime") { - node = new mathMLTree.MathNode("mo", [text]); - } else { - node = new mathMLTree.MathNode("mi", [text]); - } - - if (variant !== defaultVariant[node.type]) { - node.setAttribute("mathvariant", variant); - } - - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/symbolsSpacing.js - - - - // A map of CSS-based spacing functions to their CSS class. - -var cssSpace = { - "\\nobreak": "nobreak", - "\\allowbreak": "allowbreak" -}; // A lookup table to determine whether a spacing function/symbol should be -// treated like a regular space character. If a symbol or command is a key -// in this table, then it should be a regular space character. Furthermore, -// the associated value may have a `className` specifying an extra CSS class -// to add to the created `span`. - -var regularSpace = { - " ": {}, - "\\ ": {}, - "~": { - className: "nobreak" - }, - "\\space": {}, - "\\nobreakspace": { - className: "nobreak" - } -}; // ParseNode<"spacing"> created in Parser.js from the "spacing" symbol Groups in -// src/symbols.js. - -defineFunctionBuilders({ - type: "spacing", - htmlBuilder: function htmlBuilder(group, options) { - if (regularSpace.hasOwnProperty(group.text)) { - var className = regularSpace[group.text].className || ""; // Spaces are generated by adding an actual space. Each of these - // things has an entry in the symbols table, so these will be turned - // into appropriate outputs. - - if (group.mode === "text") { - var ord = buildCommon.makeOrd(group, options, "textord"); - ord.classes.push(className); - return ord; - } else { - return buildCommon.makeSpan(["mspace", className], [buildCommon.mathsym(group.text, group.mode, options)], options); - } - } else if (cssSpace.hasOwnProperty(group.text)) { - // Spaces based on just a CSS class. - return buildCommon.makeSpan(["mspace", cssSpace[group.text]], [], options); - } else { - throw new src_ParseError("Unknown type of space \"" + group.text + "\""); - } - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var node; - - if (regularSpace.hasOwnProperty(group.text)) { - node = new mathMLTree.MathNode("mtext", [new mathMLTree.TextNode("\xA0")]); - } else if (cssSpace.hasOwnProperty(group.text)) { - // CSS-based MathML spaces (\nobreak, \allowbreak) are ignored - return new mathMLTree.MathNode("mspace"); - } else { - throw new src_ParseError("Unknown type of space \"" + group.text + "\""); - } - - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/tag.js - - - - -var tag_pad = function pad() { - var padNode = new mathMLTree.MathNode("mtd", []); - padNode.setAttribute("width", "50%"); - return padNode; -}; - -defineFunctionBuilders({ - type: "tag", - mathmlBuilder: function mathmlBuilder(group, options) { - var table = new mathMLTree.MathNode("mtable", [new mathMLTree.MathNode("mtr", [tag_pad(), new mathMLTree.MathNode("mtd", [buildExpressionRow(group.body, options)]), tag_pad(), new mathMLTree.MathNode("mtd", [buildExpressionRow(group.tag, options)])])]); - table.setAttribute("width", "100%"); - return table; // TODO: Left-aligned tags. - // Currently, the group and options passed here do not contain - // enough info to set tag alignment. `leqno` is in Settings but it is - // not passed to Options. On the HTML side, leqno is - // set by a CSS class applied in buildTree.js. That would have worked - // in MathML if browsers supported . Since they don't, we - // need to rewrite the way this function is called. - } -}); -// CONCATENATED MODULE: ./src/functions/text.js - - - - // Non-mathy text, possibly in a font - -var textFontFamilies = { - "\\text": undefined, - "\\textrm": "textrm", - "\\textsf": "textsf", - "\\texttt": "texttt", - "\\textnormal": "textrm" -}; -var textFontWeights = { - "\\textbf": "textbf", - "\\textmd": "textmd" -}; -var textFontShapes = { - "\\textit": "textit", - "\\textup": "textup" -}; - -var optionsWithFont = function optionsWithFont(group, options) { - var font = group.font; // Checks if the argument is a font family or a font style. - - if (!font) { - return options; - } else if (textFontFamilies[font]) { - return options.withTextFontFamily(textFontFamilies[font]); - } else if (textFontWeights[font]) { - return options.withTextFontWeight(textFontWeights[font]); - } else { - return options.withTextFontShape(textFontShapes[font]); - } -}; - -defineFunction({ - type: "text", - names: [// Font families - "\\text", "\\textrm", "\\textsf", "\\texttt", "\\textnormal", // Font weights - "\\textbf", "\\textmd", // Font Shapes - "\\textit", "\\textup"], - props: { - numArgs: 1, - argTypes: ["text"], - greediness: 2, - allowedInText: true - }, - handler: function handler(_ref, args) { - var parser = _ref.parser, - funcName = _ref.funcName; - var body = args[0]; - return { - type: "text", - mode: parser.mode, - body: ordargument(body), - font: funcName - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - var newOptions = optionsWithFont(group, options); - var inner = buildHTML_buildExpression(group.body, newOptions, true); - return buildCommon.makeSpan(["mord", "text"], buildCommon.tryCombineChars(inner), newOptions); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var newOptions = optionsWithFont(group, options); - return buildExpressionRow(group.body, newOptions); - } -}); -// CONCATENATED MODULE: ./src/functions/underline.js - - - - - -defineFunction({ - type: "underline", - names: ["\\underline"], - props: { - numArgs: 1, - allowedInText: true - }, - handler: function handler(_ref, args) { - var parser = _ref.parser; - return { - type: "underline", - mode: parser.mode, - body: args[0] - }; - }, - htmlBuilder: function htmlBuilder(group, options) { - // Underlines are handled in the TeXbook pg 443, Rule 10. - // Build the inner group. - var innerGroup = buildHTML_buildGroup(group.body, options); // Create the line to go below the body - - var line = buildCommon.makeLineSpan("underline-line", options); // Generate the vlist, with the appropriate kerns - - var defaultRuleThickness = options.fontMetrics().defaultRuleThickness; - var vlist = buildCommon.makeVList({ - positionType: "top", - positionData: innerGroup.height, - children: [{ - type: "kern", - size: defaultRuleThickness - }, { - type: "elem", - elem: line - }, { - type: "kern", - size: 3 * defaultRuleThickness - }, { - type: "elem", - elem: innerGroup - }] - }, options); - return buildCommon.makeSpan(["mord", "underline"], [vlist], options); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var operator = new mathMLTree.MathNode("mo", [new mathMLTree.TextNode("\u203E")]); - operator.setAttribute("stretchy", "true"); - var node = new mathMLTree.MathNode("munder", [buildMathML_buildGroup(group.body, options), operator]); - node.setAttribute("accentunder", "true"); - return node; - } -}); -// CONCATENATED MODULE: ./src/functions/verb.js - - - - -defineFunction({ - type: "verb", - names: ["\\verb"], - props: { - numArgs: 0, - allowedInText: true - }, - handler: function handler(context, args, optArgs) { - // \verb and \verb* are dealt with directly in Parser.js. - // If we end up here, it's because of a failure to match the two delimiters - // in the regex in Lexer.js. LaTeX raises the following error when \verb is - // terminated by end of line (or file). - throw new src_ParseError("\\verb ended by end of line instead of matching delimiter"); - }, - htmlBuilder: function htmlBuilder(group, options) { - var text = makeVerb(group); - var body = []; // \verb enters text mode and therefore is sized like \textstyle - - var newOptions = options.havingStyle(options.style.text()); - - for (var i = 0; i < text.length; i++) { - var c = text[i]; - - if (c === '~') { - c = '\\textasciitilde'; - } - - body.push(buildCommon.makeSymbol(c, "Typewriter-Regular", group.mode, newOptions, ["mord", "texttt"])); - } - - return buildCommon.makeSpan(["mord", "text"].concat(newOptions.sizingClasses(options)), buildCommon.tryCombineChars(body), newOptions); - }, - mathmlBuilder: function mathmlBuilder(group, options) { - var text = new mathMLTree.TextNode(makeVerb(group)); - var node = new mathMLTree.MathNode("mtext", [text]); - node.setAttribute("mathvariant", "monospace"); - return node; - } -}); -/** - * Converts verb group into body string. - * - * \verb* replaces each space with an open box \u2423 - * \verb replaces each space with a no-break space \xA0 - */ - -var makeVerb = function makeVerb(group) { - return group.body.replace(/ /g, group.star ? "\u2423" : '\xA0'); -}; -// CONCATENATED MODULE: ./src/functions.js -/** Include this to ensure that all functions are defined. */ - -var functions = _functions; -/* harmony default export */ var src_functions = (functions); // TODO(kevinb): have functions return an object and call defineFunction with -// that object in this file instead of relying on side-effects. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// CONCATENATED MODULE: ./src/Lexer.js -/** - * The Lexer class handles tokenizing the input in various ways. Since our - * parser expects us to be able to backtrack, the lexer allows lexing from any - * given starting point. - * - * Its main exposed function is the `lex` function, which takes a position to - * lex from and a type of token to lex. It defers to the appropriate `_innerLex` - * function. - * - * The various `_innerLex` functions perform the actual lexing of different - * kinds. - */ - - - - -/* The following tokenRegex - * - matches typical whitespace (but not NBSP etc.) using its first group - * - does not match any control character \x00-\x1f except whitespace - * - does not match a bare backslash - * - matches any ASCII character except those just mentioned - * - does not match the BMP private use area \uE000-\uF8FF - * - does not match bare surrogate code units - * - matches any BMP character except for those just described - * - matches any valid Unicode surrogate pair - * - matches a backslash followed by one or more letters - * - matches a backslash followed by any BMP character, including newline - * Just because the Lexer matches something doesn't mean it's valid input: - * If there is no matching function or symbol definition, the Parser will - * still reject the input. - */ -var spaceRegexString = "[ \r\n\t]"; -var controlWordRegexString = "\\\\[a-zA-Z@]+"; -var controlSymbolRegexString = "\\\\[^\uD800-\uDFFF]"; -var controlWordWhitespaceRegexString = "" + controlWordRegexString + spaceRegexString + "*"; -var controlWordWhitespaceRegex = new RegExp("^(" + controlWordRegexString + ")" + spaceRegexString + "*$"); -var combiningDiacriticalMarkString = "[\u0300-\u036F]"; -var combiningDiacriticalMarksEndRegex = new RegExp(combiningDiacriticalMarkString + "+$"); -var tokenRegexString = "(" + spaceRegexString + "+)|" + // whitespace -"([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + ( // single codepoint -combiningDiacriticalMarkString + "*") + // ...plus accents -"|[\uD800-\uDBFF][\uDC00-\uDFFF]" + ( // surrogate pair -combiningDiacriticalMarkString + "*") + // ...plus accents -"|\\\\verb\\*([^]).*?\\3" + // \verb* -"|\\\\verb([^*a-zA-Z]).*?\\4" + // \verb unstarred -"|\\\\operatorname\\*" + ( // \operatorname* -"|" + controlWordWhitespaceRegexString) + ( // \macroName + spaces -"|" + controlSymbolRegexString + ")"); // \\, \', etc. - -/** Main Lexer class */ - -var Lexer_Lexer = -/*#__PURE__*/ -function () { - // category codes, only supports comment characters (14) for now - function Lexer(input, settings) { - this.input = void 0; - this.settings = void 0; - this.tokenRegex = void 0; - this.catcodes = void 0; - // Separate accents from characters - this.input = input; - this.settings = settings; - this.tokenRegex = new RegExp(tokenRegexString, 'g'); - this.catcodes = { - "%": 14 // comment character - - }; - } - - var _proto = Lexer.prototype; - - _proto.setCatcode = function setCatcode(char, code) { - this.catcodes[char] = code; - } - /** - * This function lexes a single token. - */ - ; - - _proto.lex = function lex() { - var input = this.input; - var pos = this.tokenRegex.lastIndex; - - if (pos === input.length) { - return new Token_Token("EOF", new SourceLocation(this, pos, pos)); - } - - var match = this.tokenRegex.exec(input); - - if (match === null || match.index !== pos) { - throw new src_ParseError("Unexpected character: '" + input[pos] + "'", new Token_Token(input[pos], new SourceLocation(this, pos, pos + 1))); - } - - var text = match[2] || " "; - - if (this.catcodes[text] === 14) { - // comment character - var nlIndex = input.indexOf('\n', this.tokenRegex.lastIndex); - - if (nlIndex === -1) { - this.tokenRegex.lastIndex = input.length; // EOF - - this.settings.reportNonstrict("commentAtEnd", "% comment has no terminating newline; LaTeX would " + "fail because of commenting the end of math mode (e.g. $)"); - } else { - this.tokenRegex.lastIndex = nlIndex + 1; - } - - return this.lex(); - } // Trim any trailing whitespace from control word match - - - var controlMatch = text.match(controlWordWhitespaceRegex); - - if (controlMatch) { - text = controlMatch[1]; - } - - return new Token_Token(text, new SourceLocation(this, pos, this.tokenRegex.lastIndex)); - }; - - return Lexer; -}(); - - -// CONCATENATED MODULE: ./src/Namespace.js -/** - * A `Namespace` refers to a space of nameable things like macros or lengths, - * which can be `set` either globally or local to a nested group, using an - * undo stack similar to how TeX implements this functionality. - * Performance-wise, `get` and local `set` take constant time, while global - * `set` takes time proportional to the depth of group nesting. - */ - - -var Namespace_Namespace = -/*#__PURE__*/ -function () { - /** - * Both arguments are optional. The first argument is an object of - * built-in mappings which never change. The second argument is an object - * of initial (global-level) mappings, which will constantly change - * according to any global/top-level `set`s done. - */ - function Namespace(builtins, globalMacros) { - if (builtins === void 0) { - builtins = {}; - } - - if (globalMacros === void 0) { - globalMacros = {}; - } - - this.current = void 0; - this.builtins = void 0; - this.undefStack = void 0; - this.current = globalMacros; - this.builtins = builtins; - this.undefStack = []; - } - /** - * Start a new nested group, affecting future local `set`s. - */ - - - var _proto = Namespace.prototype; - - _proto.beginGroup = function beginGroup() { - this.undefStack.push({}); - } - /** - * End current nested group, restoring values before the group began. - */ - ; - - _proto.endGroup = function endGroup() { - if (this.undefStack.length === 0) { - throw new src_ParseError("Unbalanced namespace destruction: attempt " + "to pop global namespace; please report this as a bug"); - } - - var undefs = this.undefStack.pop(); - - for (var undef in undefs) { - if (undefs.hasOwnProperty(undef)) { - if (undefs[undef] === undefined) { - delete this.current[undef]; - } else { - this.current[undef] = undefs[undef]; - } - } - } - } - /** - * Detect whether `name` has a definition. Equivalent to - * `get(name) != null`. - */ - ; - - _proto.has = function has(name) { - return this.current.hasOwnProperty(name) || this.builtins.hasOwnProperty(name); - } - /** - * Get the current value of a name, or `undefined` if there is no value. - * - * Note: Do not use `if (namespace.get(...))` to detect whether a macro - * is defined, as the definition may be the empty string which evaluates - * to `false` in JavaScript. Use `if (namespace.get(...) != null)` or - * `if (namespace.has(...))`. - */ - ; - - _proto.get = function get(name) { - if (this.current.hasOwnProperty(name)) { - return this.current[name]; - } else { - return this.builtins[name]; - } - } - /** - * Set the current value of a name, and optionally set it globally too. - * Local set() sets the current value and (when appropriate) adds an undo - * operation to the undo stack. Global set() may change the undo - * operation at every level, so takes time linear in their number. - */ - ; - - _proto.set = function set(name, value, global) { - if (global === void 0) { - global = false; - } - - if (global) { - // Global set is equivalent to setting in all groups. Simulate this - // by destroying any undos currently scheduled for this name, - // and adding an undo with the *new* value (in case it later gets - // locally reset within this environment). - for (var i = 0; i < this.undefStack.length; i++) { - delete this.undefStack[i][name]; - } - - if (this.undefStack.length > 0) { - this.undefStack[this.undefStack.length - 1][name] = value; - } - } else { - // Undo this set at end of this group (possibly to `undefined`), - // unless an undo is already in place, in which case that older - // value is the correct one. - var top = this.undefStack[this.undefStack.length - 1]; - - if (top && !top.hasOwnProperty(name)) { - top[name] = this.current[name]; - } - } - - this.current[name] = value; - }; - - return Namespace; -}(); - - -// CONCATENATED MODULE: ./src/macros.js -/** - * Predefined macros for KaTeX. - * This can be used to define some commands in terms of others. - */ - - - - - - -var builtinMacros = {}; -/* harmony default export */ var macros = (builtinMacros); // This function might one day accept an additional argument and do more things. - -function defineMacro(name, body) { - builtinMacros[name] = body; -} ////////////////////////////////////////////////////////////////////// -// macro tools - -defineMacro("\\noexpand", function (context) { - // The expansion is the token itself; but that token is interpreted - // as if its meaning were ‘\relax’ if it is a control sequence that - // would ordinarily be expanded by TeX’s expansion rules. - var t = context.popToken(); - - if (context.isExpandable(t.text)) { - t.noexpand = true; - t.treatAsRelax = true; - } - - return { - tokens: [t], - numArgs: 0 - }; -}); -defineMacro("\\expandafter", function (context) { - // TeX first reads the token that comes immediately after \expandafter, - // without expanding it; let’s call this token t. Then TeX reads the - // token that comes after t (and possibly more tokens, if that token - // has an argument), replacing it by its expansion. Finally TeX puts - // t back in front of that expansion. - var t = context.popToken(); - context.expandOnce(true); // expand only an expandable token - - return { - tokens: [t], - numArgs: 0 - }; -}); // LaTeX's \@firstoftwo{#1}{#2} expands to #1, skipping #2 -// TeX source: \long\def\@firstoftwo#1#2{#1} - -defineMacro("\\@firstoftwo", function (context) { - var args = context.consumeArgs(2); - return { - tokens: args[0], - numArgs: 0 - }; -}); // LaTeX's \@secondoftwo{#1}{#2} expands to #2, skipping #1 -// TeX source: \long\def\@secondoftwo#1#2{#2} - -defineMacro("\\@secondoftwo", function (context) { - var args = context.consumeArgs(2); - return { - tokens: args[1], - numArgs: 0 - }; -}); // LaTeX's \@ifnextchar{#1}{#2}{#3} looks ahead to the next (unexpanded) -// symbol that isn't a space, consuming any spaces but not consuming the -// first nonspace character. If that nonspace character matches #1, then -// the macro expands to #2; otherwise, it expands to #3. - -defineMacro("\\@ifnextchar", function (context) { - var args = context.consumeArgs(3); // symbol, if, else - - context.consumeSpaces(); - var nextToken = context.future(); - - if (args[0].length === 1 && args[0][0].text === nextToken.text) { - return { - tokens: args[1], - numArgs: 0 - }; - } else { - return { - tokens: args[2], - numArgs: 0 - }; - } -}); // LaTeX's \@ifstar{#1}{#2} looks ahead to the next (unexpanded) symbol. -// If it is `*`, then it consumes the symbol, and the macro expands to #1; -// otherwise, the macro expands to #2 (without consuming the symbol). -// TeX source: \def\@ifstar#1{\@ifnextchar *{\@firstoftwo{#1}}} - -defineMacro("\\@ifstar", "\\@ifnextchar *{\\@firstoftwo{#1}}"); // LaTeX's \TextOrMath{#1}{#2} expands to #1 in text mode, #2 in math mode - -defineMacro("\\TextOrMath", function (context) { - var args = context.consumeArgs(2); - - if (context.mode === 'text') { - return { - tokens: args[0], - numArgs: 0 - }; - } else { - return { - tokens: args[1], - numArgs: 0 - }; - } -}); // Lookup table for parsing numbers in base 8 through 16 - -var digitToNumber = { - "0": 0, - "1": 1, - "2": 2, - "3": 3, - "4": 4, - "5": 5, - "6": 6, - "7": 7, - "8": 8, - "9": 9, - "a": 10, - "A": 10, - "b": 11, - "B": 11, - "c": 12, - "C": 12, - "d": 13, - "D": 13, - "e": 14, - "E": 14, - "f": 15, - "F": 15 -}; // TeX \char makes a literal character (catcode 12) using the following forms: -// (see The TeXBook, p. 43) -// \char123 -- decimal -// \char'123 -- octal -// \char"123 -- hex -// \char`x -- character that can be written (i.e. isn't active) -// \char`\x -- character that cannot be written (e.g. %) -// These all refer to characters from the font, so we turn them into special -// calls to a function \@char dealt with in the Parser. - -defineMacro("\\char", function (context) { - var token = context.popToken(); - var base; - var number = ''; - - if (token.text === "'") { - base = 8; - token = context.popToken(); - } else if (token.text === '"') { - base = 16; - token = context.popToken(); - } else if (token.text === "`") { - token = context.popToken(); - - if (token.text[0] === "\\") { - number = token.text.charCodeAt(1); - } else if (token.text === "EOF") { - throw new src_ParseError("\\char` missing argument"); - } else { - number = token.text.charCodeAt(0); - } - } else { - base = 10; - } - - if (base) { - // Parse a number in the given base, starting with first `token`. - number = digitToNumber[token.text]; - - if (number == null || number >= base) { - throw new src_ParseError("Invalid base-" + base + " digit " + token.text); - } - - var digit; - - while ((digit = digitToNumber[context.future().text]) != null && digit < base) { - number *= base; - number += digit; - context.popToken(); - } - } - - return "\\@char{" + number + "}"; -}); // \newcommand{\macro}[args]{definition} -// \renewcommand{\macro}[args]{definition} -// TODO: Optional arguments: \newcommand{\macro}[args][default]{definition} - -var macros_newcommand = function newcommand(context, existsOK, nonexistsOK) { - var arg = context.consumeArgs(1)[0]; - - if (arg.length !== 1) { - throw new src_ParseError("\\newcommand's first argument must be a macro name"); - } - - var name = arg[0].text; - var exists = context.isDefined(name); - - if (exists && !existsOK) { - throw new src_ParseError("\\newcommand{" + name + "} attempting to redefine " + (name + "; use \\renewcommand")); - } - - if (!exists && !nonexistsOK) { - throw new src_ParseError("\\renewcommand{" + name + "} when command " + name + " " + "does not yet exist; use \\newcommand"); - } - - var numArgs = 0; - arg = context.consumeArgs(1)[0]; - - if (arg.length === 1 && arg[0].text === "[") { - var argText = ''; - var token = context.expandNextToken(); - - while (token.text !== "]" && token.text !== "EOF") { - // TODO: Should properly expand arg, e.g., ignore {}s - argText += token.text; - token = context.expandNextToken(); - } - - if (!argText.match(/^\s*[0-9]+\s*$/)) { - throw new src_ParseError("Invalid number of arguments: " + argText); - } - - numArgs = parseInt(argText); - arg = context.consumeArgs(1)[0]; - } // Final arg is the expansion of the macro - - - context.macros.set(name, { - tokens: arg, - numArgs: numArgs - }); - return ''; -}; - -defineMacro("\\newcommand", function (context) { - return macros_newcommand(context, false, true); -}); -defineMacro("\\renewcommand", function (context) { - return macros_newcommand(context, true, false); -}); -defineMacro("\\providecommand", function (context) { - return macros_newcommand(context, true, true); -}); // terminal (console) tools - -defineMacro("\\message", function (context) { - var arg = context.consumeArgs(1)[0]; // eslint-disable-next-line no-console - - console.log(arg.reverse().map(function (token) { - return token.text; - }).join("")); - return ''; -}); -defineMacro("\\errmessage", function (context) { - var arg = context.consumeArgs(1)[0]; // eslint-disable-next-line no-console - - console.error(arg.reverse().map(function (token) { - return token.text; - }).join("")); - return ''; -}); -defineMacro("\\show", function (context) { - var tok = context.popToken(); - var name = tok.text; // eslint-disable-next-line no-console - - console.log(tok, context.macros.get(name), src_functions[name], src_symbols.math[name], src_symbols.text[name]); - return ''; -}); ////////////////////////////////////////////////////////////////////// -// Grouping -// \let\bgroup={ \let\egroup=} - -defineMacro("\\bgroup", "{"); -defineMacro("\\egroup", "}"); // Symbols from latex.ltx: -// \def\lq{`} -// \def\rq{'} -// \def \aa {\r a} -// \def \AA {\r A} - -defineMacro("\\lq", "`"); -defineMacro("\\rq", "'"); -defineMacro("\\aa", "\\r a"); -defineMacro("\\AA", "\\r A"); // Copyright (C) and registered (R) symbols. Use raw symbol in MathML. -// \DeclareTextCommandDefault{\textcopyright}{\textcircled{c}} -// \DeclareTextCommandDefault{\textregistered}{\textcircled{% -// \check@mathfonts\fontsize\sf@size\z@\math@fontsfalse\selectfont R}} -// \DeclareRobustCommand{\copyright}{% -// \ifmmode{\nfss@text{\textcopyright}}\else\textcopyright\fi} - -defineMacro("\\textcopyright", "\\html@mathml{\\textcircled{c}}{\\char`©}"); -defineMacro("\\copyright", "\\TextOrMath{\\textcopyright}{\\text{\\textcopyright}}"); -defineMacro("\\textregistered", "\\html@mathml{\\textcircled{\\scriptsize R}}{\\char`®}"); // Characters omitted from Unicode range 1D400–1D7FF - -defineMacro("\u212C", "\\mathscr{B}"); // script - -defineMacro("\u2130", "\\mathscr{E}"); -defineMacro("\u2131", "\\mathscr{F}"); -defineMacro("\u210B", "\\mathscr{H}"); -defineMacro("\u2110", "\\mathscr{I}"); -defineMacro("\u2112", "\\mathscr{L}"); -defineMacro("\u2133", "\\mathscr{M}"); -defineMacro("\u211B", "\\mathscr{R}"); -defineMacro("\u212D", "\\mathfrak{C}"); // Fraktur - -defineMacro("\u210C", "\\mathfrak{H}"); -defineMacro("\u2128", "\\mathfrak{Z}"); // Define \Bbbk with a macro that works in both HTML and MathML. - -defineMacro("\\Bbbk", "\\Bbb{k}"); // Unicode middle dot -// The KaTeX fonts do not contain U+00B7. Instead, \cdotp displays -// the dot at U+22C5 and gives it punct spacing. - -defineMacro("\xB7", "\\cdotp"); // \llap and \rlap render their contents in text mode - -defineMacro("\\llap", "\\mathllap{\\textrm{#1}}"); -defineMacro("\\rlap", "\\mathrlap{\\textrm{#1}}"); -defineMacro("\\clap", "\\mathclap{\\textrm{#1}}"); // \not is defined by base/fontmath.ltx via -// \DeclareMathSymbol{\not}{\mathrel}{symbols}{"36} -// It's thus treated like a \mathrel, but defined by a symbol that has zero -// width but extends to the right. We use \rlap to get that spacing. -// For MathML we write U+0338 here. buildMathML.js will then do the overlay. - -defineMacro("\\not", '\\html@mathml{\\mathrel{\\mathrlap\\@not}}{\\char"338}'); // Negated symbols from base/fontmath.ltx: -// \def\neq{\not=} \let\ne=\neq -// \DeclareRobustCommand -// \notin{\mathrel{\m@th\mathpalette\c@ncel\in}} -// \def\c@ncel#1#2{\m@th\ooalign{$\hfil#1\mkern1mu/\hfil$\crcr$#1#2$}} - -defineMacro("\\neq", "\\html@mathml{\\mathrel{\\not=}}{\\mathrel{\\char`≠}}"); -defineMacro("\\ne", "\\neq"); -defineMacro("\u2260", "\\neq"); -defineMacro("\\notin", "\\html@mathml{\\mathrel{{\\in}\\mathllap{/\\mskip1mu}}}" + "{\\mathrel{\\char`∉}}"); -defineMacro("\u2209", "\\notin"); // Unicode stacked relations - -defineMacro("\u2258", "\\html@mathml{" + "\\mathrel{=\\kern{-1em}\\raisebox{0.4em}{$\\scriptsize\\frown$}}" + "}{\\mathrel{\\char`\u2258}}"); -defineMacro("\u2259", "\\html@mathml{\\stackrel{\\tiny\\wedge}{=}}{\\mathrel{\\char`\u2258}}"); -defineMacro("\u225A", "\\html@mathml{\\stackrel{\\tiny\\vee}{=}}{\\mathrel{\\char`\u225A}}"); -defineMacro("\u225B", "\\html@mathml{\\stackrel{\\scriptsize\\star}{=}}" + "{\\mathrel{\\char`\u225B}}"); -defineMacro("\u225D", "\\html@mathml{\\stackrel{\\tiny\\mathrm{def}}{=}}" + "{\\mathrel{\\char`\u225D}}"); -defineMacro("\u225E", "\\html@mathml{\\stackrel{\\tiny\\mathrm{m}}{=}}" + "{\\mathrel{\\char`\u225E}}"); -defineMacro("\u225F", "\\html@mathml{\\stackrel{\\tiny?}{=}}{\\mathrel{\\char`\u225F}}"); // Misc Unicode - -defineMacro("\u27C2", "\\perp"); -defineMacro("\u203C", "\\mathclose{!\\mkern-0.8mu!}"); -defineMacro("\u220C", "\\notni"); -defineMacro("\u231C", "\\ulcorner"); -defineMacro("\u231D", "\\urcorner"); -defineMacro("\u231E", "\\llcorner"); -defineMacro("\u231F", "\\lrcorner"); -defineMacro("\xA9", "\\copyright"); -defineMacro("\xAE", "\\textregistered"); -defineMacro("\uFE0F", "\\textregistered"); // The KaTeX fonts have corners at codepoints that don't match Unicode. -// For MathML purposes, use the Unicode code point. - -defineMacro("\\ulcorner", "\\html@mathml{\\@ulcorner}{\\mathop{\\char\"231c}}"); -defineMacro("\\urcorner", "\\html@mathml{\\@urcorner}{\\mathop{\\char\"231d}}"); -defineMacro("\\llcorner", "\\html@mathml{\\@llcorner}{\\mathop{\\char\"231e}}"); -defineMacro("\\lrcorner", "\\html@mathml{\\@lrcorner}{\\mathop{\\char\"231f}}"); ////////////////////////////////////////////////////////////////////// -// LaTeX_2ε -// \vdots{\vbox{\baselineskip4\p@ \lineskiplimit\z@ -// \kern6\p@\hbox{.}\hbox{.}\hbox{.}}} -// We'll call \varvdots, which gets a glyph from symbols.js. -// The zero-width rule gets us an equivalent to the vertical 6pt kern. - -defineMacro("\\vdots", "\\mathord{\\varvdots\\rule{0pt}{15pt}}"); -defineMacro("\u22EE", "\\vdots"); ////////////////////////////////////////////////////////////////////// -// amsmath.sty -// http://mirrors.concertpass.com/tex-archive/macros/latex/required/amsmath/amsmath.pdf -// Italic Greek capital letters. AMS defines these with \DeclareMathSymbol, -// but they are equivalent to \mathit{\Letter}. - -defineMacro("\\varGamma", "\\mathit{\\Gamma}"); -defineMacro("\\varDelta", "\\mathit{\\Delta}"); -defineMacro("\\varTheta", "\\mathit{\\Theta}"); -defineMacro("\\varLambda", "\\mathit{\\Lambda}"); -defineMacro("\\varXi", "\\mathit{\\Xi}"); -defineMacro("\\varPi", "\\mathit{\\Pi}"); -defineMacro("\\varSigma", "\\mathit{\\Sigma}"); -defineMacro("\\varUpsilon", "\\mathit{\\Upsilon}"); -defineMacro("\\varPhi", "\\mathit{\\Phi}"); -defineMacro("\\varPsi", "\\mathit{\\Psi}"); -defineMacro("\\varOmega", "\\mathit{\\Omega}"); //\newcommand{\substack}[1]{\subarray{c}#1\endsubarray} - -defineMacro("\\substack", "\\begin{subarray}{c}#1\\end{subarray}"); // \renewcommand{\colon}{\nobreak\mskip2mu\mathpunct{}\nonscript -// \mkern-\thinmuskip{:}\mskip6muplus1mu\relax} - -defineMacro("\\colon", "\\nobreak\\mskip2mu\\mathpunct{}" + "\\mathchoice{\\mkern-3mu}{\\mkern-3mu}{}{}{:}\\mskip6mu"); // \newcommand{\boxed}[1]{\fbox{\m@th$\displaystyle#1$}} - -defineMacro("\\boxed", "\\fbox{$\\displaystyle{#1}$}"); // \def\iff{\DOTSB\;\Longleftrightarrow\;} -// \def\implies{\DOTSB\;\Longrightarrow\;} -// \def\impliedby{\DOTSB\;\Longleftarrow\;} - -defineMacro("\\iff", "\\DOTSB\\;\\Longleftrightarrow\\;"); -defineMacro("\\implies", "\\DOTSB\\;\\Longrightarrow\\;"); -defineMacro("\\impliedby", "\\DOTSB\\;\\Longleftarrow\\;"); // AMSMath's automatic \dots, based on \mdots@@ macro. - -var dotsByToken = { - ',': '\\dotsc', - '\\not': '\\dotsb', - // \keybin@ checks for the following: - '+': '\\dotsb', - '=': '\\dotsb', - '<': '\\dotsb', - '>': '\\dotsb', - '-': '\\dotsb', - '*': '\\dotsb', - ':': '\\dotsb', - // Symbols whose definition starts with \DOTSB: - '\\DOTSB': '\\dotsb', - '\\coprod': '\\dotsb', - '\\bigvee': '\\dotsb', - '\\bigwedge': '\\dotsb', - '\\biguplus': '\\dotsb', - '\\bigcap': '\\dotsb', - '\\bigcup': '\\dotsb', - '\\prod': '\\dotsb', - '\\sum': '\\dotsb', - '\\bigotimes': '\\dotsb', - '\\bigoplus': '\\dotsb', - '\\bigodot': '\\dotsb', - '\\bigsqcup': '\\dotsb', - '\\And': '\\dotsb', - '\\longrightarrow': '\\dotsb', - '\\Longrightarrow': '\\dotsb', - '\\longleftarrow': '\\dotsb', - '\\Longleftarrow': '\\dotsb', - '\\longleftrightarrow': '\\dotsb', - '\\Longleftrightarrow': '\\dotsb', - '\\mapsto': '\\dotsb', - '\\longmapsto': '\\dotsb', - '\\hookrightarrow': '\\dotsb', - '\\doteq': '\\dotsb', - // Symbols whose definition starts with \mathbin: - '\\mathbin': '\\dotsb', - // Symbols whose definition starts with \mathrel: - '\\mathrel': '\\dotsb', - '\\relbar': '\\dotsb', - '\\Relbar': '\\dotsb', - '\\xrightarrow': '\\dotsb', - '\\xleftarrow': '\\dotsb', - // Symbols whose definition starts with \DOTSI: - '\\DOTSI': '\\dotsi', - '\\int': '\\dotsi', - '\\oint': '\\dotsi', - '\\iint': '\\dotsi', - '\\iiint': '\\dotsi', - '\\iiiint': '\\dotsi', - '\\idotsint': '\\dotsi', - // Symbols whose definition starts with \DOTSX: - '\\DOTSX': '\\dotsx' -}; -defineMacro("\\dots", function (context) { - // TODO: If used in text mode, should expand to \textellipsis. - // However, in KaTeX, \textellipsis and \ldots behave the same - // (in text mode), and it's unlikely we'd see any of the math commands - // that affect the behavior of \dots when in text mode. So fine for now - // (until we support \ifmmode ... \else ... \fi). - var thedots = '\\dotso'; - var next = context.expandAfterFuture().text; - - if (next in dotsByToken) { - thedots = dotsByToken[next]; - } else if (next.substr(0, 4) === '\\not') { - thedots = '\\dotsb'; - } else if (next in src_symbols.math) { - if (utils.contains(['bin', 'rel'], src_symbols.math[next].group)) { - thedots = '\\dotsb'; - } - } - - return thedots; -}); -var spaceAfterDots = { - // \rightdelim@ checks for the following: - ')': true, - ']': true, - '\\rbrack': true, - '\\}': true, - '\\rbrace': true, - '\\rangle': true, - '\\rceil': true, - '\\rfloor': true, - '\\rgroup': true, - '\\rmoustache': true, - '\\right': true, - '\\bigr': true, - '\\biggr': true, - '\\Bigr': true, - '\\Biggr': true, - // \extra@ also tests for the following: - '$': true, - // \extrap@ checks for the following: - ';': true, - '.': true, - ',': true -}; -defineMacro("\\dotso", function (context) { - var next = context.future().text; - - if (next in spaceAfterDots) { - return "\\ldots\\,"; - } else { - return "\\ldots"; - } -}); -defineMacro("\\dotsc", function (context) { - var next = context.future().text; // \dotsc uses \extra@ but not \extrap@, instead specially checking for - // ';' and '.', but doesn't check for ','. - - if (next in spaceAfterDots && next !== ',') { - return "\\ldots\\,"; - } else { - return "\\ldots"; - } -}); -defineMacro("\\cdots", function (context) { - var next = context.future().text; - - if (next in spaceAfterDots) { - return "\\@cdots\\,"; - } else { - return "\\@cdots"; - } -}); -defineMacro("\\dotsb", "\\cdots"); -defineMacro("\\dotsm", "\\cdots"); -defineMacro("\\dotsi", "\\!\\cdots"); // amsmath doesn't actually define \dotsx, but \dots followed by a macro -// starting with \DOTSX implies \dotso, and then \extra@ detects this case -// and forces the added `\,`. - -defineMacro("\\dotsx", "\\ldots\\,"); // \let\DOTSI\relax -// \let\DOTSB\relax -// \let\DOTSX\relax - -defineMacro("\\DOTSI", "\\relax"); -defineMacro("\\DOTSB", "\\relax"); -defineMacro("\\DOTSX", "\\relax"); // Spacing, based on amsmath.sty's override of LaTeX defaults -// \DeclareRobustCommand{\tmspace}[3]{% -// \ifmmode\mskip#1#2\else\kern#1#3\fi\relax} - -defineMacro("\\tmspace", "\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax"); // \renewcommand{\,}{\tmspace+\thinmuskip{.1667em}} -// TODO: math mode should use \thinmuskip - -defineMacro("\\,", "\\tmspace+{3mu}{.1667em}"); // \let\thinspace\, - -defineMacro("\\thinspace", "\\,"); // \def\>{\mskip\medmuskip} -// \renewcommand{\:}{\tmspace+\medmuskip{.2222em}} -// TODO: \> and math mode of \: should use \medmuskip = 4mu plus 2mu minus 4mu - -defineMacro("\\>", "\\mskip{4mu}"); -defineMacro("\\:", "\\tmspace+{4mu}{.2222em}"); // \let\medspace\: - -defineMacro("\\medspace", "\\:"); // \renewcommand{\;}{\tmspace+\thickmuskip{.2777em}} -// TODO: math mode should use \thickmuskip = 5mu plus 5mu - -defineMacro("\\;", "\\tmspace+{5mu}{.2777em}"); // \let\thickspace\; - -defineMacro("\\thickspace", "\\;"); // \renewcommand{\!}{\tmspace-\thinmuskip{.1667em}} -// TODO: math mode should use \thinmuskip - -defineMacro("\\!", "\\tmspace-{3mu}{.1667em}"); // \let\negthinspace\! - -defineMacro("\\negthinspace", "\\!"); // \newcommand{\negmedspace}{\tmspace-\medmuskip{.2222em}} -// TODO: math mode should use \medmuskip - -defineMacro("\\negmedspace", "\\tmspace-{4mu}{.2222em}"); // \newcommand{\negthickspace}{\tmspace-\thickmuskip{.2777em}} -// TODO: math mode should use \thickmuskip - -defineMacro("\\negthickspace", "\\tmspace-{5mu}{.277em}"); // \def\enspace{\kern.5em } - -defineMacro("\\enspace", "\\kern.5em "); // \def\enskip{\hskip.5em\relax} - -defineMacro("\\enskip", "\\hskip.5em\\relax"); // \def\quad{\hskip1em\relax} - -defineMacro("\\quad", "\\hskip1em\\relax"); // \def\qquad{\hskip2em\relax} - -defineMacro("\\qquad", "\\hskip2em\\relax"); // \tag@in@display form of \tag - -defineMacro("\\tag", "\\@ifstar\\tag@literal\\tag@paren"); -defineMacro("\\tag@paren", "\\tag@literal{({#1})}"); -defineMacro("\\tag@literal", function (context) { - if (context.macros.get("\\df@tag")) { - throw new src_ParseError("Multiple \\tag"); - } - - return "\\gdef\\df@tag{\\text{#1}}"; -}); // \renewcommand{\bmod}{\nonscript\mskip-\medmuskip\mkern5mu\mathbin -// {\operator@font mod}\penalty900 -// \mkern5mu\nonscript\mskip-\medmuskip} -// \newcommand{\pod}[1]{\allowbreak -// \if@display\mkern18mu\else\mkern8mu\fi(#1)} -// \renewcommand{\pmod}[1]{\pod{{\operator@font mod}\mkern6mu#1}} -// \newcommand{\mod}[1]{\allowbreak\if@display\mkern18mu -// \else\mkern12mu\fi{\operator@font mod}\,\,#1} -// TODO: math mode should use \medmuskip = 4mu plus 2mu minus 4mu - -defineMacro("\\bmod", "\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}" + "\\mathbin{\\rm mod}" + "\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}"); -defineMacro("\\pod", "\\allowbreak" + "\\mathchoice{\\mkern18mu}{\\mkern8mu}{\\mkern8mu}{\\mkern8mu}(#1)"); -defineMacro("\\pmod", "\\pod{{\\rm mod}\\mkern6mu#1}"); -defineMacro("\\mod", "\\allowbreak" + "\\mathchoice{\\mkern18mu}{\\mkern12mu}{\\mkern12mu}{\\mkern12mu}" + "{\\rm mod}\\,\\,#1"); // \pmb -- A simulation of bold. -// The version in ambsy.sty works by typesetting three copies of the argument -// with small offsets. We use two copies. We omit the vertical offset because -// of rendering problems that makeVList encounters in Safari. - -defineMacro("\\pmb", "\\html@mathml{" + "\\@binrel{#1}{\\mathrlap{#1}\\kern0.5px#1}}" + "{\\mathbf{#1}}"); ////////////////////////////////////////////////////////////////////// -// LaTeX source2e -// \\ defaults to \newline, but changes to \cr within array environment - -defineMacro("\\\\", "\\newline"); // \def\TeX{T\kern-.1667em\lower.5ex\hbox{E}\kern-.125emX\@} -// TODO: Doesn't normally work in math mode because \@ fails. KaTeX doesn't -// support \@ yet, so that's omitted, and we add \text so that the result -// doesn't look funny in math mode. - -defineMacro("\\TeX", "\\textrm{\\html@mathml{" + "T\\kern-.1667em\\raisebox{-.5ex}{E}\\kern-.125emX" + "}{TeX}}"); // \DeclareRobustCommand{\LaTeX}{L\kern-.36em% -// {\sbox\z@ T% -// \vbox to\ht\z@{\hbox{\check@mathfonts -// \fontsize\sf@size\z@ -// \math@fontsfalse\selectfont -// A}% -// \vss}% -// }% -// \kern-.15em% -// \TeX} -// This code aligns the top of the A with the T (from the perspective of TeX's -// boxes, though visually the A appears to extend above slightly). -// We compute the corresponding \raisebox when A is rendered in \normalsize -// \scriptstyle, which has a scale factor of 0.7 (see Options.js). - -var latexRaiseA = fontMetricsData['Main-Regular']["T".charCodeAt(0)][1] - 0.7 * fontMetricsData['Main-Regular']["A".charCodeAt(0)][1] + "em"; -defineMacro("\\LaTeX", "\\textrm{\\html@mathml{" + ("L\\kern-.36em\\raisebox{" + latexRaiseA + "}{\\scriptstyle A}") + "\\kern-.15em\\TeX}{LaTeX}}"); // New KaTeX logo based on tweaking LaTeX logo - -defineMacro("\\KaTeX", "\\textrm{\\html@mathml{" + ("K\\kern-.17em\\raisebox{" + latexRaiseA + "}{\\scriptstyle A}") + "\\kern-.15em\\TeX}{KaTeX}}"); // \DeclareRobustCommand\hspace{\@ifstar\@hspacer\@hspace} -// \def\@hspace#1{\hskip #1\relax} -// \def\@hspacer#1{\vrule \@width\z@\nobreak -// \hskip #1\hskip \z@skip} - -defineMacro("\\hspace", "\\@ifstar\\@hspacer\\@hspace"); -defineMacro("\\@hspace", "\\hskip #1\\relax"); -defineMacro("\\@hspacer", "\\rule{0pt}{0pt}\\hskip #1\\relax"); ////////////////////////////////////////////////////////////////////// -// mathtools.sty -//\providecommand\ordinarycolon{:} - -defineMacro("\\ordinarycolon", ":"); //\def\vcentcolon{\mathrel{\mathop\ordinarycolon}} -//TODO(edemaine): Not yet centered. Fix via \raisebox or #726 - -defineMacro("\\vcentcolon", "\\mathrel{\\mathop\\ordinarycolon}"); // \providecommand*\dblcolon{\vcentcolon\mathrel{\mkern-.9mu}\vcentcolon} - -defineMacro("\\dblcolon", "\\html@mathml{" + "\\mathrel{\\vcentcolon\\mathrel{\\mkern-.9mu}\\vcentcolon}}" + "{\\mathop{\\char\"2237}}"); // \providecommand*\coloneqq{\vcentcolon\mathrel{\mkern-1.2mu}=} - -defineMacro("\\coloneqq", "\\html@mathml{" + "\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}=}}" + "{\\mathop{\\char\"2254}}"); // ≔ -// \providecommand*\Coloneqq{\dblcolon\mathrel{\mkern-1.2mu}=} - -defineMacro("\\Coloneqq", "\\html@mathml{" + "\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}=}}" + "{\\mathop{\\char\"2237\\char\"3d}}"); // \providecommand*\coloneq{\vcentcolon\mathrel{\mkern-1.2mu}\mathrel{-}} - -defineMacro("\\coloneq", "\\html@mathml{" + "\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}" + "{\\mathop{\\char\"3a\\char\"2212}}"); // \providecommand*\Coloneq{\dblcolon\mathrel{\mkern-1.2mu}\mathrel{-}} - -defineMacro("\\Coloneq", "\\html@mathml{" + "\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}" + "{\\mathop{\\char\"2237\\char\"2212}}"); // \providecommand*\eqqcolon{=\mathrel{\mkern-1.2mu}\vcentcolon} - -defineMacro("\\eqqcolon", "\\html@mathml{" + "\\mathrel{=\\mathrel{\\mkern-1.2mu}\\vcentcolon}}" + "{\\mathop{\\char\"2255}}"); // ≕ -// \providecommand*\Eqqcolon{=\mathrel{\mkern-1.2mu}\dblcolon} - -defineMacro("\\Eqqcolon", "\\html@mathml{" + "\\mathrel{=\\mathrel{\\mkern-1.2mu}\\dblcolon}}" + "{\\mathop{\\char\"3d\\char\"2237}}"); // \providecommand*\eqcolon{\mathrel{-}\mathrel{\mkern-1.2mu}\vcentcolon} - -defineMacro("\\eqcolon", "\\html@mathml{" + "\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\vcentcolon}}" + "{\\mathop{\\char\"2239}}"); // \providecommand*\Eqcolon{\mathrel{-}\mathrel{\mkern-1.2mu}\dblcolon} - -defineMacro("\\Eqcolon", "\\html@mathml{" + "\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\dblcolon}}" + "{\\mathop{\\char\"2212\\char\"2237}}"); // \providecommand*\colonapprox{\vcentcolon\mathrel{\mkern-1.2mu}\approx} - -defineMacro("\\colonapprox", "\\html@mathml{" + "\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\approx}}" + "{\\mathop{\\char\"3a\\char\"2248}}"); // \providecommand*\Colonapprox{\dblcolon\mathrel{\mkern-1.2mu}\approx} - -defineMacro("\\Colonapprox", "\\html@mathml{" + "\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\approx}}" + "{\\mathop{\\char\"2237\\char\"2248}}"); // \providecommand*\colonsim{\vcentcolon\mathrel{\mkern-1.2mu}\sim} - -defineMacro("\\colonsim", "\\html@mathml{" + "\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\sim}}" + "{\\mathop{\\char\"3a\\char\"223c}}"); // \providecommand*\Colonsim{\dblcolon\mathrel{\mkern-1.2mu}\sim} - -defineMacro("\\Colonsim", "\\html@mathml{" + "\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\sim}}" + "{\\mathop{\\char\"2237\\char\"223c}}"); // Some Unicode characters are implemented with macros to mathtools functions. - -defineMacro("\u2237", "\\dblcolon"); // :: - -defineMacro("\u2239", "\\eqcolon"); // -: - -defineMacro("\u2254", "\\coloneqq"); // := - -defineMacro("\u2255", "\\eqqcolon"); // =: - -defineMacro("\u2A74", "\\Coloneqq"); // ::= -////////////////////////////////////////////////////////////////////// -// colonequals.sty -// Alternate names for mathtools's macros: - -defineMacro("\\ratio", "\\vcentcolon"); -defineMacro("\\coloncolon", "\\dblcolon"); -defineMacro("\\colonequals", "\\coloneqq"); -defineMacro("\\coloncolonequals", "\\Coloneqq"); -defineMacro("\\equalscolon", "\\eqqcolon"); -defineMacro("\\equalscoloncolon", "\\Eqqcolon"); -defineMacro("\\colonminus", "\\coloneq"); -defineMacro("\\coloncolonminus", "\\Coloneq"); -defineMacro("\\minuscolon", "\\eqcolon"); -defineMacro("\\minuscoloncolon", "\\Eqcolon"); // \colonapprox name is same in mathtools and colonequals. - -defineMacro("\\coloncolonapprox", "\\Colonapprox"); // \colonsim name is same in mathtools and colonequals. - -defineMacro("\\coloncolonsim", "\\Colonsim"); // Additional macros, implemented by analogy with mathtools definitions: - -defineMacro("\\simcolon", "\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\vcentcolon}"); -defineMacro("\\simcoloncolon", "\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\dblcolon}"); -defineMacro("\\approxcolon", "\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\vcentcolon}"); -defineMacro("\\approxcoloncolon", "\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\dblcolon}"); // Present in newtxmath, pxfonts and txfonts - -defineMacro("\\notni", "\\html@mathml{\\not\\ni}{\\mathrel{\\char`\u220C}}"); -defineMacro("\\limsup", "\\DOTSB\\operatorname*{lim\\,sup}"); -defineMacro("\\liminf", "\\DOTSB\\operatorname*{lim\\,inf}"); ////////////////////////////////////////////////////////////////////// -// MathML alternates for KaTeX glyphs in the Unicode private area - -defineMacro("\\gvertneqq", "\\html@mathml{\\@gvertneqq}{\u2269}"); -defineMacro("\\lvertneqq", "\\html@mathml{\\@lvertneqq}{\u2268}"); -defineMacro("\\ngeqq", "\\html@mathml{\\@ngeqq}{\u2271}"); -defineMacro("\\ngeqslant", "\\html@mathml{\\@ngeqslant}{\u2271}"); -defineMacro("\\nleqq", "\\html@mathml{\\@nleqq}{\u2270}"); -defineMacro("\\nleqslant", "\\html@mathml{\\@nleqslant}{\u2270}"); -defineMacro("\\nshortmid", "\\html@mathml{\\@nshortmid}{∤}"); -defineMacro("\\nshortparallel", "\\html@mathml{\\@nshortparallel}{∦}"); -defineMacro("\\nsubseteqq", "\\html@mathml{\\@nsubseteqq}{\u2288}"); -defineMacro("\\nsupseteqq", "\\html@mathml{\\@nsupseteqq}{\u2289}"); -defineMacro("\\varsubsetneq", "\\html@mathml{\\@varsubsetneq}{⊊}"); -defineMacro("\\varsubsetneqq", "\\html@mathml{\\@varsubsetneqq}{⫋}"); -defineMacro("\\varsupsetneq", "\\html@mathml{\\@varsupsetneq}{⊋}"); -defineMacro("\\varsupsetneqq", "\\html@mathml{\\@varsupsetneqq}{⫌}"); -defineMacro("\\imath", "\\html@mathml{\\@imath}{\u0131}"); -defineMacro("\\jmath", "\\html@mathml{\\@jmath}{\u0237}"); ////////////////////////////////////////////////////////////////////// -// stmaryrd and semantic -// The stmaryrd and semantic packages render the next four items by calling a -// glyph. Those glyphs do not exist in the KaTeX fonts. Hence the macros. - -defineMacro("\\llbracket", "\\html@mathml{" + "\\mathopen{[\\mkern-3.2mu[}}" + "{\\mathopen{\\char`\u27E6}}"); -defineMacro("\\rrbracket", "\\html@mathml{" + "\\mathclose{]\\mkern-3.2mu]}}" + "{\\mathclose{\\char`\u27E7}}"); -defineMacro("\u27E6", "\\llbracket"); // blackboard bold [ - -defineMacro("\u27E7", "\\rrbracket"); // blackboard bold ] - -defineMacro("\\lBrace", "\\html@mathml{" + "\\mathopen{\\{\\mkern-3.2mu[}}" + "{\\mathopen{\\char`\u2983}}"); -defineMacro("\\rBrace", "\\html@mathml{" + "\\mathclose{]\\mkern-3.2mu\\}}}" + "{\\mathclose{\\char`\u2984}}"); -defineMacro("\u2983", "\\lBrace"); // blackboard bold { - -defineMacro("\u2984", "\\rBrace"); // blackboard bold } -// TODO: Create variable sized versions of the last two items. I believe that -// will require new font glyphs. -// The stmaryrd function `\minuso` provides a "Plimsoll" symbol that -// superimposes the characters \circ and \mathminus. Used in chemistry. - -defineMacro("\\minuso", "\\mathbin{\\html@mathml{" + "{\\mathrlap{\\mathchoice{\\kern{0.145em}}{\\kern{0.145em}}" + "{\\kern{0.1015em}}{\\kern{0.0725em}}\\circ}{-}}}" + "{\\char`⦵}}"); -defineMacro("⦵", "\\minuso"); ////////////////////////////////////////////////////////////////////// -// texvc.sty -// The texvc package contains macros available in mediawiki pages. -// We omit the functions deprecated at -// https://en.wikipedia.org/wiki/Help:Displaying_a_formula#Deprecated_syntax -// We also omit texvc's \O, which conflicts with \text{\O} - -defineMacro("\\darr", "\\downarrow"); -defineMacro("\\dArr", "\\Downarrow"); -defineMacro("\\Darr", "\\Downarrow"); -defineMacro("\\lang", "\\langle"); -defineMacro("\\rang", "\\rangle"); -defineMacro("\\uarr", "\\uparrow"); -defineMacro("\\uArr", "\\Uparrow"); -defineMacro("\\Uarr", "\\Uparrow"); -defineMacro("\\N", "\\mathbb{N}"); -defineMacro("\\R", "\\mathbb{R}"); -defineMacro("\\Z", "\\mathbb{Z}"); -defineMacro("\\alef", "\\aleph"); -defineMacro("\\alefsym", "\\aleph"); -defineMacro("\\Alpha", "\\mathrm{A}"); -defineMacro("\\Beta", "\\mathrm{B}"); -defineMacro("\\bull", "\\bullet"); -defineMacro("\\Chi", "\\mathrm{X}"); -defineMacro("\\clubs", "\\clubsuit"); -defineMacro("\\cnums", "\\mathbb{C}"); -defineMacro("\\Complex", "\\mathbb{C}"); -defineMacro("\\Dagger", "\\ddagger"); -defineMacro("\\diamonds", "\\diamondsuit"); -defineMacro("\\empty", "\\emptyset"); -defineMacro("\\Epsilon", "\\mathrm{E}"); -defineMacro("\\Eta", "\\mathrm{H}"); -defineMacro("\\exist", "\\exists"); -defineMacro("\\harr", "\\leftrightarrow"); -defineMacro("\\hArr", "\\Leftrightarrow"); -defineMacro("\\Harr", "\\Leftrightarrow"); -defineMacro("\\hearts", "\\heartsuit"); -defineMacro("\\image", "\\Im"); -defineMacro("\\infin", "\\infty"); -defineMacro("\\Iota", "\\mathrm{I}"); -defineMacro("\\isin", "\\in"); -defineMacro("\\Kappa", "\\mathrm{K}"); -defineMacro("\\larr", "\\leftarrow"); -defineMacro("\\lArr", "\\Leftarrow"); -defineMacro("\\Larr", "\\Leftarrow"); -defineMacro("\\lrarr", "\\leftrightarrow"); -defineMacro("\\lrArr", "\\Leftrightarrow"); -defineMacro("\\Lrarr", "\\Leftrightarrow"); -defineMacro("\\Mu", "\\mathrm{M}"); -defineMacro("\\natnums", "\\mathbb{N}"); -defineMacro("\\Nu", "\\mathrm{N}"); -defineMacro("\\Omicron", "\\mathrm{O}"); -defineMacro("\\plusmn", "\\pm"); -defineMacro("\\rarr", "\\rightarrow"); -defineMacro("\\rArr", "\\Rightarrow"); -defineMacro("\\Rarr", "\\Rightarrow"); -defineMacro("\\real", "\\Re"); -defineMacro("\\reals", "\\mathbb{R}"); -defineMacro("\\Reals", "\\mathbb{R}"); -defineMacro("\\Rho", "\\mathrm{P}"); -defineMacro("\\sdot", "\\cdot"); -defineMacro("\\sect", "\\S"); -defineMacro("\\spades", "\\spadesuit"); -defineMacro("\\sub", "\\subset"); -defineMacro("\\sube", "\\subseteq"); -defineMacro("\\supe", "\\supseteq"); -defineMacro("\\Tau", "\\mathrm{T}"); -defineMacro("\\thetasym", "\\vartheta"); // TODO: defineMacro("\\varcoppa", "\\\mbox{\\coppa}"); - -defineMacro("\\weierp", "\\wp"); -defineMacro("\\Zeta", "\\mathrm{Z}"); ////////////////////////////////////////////////////////////////////// -// statmath.sty -// https://ctan.math.illinois.edu/macros/latex/contrib/statmath/statmath.pdf - -defineMacro("\\argmin", "\\DOTSB\\operatorname*{arg\\,min}"); -defineMacro("\\argmax", "\\DOTSB\\operatorname*{arg\\,max}"); -defineMacro("\\plim", "\\DOTSB\\mathop{\\operatorname{plim}}\\limits"); ////////////////////////////////////////////////////////////////////// -// braket.sty -// http://ctan.math.washington.edu/tex-archive/macros/latex/contrib/braket/braket.pdf - -defineMacro("\\bra", "\\mathinner{\\langle{#1}|}"); -defineMacro("\\ket", "\\mathinner{|{#1}\\rangle}"); -defineMacro("\\braket", "\\mathinner{\\langle{#1}\\rangle}"); -defineMacro("\\Bra", "\\left\\langle#1\\right|"); -defineMacro("\\Ket", "\\left|#1\\right\\rangle"); // Custom Khan Academy colors, should be moved to an optional package - -defineMacro("\\blue", "\\textcolor{##6495ed}{#1}"); -defineMacro("\\orange", "\\textcolor{##ffa500}{#1}"); -defineMacro("\\pink", "\\textcolor{##ff00af}{#1}"); -defineMacro("\\red", "\\textcolor{##df0030}{#1}"); -defineMacro("\\green", "\\textcolor{##28ae7b}{#1}"); -defineMacro("\\gray", "\\textcolor{gray}{#1}"); -defineMacro("\\purple", "\\textcolor{##9d38bd}{#1}"); -defineMacro("\\blueA", "\\textcolor{##ccfaff}{#1}"); -defineMacro("\\blueB", "\\textcolor{##80f6ff}{#1}"); -defineMacro("\\blueC", "\\textcolor{##63d9ea}{#1}"); -defineMacro("\\blueD", "\\textcolor{##11accd}{#1}"); -defineMacro("\\blueE", "\\textcolor{##0c7f99}{#1}"); -defineMacro("\\tealA", "\\textcolor{##94fff5}{#1}"); -defineMacro("\\tealB", "\\textcolor{##26edd5}{#1}"); -defineMacro("\\tealC", "\\textcolor{##01d1c1}{#1}"); -defineMacro("\\tealD", "\\textcolor{##01a995}{#1}"); -defineMacro("\\tealE", "\\textcolor{##208170}{#1}"); -defineMacro("\\greenA", "\\textcolor{##b6ffb0}{#1}"); -defineMacro("\\greenB", "\\textcolor{##8af281}{#1}"); -defineMacro("\\greenC", "\\textcolor{##74cf70}{#1}"); -defineMacro("\\greenD", "\\textcolor{##1fab54}{#1}"); -defineMacro("\\greenE", "\\textcolor{##0d923f}{#1}"); -defineMacro("\\goldA", "\\textcolor{##ffd0a9}{#1}"); -defineMacro("\\goldB", "\\textcolor{##ffbb71}{#1}"); -defineMacro("\\goldC", "\\textcolor{##ff9c39}{#1}"); -defineMacro("\\goldD", "\\textcolor{##e07d10}{#1}"); -defineMacro("\\goldE", "\\textcolor{##a75a05}{#1}"); -defineMacro("\\redA", "\\textcolor{##fca9a9}{#1}"); -defineMacro("\\redB", "\\textcolor{##ff8482}{#1}"); -defineMacro("\\redC", "\\textcolor{##f9685d}{#1}"); -defineMacro("\\redD", "\\textcolor{##e84d39}{#1}"); -defineMacro("\\redE", "\\textcolor{##bc2612}{#1}"); -defineMacro("\\maroonA", "\\textcolor{##ffbde0}{#1}"); -defineMacro("\\maroonB", "\\textcolor{##ff92c6}{#1}"); -defineMacro("\\maroonC", "\\textcolor{##ed5fa6}{#1}"); -defineMacro("\\maroonD", "\\textcolor{##ca337c}{#1}"); -defineMacro("\\maroonE", "\\textcolor{##9e034e}{#1}"); -defineMacro("\\purpleA", "\\textcolor{##ddd7ff}{#1}"); -defineMacro("\\purpleB", "\\textcolor{##c6b9fc}{#1}"); -defineMacro("\\purpleC", "\\textcolor{##aa87ff}{#1}"); -defineMacro("\\purpleD", "\\textcolor{##7854ab}{#1}"); -defineMacro("\\purpleE", "\\textcolor{##543b78}{#1}"); -defineMacro("\\mintA", "\\textcolor{##f5f9e8}{#1}"); -defineMacro("\\mintB", "\\textcolor{##edf2df}{#1}"); -defineMacro("\\mintC", "\\textcolor{##e0e5cc}{#1}"); -defineMacro("\\grayA", "\\textcolor{##f6f7f7}{#1}"); -defineMacro("\\grayB", "\\textcolor{##f0f1f2}{#1}"); -defineMacro("\\grayC", "\\textcolor{##e3e5e6}{#1}"); -defineMacro("\\grayD", "\\textcolor{##d6d8da}{#1}"); -defineMacro("\\grayE", "\\textcolor{##babec2}{#1}"); -defineMacro("\\grayF", "\\textcolor{##888d93}{#1}"); -defineMacro("\\grayG", "\\textcolor{##626569}{#1}"); -defineMacro("\\grayH", "\\textcolor{##3b3e40}{#1}"); -defineMacro("\\grayI", "\\textcolor{##21242c}{#1}"); -defineMacro("\\kaBlue", "\\textcolor{##314453}{#1}"); -defineMacro("\\kaGreen", "\\textcolor{##71B307}{#1}"); -// CONCATENATED MODULE: ./src/MacroExpander.js -/** - * This file contains the “gullet” where macros are expanded - * until only non-macro tokens remain. - */ - - - - - - - -// List of commands that act like macros but aren't defined as a macro, -// function, or symbol. Used in `isDefined`. -var implicitCommands = { - "\\relax": true, - // MacroExpander.js - "^": true, - // Parser.js - "_": true, - // Parser.js - "\\limits": true, - // Parser.js - "\\nolimits": true // Parser.js - -}; - -var MacroExpander_MacroExpander = -/*#__PURE__*/ -function () { - function MacroExpander(input, settings, mode) { - this.settings = void 0; - this.expansionCount = void 0; - this.lexer = void 0; - this.macros = void 0; - this.stack = void 0; - this.mode = void 0; - this.settings = settings; - this.expansionCount = 0; - this.feed(input); // Make new global namespace - - this.macros = new Namespace_Namespace(macros, settings.macros); - this.mode = mode; - this.stack = []; // contains tokens in REVERSE order - } - /** - * Feed a new input string to the same MacroExpander - * (with existing macros etc.). - */ - - - var _proto = MacroExpander.prototype; - - _proto.feed = function feed(input) { - this.lexer = new Lexer_Lexer(input, this.settings); - } - /** - * Switches between "text" and "math" modes. - */ - ; - - _proto.switchMode = function switchMode(newMode) { - this.mode = newMode; - } - /** - * Start a new group nesting within all namespaces. - */ - ; - - _proto.beginGroup = function beginGroup() { - this.macros.beginGroup(); - } - /** - * End current group nesting within all namespaces. - */ - ; - - _proto.endGroup = function endGroup() { - this.macros.endGroup(); - } - /** - * Returns the topmost token on the stack, without expanding it. - * Similar in behavior to TeX's `\futurelet`. - */ - ; - - _proto.future = function future() { - if (this.stack.length === 0) { - this.pushToken(this.lexer.lex()); - } - - return this.stack[this.stack.length - 1]; - } - /** - * Remove and return the next unexpanded token. - */ - ; - - _proto.popToken = function popToken() { - this.future(); // ensure non-empty stack - - return this.stack.pop(); - } - /** - * Add a given token to the token stack. In particular, this get be used - * to put back a token returned from one of the other methods. - */ - ; - - _proto.pushToken = function pushToken(token) { - this.stack.push(token); - } - /** - * Append an array of tokens to the token stack. - */ - ; - - _proto.pushTokens = function pushTokens(tokens) { - var _this$stack; - - (_this$stack = this.stack).push.apply(_this$stack, tokens); - } - /** - * Consume all following space tokens, without expansion. - */ - ; - - _proto.consumeSpaces = function consumeSpaces() { - for (;;) { - var token = this.future(); - - if (token.text === " ") { - this.stack.pop(); - } else { - break; - } - } - } - /** - * Consume the specified number of arguments from the token stream, - * and return the resulting array of arguments. - */ - ; - - _proto.consumeArgs = function consumeArgs(numArgs) { - var args = []; // obtain arguments, either single token or balanced {…} group - - for (var i = 0; i < numArgs; ++i) { - this.consumeSpaces(); // ignore spaces before each argument - - var startOfArg = this.popToken(); - - if (startOfArg.text === "{") { - var arg = []; - var depth = 1; - - while (depth !== 0) { - var tok = this.popToken(); - arg.push(tok); - - if (tok.text === "{") { - ++depth; - } else if (tok.text === "}") { - --depth; - } else if (tok.text === "EOF") { - throw new src_ParseError("End of input in macro argument", startOfArg); - } - } - - arg.pop(); // remove last } - - arg.reverse(); // like above, to fit in with stack order - - args[i] = arg; - } else if (startOfArg.text === "EOF") { - throw new src_ParseError("End of input expecting macro argument"); - } else { - args[i] = [startOfArg]; - } - } - - return args; - } - /** - * Expand the next token only once if possible. - * - * If the token is expanded, the resulting tokens will be pushed onto - * the stack in reverse order and will be returned as an array, - * also in reverse order. - * - * If not, the next token will be returned without removing it - * from the stack. This case can be detected by a `Token` return value - * instead of an `Array` return value. - * - * In either case, the next token will be on the top of the stack, - * or the stack will be empty. - * - * Used to implement `expandAfterFuture` and `expandNextToken`. - * - * At the moment, macro expansion doesn't handle delimited macros, - * i.e. things like those defined by \def\foo#1\end{…}. - * See the TeX book page 202ff. for details on how those should behave. - * - * If expandableOnly, only expandable tokens are expanded and - * an undefined control sequence results in an error. - */ - ; - - _proto.expandOnce = function expandOnce(expandableOnly) { - var topToken = this.popToken(); - var name = topToken.text; - var expansion = !topToken.noexpand ? this._getExpansion(name) : null; - - if (expansion == null || expandableOnly && expansion.unexpandable) { - if (expandableOnly && expansion == null && name[0] === "\\" && !this.isDefined(name)) { - throw new src_ParseError("Undefined control sequence: " + name); - } - - this.pushToken(topToken); - return topToken; - } - - this.expansionCount++; - - if (this.expansionCount > this.settings.maxExpand) { - throw new src_ParseError("Too many expansions: infinite loop or " + "need to increase maxExpand setting"); - } - - var tokens = expansion.tokens; - - if (expansion.numArgs) { - var args = this.consumeArgs(expansion.numArgs); // paste arguments in place of the placeholders - - tokens = tokens.slice(); // make a shallow copy - - for (var i = tokens.length - 1; i >= 0; --i) { - var tok = tokens[i]; - - if (tok.text === "#") { - if (i === 0) { - throw new src_ParseError("Incomplete placeholder at end of macro body", tok); - } - - tok = tokens[--i]; // next token on stack - - if (tok.text === "#") { - // ## → # - tokens.splice(i + 1, 1); // drop first # - } else if (/^[1-9]$/.test(tok.text)) { - var _tokens; - - // replace the placeholder with the indicated argument - (_tokens = tokens).splice.apply(_tokens, [i, 2].concat(args[+tok.text - 1])); - } else { - throw new src_ParseError("Not a valid argument number", tok); - } - } - } - } // Concatenate expansion onto top of stack. - - - this.pushTokens(tokens); - return tokens; - } - /** - * Expand the next token only once (if possible), and return the resulting - * top token on the stack (without removing anything from the stack). - * Similar in behavior to TeX's `\expandafter\futurelet`. - * Equivalent to expandOnce() followed by future(). - */ - ; - - _proto.expandAfterFuture = function expandAfterFuture() { - this.expandOnce(); - return this.future(); - } - /** - * Recursively expand first token, then return first non-expandable token. - */ - ; - - _proto.expandNextToken = function expandNextToken() { - for (;;) { - var expanded = this.expandOnce(); // expandOnce returns Token if and only if it's fully expanded. - - if (expanded instanceof Token_Token) { - // \relax stops the expansion, but shouldn't get returned (a - // null return value couldn't get implemented as a function). - // the token after \noexpand is interpreted as if its meaning - // were ‘\relax’ - if (expanded.text === "\\relax" || expanded.treatAsRelax) { - this.stack.pop(); - } else { - return this.stack.pop(); // === expanded - } - } - } // Flow unable to figure out that this pathway is impossible. - // https://github.com/facebook/flow/issues/4808 - - - throw new Error(); // eslint-disable-line no-unreachable - } - /** - * Fully expand the given macro name and return the resulting list of - * tokens, or return `undefined` if no such macro is defined. - */ - ; - - _proto.expandMacro = function expandMacro(name) { - return this.macros.has(name) ? this.expandTokens([new Token_Token(name)]) : undefined; - } - /** - * Fully expand the given token stream and return the resulting list of tokens - */ - ; - - _proto.expandTokens = function expandTokens(tokens) { - var output = []; - var oldStackLength = this.stack.length; - this.pushTokens(tokens); - - while (this.stack.length > oldStackLength) { - var expanded = this.expandOnce(true); // expand only expandable tokens - // expandOnce returns Token if and only if it's fully expanded. - - if (expanded instanceof Token_Token) { - if (expanded.treatAsRelax) { - // the expansion of \noexpand is the token itself - expanded.noexpand = false; - expanded.treatAsRelax = false; - } - - output.push(this.stack.pop()); - } - } - - return output; - } - /** - * Fully expand the given macro name and return the result as a string, - * or return `undefined` if no such macro is defined. - */ - ; - - _proto.expandMacroAsText = function expandMacroAsText(name) { - var tokens = this.expandMacro(name); - - if (tokens) { - return tokens.map(function (token) { - return token.text; - }).join(""); - } else { - return tokens; - } - } - /** - * Returns the expanded macro as a reversed array of tokens and a macro - * argument count. Or returns `null` if no such macro. - */ - ; - - _proto._getExpansion = function _getExpansion(name) { - var definition = this.macros.get(name); - - if (definition == null) { - // mainly checking for undefined here - return definition; - } - - var expansion = typeof definition === "function" ? definition(this) : definition; - - if (typeof expansion === "string") { - var numArgs = 0; - - if (expansion.indexOf("#") !== -1) { - var stripped = expansion.replace(/##/g, ""); - - while (stripped.indexOf("#" + (numArgs + 1)) !== -1) { - ++numArgs; - } - } - - var bodyLexer = new Lexer_Lexer(expansion, this.settings); - var tokens = []; - var tok = bodyLexer.lex(); - - while (tok.text !== "EOF") { - tokens.push(tok); - tok = bodyLexer.lex(); - } - - tokens.reverse(); // to fit in with stack using push and pop - - var expanded = { - tokens: tokens, - numArgs: numArgs - }; - return expanded; - } - - return expansion; - } - /** - * Determine whether a command is currently "defined" (has some - * functionality), meaning that it's a macro (in the current group), - * a function, a symbol, or one of the special commands listed in - * `implicitCommands`. - */ - ; - - _proto.isDefined = function isDefined(name) { - return this.macros.has(name) || src_functions.hasOwnProperty(name) || src_symbols.math.hasOwnProperty(name) || src_symbols.text.hasOwnProperty(name) || implicitCommands.hasOwnProperty(name); - } - /** - * Determine whether a command is expandable. - */ - ; - - _proto.isExpandable = function isExpandable(name) { - var macro = this.macros.get(name); - return macro != null ? typeof macro === "string" || typeof macro === "function" || !macro.unexpandable // TODO(ylem): #2085 - : src_functions.hasOwnProperty(name) - /* && !functions[name].primitive*/ - ; - }; - - return MacroExpander; -}(); - - -// CONCATENATED MODULE: ./src/Parser.js -/* eslint no-constant-condition:0 */ - - - - - - - - - - // Pre-evaluate both modules as unicodeSymbols require String.normalize() - -var unicodeAccents = { - "́": { - "text": "\\'", - "math": "\\acute" - }, - "̀": { - "text": "\\`", - "math": "\\grave" - }, - "̈": { - "text": "\\\"", - "math": "\\ddot" - }, - "̃": { - "text": "\\~", - "math": "\\tilde" - }, - "̄": { - "text": "\\=", - "math": "\\bar" - }, - "̆": { - "text": "\\u", - "math": "\\breve" - }, - "̌": { - "text": "\\v", - "math": "\\check" - }, - "̂": { - "text": "\\^", - "math": "\\hat" - }, - "̇": { - "text": "\\.", - "math": "\\dot" - }, - "̊": { - "text": "\\r", - "math": "\\mathring" - }, - "̋": { - "text": "\\H" - } -}; -var unicodeSymbols = { - "á": "á", - "à": "à", - "ä": "ä", - "ǟ": "ǟ", - "ã": "ã", - "ā": "ā", - "ă": "ă", - "ắ": "ắ", - "ằ": "ằ", - "ẵ": "ẵ", - "ǎ": "ǎ", - "â": "â", - "ấ": "ấ", - "ầ": "ầ", - "ẫ": "ẫ", - "ȧ": "ȧ", - "ǡ": "ǡ", - "å": "å", - "ǻ": "ǻ", - "ḃ": "ḃ", - "ć": "ć", - "č": "č", - "ĉ": "ĉ", - "ċ": "ċ", - "ď": "ď", - "ḋ": "ḋ", - "é": "é", - "è": "è", - "ë": "ë", - "ẽ": "ẽ", - "ē": "ē", - "ḗ": "ḗ", - "ḕ": "ḕ", - "ĕ": "ĕ", - "ě": "ě", - "ê": "ê", - "ế": "ế", - "ề": "ề", - "ễ": "ễ", - "ė": "ė", - "ḟ": "ḟ", - "ǵ": "ǵ", - "ḡ": "ḡ", - "ğ": "ğ", - "ǧ": "ǧ", - "ĝ": "ĝ", - "ġ": "ġ", - "ḧ": "ḧ", - "ȟ": "ȟ", - "ĥ": "ĥ", - "ḣ": "ḣ", - "í": "í", - "ì": "ì", - "ï": "ï", - "ḯ": "ḯ", - "ĩ": "ĩ", - "ī": "ī", - "ĭ": "ĭ", - "ǐ": "ǐ", - "î": "î", - "ǰ": "ǰ", - "ĵ": "ĵ", - "ḱ": "ḱ", - "ǩ": "ǩ", - "ĺ": "ĺ", - "ľ": "ľ", - "ḿ": "ḿ", - "ṁ": "ṁ", - "ń": "ń", - "ǹ": "ǹ", - "ñ": "ñ", - "ň": "ň", - "ṅ": "ṅ", - "ó": "ó", - "ò": "ò", - "ö": "ö", - "ȫ": "ȫ", - "õ": "õ", - "ṍ": "ṍ", - "ṏ": "ṏ", - "ȭ": "ȭ", - "ō": "ō", - "ṓ": "ṓ", - "ṑ": "ṑ", - "ŏ": "ŏ", - "ǒ": "ǒ", - "ô": "ô", - "ố": "ố", - "ồ": "ồ", - "ỗ": "ỗ", - "ȯ": "ȯ", - "ȱ": "ȱ", - "ő": "ő", - "ṕ": "ṕ", - "ṗ": "ṗ", - "ŕ": "ŕ", - "ř": "ř", - "ṙ": "ṙ", - "ś": "ś", - "ṥ": "ṥ", - "š": "š", - "ṧ": "ṧ", - "ŝ": "ŝ", - "ṡ": "ṡ", - "ẗ": "ẗ", - "ť": "ť", - "ṫ": "ṫ", - "ú": "ú", - "ù": "ù", - "ü": "ü", - "ǘ": "ǘ", - "ǜ": "ǜ", - "ǖ": "ǖ", - "ǚ": "ǚ", - "ũ": "ũ", - "ṹ": "ṹ", - "ū": "ū", - "ṻ": "ṻ", - "ŭ": "ŭ", - "ǔ": "ǔ", - "û": "û", - "ů": "ů", - "ű": "ű", - "ṽ": "ṽ", - "ẃ": "ẃ", - "ẁ": "ẁ", - "ẅ": "ẅ", - "ŵ": "ŵ", - "ẇ": "ẇ", - "ẘ": "ẘ", - "ẍ": "ẍ", - "ẋ": "ẋ", - "ý": "ý", - "ỳ": "ỳ", - "ÿ": "ÿ", - "ỹ": "ỹ", - "ȳ": "ȳ", - "ŷ": "ŷ", - "ẏ": "ẏ", - "ẙ": "ẙ", - "ź": "ź", - "ž": "ž", - "ẑ": "ẑ", - "ż": "ż", - "Á": "Á", - "À": "À", - "Ä": "Ä", - "Ǟ": "Ǟ", - "Ã": "Ã", - "Ā": "Ā", - "Ă": "Ă", - "Ắ": "Ắ", - "Ằ": "Ằ", - "Ẵ": "Ẵ", - "Ǎ": "Ǎ", - "Â": "Â", - "Ấ": "Ấ", - "Ầ": "Ầ", - "Ẫ": "Ẫ", - "Ȧ": "Ȧ", - "Ǡ": "Ǡ", - "Å": "Å", - "Ǻ": "Ǻ", - "Ḃ": "Ḃ", - "Ć": "Ć", - "Č": "Č", - "Ĉ": "Ĉ", - "Ċ": "Ċ", - "Ď": "Ď", - "Ḋ": "Ḋ", - "É": "É", - "È": "È", - "Ë": "Ë", - "Ẽ": "Ẽ", - "Ē": "Ē", - "Ḗ": "Ḗ", - "Ḕ": "Ḕ", - "Ĕ": "Ĕ", - "Ě": "Ě", - "Ê": "Ê", - "Ế": "Ế", - "Ề": "Ề", - "Ễ": "Ễ", - "Ė": "Ė", - "Ḟ": "Ḟ", - "Ǵ": "Ǵ", - "Ḡ": "Ḡ", - "Ğ": "Ğ", - "Ǧ": "Ǧ", - "Ĝ": "Ĝ", - "Ġ": "Ġ", - "Ḧ": "Ḧ", - "Ȟ": "Ȟ", - "Ĥ": "Ĥ", - "Ḣ": "Ḣ", - "Í": "Í", - "Ì": "Ì", - "Ï": "Ï", - "Ḯ": "Ḯ", - "Ĩ": "Ĩ", - "Ī": "Ī", - "Ĭ": "Ĭ", - "Ǐ": "Ǐ", - "Î": "Î", - "İ": "İ", - "Ĵ": "Ĵ", - "Ḱ": "Ḱ", - "Ǩ": "Ǩ", - "Ĺ": "Ĺ", - "Ľ": "Ľ", - "Ḿ": "Ḿ", - "Ṁ": "Ṁ", - "Ń": "Ń", - "Ǹ": "Ǹ", - "Ñ": "Ñ", - "Ň": "Ň", - "Ṅ": "Ṅ", - "Ó": "Ó", - "Ò": "Ò", - "Ö": "Ö", - "Ȫ": "Ȫ", - "Õ": "Õ", - "Ṍ": "Ṍ", - "Ṏ": "Ṏ", - "Ȭ": "Ȭ", - "Ō": "Ō", - "Ṓ": "Ṓ", - "Ṑ": "Ṑ", - "Ŏ": "Ŏ", - "Ǒ": "Ǒ", - "Ô": "Ô", - "Ố": "Ố", - "Ồ": "Ồ", - "Ỗ": "Ỗ", - "Ȯ": "Ȯ", - "Ȱ": "Ȱ", - "Ő": "Ő", - "Ṕ": "Ṕ", - "Ṗ": "Ṗ", - "Ŕ": "Ŕ", - "Ř": "Ř", - "Ṙ": "Ṙ", - "Ś": "Ś", - "Ṥ": "Ṥ", - "Š": "Š", - "Ṧ": "Ṧ", - "Ŝ": "Ŝ", - "Ṡ": "Ṡ", - "Ť": "Ť", - "Ṫ": "Ṫ", - "Ú": "Ú", - "Ù": "Ù", - "Ü": "Ü", - "Ǘ": "Ǘ", - "Ǜ": "Ǜ", - "Ǖ": "Ǖ", - "Ǚ": "Ǚ", - "Ũ": "Ũ", - "Ṹ": "Ṹ", - "Ū": "Ū", - "Ṻ": "Ṻ", - "Ŭ": "Ŭ", - "Ǔ": "Ǔ", - "Û": "Û", - "Ů": "Ů", - "Ű": "Ű", - "Ṽ": "Ṽ", - "Ẃ": "Ẃ", - "Ẁ": "Ẁ", - "Ẅ": "Ẅ", - "Ŵ": "Ŵ", - "Ẇ": "Ẇ", - "Ẍ": "Ẍ", - "Ẋ": "Ẋ", - "Ý": "Ý", - "Ỳ": "Ỳ", - "Ÿ": "Ÿ", - "Ỹ": "Ỹ", - "Ȳ": "Ȳ", - "Ŷ": "Ŷ", - "Ẏ": "Ẏ", - "Ź": "Ź", - "Ž": "Ž", - "Ẑ": "Ẑ", - "Ż": "Ż", - "ά": "ά", - "ὰ": "ὰ", - "ᾱ": "ᾱ", - "ᾰ": "ᾰ", - "έ": "έ", - "ὲ": "ὲ", - "ή": "ή", - "ὴ": "ὴ", - "ί": "ί", - "ὶ": "ὶ", - "ϊ": "ϊ", - "ΐ": "ΐ", - "ῒ": "ῒ", - "ῑ": "ῑ", - "ῐ": "ῐ", - "ό": "ό", - "ὸ": "ὸ", - "ύ": "ύ", - "ὺ": "ὺ", - "ϋ": "ϋ", - "ΰ": "ΰ", - "ῢ": "ῢ", - "ῡ": "ῡ", - "ῠ": "ῠ", - "ώ": "ώ", - "ὼ": "ὼ", - "Ύ": "Ύ", - "Ὺ": "Ὺ", - "Ϋ": "Ϋ", - "Ῡ": "Ῡ", - "Ῠ": "Ῠ", - "Ώ": "Ώ", - "Ὼ": "Ὼ" -}; - -/** - * This file contains the parser used to parse out a TeX expression from the - * input. Since TeX isn't context-free, standard parsers don't work particularly - * well. - * - * The strategy of this parser is as such: - * - * The main functions (the `.parse...` ones) take a position in the current - * parse string to parse tokens from. The lexer (found in Lexer.js, stored at - * this.gullet.lexer) also supports pulling out tokens at arbitrary places. When - * individual tokens are needed at a position, the lexer is called to pull out a - * token, which is then used. - * - * The parser has a property called "mode" indicating the mode that - * the parser is currently in. Currently it has to be one of "math" or - * "text", which denotes whether the current environment is a math-y - * one or a text-y one (e.g. inside \text). Currently, this serves to - * limit the functions which can be used in text mode. - * - * The main functions then return an object which contains the useful data that - * was parsed at its given point, and a new position at the end of the parsed - * data. The main functions can call each other and continue the parsing by - * using the returned position as a new starting point. - * - * There are also extra `.handle...` functions, which pull out some reused - * functionality into self-contained functions. - * - * The functions return ParseNodes. - */ -var Parser_Parser = -/*#__PURE__*/ -function () { - function Parser(input, settings) { - this.mode = void 0; - this.gullet = void 0; - this.settings = void 0; - this.leftrightDepth = void 0; - this.nextToken = void 0; - // Start in math mode - this.mode = "math"; // Create a new macro expander (gullet) and (indirectly via that) also a - // new lexer (mouth) for this parser (stomach, in the language of TeX) - - this.gullet = new MacroExpander_MacroExpander(input, settings, this.mode); // Store the settings for use in parsing - - this.settings = settings; // Count leftright depth (for \middle errors) - - this.leftrightDepth = 0; - } - /** - * Checks a result to make sure it has the right type, and throws an - * appropriate error otherwise. - */ - - - var _proto = Parser.prototype; - - _proto.expect = function expect(text, consume) { - if (consume === void 0) { - consume = true; - } - - if (this.fetch().text !== text) { - throw new src_ParseError("Expected '" + text + "', got '" + this.fetch().text + "'", this.fetch()); - } - - if (consume) { - this.consume(); - } - } - /** - * Discards the current lookahead token, considering it consumed. - */ - ; - - _proto.consume = function consume() { - this.nextToken = null; - } - /** - * Return the current lookahead token, or if there isn't one (at the - * beginning, or if the previous lookahead token was consume()d), - * fetch the next token as the new lookahead token and return it. - */ - ; - - _proto.fetch = function fetch() { - if (this.nextToken == null) { - this.nextToken = this.gullet.expandNextToken(); - } - - return this.nextToken; - } - /** - * Switches between "text" and "math" modes. - */ - ; - - _proto.switchMode = function switchMode(newMode) { - this.mode = newMode; - this.gullet.switchMode(newMode); - } - /** - * Main parsing function, which parses an entire input. - */ - ; - - _proto.parse = function parse() { - if (!this.settings.globalGroup) { - // Create a group namespace for the math expression. - // (LaTeX creates a new group for every $...$, $$...$$, \[...\].) - this.gullet.beginGroup(); - } // Use old \color behavior (same as LaTeX's \textcolor) if requested. - // We do this within the group for the math expression, so it doesn't - // pollute settings.macros. - - - if (this.settings.colorIsTextColor) { - this.gullet.macros.set("\\color", "\\textcolor"); - } // Try to parse the input - - - var parse = this.parseExpression(false); // If we succeeded, make sure there's an EOF at the end - - this.expect("EOF"); // End the group namespace for the expression - - if (!this.settings.globalGroup) { - this.gullet.endGroup(); - } - - return parse; - }; - - _proto.parseExpression = function parseExpression(breakOnInfix, breakOnTokenText) { - var body = []; // Keep adding atoms to the body until we can't parse any more atoms (either - // we reached the end, a }, or a \right) - - while (true) { - // Ignore spaces in math mode - if (this.mode === "math") { - this.consumeSpaces(); - } - - var lex = this.fetch(); - - if (Parser.endOfExpression.indexOf(lex.text) !== -1) { - break; - } - - if (breakOnTokenText && lex.text === breakOnTokenText) { - break; - } - - if (breakOnInfix && src_functions[lex.text] && src_functions[lex.text].infix) { - break; - } - - var atom = this.parseAtom(breakOnTokenText); - - if (!atom) { - break; - } else if (atom.type === "internal") { - continue; - } - - body.push(atom); - } - - if (this.mode === "text") { - this.formLigatures(body); - } - - return this.handleInfixNodes(body); - } - /** - * Rewrites infix operators such as \over with corresponding commands such - * as \frac. - * - * There can only be one infix operator per group. If there's more than one - * then the expression is ambiguous. This can be resolved by adding {}. - */ - ; - - _proto.handleInfixNodes = function handleInfixNodes(body) { - var overIndex = -1; - var funcName; - - for (var i = 0; i < body.length; i++) { - if (body[i].type === "infix") { - if (overIndex !== -1) { - throw new src_ParseError("only one infix operator per group", body[i].token); - } - - overIndex = i; - funcName = body[i].replaceWith; - } - } - - if (overIndex !== -1 && funcName) { - var numerNode; - var denomNode; - var numerBody = body.slice(0, overIndex); - var denomBody = body.slice(overIndex + 1); - - if (numerBody.length === 1 && numerBody[0].type === "ordgroup") { - numerNode = numerBody[0]; - } else { - numerNode = { - type: "ordgroup", - mode: this.mode, - body: numerBody - }; - } - - if (denomBody.length === 1 && denomBody[0].type === "ordgroup") { - denomNode = denomBody[0]; - } else { - denomNode = { - type: "ordgroup", - mode: this.mode, - body: denomBody - }; - } - - var node; - - if (funcName === "\\\\abovefrac") { - node = this.callFunction(funcName, [numerNode, body[overIndex], denomNode], []); - } else { - node = this.callFunction(funcName, [numerNode, denomNode], []); - } - - return [node]; - } else { - return body; - } - } // The greediness of a superscript or subscript - ; - - /** - * Handle a subscript or superscript with nice errors. - */ - _proto.handleSupSubscript = function handleSupSubscript(name) { - var symbolToken = this.fetch(); - var symbol = symbolToken.text; - this.consume(); - var group = this.parseGroup(name, false, Parser.SUPSUB_GREEDINESS, undefined, undefined, true); // ignore spaces before sup/subscript argument - - if (!group) { - throw new src_ParseError("Expected group after '" + symbol + "'", symbolToken); - } - - return group; - } - /** - * Converts the textual input of an unsupported command into a text node - * contained within a color node whose color is determined by errorColor - */ - ; - - _proto.formatUnsupportedCmd = function formatUnsupportedCmd(text) { - var textordArray = []; - - for (var i = 0; i < text.length; i++) { - textordArray.push({ - type: "textord", - mode: "text", - text: text[i] - }); - } - - var textNode = { - type: "text", - mode: this.mode, - body: textordArray - }; - var colorNode = { - type: "color", - mode: this.mode, - color: this.settings.errorColor, - body: [textNode] - }; - return colorNode; - } - /** - * Parses a group with optional super/subscripts. - */ - ; - - _proto.parseAtom = function parseAtom(breakOnTokenText) { - // The body of an atom is an implicit group, so that things like - // \left(x\right)^2 work correctly. - var base = this.parseGroup("atom", false, null, breakOnTokenText); // In text mode, we don't have superscripts or subscripts - - if (this.mode === "text") { - return base; - } // Note that base may be empty (i.e. null) at this point. - - - var superscript; - var subscript; - - while (true) { - // Guaranteed in math mode, so eat any spaces first. - this.consumeSpaces(); // Lex the first token - - var lex = this.fetch(); - - if (lex.text === "\\limits" || lex.text === "\\nolimits") { - // We got a limit control - if (base && base.type === "op") { - var limits = lex.text === "\\limits"; - base.limits = limits; - base.alwaysHandleSupSub = true; - } else if (base && base.type === "operatorname" && base.alwaysHandleSupSub) { - var _limits = lex.text === "\\limits"; - - base.limits = _limits; - } else { - throw new src_ParseError("Limit controls must follow a math operator", lex); - } - - this.consume(); - } else if (lex.text === "^") { - // We got a superscript start - if (superscript) { - throw new src_ParseError("Double superscript", lex); - } - - superscript = this.handleSupSubscript("superscript"); - } else if (lex.text === "_") { - // We got a subscript start - if (subscript) { - throw new src_ParseError("Double subscript", lex); - } - - subscript = this.handleSupSubscript("subscript"); - } else if (lex.text === "'") { - // We got a prime - if (superscript) { - throw new src_ParseError("Double superscript", lex); - } - - var prime = { - type: "textord", - mode: this.mode, - text: "\\prime" - }; // Many primes can be grouped together, so we handle this here - - var primes = [prime]; - this.consume(); // Keep lexing tokens until we get something that's not a prime - - while (this.fetch().text === "'") { - // For each one, add another prime to the list - primes.push(prime); - this.consume(); - } // If there's a superscript following the primes, combine that - // superscript in with the primes. - - - if (this.fetch().text === "^") { - primes.push(this.handleSupSubscript("superscript")); - } // Put everything into an ordgroup as the superscript - - - superscript = { - type: "ordgroup", - mode: this.mode, - body: primes - }; - } else { - // If it wasn't ^, _, or ', stop parsing super/subscripts - break; - } - } // Base must be set if superscript or subscript are set per logic above, - // but need to check here for type check to pass. - - - if (superscript || subscript) { - // If we got either a superscript or subscript, create a supsub - return { - type: "supsub", - mode: this.mode, - base: base, - sup: superscript, - sub: subscript - }; - } else { - // Otherwise return the original body - return base; - } - } - /** - * Parses an entire function, including its base and all of its arguments. - */ - ; - - _proto.parseFunction = function parseFunction(breakOnTokenText, name, // For error reporting. - greediness) { - var token = this.fetch(); - var func = token.text; - var funcData = src_functions[func]; - - if (!funcData) { - return null; - } - - this.consume(); // consume command token - - if (greediness != null && funcData.greediness <= greediness) { - throw new src_ParseError("Got function '" + func + "' with no arguments" + (name ? " as " + name : ""), token); - } else if (this.mode === "text" && !funcData.allowedInText) { - throw new src_ParseError("Can't use function '" + func + "' in text mode", token); - } else if (this.mode === "math" && funcData.allowedInMath === false) { - throw new src_ParseError("Can't use function '" + func + "' in math mode", token); - } - - var _this$parseArguments = this.parseArguments(func, funcData), - args = _this$parseArguments.args, - optArgs = _this$parseArguments.optArgs; - - return this.callFunction(func, args, optArgs, token, breakOnTokenText); - } - /** - * Call a function handler with a suitable context and arguments. - */ - ; - - _proto.callFunction = function callFunction(name, args, optArgs, token, breakOnTokenText) { - var context = { - funcName: name, - parser: this, - token: token, - breakOnTokenText: breakOnTokenText - }; - var func = src_functions[name]; - - if (func && func.handler) { - return func.handler(context, args, optArgs); - } else { - throw new src_ParseError("No function handler for " + name); - } - } - /** - * Parses the arguments of a function or environment - */ - ; - - _proto.parseArguments = function parseArguments(func, // Should look like "\name" or "\begin{name}". - funcData) { - var totalArgs = funcData.numArgs + funcData.numOptionalArgs; - - if (totalArgs === 0) { - return { - args: [], - optArgs: [] - }; - } - - var baseGreediness = funcData.greediness; - var args = []; - var optArgs = []; - - for (var i = 0; i < totalArgs; i++) { - var argType = funcData.argTypes && funcData.argTypes[i]; - var isOptional = i < funcData.numOptionalArgs; // Ignore spaces between arguments. As the TeXbook says: - // "After you have said ‘\def\row#1#2{...}’, you are allowed to - // put spaces between the arguments (e.g., ‘\row x n’), because - // TeX doesn’t use single spaces as undelimited arguments." - - var consumeSpaces = i > 0 && !isOptional || // Also consume leading spaces in math mode, as parseSymbol - // won't know what to do with them. This can only happen with - // macros, e.g. \frac\foo\foo where \foo expands to a space symbol. - // In LaTeX, the \foo's get treated as (blank) arguments. - // In KaTeX, for now, both spaces will get consumed. - // TODO(edemaine) - i === 0 && !isOptional && this.mode === "math"; - var arg = this.parseGroupOfType("argument to '" + func + "'", argType, isOptional, baseGreediness, consumeSpaces); - - if (!arg) { - if (isOptional) { - optArgs.push(null); - continue; - } - - throw new src_ParseError("Expected group after '" + func + "'", this.fetch()); - } - - (isOptional ? optArgs : args).push(arg); - } - - return { - args: args, - optArgs: optArgs - }; - } - /** - * Parses a group when the mode is changing. - */ - ; - - _proto.parseGroupOfType = function parseGroupOfType(name, type, optional, greediness, consumeSpaces) { - switch (type) { - case "color": - if (consumeSpaces) { - this.consumeSpaces(); - } - - return this.parseColorGroup(optional); - - case "size": - if (consumeSpaces) { - this.consumeSpaces(); - } - - return this.parseSizeGroup(optional); - - case "url": - return this.parseUrlGroup(optional, consumeSpaces); - - case "math": - case "text": - return this.parseGroup(name, optional, greediness, undefined, type, consumeSpaces); - - case "hbox": - { - // hbox argument type wraps the argument in the equivalent of - // \hbox, which is like \text but switching to \textstyle size. - var group = this.parseGroup(name, optional, greediness, undefined, "text", consumeSpaces); - - if (!group) { - return group; - } - - var styledGroup = { - type: "styling", - mode: group.mode, - body: [group], - style: "text" // simulate \textstyle - - }; - return styledGroup; - } - - case "raw": - { - if (consumeSpaces) { - this.consumeSpaces(); - } - - if (optional && this.fetch().text === "{") { - return null; - } - - var token = this.parseStringGroup("raw", optional, true); - - if (token) { - return { - type: "raw", - mode: "text", - string: token.text - }; - } else { - throw new src_ParseError("Expected raw group", this.fetch()); - } - } - - case "original": - case null: - case undefined: - return this.parseGroup(name, optional, greediness, undefined, undefined, consumeSpaces); - - default: - throw new src_ParseError("Unknown group type as " + name, this.fetch()); - } - } - /** - * Discard any space tokens, fetching the next non-space token. - */ - ; - - _proto.consumeSpaces = function consumeSpaces() { - while (this.fetch().text === " ") { - this.consume(); - } - } - /** - * Parses a group, essentially returning the string formed by the - * brace-enclosed tokens plus some position information. - */ - ; - - _proto.parseStringGroup = function parseStringGroup(modeName, // Used to describe the mode in error messages. - optional, raw) { - var groupBegin = optional ? "[" : "{"; - var groupEnd = optional ? "]" : "}"; - var beginToken = this.fetch(); - - if (beginToken.text !== groupBegin) { - if (optional) { - return null; - } else if (raw && beginToken.text !== "EOF" && /[^{}[\]]/.test(beginToken.text)) { - this.consume(); - return beginToken; - } - } - - var outerMode = this.mode; - this.mode = "text"; - this.expect(groupBegin); - var str = ""; - var firstToken = this.fetch(); - var nested = 0; // allow nested braces in raw string group - - var lastToken = firstToken; - var nextToken; - - while ((nextToken = this.fetch()).text !== groupEnd || raw && nested > 0) { - switch (nextToken.text) { - case "EOF": - throw new src_ParseError("Unexpected end of input in " + modeName, firstToken.range(lastToken, str)); - - case groupBegin: - nested++; - break; - - case groupEnd: - nested--; - break; - } - - lastToken = nextToken; - str += lastToken.text; - this.consume(); - } - - this.expect(groupEnd); - this.mode = outerMode; - return firstToken.range(lastToken, str); - } - /** - * Parses a regex-delimited group: the largest sequence of tokens - * whose concatenated strings match `regex`. Returns the string - * formed by the tokens plus some position information. - */ - ; - - _proto.parseRegexGroup = function parseRegexGroup(regex, modeName) { - var outerMode = this.mode; - this.mode = "text"; - var firstToken = this.fetch(); - var lastToken = firstToken; - var str = ""; - var nextToken; - - while ((nextToken = this.fetch()).text !== "EOF" && regex.test(str + nextToken.text)) { - lastToken = nextToken; - str += lastToken.text; - this.consume(); - } - - if (str === "") { - throw new src_ParseError("Invalid " + modeName + ": '" + firstToken.text + "'", firstToken); - } - - this.mode = outerMode; - return firstToken.range(lastToken, str); - } - /** - * Parses a color description. - */ - ; - - _proto.parseColorGroup = function parseColorGroup(optional) { - var res = this.parseStringGroup("color", optional); - - if (!res) { - return null; - } - - var match = /^(#[a-f0-9]{3}|#?[a-f0-9]{6}|[a-z]+)$/i.exec(res.text); - - if (!match) { - throw new src_ParseError("Invalid color: '" + res.text + "'", res); - } - - var color = match[0]; - - if (/^[0-9a-f]{6}$/i.test(color)) { - // We allow a 6-digit HTML color spec without a leading "#". - // This follows the xcolor package's HTML color model. - // Predefined color names are all missed by this RegEx pattern. - color = "#" + color; - } - - return { - type: "color-token", - mode: this.mode, - color: color - }; - } - /** - * Parses a size specification, consisting of magnitude and unit. - */ - ; - - _proto.parseSizeGroup = function parseSizeGroup(optional) { - var res; - var isBlank = false; - - if (!optional && this.fetch().text !== "{") { - res = this.parseRegexGroup(/^[-+]? *(?:$|\d+|\d+\.\d*|\.\d*) *[a-z]{0,2} *$/, "size"); - } else { - res = this.parseStringGroup("size", optional); - } - - if (!res) { - return null; - } - - if (!optional && res.text.length === 0) { - // Because we've tested for what is !optional, this block won't - // affect \kern, \hspace, etc. It will capture the mandatory arguments - // to \genfrac and \above. - res.text = "0pt"; // Enable \above{} - - isBlank = true; // This is here specifically for \genfrac - } - - var match = /([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/.exec(res.text); - - if (!match) { - throw new src_ParseError("Invalid size: '" + res.text + "'", res); - } - - var data = { - number: +(match[1] + match[2]), - // sign + magnitude, cast to number - unit: match[3] - }; - - if (!validUnit(data)) { - throw new src_ParseError("Invalid unit: '" + data.unit + "'", res); - } - - return { - type: "size", - mode: this.mode, - value: data, - isBlank: isBlank - }; - } - /** - * Parses an URL, checking escaped letters and allowed protocols, - * and setting the catcode of % as an active character (as in \hyperref). - */ - ; - - _proto.parseUrlGroup = function parseUrlGroup(optional, consumeSpaces) { - this.gullet.lexer.setCatcode("%", 13); // active character - - var res = this.parseStringGroup("url", optional, true); // get raw string - - this.gullet.lexer.setCatcode("%", 14); // comment character - - if (!res) { - return null; - } // hyperref package allows backslashes alone in href, but doesn't - // generate valid links in such cases; we interpret this as - // "undefined" behaviour, and keep them as-is. Some browser will - // replace backslashes with forward slashes. - - - var url = res.text.replace(/\\([#$%&~_^{}])/g, '$1'); - return { - type: "url", - mode: this.mode, - url: url - }; - } - /** - * If `optional` is false or absent, this parses an ordinary group, - * which is either a single nucleus (like "x") or an expression - * in braces (like "{x+y}") or an implicit group, a group that starts - * at the current position, and ends right before a higher explicit - * group ends, or at EOF. - * If `optional` is true, it parses either a bracket-delimited expression - * (like "[x+y]") or returns null to indicate the absence of a - * bracket-enclosed group. - * If `mode` is present, switches to that mode while parsing the group, - * and switches back after. - */ - ; - - _proto.parseGroup = function parseGroup(name, // For error reporting. - optional, greediness, breakOnTokenText, mode, consumeSpaces) { - // Switch to specified mode - var outerMode = this.mode; - - if (mode) { - this.switchMode(mode); - } // Consume spaces if requested, crucially *after* we switch modes, - // so that the next non-space token is parsed in the correct mode. - - - if (consumeSpaces) { - this.consumeSpaces(); - } // Get first token - - - var firstToken = this.fetch(); - var text = firstToken.text; - var result; // Try to parse an open brace or \begingroup - - if (optional ? text === "[" : text === "{" || text === "\\begingroup") { - this.consume(); - var groupEnd = Parser.endOfGroup[text]; // Start a new group namespace - - this.gullet.beginGroup(); // If we get a brace, parse an expression - - var expression = this.parseExpression(false, groupEnd); - var lastToken = this.fetch(); // Check that we got a matching closing brace - - this.expect(groupEnd); // End group namespace - - this.gullet.endGroup(); - result = { - type: "ordgroup", - mode: this.mode, - loc: SourceLocation.range(firstToken, lastToken), - body: expression, - // A group formed by \begingroup...\endgroup is a semi-simple group - // which doesn't affect spacing in math mode, i.e., is transparent. - // https://tex.stackexchange.com/questions/1930/when-should-one- - // use-begingroup-instead-of-bgroup - semisimple: text === "\\begingroup" || undefined - }; - } else if (optional) { - // Return nothing for an optional group - result = null; - } else { - // If there exists a function with this name, parse the function. - // Otherwise, just return a nucleus - result = this.parseFunction(breakOnTokenText, name, greediness) || this.parseSymbol(); - - if (result == null && text[0] === "\\" && !implicitCommands.hasOwnProperty(text)) { - if (this.settings.throwOnError) { - throw new src_ParseError("Undefined control sequence: " + text, firstToken); - } - - result = this.formatUnsupportedCmd(text); - this.consume(); - } - } // Switch mode back - - - if (mode) { - this.switchMode(outerMode); - } - - return result; - } - /** - * Form ligature-like combinations of characters for text mode. - * This includes inputs like "--", "---", "``" and "''". - * The result will simply replace multiple textord nodes with a single - * character in each value by a single textord node having multiple - * characters in its value. The representation is still ASCII source. - * The group will be modified in place. - */ - ; - - _proto.formLigatures = function formLigatures(group) { - var n = group.length - 1; - - for (var i = 0; i < n; ++i) { - var a = group[i]; // $FlowFixMe: Not every node type has a `text` property. - - var v = a.text; - - if (v === "-" && group[i + 1].text === "-") { - if (i + 1 < n && group[i + 2].text === "-") { - group.splice(i, 3, { - type: "textord", - mode: "text", - loc: SourceLocation.range(a, group[i + 2]), - text: "---" - }); - n -= 2; - } else { - group.splice(i, 2, { - type: "textord", - mode: "text", - loc: SourceLocation.range(a, group[i + 1]), - text: "--" - }); - n -= 1; - } - } - - if ((v === "'" || v === "`") && group[i + 1].text === v) { - group.splice(i, 2, { - type: "textord", - mode: "text", - loc: SourceLocation.range(a, group[i + 1]), - text: v + v - }); - n -= 1; - } - } - } - /** - * Parse a single symbol out of the string. Here, we handle single character - * symbols and special functions like \verb. - */ - ; - - _proto.parseSymbol = function parseSymbol() { - var nucleus = this.fetch(); - var text = nucleus.text; - - if (/^\\verb[^a-zA-Z]/.test(text)) { - this.consume(); - var arg = text.slice(5); - var star = arg.charAt(0) === "*"; - - if (star) { - arg = arg.slice(1); - } // Lexer's tokenRegex is constructed to always have matching - // first/last characters. - - - if (arg.length < 2 || arg.charAt(0) !== arg.slice(-1)) { - throw new src_ParseError("\\verb assertion failed --\n please report what input caused this bug"); - } - - arg = arg.slice(1, -1); // remove first and last char - - return { - type: "verb", - mode: "text", - body: arg, - star: star - }; - } // At this point, we should have a symbol, possibly with accents. - // First expand any accented base symbol according to unicodeSymbols. - - - if (unicodeSymbols.hasOwnProperty(text[0]) && !src_symbols[this.mode][text[0]]) { - // This behavior is not strict (XeTeX-compatible) in math mode. - if (this.settings.strict && this.mode === "math") { - this.settings.reportNonstrict("unicodeTextInMathMode", "Accented Unicode text character \"" + text[0] + "\" used in " + "math mode", nucleus); - } - - text = unicodeSymbols[text[0]] + text.substr(1); - } // Strip off any combining characters - - - var match = combiningDiacriticalMarksEndRegex.exec(text); - - if (match) { - text = text.substring(0, match.index); - - if (text === 'i') { - text = "\u0131"; // dotless i, in math and text mode - } else if (text === 'j') { - text = "\u0237"; // dotless j, in math and text mode - } - } // Recognize base symbol - - - var symbol; - - if (src_symbols[this.mode][text]) { - if (this.settings.strict && this.mode === 'math' && extraLatin.indexOf(text) >= 0) { - this.settings.reportNonstrict("unicodeTextInMathMode", "Latin-1/Unicode text character \"" + text[0] + "\" used in " + "math mode", nucleus); - } - - var group = src_symbols[this.mode][text].group; - var loc = SourceLocation.range(nucleus); - var s; - - if (ATOMS.hasOwnProperty(group)) { - // $FlowFixMe - var family = group; - s = { - type: "atom", - mode: this.mode, - family: family, - loc: loc, - text: text - }; - } else { - // $FlowFixMe - s = { - type: group, - mode: this.mode, - loc: loc, - text: text - }; - } - - symbol = s; - } else if (text.charCodeAt(0) >= 0x80) { - // no symbol for e.g. ^ - if (this.settings.strict) { - if (!supportedCodepoint(text.charCodeAt(0))) { - this.settings.reportNonstrict("unknownSymbol", "Unrecognized Unicode character \"" + text[0] + "\"" + (" (" + text.charCodeAt(0) + ")"), nucleus); - } else if (this.mode === "math") { - this.settings.reportNonstrict("unicodeTextInMathMode", "Unicode text character \"" + text[0] + "\" used in math mode", nucleus); - } - } // All nonmathematical Unicode characters are rendered as if they - // are in text mode (wrapped in \text) because that's what it - // takes to render them in LaTeX. Setting `mode: this.mode` is - // another natural choice (the user requested math mode), but - // this makes it more difficult for getCharacterMetrics() to - // distinguish Unicode characters without metrics and those for - // which we want to simulate the letter M. - - - symbol = { - type: "textord", - mode: "text", - loc: SourceLocation.range(nucleus), - text: text - }; - } else { - return null; // EOF, ^, _, {, }, etc. - } - - this.consume(); // Transform combining characters into accents - - if (match) { - for (var i = 0; i < match[0].length; i++) { - var accent = match[0][i]; - - if (!unicodeAccents[accent]) { - throw new src_ParseError("Unknown accent ' " + accent + "'", nucleus); - } - - var command = unicodeAccents[accent][this.mode]; - - if (!command) { - throw new src_ParseError("Accent " + accent + " unsupported in " + this.mode + " mode", nucleus); - } - - symbol = { - type: "accent", - mode: this.mode, - loc: SourceLocation.range(nucleus), - label: command, - isStretchy: false, - isShifty: true, - base: symbol - }; - } - } - - return symbol; - }; - - return Parser; -}(); - -Parser_Parser.endOfExpression = ["}", "\\endgroup", "\\end", "\\right", "&"]; -Parser_Parser.endOfGroup = { - "[": "]", - "{": "}", - "\\begingroup": "\\endgroup" - /** - * Parses an "expression", which is a list of atoms. - * - * `breakOnInfix`: Should the parsing stop when we hit infix nodes? This - * happens when functions have higher precendence han infix - * nodes in implicit parses. - * - * `breakOnTokenText`: The text of the token that the expression should end - * with, or `null` if something else should end the - * expression. - */ - -}; -Parser_Parser.SUPSUB_GREEDINESS = 1; - -// CONCATENATED MODULE: ./src/parseTree.js -/** - * Provides a single function for parsing an expression using a Parser - * TODO(emily): Remove this - */ - - - -/** - * Parses an expression using a Parser, then returns the parsed result. - */ -var parseTree_parseTree = function parseTree(toParse, settings) { - if (!(typeof toParse === 'string' || toParse instanceof String)) { - throw new TypeError('KaTeX can only parse string typed expression'); - } - - var parser = new Parser_Parser(toParse, settings); // Blank out any \df@tag to avoid spurious "Duplicate \tag" errors - - delete parser.gullet.macros.current["\\df@tag"]; - var tree = parser.parse(); // If the input used \tag, it will set the \df@tag macro to the tag. - // In this case, we separately parse the tag and wrap the tree. - - if (parser.gullet.macros.get("\\df@tag")) { - if (!settings.displayMode) { - throw new src_ParseError("\\tag works only in display equations"); - } - - parser.gullet.feed("\\df@tag"); - tree = [{ - type: "tag", - mode: "text", - body: tree, - tag: parser.parse() - }]; - } - - return tree; -}; - -/* harmony default export */ var src_parseTree = (parseTree_parseTree); -// CONCATENATED MODULE: ./katex.js -/* eslint no-console:0 */ - -/** - * This is the main entry point for KaTeX. Here, we expose functions for - * rendering expressions either to DOM nodes or to markup strings. - * - * We also expose the ParseError class to check if errors thrown from KaTeX are - * errors in the expression, or errors in javascript handling. - */ - - - - - - - - - - -/** - * Parse and build an expression, and place that expression in the DOM node - * given. - */ -var katex_render = function render(expression, baseNode, options) { - baseNode.textContent = ""; - var node = katex_renderToDomTree(expression, options).toNode(); - baseNode.appendChild(node); -}; // KaTeX's styles don't work properly in quirks mode. Print out an error, and -// disable rendering. - - -if (typeof document !== "undefined") { - if (document.compatMode !== "CSS1Compat") { - typeof console !== "undefined" && console.warn("Warning: KaTeX doesn't work in quirks mode. Make sure your " + "website has a suitable doctype."); - - katex_render = function render() { - throw new src_ParseError("KaTeX doesn't work in quirks mode."); - }; - } -} -/** - * Parse and build an expression, and return the markup for that. - */ - - -var renderToString = function renderToString(expression, options) { - var markup = katex_renderToDomTree(expression, options).toMarkup(); - return markup; -}; -/** - * Parse an expression and return the parse tree. - */ - - -var katex_generateParseTree = function generateParseTree(expression, options) { - var settings = new Settings_Settings(options); - return src_parseTree(expression, settings); -}; -/** - * If the given error is a KaTeX ParseError and options.throwOnError is false, - * renders the invalid LaTeX as a span with hover title giving the KaTeX - * error message. Otherwise, simply throws the error. - */ - - -var katex_renderError = function renderError(error, expression, options) { - if (options.throwOnError || !(error instanceof src_ParseError)) { - throw error; - } - - var node = buildCommon.makeSpan(["katex-error"], [new domTree_SymbolNode(expression)]); - node.setAttribute("title", error.toString()); - node.setAttribute("style", "color:" + options.errorColor); - return node; -}; -/** - * Generates and returns the katex build tree. This is used for advanced - * use cases (like rendering to custom output). - */ - - -var katex_renderToDomTree = function renderToDomTree(expression, options) { - var settings = new Settings_Settings(options); - - try { - var tree = src_parseTree(expression, settings); - return buildTree_buildTree(tree, expression, settings); - } catch (error) { - return katex_renderError(error, expression, settings); - } -}; -/** - * Generates and returns the katex build tree, with just HTML (no MathML). - * This is used for advanced use cases (like rendering to custom output). - */ - - -var katex_renderToHTMLTree = function renderToHTMLTree(expression, options) { - var settings = new Settings_Settings(options); - - try { - var tree = src_parseTree(expression, settings); - return buildTree_buildHTMLTree(tree, expression, settings); - } catch (error) { - return katex_renderError(error, expression, settings); - } -}; - -/* harmony default export */ var katex_0 = ({ - /** - * Current KaTeX version - */ - version: "0.12.0", - - /** - * Renders the given LaTeX into an HTML+MathML combination, and adds - * it as a child to the specified DOM node. - */ - render: katex_render, - - /** - * Renders the given LaTeX into an HTML+MathML combination string, - * for sending to the client. - */ - renderToString: renderToString, - - /** - * KaTeX error, usually during parsing. - */ - ParseError: src_ParseError, - - /** - * Parses the given LaTeX into KaTeX's internal parse tree structure, - * without rendering to HTML or MathML. - * - * NOTE: This method is not currently recommended for public use. - * The internal tree representation is unstable and is very likely - * to change. Use at your own risk. - */ - __parse: katex_generateParseTree, - - /** - * Renders the given LaTeX into an HTML+MathML internal DOM tree - * representation, without flattening that representation to a string. - * - * NOTE: This method is not currently recommended for public use. - * The internal tree representation is unstable and is very likely - * to change. Use at your own risk. - */ - __renderToDomTree: katex_renderToDomTree, - - /** - * Renders the given LaTeX into an HTML internal DOM tree representation, - * without MathML and without flattening that representation to a string. - * - * NOTE: This method is not currently recommended for public use. - * The internal tree representation is unstable and is very likely - * to change. Use at your own risk. - */ - __renderToHTMLTree: katex_renderToHTMLTree, - - /** - * extends internal font metrics object with a new object - * each key in the new object represents a font name - */ - __setFontMetrics: setFontMetrics, - - /** - * adds a new symbol to builtin symbols table - */ - __defineSymbol: defineSymbol, - - /** - * adds a new macro to builtin macro list - */ - __defineMacro: defineMacro, - - /** - * Expose the dom tree node types, which can be useful for type checking nodes. - * - * NOTE: This method is not currently recommended for public use. - * The internal tree representation is unstable and is very likely - * to change. Use at your own risk. - */ - __domTree: { - Span: domTree_Span, - Anchor: domTree_Anchor, - SymbolNode: domTree_SymbolNode, - SvgNode: SvgNode, - PathNode: domTree_PathNode, - LineNode: LineNode - } -}); -// CONCATENATED MODULE: ./katex.webpack.js -/** - * This is the webpack entry point for KaTeX. As ECMAScript, flow[1] and jest[2] - * doesn't support CSS modules natively, a separate entry point is used and - * it is not flowtyped. - * - * [1] https://gist.github.com/lambdahands/d19e0da96285b749f0ef - * [2] https://facebook.github.io/jest/docs/en/webpack.html - */ - - -/* harmony default export */ var katex_webpack = __webpack_exports__["default"] = (katex_0); - -/***/ }) -/******/ ])["default"]; -}); \ No newline at end of file diff --git a/docs/static/katex/katex.min.css b/docs/static/katex/katex.min.css deleted file mode 100644 index 98b7c7bde86..00000000000 --- a/docs/static/katex/katex.min.css +++ /dev/null @@ -1 +0,0 @@ -@font-face{font-family:KaTeX_AMS;src:url(fonts/KaTeX_AMS-Regular.woff2) format("woff2"),url(fonts/KaTeX_AMS-Regular.woff) format("woff"),url(fonts/KaTeX_AMS-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Caligraphic;src:url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Bold.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Caligraphic;src:url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Regular.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Fraktur;src:url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Bold.woff) format("woff"),url(fonts/KaTeX_Fraktur-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Fraktur;src:url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Regular.woff) format("woff"),url(fonts/KaTeX_Fraktur-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Bold.woff2) format("woff2"),url(fonts/KaTeX_Main-Bold.woff) format("woff"),url(fonts/KaTeX_Main-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Main-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Main-BoldItalic.ttf) format("truetype");font-weight:700;font-style:italic}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Italic.woff2) format("woff2"),url(fonts/KaTeX_Main-Italic.woff) format("woff"),url(fonts/KaTeX_Main-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2"),url(fonts/KaTeX_Main-Regular.woff) format("woff"),url(fonts/KaTeX_Main-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Math;src:url(fonts/KaTeX_Math-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Math-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Math-BoldItalic.ttf) format("truetype");font-weight:700;font-style:italic}@font-face{font-family:KaTeX_Math;src:url(fonts/KaTeX_Math-Italic.woff2) format("woff2"),url(fonts/KaTeX_Math-Italic.woff) format("woff"),url(fonts/KaTeX_Math-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:"KaTeX_SansSerif";src:url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Bold.woff) format("woff"),url(fonts/KaTeX_SansSerif-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:"KaTeX_SansSerif";src:url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Italic.woff) format("woff"),url(fonts/KaTeX_SansSerif-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:"KaTeX_SansSerif";src:url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Regular.woff) format("woff"),url(fonts/KaTeX_SansSerif-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Script;src:url(fonts/KaTeX_Script-Regular.woff2) format("woff2"),url(fonts/KaTeX_Script-Regular.woff) format("woff"),url(fonts/KaTeX_Script-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size1;src:url(fonts/KaTeX_Size1-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size1-Regular.woff) format("woff"),url(fonts/KaTeX_Size1-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size2;src:url(fonts/KaTeX_Size2-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size2-Regular.woff) format("woff"),url(fonts/KaTeX_Size2-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size3;src:url(fonts/KaTeX_Size3-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size3-Regular.woff) format("woff"),url(fonts/KaTeX_Size3-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size4;src:url(fonts/KaTeX_Size4-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size4-Regular.woff) format("woff"),url(fonts/KaTeX_Size4-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Typewriter;src:url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2"),url(fonts/KaTeX_Typewriter-Regular.woff) format("woff"),url(fonts/KaTeX_Typewriter-Regular.ttf) format("truetype");font-weight:400;font-style:normal}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;text-indent:0;text-rendering:auto;border-color:currentColor}.katex *{-ms-high-contrast-adjust:none!important}.katex .katex-version:after{content:"0.12.0"}.katex .katex-mathml{position:absolute;clip:rect(1px,1px,1px,1px);padding:0;border:0;height:1px;width:1px;overflow:hidden}.katex .katex-html>.newline{display:block}.katex .base{position:relative;white-space:nowrap;width:min-content}.katex .base,.katex .strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-weight:700;font-style:italic}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{display:inline-table;table-layout:fixed;border-collapse:collapse}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;vertical-align:bottom;position:relative}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;vertical-align:bottom;font-size:1px;width:2px;min-width:2px}.katex .vbox{-ms-flex-direction:column;flex-direction:column;align-items:baseline}.katex .hbox,.katex .vbox{display:-ms-inline-flexbox;display:inline-flex}.katex .hbox{-ms-flex-direction:row;flex-direction:row;width:100%}.katex .thinbox{display:inline-flex;flex-direction:row;width:0;max-width:0}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{display:inline-block;width:100%;border-bottom-style:solid}.katex .hdashline,.katex .hline,.katex .mfrac .frac-line,.katex .overline .overline-line,.katex .rule,.katex .underline .underline-line{min-height:1px}.katex .mspace{display:inline-block}.katex .clap,.katex .llap,.katex .rlap{width:0;position:relative}.katex .clap>.inner,.katex .llap>.inner,.katex .rlap>.inner{position:absolute}.katex .clap>.fix,.katex .llap>.fix,.katex .rlap>.fix{display:inline-block}.katex .llap>.inner{right:0}.katex .clap>.inner,.katex .rlap>.inner{left:0}.katex .clap>.inner>span{margin-left:-50%;margin-right:50%}.katex .rule{display:inline-block;border:0 solid;position:relative}.katex .hline,.katex .overline .overline-line,.katex .underline .underline-line{display:inline-block;width:100%;border-bottom-style:solid}.katex .hdashline{display:inline-block;width:100%;border-bottom-style:dashed}.katex .sqrt>.root{margin-left:.27777778em;margin-right:-.55555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .sizing.reset-size2.size1{font-size:.83333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .sizing.reset-size2.size3{font-size:1.16666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .sizing.reset-size2.size4{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .sizing.reset-size2.size6{font-size:1.66666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .sizing.reset-size2.size10{font-size:3.45666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .sizing.reset-size2.size11{font-size:4.14666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .sizing.reset-size3.size1{font-size:.71428571em}.katex .fontsize-ensurer.reset-size3.size2,.katex .sizing.reset-size3.size2{font-size:.85714286em}.katex .fontsize-ensurer.reset-size3.size3,.katex .sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .sizing.reset-size3.size4{font-size:1.14285714em}.katex .fontsize-ensurer.reset-size3.size5,.katex .sizing.reset-size3.size5{font-size:1.28571429em}.katex .fontsize-ensurer.reset-size3.size6,.katex .sizing.reset-size3.size6{font-size:1.42857143em}.katex .fontsize-ensurer.reset-size3.size7,.katex .sizing.reset-size3.size7{font-size:1.71428571em}.katex .fontsize-ensurer.reset-size3.size8,.katex .sizing.reset-size3.size8{font-size:2.05714286em}.katex .fontsize-ensurer.reset-size3.size9,.katex .sizing.reset-size3.size9{font-size:2.46857143em}.katex .fontsize-ensurer.reset-size3.size10,.katex .sizing.reset-size3.size10{font-size:2.96285714em}.katex .fontsize-ensurer.reset-size3.size11,.katex .sizing.reset-size3.size11{font-size:3.55428571em}.katex .fontsize-ensurer.reset-size4.size1,.katex .sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .sizing.reset-size5.size1{font-size:.55555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .sizing.reset-size5.size2{font-size:.66666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .sizing.reset-size5.size3{font-size:.77777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .sizing.reset-size5.size4{font-size:.88888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .sizing.reset-size5.size6{font-size:1.11111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .sizing.reset-size5.size7{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .sizing.reset-size5.size10{font-size:2.30444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .sizing.reset-size5.size11{font-size:2.76444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .sizing.reset-size7.size1{font-size:.41666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .sizing.reset-size7.size3{font-size:.58333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .sizing.reset-size7.size4{font-size:.66666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .sizing.reset-size7.size6{font-size:.83333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .sizing.reset-size7.size10{font-size:1.72833333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .sizing.reset-size7.size11{font-size:2.07333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .sizing.reset-size8.size1{font-size:.34722222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .sizing.reset-size8.size2{font-size:.41666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .sizing.reset-size8.size3{font-size:.48611111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .sizing.reset-size8.size4{font-size:.55555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .sizing.reset-size8.size6{font-size:.69444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .sizing.reset-size8.size7{font-size:.83333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .sizing.reset-size8.size10{font-size:1.44027778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .sizing.reset-size8.size11{font-size:1.72777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .sizing.reset-size9.size1{font-size:.28935185em}.katex .fontsize-ensurer.reset-size9.size2,.katex .sizing.reset-size9.size2{font-size:.34722222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .sizing.reset-size9.size3{font-size:.40509259em}.katex .fontsize-ensurer.reset-size9.size4,.katex .sizing.reset-size9.size4{font-size:.46296296em}.katex .fontsize-ensurer.reset-size9.size5,.katex .sizing.reset-size9.size5{font-size:.52083333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .sizing.reset-size9.size6{font-size:.5787037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .sizing.reset-size9.size7{font-size:.69444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .sizing.reset-size9.size8{font-size:.83333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .sizing.reset-size9.size10{font-size:1.20023148em}.katex .fontsize-ensurer.reset-size9.size11,.katex .sizing.reset-size9.size11{font-size:1.43981481em}.katex .fontsize-ensurer.reset-size10.size1,.katex .sizing.reset-size10.size1{font-size:.24108004em}.katex .fontsize-ensurer.reset-size10.size2,.katex .sizing.reset-size10.size2{font-size:.28929605em}.katex .fontsize-ensurer.reset-size10.size3,.katex .sizing.reset-size10.size3{font-size:.33751205em}.katex .fontsize-ensurer.reset-size10.size4,.katex .sizing.reset-size10.size4{font-size:.38572806em}.katex .fontsize-ensurer.reset-size10.size5,.katex .sizing.reset-size10.size5{font-size:.43394407em}.katex .fontsize-ensurer.reset-size10.size6,.katex .sizing.reset-size10.size6{font-size:.48216008em}.katex .fontsize-ensurer.reset-size10.size7,.katex .sizing.reset-size10.size7{font-size:.57859209em}.katex .fontsize-ensurer.reset-size10.size8,.katex .sizing.reset-size10.size8{font-size:.69431051em}.katex .fontsize-ensurer.reset-size10.size9,.katex .sizing.reset-size10.size9{font-size:.83317261em}.katex .fontsize-ensurer.reset-size10.size10,.katex .sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .sizing.reset-size10.size11{font-size:1.19961427em}.katex .fontsize-ensurer.reset-size11.size1,.katex .sizing.reset-size11.size1{font-size:.20096463em}.katex .fontsize-ensurer.reset-size11.size2,.katex .sizing.reset-size11.size2{font-size:.24115756em}.katex .fontsize-ensurer.reset-size11.size3,.katex .sizing.reset-size11.size3{font-size:.28135048em}.katex .fontsize-ensurer.reset-size11.size4,.katex .sizing.reset-size11.size4{font-size:.32154341em}.katex .fontsize-ensurer.reset-size11.size5,.katex .sizing.reset-size11.size5{font-size:.36173633em}.katex .fontsize-ensurer.reset-size11.size6,.katex .sizing.reset-size11.size6{font-size:.40192926em}.katex .fontsize-ensurer.reset-size11.size7,.katex .sizing.reset-size11.size7{font-size:.48231511em}.katex .fontsize-ensurer.reset-size11.size8,.katex .sizing.reset-size11.size8{font-size:.57877814em}.katex .fontsize-ensurer.reset-size11.size9,.katex .sizing.reset-size11.size9{font-size:.69453376em}.katex .fontsize-ensurer.reset-size11.size10,.katex .sizing.reset-size11.size10{font-size:.83360129em}.katex .fontsize-ensurer.reset-size11.size11,.katex .sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .op-limits>.vlist-t{text-align:center}.katex .accent>.vlist-t{text-align:center}.katex .accent .accent-body{position:relative}.katex .accent .accent-body:not(.accent-full){width:0}.katex .overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{display:block;position:absolute;width:100%;height:inherit;fill:currentColor;stroke:currentColor;fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex svg path{stroke:none}.katex img{border-style:none;min-width:0;min-height:0;max-width:none;max-height:none}.katex .stretchy{width:100%;display:block;position:relative;overflow:hidden}.katex .stretchy:after,.katex .stretchy:before{content:""}.katex .hide-tail{width:100%;position:relative;overflow:hidden}.katex .halfarrow-left{position:absolute;left:0;width:50.2%;overflow:hidden}.katex .halfarrow-right{position:absolute;right:0;width:50.2%;overflow:hidden}.katex .brace-left{position:absolute;left:0;width:25.1%;overflow:hidden}.katex .brace-center{position:absolute;left:25%;width:50%;overflow:hidden}.katex .brace-right{position:absolute;right:0;width:25.1%;overflow:hidden}.katex .x-arrow-pad{padding:0 .5em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{box-sizing:border-box;border:.04em solid}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .sout{border-bottom-style:solid;border-bottom-width:.08em}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.tag{left:0;right:auto}.katex-display.fleqn>.katex{text-align:left;padding-left:2em} diff --git a/docs/static/katex/katex.min.js b/docs/static/katex/katex.min.js deleted file mode 100644 index 4df7e1722c7..00000000000 --- a/docs/static/katex/katex.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.katex=e():t.katex=e()}("undefined"!=typeof self?self:this,function(){return function(t){var e={};function r(a){if(e[a])return e[a].exports;var n=e[a]={i:a,l:!1,exports:{}};return t[a].call(n.exports,n,n.exports,r),n.l=!0,n.exports}return r.m=t,r.c=e,r.d=function(t,e,a){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:a})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var a=Object.create(null);if(r.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var n in t)r.d(a,n,function(e){return t[e]}.bind(null,n));return a},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=1)}([function(t,e,r){},function(t,e,r){"use strict";r.r(e);r(0);var a=function(){function t(t,e,r){this.lexer=void 0,this.start=void 0,this.end=void 0,this.lexer=t,this.start=e,this.end=r}return t.range=function(e,r){return r?e&&e.loc&&r.loc&&e.loc.lexer===r.loc.lexer?new t(e.loc.lexer,e.loc.start,r.loc.end):null:e&&e.loc},t}(),n=function(){function t(t,e){this.text=void 0,this.loc=void 0,this.noexpand=void 0,this.treatAsRelax=void 0,this.text=t,this.loc=e}return t.prototype.range=function(e,r){return new t(r,a.range(this,e))},t}(),i=function t(e,r){this.position=void 0;var a,n="KaTeX parse error: "+e,i=r&&r.loc;if(i&&i.start<=i.end){var o=i.lexer.input;a=i.start;var s=i.end;a===o.length?n+=" at end of input: ":n+=" at position "+(a+1)+": ";var l=o.slice(a,s).replace(/[^]/g,"$&\u0332");n+=(a>15?"\u2026"+o.slice(a-15,a):o.slice(0,a))+l+(s+15":">","<":"<",'"':""","'":"'"},h=/[&><"']/g;var m=function t(e){return"ordgroup"===e.type?1===e.body.length?t(e.body[0]):e:"color"===e.type?1===e.body.length?t(e.body[0]):e:"font"===e.type?t(e.body):e},c={contains:function(t,e){return-1!==t.indexOf(e)},deflt:function(t,e){return void 0===t?e:t},escape:function(t){return String(t).replace(h,function(t){return l[t]})},hyphenate:function(t){return t.replace(s,"-$1").toLowerCase()},getBaseElem:m,isCharacterBox:function(t){var e=m(t);return"mathord"===e.type||"textord"===e.type||"atom"===e.type},protocolFromUrl:function(t){var e=/^\s*([^\\/#]*?)(?::|�*58|�*3a)/i.exec(t);return null!=e?e[1]:"_relative"}},u=function(){function t(t){this.displayMode=void 0,this.output=void 0,this.leqno=void 0,this.fleqn=void 0,this.throwOnError=void 0,this.errorColor=void 0,this.macros=void 0,this.minRuleThickness=void 0,this.colorIsTextColor=void 0,this.strict=void 0,this.trust=void 0,this.maxSize=void 0,this.maxExpand=void 0,this.globalGroup=void 0,t=t||{},this.displayMode=c.deflt(t.displayMode,!1),this.output=c.deflt(t.output,"htmlAndMathml"),this.leqno=c.deflt(t.leqno,!1),this.fleqn=c.deflt(t.fleqn,!1),this.throwOnError=c.deflt(t.throwOnError,!0),this.errorColor=c.deflt(t.errorColor,"#cc0000"),this.macros=t.macros||{},this.minRuleThickness=Math.max(0,c.deflt(t.minRuleThickness,0)),this.colorIsTextColor=c.deflt(t.colorIsTextColor,!1),this.strict=c.deflt(t.strict,"warn"),this.trust=c.deflt(t.trust,!1),this.maxSize=Math.max(0,c.deflt(t.maxSize,1/0)),this.maxExpand=Math.max(0,c.deflt(t.maxExpand,1e3)),this.globalGroup=c.deflt(t.globalGroup,!1)}var e=t.prototype;return e.reportNonstrict=function(t,e,r){var a=this.strict;if("function"==typeof a&&(a=a(t,e,r)),a&&"ignore"!==a){if(!0===a||"error"===a)throw new o("LaTeX-incompatible input and strict mode is set to 'error': "+e+" ["+t+"]",r);"warn"===a?"undefined"!=typeof console&&console.warn("LaTeX-incompatible input and strict mode is set to 'warn': "+e+" ["+t+"]"):"undefined"!=typeof console&&console.warn("LaTeX-incompatible input and strict mode is set to unrecognized '"+a+"': "+e+" ["+t+"]")}},e.useStrictBehavior=function(t,e,r){var a=this.strict;if("function"==typeof a)try{a=a(t,e,r)}catch(t){a="error"}return!(!a||"ignore"===a)&&(!0===a||"error"===a||("warn"===a?("undefined"!=typeof console&&console.warn("LaTeX-incompatible input and strict mode is set to 'warn': "+e+" ["+t+"]"),!1):("undefined"!=typeof console&&console.warn("LaTeX-incompatible input and strict mode is set to unrecognized '"+a+"': "+e+" ["+t+"]"),!1)))},e.isTrusted=function(t){t.url&&!t.protocol&&(t.protocol=c.protocolFromUrl(t.url));var e="function"==typeof this.trust?this.trust(t):this.trust;return Boolean(e)},t}(),p=function(){function t(t,e,r){this.id=void 0,this.size=void 0,this.cramped=void 0,this.id=t,this.size=e,this.cramped=r}var e=t.prototype;return e.sup=function(){return d[f[this.id]]},e.sub=function(){return d[g[this.id]]},e.fracNum=function(){return d[x[this.id]]},e.fracDen=function(){return d[v[this.id]]},e.cramp=function(){return d[b[this.id]]},e.text=function(){return d[y[this.id]]},e.isTight=function(){return this.size>=2},t}(),d=[new p(0,0,!1),new p(1,0,!0),new p(2,1,!1),new p(3,1,!0),new p(4,2,!1),new p(5,2,!0),new p(6,3,!1),new p(7,3,!0)],f=[4,5,4,5,6,7,6,7],g=[5,5,5,5,7,7,7,7],x=[2,3,4,5,6,7,6,7],v=[3,3,5,5,7,7,7,7],b=[1,1,3,3,5,5,7,7],y=[0,1,2,3,2,3,2,3],w={DISPLAY:d[0],TEXT:d[2],SCRIPT:d[4],SCRIPTSCRIPT:d[6]},k=[{name:"latin",blocks:[[256,591],[768,879]]},{name:"cyrillic",blocks:[[1024,1279]]},{name:"brahmic",blocks:[[2304,4255]]},{name:"georgian",blocks:[[4256,4351]]},{name:"cjk",blocks:[[12288,12543],[19968,40879],[65280,65376]]},{name:"hangul",blocks:[[44032,55215]]}];var S=[];function M(t){for(var e=0;e=S[e]&&t<=S[e+1])return!0;return!1}k.forEach(function(t){return t.blocks.forEach(function(t){return S.push.apply(S,t)})});var z={leftParenInner:"M291 0 H417 V300 H291 z",rightParenInner:"M457 0 H583 V300 H457 z",doubleleftarrow:"M262 157\nl10-10c34-36 62.7-77 86-123 3.3-8 5-13.3 5-16 0-5.3-6.7-8-20-8-7.3\n 0-12.2.5-14.5 1.5-2.3 1-4.8 4.5-7.5 10.5-49.3 97.3-121.7 169.3-217 216-28\n 14-57.3 25-88 33-6.7 2-11 3.8-13 5.5-2 1.7-3 4.2-3 7.5s1 5.8 3 7.5\nc2 1.7 6.3 3.5 13 5.5 68 17.3 128.2 47.8 180.5 91.5 52.3 43.7 93.8 96.2 124.5\n 157.5 9.3 8 15.3 12.3 18 13h6c12-.7 18-4 18-10 0-2-1.7-7-5-15-23.3-46-52-87\n-86-123l-10-10h399738v-40H218c328 0 0 0 0 0l-10-8c-26.7-20-65.7-43-117-69 2.7\n-2 6-3.7 10-5 36.7-16 72.3-37.3 107-64l10-8h399782v-40z\nm8 0v40h399730v-40zm0 194v40h399730v-40z",doublerightarrow:"M399738 392l\n-10 10c-34 36-62.7 77-86 123-3.3 8-5 13.3-5 16 0 5.3 6.7 8 20 8 7.3 0 12.2-.5\n 14.5-1.5 2.3-1 4.8-4.5 7.5-10.5 49.3-97.3 121.7-169.3 217-216 28-14 57.3-25 88\n-33 6.7-2 11-3.8 13-5.5 2-1.7 3-4.2 3-7.5s-1-5.8-3-7.5c-2-1.7-6.3-3.5-13-5.5-68\n-17.3-128.2-47.8-180.5-91.5-52.3-43.7-93.8-96.2-124.5-157.5-9.3-8-15.3-12.3-18\n-13h-6c-12 .7-18 4-18 10 0 2 1.7 7 5 15 23.3 46 52 87 86 123l10 10H0v40h399782\nc-328 0 0 0 0 0l10 8c26.7 20 65.7 43 117 69-2.7 2-6 3.7-10 5-36.7 16-72.3 37.3\n-107 64l-10 8H0v40zM0 157v40h399730v-40zm0 194v40h399730v-40z",leftarrow:"M400000 241H110l3-3c68.7-52.7 113.7-120\n 135-202 4-14.7 6-23 6-25 0-7.3-7-11-21-11-8 0-13.2.8-15.5 2.5-2.3 1.7-4.2 5.8\n-5.5 12.5-1.3 4.7-2.7 10.3-4 17-12 48.7-34.8 92-68.5 130S65.3 228.3 18 247\nc-10 4-16 7.7-18 11 0 8.7 6 14.3 18 17 47.3 18.7 87.8 47 121.5 85S196 441.3 208\n 490c.7 2 1.3 5 2 9s1.2 6.7 1.5 8c.3 1.3 1 3.3 2 6s2.2 4.5 3.5 5.5c1.3 1 3.3\n 1.8 6 2.5s6 1 10 1c14 0 21-3.7 21-11 0-2-2-10.3-6-25-20-79.3-65-146.7-135-202\n l-3-3h399890zM100 241v40h399900v-40z",leftbrace:"M6 548l-6-6v-35l6-11c56-104 135.3-181.3 238-232 57.3-28.7 117\n-45 179-50h399577v120H403c-43.3 7-81 15-113 26-100.7 33-179.7 91-237 174-2.7\n 5-6 9-10 13-.7 1-7.3 1-20 1H6z",leftbraceunder:"M0 6l6-6h17c12.688 0 19.313.3 20 1 4 4 7.313 8.3 10 13\n 35.313 51.3 80.813 93.8 136.5 127.5 55.688 33.7 117.188 55.8 184.5 66.5.688\n 0 2 .3 4 1 18.688 2.7 76 4.3 172 5h399450v120H429l-6-1c-124.688-8-235-61.7\n-331-161C60.687 138.7 32.312 99.3 7 54L0 41V6z",leftgroup:"M400000 80\nH435C64 80 168.3 229.4 21 260c-5.9 1.2-18 0-18 0-2 0-3-1-3-3v-38C76 61 257 0\n 435 0h399565z",leftgroupunder:"M400000 262\nH435C64 262 168.3 112.6 21 82c-5.9-1.2-18 0-18 0-2 0-3 1-3 3v38c76 158 257 219\n 435 219h399565z",leftharpoon:"M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3\n-3.3 10.2-9.5 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5\n-18.3 3-21-1.3-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7\n-196 228-6.7 4.7-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40z",leftharpoonplus:"M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3-3.3 10.2-9.5\n 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5-18.3 3-21-1.3\n-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7-196 228-6.7 4.7\n-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40zM0 435v40h400000v-40z\nm0 0v40h400000v-40z",leftharpoondown:"M7 241c-4 4-6.333 8.667-7 14 0 5.333.667 9 2 11s5.333\n 5.333 12 10c90.667 54 156 130 196 228 3.333 10.667 6.333 16.333 9 17 2 .667 5\n 1 9 1h5c10.667 0 16.667-2 18-6 2-2.667 1-9.667-3-21-32-87.333-82.667-157.667\n-152-211l-3-3h399907v-40zM93 281 H400000 v-40L7 241z",leftharpoondownplus:"M7 435c-4 4-6.3 8.7-7 14 0 5.3.7 9 2 11s5.3 5.3 12\n 10c90.7 54 156 130 196 228 3.3 10.7 6.3 16.3 9 17 2 .7 5 1 9 1h5c10.7 0 16.7\n-2 18-6 2-2.7 1-9.7-3-21-32-87.3-82.7-157.7-152-211l-3-3h399907v-40H7zm93 0\nv40h399900v-40zM0 241v40h399900v-40zm0 0v40h399900v-40z",lefthook:"M400000 281 H103s-33-11.2-61-33.5S0 197.3 0 164s14.2-61.2 42.5\n-83.5C70.8 58.2 104 47 142 47 c16.7 0 25 6.7 25 20 0 12-8.7 18.7-26 20-40 3.3\n-68.7 15.7-86 37-10 12-15 25.3-15 40 0 22.7 9.8 40.7 29.5 54 19.7 13.3 43.5 21\n 71.5 23h399859zM103 281v-40h399897v40z",leftlinesegment:"M40 281 V428 H0 V94 H40 V241 H400000 v40z\nM40 281 V428 H0 V94 H40 V241 H400000 v40z",leftmapsto:"M40 281 V448H0V74H40V241H400000v40z\nM40 281 V448H0V74H40V241H400000v40z",leftToFrom:"M0 147h400000v40H0zm0 214c68 40 115.7 95.7 143 167h22c15.3 0 23\n-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69-70-101l-7-8h399905v-40H95l7-8\nc28.7-32 52-65.7 70-101 10.7-23.3 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 265.3\n 68 321 0 361zm0-174v-40h399900v40zm100 154v40h399900v-40z",longequal:"M0 50 h400000 v40H0z m0 194h40000v40H0z\nM0 50 h400000 v40H0z m0 194h40000v40H0z",midbrace:"M200428 334\nc-100.7-8.3-195.3-44-280-108-55.3-42-101.7-93-139-153l-9-14c-2.7 4-5.7 8.7-9 14\n-53.3 86.7-123.7 153-211 199-66.7 36-137.3 56.3-212 62H0V214h199568c178.3-11.7\n 311.7-78.3 403-201 6-8 9.7-12 11-12 .7-.7 6.7-1 18-1s17.3.3 18 1c1.3 0 5 4 11\n 12 44.7 59.3 101.3 106.3 170 141s145.3 54.3 229 60h199572v120z",midbraceunder:"M199572 214\nc100.7 8.3 195.3 44 280 108 55.3 42 101.7 93 139 153l9 14c2.7-4 5.7-8.7 9-14\n 53.3-86.7 123.7-153 211-199 66.7-36 137.3-56.3 212-62h199568v120H200432c-178.3\n 11.7-311.7 78.3-403 201-6 8-9.7 12-11 12-.7.7-6.7 1-18 1s-17.3-.3-18-1c-1.3 0\n-5-4-11-12-44.7-59.3-101.3-106.3-170-141s-145.3-54.3-229-60H0V214z",oiintSize1:"M512.6 71.6c272.6 0 320.3 106.8 320.3 178.2 0 70.8-47.7 177.6\n-320.3 177.6S193.1 320.6 193.1 249.8c0-71.4 46.9-178.2 319.5-178.2z\nm368.1 178.2c0-86.4-60.9-215.4-368.1-215.4-306.4 0-367.3 129-367.3 215.4 0 85.8\n60.9 214.8 367.3 214.8 307.2 0 368.1-129 368.1-214.8z",oiintSize2:"M757.8 100.1c384.7 0 451.1 137.6 451.1 230 0 91.3-66.4 228.8\n-451.1 228.8-386.3 0-452.7-137.5-452.7-228.8 0-92.4 66.4-230 452.7-230z\nm502.4 230c0-111.2-82.4-277.2-502.4-277.2s-504 166-504 277.2\nc0 110 84 276 504 276s502.4-166 502.4-276z",oiiintSize1:"M681.4 71.6c408.9 0 480.5 106.8 480.5 178.2 0 70.8-71.6 177.6\n-480.5 177.6S202.1 320.6 202.1 249.8c0-71.4 70.5-178.2 479.3-178.2z\nm525.8 178.2c0-86.4-86.8-215.4-525.7-215.4-437.9 0-524.7 129-524.7 215.4 0\n85.8 86.8 214.8 524.7 214.8 438.9 0 525.7-129 525.7-214.8z",oiiintSize2:"M1021.2 53c603.6 0 707.8 165.8 707.8 277.2 0 110-104.2 275.8\n-707.8 275.8-606 0-710.2-165.8-710.2-275.8C311 218.8 415.2 53 1021.2 53z\nm770.4 277.1c0-131.2-126.4-327.6-770.5-327.6S248.4 198.9 248.4 330.1\nc0 130 128.8 326.4 772.7 326.4s770.5-196.4 770.5-326.4z",rightarrow:"M0 241v40h399891c-47.3 35.3-84 78-110 128\n-16.7 32-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20\n 11 8 0 13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7\n 39-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85\n-40.5-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5\n-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67\n 151.7 139 205zm0 0v40h399900v-40z",rightbrace:"M400000 542l\n-6 6h-17c-12.7 0-19.3-.3-20-1-4-4-7.3-8.3-10-13-35.3-51.3-80.8-93.8-136.5-127.5\ns-117.2-55.8-184.5-66.5c-.7 0-2-.3-4-1-18.7-2.7-76-4.3-172-5H0V214h399571l6 1\nc124.7 8 235 61.7 331 161 31.3 33.3 59.7 72.7 85 118l7 13v35z",rightbraceunder:"M399994 0l6 6v35l-6 11c-56 104-135.3 181.3-238 232-57.3\n 28.7-117 45-179 50H-300V214h399897c43.3-7 81-15 113-26 100.7-33 179.7-91 237\n-174 2.7-5 6-9 10-13 .7-1 7.3-1 20-1h17z",rightgroup:"M0 80h399565c371 0 266.7 149.4 414 180 5.9 1.2 18 0 18 0 2 0\n 3-1 3-3v-38c-76-158-257-219-435-219H0z",rightgroupunder:"M0 262h399565c371 0 266.7-149.4 414-180 5.9-1.2 18 0 18\n 0 2 0 3 1 3 3v38c-76 158-257 219-435 219H0z",rightharpoon:"M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3\n-3.7-15.3-11-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2\n-10.7 0-16.7 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58\n 69.2 92 94.5zm0 0v40h399900v-40z",rightharpoonplus:"M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3-3.7-15.3-11\n-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2-10.7 0-16.7\n 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58 69.2 92 94.5z\nm0 0v40h399900v-40z m100 194v40h399900v-40zm0 0v40h399900v-40z",rightharpoondown:"M399747 511c0 7.3 6.7 11 20 11 8 0 13-.8 15-2.5s4.7-6.8\n 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3 8.5-5.8 9.5\n-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3-64.7 57-92 95\n-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 241v40h399900v-40z",rightharpoondownplus:"M399747 705c0 7.3 6.7 11 20 11 8 0 13-.8\n 15-2.5s4.7-6.8 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3\n 8.5-5.8 9.5-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3\n-64.7 57-92 95-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 435v40h399900v-40z\nm0-194v40h400000v-40zm0 0v40h400000v-40z",righthook:"M399859 241c-764 0 0 0 0 0 40-3.3 68.7-15.7 86-37 10-12 15-25.3\n 15-40 0-22.7-9.8-40.7-29.5-54-19.7-13.3-43.5-21-71.5-23-17.3-1.3-26-8-26-20 0\n-13.3 8.7-20 26-20 38 0 71 11.2 99 33.5 0 0 7 5.6 21 16.7 14 11.2 21 33.5 21\n 66.8s-14 61.2-42 83.5c-28 22.3-61 33.5-99 33.5L0 241z M0 281v-40h399859v40z",rightlinesegment:"M399960 241 V94 h40 V428 h-40 V281 H0 v-40z\nM399960 241 V94 h40 V428 h-40 V281 H0 v-40z",rightToFrom:"M400000 167c-70.7-42-118-97.7-142-167h-23c-15.3 0-23 .3-23\n 1 0 1.3 5.3 13.7 16 37 18 35.3 41.3 69 70 101l7 8H0v40h399905l-7 8c-28.7 32\n-52 65.7-70 101-10.7 23.3-16 35.7-16 37 0 .7 7.7 1 23 1h23c24-69.3 71.3-125 142\n-167z M100 147v40h399900v-40zM0 341v40h399900v-40z",twoheadleftarrow:"M0 167c68 40\n 115.7 95.7 143 167h22c15.3 0 23-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69\n-70-101l-7-8h125l9 7c50.7 39.3 85 86 103 140h46c0-4.7-6.3-18.7-19-42-18-35.3\n-40-67.3-66-96l-9-9h399716v-40H284l9-9c26-28.7 48-60.7 66-96 12.7-23.333 19\n-37.333 19-42h-46c-18 54-52.3 100.7-103 140l-9 7H95l7-8c28.7-32 52-65.7 70-101\n 10.7-23.333 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 71.3 68 127 0 167z",twoheadrightarrow:"M400000 167\nc-68-40-115.7-95.7-143-167h-22c-15.3 0-23 .3-23 1 0 1.3 5.3 13.7 16 37 18 35.3\n 41.3 69 70 101l7 8h-125l-9-7c-50.7-39.3-85-86-103-140h-46c0 4.7 6.3 18.7 19 42\n 18 35.3 40 67.3 66 96l9 9H0v40h399716l-9 9c-26 28.7-48 60.7-66 96-12.7 23.333\n-19 37.333-19 42h46c18-54 52.3-100.7 103-140l9-7h125l-7 8c-28.7 32-52 65.7-70\n 101-10.7 23.333-16 35.7-16 37 0 .7 7.7 1 23 1h22c27.3-71.3 75-127 143-167z",tilde1:"M200 55.538c-77 0-168 73.953-177 73.953-3 0-7\n-2.175-9-5.437L2 97c-1-2-2-4-2-6 0-4 2-7 5-9l20-12C116 12 171 0 207 0c86 0\n 114 68 191 68 78 0 168-68 177-68 4 0 7 2 9 5l12 19c1 2.175 2 4.35 2 6.525 0\n 4.35-2 7.613-5 9.788l-19 13.05c-92 63.077-116.937 75.308-183 76.128\n-68.267.847-113-73.952-191-73.952z",tilde2:"M344 55.266c-142 0-300.638 81.316-311.5 86.418\n-8.01 3.762-22.5 10.91-23.5 5.562L1 120c-1-2-1-3-1-4 0-5 3-9 8-10l18.4-9C160.9\n 31.9 283 0 358 0c148 0 188 122 331 122s314-97 326-97c4 0 8 2 10 7l7 21.114\nc1 2.14 1 3.21 1 4.28 0 5.347-3 9.626-7 10.696l-22.3 12.622C852.6 158.372 751\n 181.476 676 181.476c-149 0-189-126.21-332-126.21z",tilde3:"M786 59C457 59 32 175.242 13 175.242c-6 0-10-3.457\n-11-10.37L.15 138c-1-7 3-12 10-13l19.2-6.4C378.4 40.7 634.3 0 804.3 0c337 0\n 411.8 157 746.8 157 328 0 754-112 773-112 5 0 10 3 11 9l1 14.075c1 8.066-.697\n 16.595-6.697 17.492l-21.052 7.31c-367.9 98.146-609.15 122.696-778.15 122.696\n -338 0-409-156.573-744-156.573z",tilde4:"M786 58C457 58 32 177.487 13 177.487c-6 0-10-3.345\n-11-10.035L.15 143c-1-7 3-12 10-13l22-6.7C381.2 35 637.15 0 807.15 0c337 0 409\n 177 744 177 328 0 754-127 773-127 5 0 10 3 11 9l1 14.794c1 7.805-3 13.38-9\n 14.495l-20.7 5.574c-366.85 99.79-607.3 139.372-776.3 139.372-338 0-409\n -175.236-744-175.236z",vec:"M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5\n3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11\n10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63\n-1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1\n-7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59\nH213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359\nc-16-25.333-24-45-24-59z",widehat1:"M529 0h5l519 115c5 1 9 5 9 10 0 1-1 2-1 3l-4 22\nc-1 5-5 9-11 9h-2L532 67 19 159h-2c-5 0-9-4-11-9l-5-22c-1-6 2-12 8-13z",widehat2:"M1181 0h2l1171 176c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 220h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z",widehat3:"M1181 0h2l1171 236c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 280h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z",widehat4:"M1181 0h2l1171 296c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 340h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z",widecheck1:"M529,159h5l519,-115c5,-1,9,-5,9,-10c0,-1,-1,-2,-1,-3l-4,-22c-1,\n-5,-5,-9,-11,-9h-2l-512,92l-513,-92h-2c-5,0,-9,4,-11,9l-5,22c-1,6,2,12,8,13z",widecheck2:"M1181,220h2l1171,-176c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,153l-1167,-153h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z",widecheck3:"M1181,280h2l1171,-236c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,213l-1167,-213h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z",widecheck4:"M1181,340h2l1171,-296c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,273l-1167,-273h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z",baraboveleftarrow:"M400000 620h-399890l3 -3c68.7 -52.7 113.7 -120 135 -202\nc4 -14.7 6 -23 6 -25c0 -7.3 -7 -11 -21 -11c-8 0 -13.2 0.8 -15.5 2.5\nc-2.3 1.7 -4.2 5.8 -5.5 12.5c-1.3 4.7 -2.7 10.3 -4 17c-12 48.7 -34.8 92 -68.5 130\ns-74.2 66.3 -121.5 85c-10 4 -16 7.7 -18 11c0 8.7 6 14.3 18 17c47.3 18.7 87.8 47\n121.5 85s56.5 81.3 68.5 130c0.7 2 1.3 5 2 9s1.2 6.7 1.5 8c0.3 1.3 1 3.3 2 6\ns2.2 4.5 3.5 5.5c1.3 1 3.3 1.8 6 2.5s6 1 10 1c14 0 21 -3.7 21 -11\nc0 -2 -2 -10.3 -6 -25c-20 -79.3 -65 -146.7 -135 -202l-3 -3h399890z\nM100 620v40h399900v-40z M0 241v40h399900v-40zM0 241v40h399900v-40z",rightarrowabovebar:"M0 241v40h399891c-47.3 35.3-84 78-110 128-16.7 32\n-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 11 8 0\n13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 39\n-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85-40.5\n-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5\n-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67\n151.7 139 205zm96 379h399894v40H0zm0 0h399904v40H0z",baraboveshortleftharpoon:"M507,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11\nc1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17\nc2,0.7,5,1,9,1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21\nc-32,-87.3,-82.7,-157.7,-152,-211c0,0,-3,-3,-3,-3l399351,0l0,-40\nc-398570,0,-399437,0,-399437,0z M593 435 v40 H399500 v-40z\nM0 281 v-40 H399908 v40z M0 281 v-40 H399908 v40z",rightharpoonaboveshortbar:"M0,241 l0,40c399126,0,399993,0,399993,0\nc4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,\n-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6\nc-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z\nM0 241 v40 H399908 v-40z M0 475 v-40 H399500 v40z M0 475 v-40 H399500 v40z",shortbaraboveleftharpoon:"M7,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11\nc1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17c2,0.7,5,1,9,\n1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21c-32,-87.3,-82.7,-157.7,\n-152,-211c0,0,-3,-3,-3,-3l399907,0l0,-40c-399126,0,-399993,0,-399993,0z\nM93 435 v40 H400000 v-40z M500 241 v40 H400000 v-40z M500 241 v40 H400000 v-40z",shortrightharpoonabovebar:"M53,241l0,40c398570,0,399437,0,399437,0\nc4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,\n-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6\nc-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z\nM500 241 v40 H399408 v-40z M500 435 v40 H400000 v-40z"},A=function(){function t(t){this.children=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.children=t,this.classes=[],this.height=0,this.depth=0,this.maxFontSize=0,this.style={}}var e=t.prototype;return e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){for(var t=document.createDocumentFragment(),e=0;e"},N=function(){function t(t,e,r,a){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.width=void 0,this.maxFontSize=void 0,this.style=void 0,B.call(this,t,r,a),this.children=e||[]}var e=t.prototype;return e.setAttribute=function(t,e){this.attributes[t]=e},e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){return C.call(this,"span")},e.toMarkup=function(){return q.call(this,"span")},t}(),I=function(){function t(t,e,r,a){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,B.call(this,e,a),this.children=r||[],this.setAttribute("href",t)}var e=t.prototype;return e.setAttribute=function(t,e){this.attributes[t]=e},e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){return C.call(this,"a")},e.toMarkup=function(){return q.call(this,"a")},t}(),O=function(){function t(t,e,r){this.src=void 0,this.alt=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.alt=e,this.src=t,this.classes=["mord"],this.style=r}var e=t.prototype;return e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){var t=document.createElement("img");for(var e in t.src=this.src,t.alt=this.alt,t.className="mord",this.style)this.style.hasOwnProperty(e)&&(t.style[e]=this.style[e]);return t},e.toMarkup=function(){var t=""+this.alt+"=n[0]&&t<=n[1])return r.name}return null}(this.text.charCodeAt(0));l&&this.classes.push(l+"_fallback"),/[\xee\xef\xed\xec]/.test(this.text)&&(this.text=R[this.text])}var e=t.prototype;return e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){var t=document.createTextNode(this.text),e=null;for(var r in this.italic>0&&((e=document.createElement("span")).style.marginRight=this.italic+"em"),this.classes.length>0&&((e=e||document.createElement("span")).className=T(this.classes)),this.style)this.style.hasOwnProperty(r)&&((e=e||document.createElement("span")).style[r]=this.style[r]);return e?(e.appendChild(t),e):t},e.toMarkup=function(){var t=!1,e="0&&(r+="margin-right:"+this.italic+"em;"),this.style)this.style.hasOwnProperty(a)&&(r+=c.hyphenate(a)+":"+this.style[a]+";");r&&(t=!0,e+=' style="'+c.escape(r)+'"');var n=c.escape(this.text);return t?(e+=">",e+=n,e+=""):n},t}(),L=function(){function t(t,e){this.children=void 0,this.attributes=void 0,this.children=t||[],this.attributes=e||{}}var e=t.prototype;return e.toNode=function(){var t=document.createElementNS("http://www.w3.org/2000/svg","svg");for(var e in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,e)&&t.setAttribute(e,this.attributes[e]);for(var r=0;r":""},t}(),D=function(){function t(t){this.attributes=void 0,this.attributes=t||{}}var e=t.prototype;return e.toNode=function(){var t=document.createElementNS("http://www.w3.org/2000/svg","line");for(var e in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,e)&&t.setAttribute(e,this.attributes[e]);return t},e.toMarkup=function(){var t="","\\gt",!0),$("math",Z,et,"\u2208","\\in",!0),$("math",Z,et,"\ue020","\\@not"),$("math",Z,et,"\u2282","\\subset",!0),$("math",Z,et,"\u2283","\\supset",!0),$("math",Z,et,"\u2286","\\subseteq",!0),$("math",Z,et,"\u2287","\\supseteq",!0),$("math",K,et,"\u2288","\\nsubseteq",!0),$("math",K,et,"\u2289","\\nsupseteq",!0),$("math",Z,et,"\u22a8","\\models"),$("math",Z,et,"\u2190","\\leftarrow",!0),$("math",Z,et,"\u2264","\\le"),$("math",Z,et,"\u2264","\\leq",!0),$("math",Z,et,"<","\\lt",!0),$("math",Z,et,"\u2192","\\rightarrow",!0),$("math",Z,et,"\u2192","\\to"),$("math",K,et,"\u2271","\\ngeq",!0),$("math",K,et,"\u2270","\\nleq",!0),$("math",Z,"spacing","\xa0","\\ "),$("math",Z,"spacing","\xa0","~"),$("math",Z,"spacing","\xa0","\\space"),$("math",Z,"spacing","\xa0","\\nobreakspace"),$("text",Z,"spacing","\xa0","\\ "),$("text",Z,"spacing","\xa0"," "),$("text",Z,"spacing","\xa0","~"),$("text",Z,"spacing","\xa0","\\space"),$("text",Z,"spacing","\xa0","\\nobreakspace"),$("math",Z,"spacing",null,"\\nobreak"),$("math",Z,"spacing",null,"\\allowbreak"),$("math",Z,"punct",",",","),$("math",Z,"punct",";",";"),$("math",K,J,"\u22bc","\\barwedge",!0),$("math",K,J,"\u22bb","\\veebar",!0),$("math",Z,J,"\u2299","\\odot",!0),$("math",Z,J,"\u2295","\\oplus",!0),$("math",Z,J,"\u2297","\\otimes",!0),$("math",Z,"textord","\u2202","\\partial",!0),$("math",Z,J,"\u2298","\\oslash",!0),$("math",K,J,"\u229a","\\circledcirc",!0),$("math",K,J,"\u22a1","\\boxdot",!0),$("math",Z,J,"\u25b3","\\bigtriangleup"),$("math",Z,J,"\u25bd","\\bigtriangledown"),$("math",Z,J,"\u2020","\\dagger"),$("math",Z,J,"\u22c4","\\diamond"),$("math",Z,J,"\u22c6","\\star"),$("math",Z,J,"\u25c3","\\triangleleft"),$("math",Z,J,"\u25b9","\\triangleright"),$("math",Z,"open","{","\\{"),$("text",Z,"textord","{","\\{"),$("text",Z,"textord","{","\\textbraceleft"),$("math",Z,"close","}","\\}"),$("text",Z,"textord","}","\\}"),$("text",Z,"textord","}","\\textbraceright"),$("math",Z,"open","{","\\lbrace"),$("math",Z,"close","}","\\rbrace"),$("math",Z,"open","[","\\lbrack",!0),$("text",Z,"textord","[","\\lbrack",!0),$("math",Z,"close","]","\\rbrack",!0),$("text",Z,"textord","]","\\rbrack",!0),$("math",Z,"open","(","\\lparen",!0),$("math",Z,"close",")","\\rparen",!0),$("text",Z,"textord","<","\\textless",!0),$("text",Z,"textord",">","\\textgreater",!0),$("math",Z,"open","\u230a","\\lfloor",!0),$("math",Z,"close","\u230b","\\rfloor",!0),$("math",Z,"open","\u2308","\\lceil",!0),$("math",Z,"close","\u2309","\\rceil",!0),$("math",Z,"textord","\\","\\backslash"),$("math",Z,"textord","\u2223","|"),$("math",Z,"textord","\u2223","\\vert"),$("text",Z,"textord","|","\\textbar",!0),$("math",Z,"textord","\u2225","\\|"),$("math",Z,"textord","\u2225","\\Vert"),$("text",Z,"textord","\u2225","\\textbardbl"),$("text",Z,"textord","~","\\textasciitilde"),$("text",Z,"textord","\\","\\textbackslash"),$("text",Z,"textord","^","\\textasciicircum"),$("math",Z,et,"\u2191","\\uparrow",!0),$("math",Z,et,"\u21d1","\\Uparrow",!0),$("math",Z,et,"\u2193","\\downarrow",!0),$("math",Z,et,"\u21d3","\\Downarrow",!0),$("math",Z,et,"\u2195","\\updownarrow",!0),$("math",Z,et,"\u21d5","\\Updownarrow",!0),$("math",Z,tt,"\u2210","\\coprod"),$("math",Z,tt,"\u22c1","\\bigvee"),$("math",Z,tt,"\u22c0","\\bigwedge"),$("math",Z,tt,"\u2a04","\\biguplus"),$("math",Z,tt,"\u22c2","\\bigcap"),$("math",Z,tt,"\u22c3","\\bigcup"),$("math",Z,tt,"\u222b","\\int"),$("math",Z,tt,"\u222b","\\intop"),$("math",Z,tt,"\u222c","\\iint"),$("math",Z,tt,"\u222d","\\iiint"),$("math",Z,tt,"\u220f","\\prod"),$("math",Z,tt,"\u2211","\\sum"),$("math",Z,tt,"\u2a02","\\bigotimes"),$("math",Z,tt,"\u2a01","\\bigoplus"),$("math",Z,tt,"\u2a00","\\bigodot"),$("math",Z,tt,"\u222e","\\oint"),$("math",Z,tt,"\u2a06","\\bigsqcup"),$("math",Z,tt,"\u222b","\\smallint"),$("text",Z,"inner","\u2026","\\textellipsis"),$("math",Z,"inner","\u2026","\\mathellipsis"),$("text",Z,"inner","\u2026","\\ldots",!0),$("math",Z,"inner","\u2026","\\ldots",!0),$("math",Z,"inner","\u22ef","\\@cdots",!0),$("math",Z,"inner","\u22f1","\\ddots",!0),$("math",Z,"textord","\u22ee","\\varvdots"),$("math",Z,"accent-token","\u02ca","\\acute"),$("math",Z,"accent-token","\u02cb","\\grave"),$("math",Z,"accent-token","\xa8","\\ddot"),$("math",Z,"accent-token","~","\\tilde"),$("math",Z,"accent-token","\u02c9","\\bar"),$("math",Z,"accent-token","\u02d8","\\breve"),$("math",Z,"accent-token","\u02c7","\\check"),$("math",Z,"accent-token","^","\\hat"),$("math",Z,"accent-token","\u20d7","\\vec"),$("math",Z,"accent-token","\u02d9","\\dot"),$("math",Z,"accent-token","\u02da","\\mathring"),$("math",Z,Q,"\ue131","\\@imath"),$("math",Z,Q,"\ue237","\\@jmath"),$("math",Z,"textord","\u0131","\u0131"),$("math",Z,"textord","\u0237","\u0237"),$("text",Z,"textord","\u0131","\\i",!0),$("text",Z,"textord","\u0237","\\j",!0),$("text",Z,"textord","\xdf","\\ss",!0),$("text",Z,"textord","\xe6","\\ae",!0),$("text",Z,"textord","\u0153","\\oe",!0),$("text",Z,"textord","\xf8","\\o",!0),$("text",Z,"textord","\xc6","\\AE",!0),$("text",Z,"textord","\u0152","\\OE",!0),$("text",Z,"textord","\xd8","\\O",!0),$("text",Z,"accent-token","\u02ca","\\'"),$("text",Z,"accent-token","\u02cb","\\`"),$("text",Z,"accent-token","\u02c6","\\^"),$("text",Z,"accent-token","\u02dc","\\~"),$("text",Z,"accent-token","\u02c9","\\="),$("text",Z,"accent-token","\u02d8","\\u"),$("text",Z,"accent-token","\u02d9","\\."),$("text",Z,"accent-token","\u02da","\\r"),$("text",Z,"accent-token","\u02c7","\\v"),$("text",Z,"accent-token","\xa8",'\\"'),$("text",Z,"accent-token","\u02dd","\\H"),$("text",Z,"accent-token","\u25ef","\\textcircled");var rt={"--":!0,"---":!0,"``":!0,"''":!0};$("text",Z,"textord","\u2013","--",!0),$("text",Z,"textord","\u2013","\\textendash"),$("text",Z,"textord","\u2014","---",!0),$("text",Z,"textord","\u2014","\\textemdash"),$("text",Z,"textord","\u2018","`",!0),$("text",Z,"textord","\u2018","\\textquoteleft"),$("text",Z,"textord","\u2019","'",!0),$("text",Z,"textord","\u2019","\\textquoteright"),$("text",Z,"textord","\u201c","``",!0),$("text",Z,"textord","\u201c","\\textquotedblleft"),$("text",Z,"textord","\u201d","''",!0),$("text",Z,"textord","\u201d","\\textquotedblright"),$("math",Z,"textord","\xb0","\\degree",!0),$("text",Z,"textord","\xb0","\\degree"),$("text",Z,"textord","\xb0","\\textdegree",!0),$("math",Z,"textord","\xa3","\\pounds"),$("math",Z,"textord","\xa3","\\mathsterling",!0),$("text",Z,"textord","\xa3","\\pounds"),$("text",Z,"textord","\xa3","\\textsterling",!0),$("math",K,"textord","\u2720","\\maltese"),$("text",K,"textord","\u2720","\\maltese");for(var at=0;at<'0123456789/@."'.length;at++){var nt='0123456789/@."'.charAt(at);$("math",Z,"textord",nt,nt)}for(var it=0;it<'0123456789!@*()-=+";:?/.,'.length;it++){var ot='0123456789!@*()-=+";:?/.,'.charAt(it);$("text",Z,"textord",ot,ot)}for(var st="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",lt=0;lt=5?0:t>=3?1:2]){var r=Y[e]={cssEmPerMu:V.quad[e]/18};for(var a in V)V.hasOwnProperty(a)&&(r[a]=V[a][e])}return Y[e]}(this.size)),this._fontMetrics},e.getColor=function(){return this.phantom?"transparent":this.color},t}();kt.BASESIZE=6;var St=kt,Mt={pt:1,mm:7227/2540,cm:7227/254,in:72.27,bp:1.00375,pc:12,dd:1238/1157,cc:14856/1157,nd:685/642,nc:1370/107,sp:1/65536,px:1.00375},zt={ex:!0,em:!0,mu:!0},At=function(t){return"string"!=typeof t&&(t=t.unit),t in Mt||t in zt||"ex"===t},Tt=function(t,e){var r;if(t.unit in Mt)r=Mt[t.unit]/e.fontMetrics().ptPerEm/e.sizeMultiplier;else if("mu"===t.unit)r=e.fontMetrics().cssEmPerMu;else{var a;if(a=e.style.isTight()?e.havingStyle(e.style.text()):e,"ex"===t.unit)r=a.fontMetrics().xHeight;else{if("em"!==t.unit)throw new o("Invalid unit: '"+t.unit+"'");r=a.fontMetrics().quad}a!==e&&(r*=a.sizeMultiplier/e.sizeMultiplier)}return Math.min(t.number*r,e.maxSize)},Bt=function(t,e,r){return j[r][t]&&j[r][t].replace&&(t=j[r][t].replace),{value:t,metrics:G(t,e,r)}},Ct=function(t,e,r,a,n){var i,o=Bt(t,e,r),s=o.metrics;if(t=o.value,s){var l=s.italic;("text"===r||a&&"mathit"===a.font)&&(l=0),i=new E(t,s.height,s.depth,l,s.skew,s.width,n)}else"undefined"!=typeof console&&console.warn("No character metrics for '"+t+"' in style '"+e+"' and mode '"+r+"'"),i=new E(t,0,0,0,0,0,n);if(a){i.maxFontSize=a.sizeMultiplier,a.style.isTight()&&i.classes.push("mtight");var h=a.getColor();h&&(i.style.color=h)}return i},qt=function(t,e){if(T(t.classes)!==T(e.classes)||t.skew!==e.skew||t.maxFontSize!==e.maxFontSize)return!1;for(var r in t.style)if(t.style.hasOwnProperty(r)&&t.style[r]!==e.style[r])return!1;for(var a in e.style)if(e.style.hasOwnProperty(a)&&t.style[a]!==e.style[a])return!1;return!0},Nt=function(t){for(var e=0,r=0,a=0,n=0;ne&&(e=i.height),i.depth>r&&(r=i.depth),i.maxFontSize>a&&(a=i.maxFontSize)}t.height=e,t.depth=r,t.maxFontSize=a},It=function(t,e,r,a){var n=new N(t,e,r,a);return Nt(n),n},Ot=function(t,e,r,a){return new N(t,e,r,a)},Rt=function(t){var e=new A(t);return Nt(e),e},Et=function(t,e,r){var a="";switch(t){case"amsrm":a="AMS";break;case"textrm":a="Main";break;case"textsf":a="SansSerif";break;case"texttt":a="Typewriter";break;default:a=t}return a+"-"+("textbf"===e&&"textit"===r?"BoldItalic":"textbf"===e?"Bold":"textit"===e?"Italic":"Regular")},Lt={mathbf:{variant:"bold",fontName:"Main-Bold"},mathrm:{variant:"normal",fontName:"Main-Regular"},textit:{variant:"italic",fontName:"Main-Italic"},mathit:{variant:"italic",fontName:"Main-Italic"},mathnormal:{variant:"italic",fontName:"Math-Italic"},mathbb:{variant:"double-struck",fontName:"AMS-Regular"},mathcal:{variant:"script",fontName:"Caligraphic-Regular"},mathfrak:{variant:"fraktur",fontName:"Fraktur-Regular"},mathscr:{variant:"script",fontName:"Script-Regular"},mathsf:{variant:"sans-serif",fontName:"SansSerif-Regular"},mathtt:{variant:"monospace",fontName:"Typewriter-Regular"}},Pt={vec:["vec",.471,.714],oiintSize1:["oiintSize1",.957,.499],oiintSize2:["oiintSize2",1.472,.659],oiiintSize1:["oiiintSize1",1.304,.499],oiiintSize2:["oiiintSize2",1.98,.659],leftParenInner:["leftParenInner",.875,.3],rightParenInner:["rightParenInner",.875,.3]},Dt={fontMap:Lt,makeSymbol:Ct,mathsym:function(t,e,r,a){return void 0===a&&(a=[]),"boldsymbol"===r.font&&Bt(t,"Main-Bold",e).metrics?Ct(t,"Main-Bold",e,r,a.concat(["mathbf"])):"\\"===t||"main"===j[e][t].font?Ct(t,"Main-Regular",e,r,a):Ct(t,"AMS-Regular",e,r,a.concat(["amsrm"]))},makeSpan:It,makeSvgSpan:Ot,makeLineSpan:function(t,e,r){var a=It([t],[],e);return a.height=Math.max(r||e.fontMetrics().defaultRuleThickness,e.minRuleThickness),a.style.borderBottomWidth=a.height+"em",a.maxFontSize=1,a},makeAnchor:function(t,e,r,a){var n=new I(t,e,r,a);return Nt(n),n},makeFragment:Rt,wrapFragment:function(t,e){return t instanceof A?It([],[t],e):t},makeVList:function(t,e){for(var r=function(t){if("individualShift"===t.positionType){for(var e=t.children,r=[e[0]],a=-e[0].shift-e[0].elem.depth,n=a,i=1;i0&&(i.push(se(o,e)),o=[]),i.push(n[s]));o.length>0&&i.push(se(o,e)),r&&((a=se(ee(r,e,!0))).classes=["tag"],i.push(a));var h=Zt(["katex-html"],i);if(h.setAttribute("aria-hidden","true"),a){var m=a.children[0];m.style.height=h.height+h.depth+"em",m.style.verticalAlign=-h.depth+"em"}return h}function he(t){return new A(t)}var me=function(){function t(t,e){this.type=void 0,this.attributes=void 0,this.children=void 0,this.type=t,this.attributes={},this.children=e||[]}var e=t.prototype;return e.setAttribute=function(t,e){this.attributes[t]=e},e.getAttribute=function(t){return this.attributes[t]},e.toNode=function(){var t=document.createElementNS("http://www.w3.org/1998/Math/MathML",this.type);for(var e in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,e)&&t.setAttribute(e,this.attributes[e]);for(var r=0;r"},e.toText=function(){return this.children.map(function(t){return t.toText()}).join("")},t}(),ce=function(){function t(t){this.text=void 0,this.text=t}var e=t.prototype;return e.toNode=function(){return document.createTextNode(this.text)},e.toMarkup=function(){return c.escape(this.toText())},e.toText=function(){return this.text},t}(),ue={MathNode:me,TextNode:ce,SpaceNode:function(){function t(t){this.width=void 0,this.character=void 0,this.width=t,this.character=t>=.05555&&t<=.05556?"\u200a":t>=.1666&&t<=.1667?"\u2009":t>=.2222&&t<=.2223?"\u2005":t>=.2777&&t<=.2778?"\u2005\u200a":t>=-.05556&&t<=-.05555?"\u200a\u2063":t>=-.1667&&t<=-.1666?"\u2009\u2063":t>=-.2223&&t<=-.2222?"\u205f\u2063":t>=-.2778&&t<=-.2777?"\u2005\u2063":null}var e=t.prototype;return e.toNode=function(){if(this.character)return document.createTextNode(this.character);var t=document.createElementNS("http://www.w3.org/1998/Math/MathML","mspace");return t.setAttribute("width",this.width+"em"),t},e.toMarkup=function(){return this.character?""+this.character+"":''},e.toText=function(){return this.character?this.character:" "},t}(),newDocumentFragment:he},pe=function(t,e,r){return!j[e][t]||!j[e][t].replace||55349===t.charCodeAt(0)||rt.hasOwnProperty(t)&&r&&(r.fontFamily&&"tt"===r.fontFamily.substr(4,2)||r.font&&"tt"===r.font.substr(4,2))||(t=j[e][t].replace),new ue.TextNode(t)},de=function(t){return 1===t.length?t[0]:new ue.MathNode("mrow",t)},fe=function(t,e){if("texttt"===e.fontFamily)return"monospace";if("textsf"===e.fontFamily)return"textit"===e.fontShape&&"textbf"===e.fontWeight?"sans-serif-bold-italic":"textit"===e.fontShape?"sans-serif-italic":"textbf"===e.fontWeight?"bold-sans-serif":"sans-serif";if("textit"===e.fontShape&&"textbf"===e.fontWeight)return"bold-italic";if("textit"===e.fontShape)return"italic";if("textbf"===e.fontWeight)return"bold";var r=e.font;if(!r||"mathnormal"===r)return null;var a=t.mode;if("mathit"===r)return"italic";if("boldsymbol"===r)return"textord"===t.type?"bold":"bold-italic";if("mathbf"===r)return"bold";if("mathbb"===r)return"double-struck";if("mathfrak"===r)return"fraktur";if("mathscr"===r||"mathcal"===r)return"script";if("mathsf"===r)return"sans-serif";if("mathtt"===r)return"monospace";var n=t.text;return c.contains(["\\imath","\\jmath"],n)?null:(j[a][n]&&j[a][n].replace&&(n=j[a][n].replace),G(n,Dt.fontMap[r].fontName,a)?Dt.fontMap[r].variant:null)},ge=function(t,e,r){if(1===t.length){var a=ve(t[0],e);return r&&a instanceof me&&"mo"===a.type&&(a.setAttribute("lspace","0em"),a.setAttribute("rspace","0em")),[a]}for(var n,i=[],o=0;o0&&(p.text=p.text.slice(0,1)+"\u0338"+p.text.slice(1),i.pop())}}}i.push(s),n=s}return i},xe=function(t,e,r){return de(ge(t,e,r))},ve=function(t,e){if(!t)return new ue.MathNode("mrow");if(Xt[t.type])return Xt[t.type](t,e);throw new o("Got group of unknown type: '"+t.type+"'")};function be(t,e,r,a,n){var i,o=ge(t,r);i=1===o.length&&o[0]instanceof me&&c.contains(["mrow","mtable"],o[0].type)?o[0]:new ue.MathNode("mrow",o);var s=new ue.MathNode("annotation",[new ue.TextNode(e)]);s.setAttribute("encoding","application/x-tex");var l=new ue.MathNode("semantics",[i,s]),h=new ue.MathNode("math",[l]);h.setAttribute("xmlns","http://www.w3.org/1998/Math/MathML"),a&&h.setAttribute("display","block");var m=n?"katex":"katex-mathml";return Dt.makeSpan([m],[h])}var ye=function(t){return new St({style:t.displayMode?w.DISPLAY:w.TEXT,maxSize:t.maxSize,minRuleThickness:t.minRuleThickness})},we=function(t,e){if(e.displayMode){var r=["katex-display"];e.leqno&&r.push("leqno"),e.fleqn&&r.push("fleqn"),t=Dt.makeSpan(r,[t])}return t},ke=function(t,e,r){var a,n=ye(r);if("mathml"===r.output)return be(t,e,n,r.displayMode,!0);if("html"===r.output){var i=le(t,n);a=Dt.makeSpan(["katex"],[i])}else{var o=be(t,e,n,r.displayMode,!1),s=le(t,n);a=Dt.makeSpan(["katex"],[o,s])}return we(a,r)},Se={widehat:"^",widecheck:"\u02c7",widetilde:"~",utilde:"~",overleftarrow:"\u2190",underleftarrow:"\u2190",xleftarrow:"\u2190",overrightarrow:"\u2192",underrightarrow:"\u2192",xrightarrow:"\u2192",underbrace:"\u23df",overbrace:"\u23de",overgroup:"\u23e0",undergroup:"\u23e1",overleftrightarrow:"\u2194",underleftrightarrow:"\u2194",xleftrightarrow:"\u2194",Overrightarrow:"\u21d2",xRightarrow:"\u21d2",overleftharpoon:"\u21bc",xleftharpoonup:"\u21bc",overrightharpoon:"\u21c0",xrightharpoonup:"\u21c0",xLeftarrow:"\u21d0",xLeftrightarrow:"\u21d4",xhookleftarrow:"\u21a9",xhookrightarrow:"\u21aa",xmapsto:"\u21a6",xrightharpoondown:"\u21c1",xleftharpoondown:"\u21bd",xrightleftharpoons:"\u21cc",xleftrightharpoons:"\u21cb",xtwoheadleftarrow:"\u219e",xtwoheadrightarrow:"\u21a0",xlongequal:"=",xtofrom:"\u21c4",xrightleftarrows:"\u21c4",xrightequilibrium:"\u21cc",xleftequilibrium:"\u21cb"},Me={overrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],overleftarrow:[["leftarrow"],.888,522,"xMinYMin"],underrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],underleftarrow:[["leftarrow"],.888,522,"xMinYMin"],xrightarrow:[["rightarrow"],1.469,522,"xMaxYMin"],xleftarrow:[["leftarrow"],1.469,522,"xMinYMin"],Overrightarrow:[["doublerightarrow"],.888,560,"xMaxYMin"],xRightarrow:[["doublerightarrow"],1.526,560,"xMaxYMin"],xLeftarrow:[["doubleleftarrow"],1.526,560,"xMinYMin"],overleftharpoon:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoonup:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoondown:[["leftharpoondown"],.888,522,"xMinYMin"],overrightharpoon:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoonup:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoondown:[["rightharpoondown"],.888,522,"xMaxYMin"],xlongequal:[["longequal"],.888,334,"xMinYMin"],xtwoheadleftarrow:[["twoheadleftarrow"],.888,334,"xMinYMin"],xtwoheadrightarrow:[["twoheadrightarrow"],.888,334,"xMaxYMin"],overleftrightarrow:[["leftarrow","rightarrow"],.888,522],overbrace:[["leftbrace","midbrace","rightbrace"],1.6,548],underbrace:[["leftbraceunder","midbraceunder","rightbraceunder"],1.6,548],underleftrightarrow:[["leftarrow","rightarrow"],.888,522],xleftrightarrow:[["leftarrow","rightarrow"],1.75,522],xLeftrightarrow:[["doubleleftarrow","doublerightarrow"],1.75,560],xrightleftharpoons:[["leftharpoondownplus","rightharpoonplus"],1.75,716],xleftrightharpoons:[["leftharpoonplus","rightharpoondownplus"],1.75,716],xhookleftarrow:[["leftarrow","righthook"],1.08,522],xhookrightarrow:[["lefthook","rightarrow"],1.08,522],overlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],underlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],overgroup:[["leftgroup","rightgroup"],.888,342],undergroup:[["leftgroupunder","rightgroupunder"],.888,342],xmapsto:[["leftmapsto","rightarrow"],1.5,522],xtofrom:[["leftToFrom","rightToFrom"],1.75,528],xrightleftarrows:[["baraboveleftarrow","rightarrowabovebar"],1.75,901],xrightequilibrium:[["baraboveshortleftharpoon","rightharpoonaboveshortbar"],1.75,716],xleftequilibrium:[["shortbaraboveleftharpoon","shortrightharpoonabovebar"],1.75,716]},ze=function(t){return"ordgroup"===t.type?t.body.length:1},Ae=function(t,e,r,a){var n,i=t.height+t.depth+2*r;if(/fbox|color/.test(e)){if(n=Dt.makeSpan(["stretchy",e],[],a),"fbox"===e){var o=a.color&&a.getColor();o&&(n.style.borderColor=o)}}else{var s=[];/^[bx]cancel$/.test(e)&&s.push(new D({x1:"0",y1:"0",x2:"100%",y2:"100%","stroke-width":"0.046em"})),/^x?cancel$/.test(e)&&s.push(new D({x1:"0",y1:"100%",x2:"100%",y2:"0","stroke-width":"0.046em"}));var l=new L(s,{width:"100%",height:i+"em"});n=Dt.makeSvgSpan([],[l],a)}return n.height=i,n.style.height=i+"em",n},Te=function(t){var e=new ue.MathNode("mo",[new ue.TextNode(Se[t.substr(1)])]);return e.setAttribute("stretchy","true"),e},Be=function(t,e){var r=function(){var r=4e5,a=t.label.substr(1);if(c.contains(["widehat","widecheck","widetilde","utilde"],a)){var n,i,o,s=ze(t.base);if(s>5)"widehat"===a||"widecheck"===a?(n=420,r=2364,o=.42,i=a+"4"):(n=312,r=2340,o=.34,i="tilde4");else{var l=[1,1,2,2,3,3][s];"widehat"===a||"widecheck"===a?(r=[0,1062,2364,2364,2364][l],n=[0,239,300,360,420][l],o=[0,.24,.3,.3,.36,.42][l],i=a+l):(r=[0,600,1033,2339,2340][l],n=[0,260,286,306,312][l],o=[0,.26,.286,.3,.306,.34][l],i="tilde"+l)}var h=new P(i),m=new L([h],{width:"100%",height:o+"em",viewBox:"0 0 "+r+" "+n,preserveAspectRatio:"none"});return{span:Dt.makeSvgSpan([],[m],e),minWidth:0,height:o}}var u,p,d=[],f=Me[a],g=f[0],x=f[1],v=f[2],b=v/1e3,y=g.length;if(1===y)u=["hide-tail"],p=[f[3]];else if(2===y)u=["halfarrow-left","halfarrow-right"],p=["xMinYMin","xMaxYMin"];else{if(3!==y)throw new Error("Correct katexImagesData or update code here to support\n "+y+" children.");u=["brace-left","brace-center","brace-right"],p=["xMinYMin","xMidYMin","xMaxYMin"]}for(var w=0;w0&&(a.style.minWidth=n+"em"),a};function Ce(t,e){if(!t||t.type!==e)throw new Error("Expected node of type "+e+", but got "+(t?"node of type "+t.type:String(t)));return t}function qe(t){var e=Ne(t);if(!e)throw new Error("Expected node of symbol group type, but got "+(t?"node of type "+t.type:String(t)));return e}function Ne(t){return t&&("atom"===t.type||X.hasOwnProperty(t.type))?t:null}var Ie=function(t,e){var r,a,n;t&&"supsub"===t.type?(r=(a=Ce(t.base,"accent")).base,t.base=r,n=function(t){if(t instanceof N)return t;throw new Error("Expected span but got "+String(t)+".")}(oe(t,e)),t.base=a):r=(a=Ce(t,"accent")).base;var i=oe(r,e.havingCrampedStyle()),o=0;if(a.isShifty&&c.isCharacterBox(r)){var s=c.getBaseElem(r);o=H(oe(s,e.havingCrampedStyle())).skew}var l,h=Math.min(i.height,e.fontMetrics().xHeight);if(a.isStretchy)l=Be(a,e),l=Dt.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:i},{type:"elem",elem:l,wrapperClasses:["svg-align"],wrapperStyle:o>0?{width:"calc(100% - "+2*o+"em)",marginLeft:2*o+"em"}:void 0}]},e);else{var m,u;"\\vec"===a.label?(m=Dt.staticSvg("vec",e),u=Dt.svgData.vec[1]):((m=H(m=Dt.makeOrd({mode:a.mode,text:a.label},e,"textord"))).italic=0,u=m.width),l=Dt.makeSpan(["accent-body"],[m]);var p="\\textcircled"===a.label;p&&(l.classes.push("accent-full"),h=i.height);var d=o;p||(d-=u/2),l.style.left=d+"em","\\textcircled"===a.label&&(l.style.top=".2em"),l=Dt.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:i},{type:"kern",size:-h},{type:"elem",elem:l}]},e)}var f=Dt.makeSpan(["mord","accent"],[l],e);return n?(n.children[0]=f,n.height=Math.max(f.height,n.height),n.classes[0]="mord",n):f},Oe=function(t,e){var r=t.isStretchy?Te(t.label):new ue.MathNode("mo",[pe(t.label,t.mode)]),a=new ue.MathNode("mover",[ve(t.base,e),r]);return a.setAttribute("accent","true"),a},Re=new RegExp(["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring"].map(function(t){return"\\"+t}).join("|"));_t({type:"accent",names:["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring","\\widecheck","\\widehat","\\widetilde","\\overrightarrow","\\overleftarrow","\\Overrightarrow","\\overleftrightarrow","\\overgroup","\\overlinesegment","\\overleftharpoon","\\overrightharpoon"],props:{numArgs:1},handler:function(t,e){var r=e[0],a=!Re.test(t.funcName),n=!a||"\\widehat"===t.funcName||"\\widetilde"===t.funcName||"\\widecheck"===t.funcName;return{type:"accent",mode:t.parser.mode,label:t.funcName,isStretchy:a,isShifty:n,base:r}},htmlBuilder:Ie,mathmlBuilder:Oe}),_t({type:"accent",names:["\\'","\\`","\\^","\\~","\\=","\\u","\\.",'\\"',"\\r","\\H","\\v","\\textcircled"],props:{numArgs:1,allowedInText:!0,allowedInMath:!1},handler:function(t,e){var r=e[0];return{type:"accent",mode:t.parser.mode,label:t.funcName,isStretchy:!1,isShifty:!0,base:r}},htmlBuilder:Ie,mathmlBuilder:Oe}),_t({type:"accentUnder",names:["\\underleftarrow","\\underrightarrow","\\underleftrightarrow","\\undergroup","\\underlinesegment","\\utilde"],props:{numArgs:1},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];return{type:"accentUnder",mode:r.mode,label:a,base:n}},htmlBuilder:function(t,e){var r=oe(t.base,e),a=Be(t,e),n="\\utilde"===t.label?.12:0,i=Dt.makeVList({positionType:"top",positionData:r.height,children:[{type:"elem",elem:a,wrapperClasses:["svg-align"]},{type:"kern",size:n},{type:"elem",elem:r}]},e);return Dt.makeSpan(["mord","accentunder"],[i],e)},mathmlBuilder:function(t,e){var r=Te(t.label),a=new ue.MathNode("munder",[ve(t.base,e),r]);return a.setAttribute("accentunder","true"),a}});var Ee=function(t){var e=new ue.MathNode("mpadded",t?[t]:[]);return e.setAttribute("width","+0.6em"),e.setAttribute("lspace","0.3em"),e};_t({type:"xArrow",names:["\\xleftarrow","\\xrightarrow","\\xLeftarrow","\\xRightarrow","\\xleftrightarrow","\\xLeftrightarrow","\\xhookleftarrow","\\xhookrightarrow","\\xmapsto","\\xrightharpoondown","\\xrightharpoonup","\\xleftharpoondown","\\xleftharpoonup","\\xrightleftharpoons","\\xleftrightharpoons","\\xlongequal","\\xtwoheadrightarrow","\\xtwoheadleftarrow","\\xtofrom","\\xrightleftarrows","\\xrightequilibrium","\\xleftequilibrium"],props:{numArgs:1,numOptionalArgs:1},handler:function(t,e,r){var a=t.parser,n=t.funcName;return{type:"xArrow",mode:a.mode,label:n,body:e[0],below:r[0]}},htmlBuilder:function(t,e){var r,a=e.style,n=e.havingStyle(a.sup()),i=Dt.wrapFragment(oe(t.body,n,e),e);i.classes.push("x-arrow-pad"),t.below&&(n=e.havingStyle(a.sub()),(r=Dt.wrapFragment(oe(t.below,n,e),e)).classes.push("x-arrow-pad"));var o,s=Be(t,e),l=-e.fontMetrics().axisHeight+.5*s.height,h=-e.fontMetrics().axisHeight-.5*s.height-.111;if((i.depth>.25||"\\xleftequilibrium"===t.label)&&(h-=i.depth),r){var m=-e.fontMetrics().axisHeight+r.height+.5*s.height+.111;o=Dt.makeVList({positionType:"individualShift",children:[{type:"elem",elem:i,shift:h},{type:"elem",elem:s,shift:l},{type:"elem",elem:r,shift:m}]},e)}else o=Dt.makeVList({positionType:"individualShift",children:[{type:"elem",elem:i,shift:h},{type:"elem",elem:s,shift:l}]},e);return o.children[0].children[0].children[1].classes.push("svg-align"),Dt.makeSpan(["mrel","x-arrow"],[o],e)},mathmlBuilder:function(t,e){var r,a=Te(t.label);if(t.body){var n=Ee(ve(t.body,e));if(t.below){var i=Ee(ve(t.below,e));r=new ue.MathNode("munderover",[a,i,n])}else r=new ue.MathNode("mover",[a,n])}else if(t.below){var o=Ee(ve(t.below,e));r=new ue.MathNode("munder",[a,o])}else r=Ee(),r=new ue.MathNode("mover",[a,r]);return r}}),_t({type:"textord",names:["\\@char"],props:{numArgs:1,allowedInText:!0},handler:function(t,e){for(var r=t.parser,a=Ce(e[0],"ordgroup").body,n="",i=0;i","\\langle","\\rangle","/","\\backslash","\\lt","\\gt"],Je=[0,1.2,1.8,2.4,3],Qe=[{type:"small",style:w.SCRIPTSCRIPT},{type:"small",style:w.SCRIPT},{type:"small",style:w.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4}],tr=[{type:"small",style:w.SCRIPTSCRIPT},{type:"small",style:w.SCRIPT},{type:"small",style:w.TEXT},{type:"stack"}],er=[{type:"small",style:w.SCRIPTSCRIPT},{type:"small",style:w.SCRIPT},{type:"small",style:w.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4},{type:"stack"}],rr=function(t){if("small"===t.type)return"Main-Regular";if("large"===t.type)return"Size"+t.size+"-Regular";if("stack"===t.type)return"Size4-Regular";throw new Error("Add support for delim type '"+t.type+"' here.")},ar=function(t,e,r,a){for(var n=Math.min(2,3-a.style.size);ne)return r[n]}return r[r.length-1]},nr=function(t,e,r,a,n,i){var o;"<"===t||"\\lt"===t||"\u27e8"===t?t="\\langle":">"!==t&&"\\gt"!==t&&"\u27e9"!==t||(t="\\rangle"),o=c.contains(Ke,t)?Qe:c.contains($e,t)?er:tr;var s=ar(t,e,o,a);return"small"===s.type?function(t,e,r,a,n,i){var o=Dt.makeSymbol(t,"Main-Regular",n,a),s=Ue(o,e,a,i);return r&&Ge(s,a,e),s}(t,s.style,r,a,n,i):"large"===s.type?Ye(t,s.size,r,a,n,i):_e(t,e,r,a,n,i)},ir=function(t,e){var r,a,n=e.havingBaseSizing(),i=ar("\\surd",t*n.sizeMultiplier,er,n),o=n.sizeMultiplier,s=Math.max(0,e.minRuleThickness-e.fontMetrics().sqrtRuleThickness),l=0,h=0,m=0;return"small"===i.type?(t<1?o=1:t<1.4&&(o=.7),h=(1+s)/o,(r=je("sqrtMain",l=(1+s+.08)/o,m=1e3+1e3*s+80,s,e)).style.minWidth="0.853em",a=.833/o):"large"===i.type?(m=1080*Je[i.size],h=(Je[i.size]+s)/o,l=(Je[i.size]+s+.08)/o,(r=je("sqrtSize"+i.size,l,m,s,e)).style.minWidth="1.02em",a=1/o):(l=t+s+.08,h=t+s,m=Math.floor(1e3*t+s)+80,(r=je("sqrtTall",l,m,s,e)).style.minWidth="0.742em",a=1.056),r.height=h,r.style.height=l+"em",{span:r,advanceWidth:a,ruleWidth:(e.fontMetrics().sqrtRuleThickness+s)*o}},or=function(t,e,r,a,n){if("<"===t||"\\lt"===t||"\u27e8"===t?t="\\langle":">"!==t&&"\\gt"!==t&&"\u27e9"!==t||(t="\\rangle"),c.contains($e,t)||c.contains(Ke,t))return Ye(t,e,!1,r,a,n);if(c.contains(Ze,t))return _e(t,Je[e],!1,r,a,n);throw new o("Illegal delimiter: '"+t+"'")},sr=nr,lr=function(t,e,r,a,n,i){var o=a.fontMetrics().axisHeight*a.sizeMultiplier,s=5/a.fontMetrics().ptPerEm,l=Math.max(e-o,r+o),h=Math.max(l/500*901,2*l-s);return nr(t,h,!0,a,n,i)},hr={"\\bigl":{mclass:"mopen",size:1},"\\Bigl":{mclass:"mopen",size:2},"\\biggl":{mclass:"mopen",size:3},"\\Biggl":{mclass:"mopen",size:4},"\\bigr":{mclass:"mclose",size:1},"\\Bigr":{mclass:"mclose",size:2},"\\biggr":{mclass:"mclose",size:3},"\\Biggr":{mclass:"mclose",size:4},"\\bigm":{mclass:"mrel",size:1},"\\Bigm":{mclass:"mrel",size:2},"\\biggm":{mclass:"mrel",size:3},"\\Biggm":{mclass:"mrel",size:4},"\\big":{mclass:"mord",size:1},"\\Big":{mclass:"mord",size:2},"\\bigg":{mclass:"mord",size:3},"\\Bigg":{mclass:"mord",size:4}},mr=["(","\\lparen",")","\\rparen","[","\\lbrack","]","\\rbrack","\\{","\\lbrace","\\}","\\rbrace","\\lfloor","\\rfloor","\u230a","\u230b","\\lceil","\\rceil","\u2308","\u2309","<",">","\\langle","\u27e8","\\rangle","\u27e9","\\lt","\\gt","\\lvert","\\rvert","\\lVert","\\rVert","\\lgroup","\\rgroup","\u27ee","\u27ef","\\lmoustache","\\rmoustache","\u23b0","\u23b1","/","\\backslash","|","\\vert","\\|","\\Vert","\\uparrow","\\Uparrow","\\downarrow","\\Downarrow","\\updownarrow","\\Updownarrow","."];function cr(t,e){var r=Ne(t);if(r&&c.contains(mr,r.text))return r;throw new o(r?"Invalid delimiter '"+r.text+"' after '"+e.funcName+"'":"Invalid delimiter type '"+t.type+"'",t)}function ur(t){if(!t.body)throw new Error("Bug: The leftright ParseNode wasn't fully parsed.")}_t({type:"delimsizing",names:["\\bigl","\\Bigl","\\biggl","\\Biggl","\\bigr","\\Bigr","\\biggr","\\Biggr","\\bigm","\\Bigm","\\biggm","\\Biggm","\\big","\\Big","\\bigg","\\Bigg"],props:{numArgs:1},handler:function(t,e){var r=cr(e[0],t);return{type:"delimsizing",mode:t.parser.mode,size:hr[t.funcName].size,mclass:hr[t.funcName].mclass,delim:r.text}},htmlBuilder:function(t,e){return"."===t.delim?Dt.makeSpan([t.mclass]):or(t.delim,t.size,e,t.mode,[t.mclass])},mathmlBuilder:function(t){var e=[];"."!==t.delim&&e.push(pe(t.delim,t.mode));var r=new ue.MathNode("mo",e);return"mopen"===t.mclass||"mclose"===t.mclass?r.setAttribute("fence","true"):r.setAttribute("fence","false"),r}}),_t({type:"leftright-right",names:["\\right"],props:{numArgs:1},handler:function(t,e){var r=t.parser.gullet.macros.get("\\current@color");if(r&&"string"!=typeof r)throw new o("\\current@color set to non-string in \\right");return{type:"leftright-right",mode:t.parser.mode,delim:cr(e[0],t).text,color:r}}}),_t({type:"leftright",names:["\\left"],props:{numArgs:1},handler:function(t,e){var r=cr(e[0],t),a=t.parser;++a.leftrightDepth;var n=a.parseExpression(!1);--a.leftrightDepth,a.expect("\\right",!1);var i=Ce(a.parseFunction(),"leftright-right");return{type:"leftright",mode:a.mode,body:n,left:r.text,right:i.delim,rightColor:i.color}},htmlBuilder:function(t,e){ur(t);for(var r,a,n=ee(t.body,e,!0,["mopen","mclose"]),i=0,o=0,s=!1,l=0;l-1?"mpadded":"menclose",[ve(t.body,e)]);switch(t.label){case"\\cancel":a.setAttribute("notation","updiagonalstrike");break;case"\\bcancel":a.setAttribute("notation","downdiagonalstrike");break;case"\\sout":a.setAttribute("notation","horizontalstrike");break;case"\\fbox":a.setAttribute("notation","box");break;case"\\fcolorbox":case"\\colorbox":if(r=e.fontMetrics().fboxsep*e.fontMetrics().ptPerEm,a.setAttribute("width","+"+2*r+"pt"),a.setAttribute("height","+"+2*r+"pt"),a.setAttribute("lspace",r+"pt"),a.setAttribute("voffset",r+"pt"),"\\fcolorbox"===t.label){var n=Math.max(e.fontMetrics().fboxrule,e.minRuleThickness);a.setAttribute("style","border: "+n+"em solid "+String(t.borderColor))}break;case"\\xcancel":a.setAttribute("notation","updiagonalstrike downdiagonalstrike")}return t.backgroundColor&&a.setAttribute("mathbackground",t.backgroundColor),a};_t({type:"enclose",names:["\\colorbox"],props:{numArgs:2,allowedInText:!0,greediness:3,argTypes:["color","text"]},handler:function(t,e,r){var a=t.parser,n=t.funcName,i=Ce(e[0],"color-token").color,o=e[1];return{type:"enclose",mode:a.mode,label:n,backgroundColor:i,body:o}},htmlBuilder:pr,mathmlBuilder:dr}),_t({type:"enclose",names:["\\fcolorbox"],props:{numArgs:3,allowedInText:!0,greediness:3,argTypes:["color","color","text"]},handler:function(t,e,r){var a=t.parser,n=t.funcName,i=Ce(e[0],"color-token").color,o=Ce(e[1],"color-token").color,s=e[2];return{type:"enclose",mode:a.mode,label:n,backgroundColor:o,borderColor:i,body:s}},htmlBuilder:pr,mathmlBuilder:dr}),_t({type:"enclose",names:["\\fbox"],props:{numArgs:1,argTypes:["hbox"],allowedInText:!0},handler:function(t,e){return{type:"enclose",mode:t.parser.mode,label:"\\fbox",body:e[0]}}}),_t({type:"enclose",names:["\\cancel","\\bcancel","\\xcancel","\\sout"],props:{numArgs:1},handler:function(t,e,r){var a=t.parser,n=t.funcName,i=e[0];return{type:"enclose",mode:a.mode,label:n,body:i}},htmlBuilder:pr,mathmlBuilder:dr});var fr={};function gr(t){for(var e=t.type,r=t.names,a=t.props,n=t.handler,i=t.htmlBuilder,o=t.mathmlBuilder,s={type:e,numArgs:a.numArgs||0,greediness:1,allowedInText:!1,numOptionalArgs:0,handler:n},l=0;l0&&(b+=.25),h.push({pos:b,isDashed:t[e]})}for(y(i[0]),r=0;r0&&(M<(B+=v)&&(M=B),B=0),t.addJot&&(M+=f),z.height=S,z.depth=M,b+=S,z.pos=b,b+=M+B,l[r]=z,y(i[r+1])}var C,q,N=b/2+e.fontMetrics().axisHeight,I=t.cols||[],O=[];for(a=0,q=0;a=s)){var D=void 0;(a>0||t.hskipBeforeAndAfter)&&0!==(D=c.deflt(R.pregap,p))&&((C=Dt.makeSpan(["arraycolsep"],[])).style.width=D+"em",O.push(C));var H=[];for(r=0;r0){for(var G=Dt.makeLineSpan("hline",e,m),Y=Dt.makeLineSpan("hdashline",e,m),W=[{type:"elem",elem:l,shift:0}];h.length>0;){var X=h.pop(),_=X.pos-N;X.isDashed?W.push({type:"elem",elem:Y,shift:_}):W.push({type:"elem",elem:G,shift:_})}l=Dt.makeVList({positionType:"individualShift",children:W},e)}return Dt.makeSpan(["mord"],[l],e)},wr={c:"center ",l:"left ",r:"right "},kr=function(t,e){var r=new ue.MathNode("mtable",t.body.map(function(t){return new ue.MathNode("mtr",t.map(function(t){return new ue.MathNode("mtd",[ve(t,e)])}))})),a=.5===t.arraystretch?.1:.16+t.arraystretch-1+(t.addJot?.09:0);r.setAttribute("rowspacing",a+"em");var n="",i="";if(t.cols&&t.cols.length>0){var o=t.cols,s="",l=!1,h=0,m=o.length;"separator"===o[0].type&&(n+="top ",h=1),"separator"===o[o.length-1].type&&(n+="bottom ",m-=1);for(var c=h;c0?"left ":"",n+=g[g.length-1].length>0?"right ":"";for(var x=1;x0&&m&&(p=1),a[c]={type:"align",align:u,pregap:p,postgap:0}}return n.colSeparationType=m?"align":"alignat",n};gr({type:"array",names:["array","darray"],props:{numArgs:1},handler:function(t,e){var r={cols:(Ne(e[0])?[e[0]]:Ce(e[0],"ordgroup").body).map(function(t){var e=qe(t).text;if(-1!=="lcr".indexOf(e))return{type:"align",align:e};if("|"===e)return{type:"separator",separator:"|"};if(":"===e)return{type:"separator",separator:":"};throw new o("Unknown column alignment: "+e,t)}),hskipBeforeAndAfter:!0};return vr(t.parser,r,br(t.envName))},htmlBuilder:yr,mathmlBuilder:kr}),gr({type:"array",names:["matrix","pmatrix","bmatrix","Bmatrix","vmatrix","Vmatrix"],props:{numArgs:0},handler:function(t){var e={matrix:null,pmatrix:["(",")"],bmatrix:["[","]"],Bmatrix:["\\{","\\}"],vmatrix:["|","|"],Vmatrix:["\\Vert","\\Vert"]}[t.envName],r=vr(t.parser,{hskipBeforeAndAfter:!1},br(t.envName));return e?{type:"leftright",mode:t.mode,body:[r],left:e[0],right:e[1],rightColor:void 0}:r},htmlBuilder:yr,mathmlBuilder:kr}),gr({type:"array",names:["smallmatrix"],props:{numArgs:0},handler:function(t){var e=vr(t.parser,{arraystretch:.5},"script");return e.colSeparationType="small",e},htmlBuilder:yr,mathmlBuilder:kr}),gr({type:"array",names:["subarray"],props:{numArgs:1},handler:function(t,e){var r=(Ne(e[0])?[e[0]]:Ce(e[0],"ordgroup").body).map(function(t){var e=qe(t).text;if(-1!=="lc".indexOf(e))return{type:"align",align:e};throw new o("Unknown column alignment: "+e,t)});if(r.length>1)throw new o("{subarray} can contain only one column");var a={cols:r,hskipBeforeAndAfter:!1,arraystretch:.5};if((a=vr(t.parser,a,"script")).body.length>0&&a.body[0].length>1)throw new o("{subarray} can contain only one column");return a},htmlBuilder:yr,mathmlBuilder:kr}),gr({type:"array",names:["cases","dcases","rcases","drcases"],props:{numArgs:0},handler:function(t){var e=vr(t.parser,{arraystretch:1.2,cols:[{type:"align",align:"l",pregap:0,postgap:1},{type:"align",align:"l",pregap:0,postgap:0}]},br(t.envName));return{type:"leftright",mode:t.mode,body:[e],left:t.envName.indexOf("r")>-1?".":"\\{",right:t.envName.indexOf("r")>-1?"\\}":".",rightColor:void 0}},htmlBuilder:yr,mathmlBuilder:kr}),gr({type:"array",names:["aligned"],props:{numArgs:0},handler:Sr,htmlBuilder:yr,mathmlBuilder:kr}),gr({type:"array",names:["gathered"],props:{numArgs:0},handler:function(t){return vr(t.parser,{cols:[{type:"align",align:"c"}],addJot:!0},"display")},htmlBuilder:yr,mathmlBuilder:kr}),gr({type:"array",names:["alignedat"],props:{numArgs:1},handler:Sr,htmlBuilder:yr,mathmlBuilder:kr}),_t({type:"text",names:["\\hline","\\hdashline"],props:{numArgs:0,allowedInText:!0,allowedInMath:!0},handler:function(t,e){throw new o(t.funcName+" valid only within array environment")}});var Mr=fr;_t({type:"environment",names:["\\begin","\\end"],props:{numArgs:1,argTypes:["text"]},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];if("ordgroup"!==n.type)throw new o("Invalid environment name",n);for(var i="",s=0;s=w.SCRIPT.id?r.text():w.DISPLAY:"text"===t&&r.size===w.DISPLAY.size?r=w.TEXT:"script"===t?r=w.SCRIPT:"scriptscript"===t&&(r=w.SCRIPTSCRIPT),r},Or=function(t,e){var r,a=Ir(t.size,e.style),n=a.fracNum(),i=a.fracDen();r=e.havingStyle(n);var o=oe(t.numer,r,e);if(t.continued){var s=8.5/e.fontMetrics().ptPerEm,l=3.5/e.fontMetrics().ptPerEm;o.height=o.height0?3*c:7*c,d=e.fontMetrics().denom1):(m>0?(u=e.fontMetrics().num2,p=c):(u=e.fontMetrics().num3,p=3*c),d=e.fontMetrics().denom2),h){var y=e.fontMetrics().axisHeight;u-o.depth-(y+.5*m)0&&(e="."===(e=t)?null:e),e};_t({type:"genfrac",names:["\\genfrac"],props:{numArgs:6,greediness:6,argTypes:["math","math","size","text","math","math"]},handler:function(t,e){var r,a=t.parser,n=e[4],i=e[5],o="atom"===e[0].type&&"open"===e[0].family?Lr(e[0].text):null,s="atom"===e[1].type&&"close"===e[1].family?Lr(e[1].text):null,l=Ce(e[2],"size"),h=null;r=!!l.isBlank||(h=l.value).number>0;var m="auto",c=e[3];if("ordgroup"===c.type){if(c.body.length>0){var u=Ce(c.body[0],"textord");m=Er[Number(u.text)]}}else c=Ce(c,"textord"),m=Er[Number(c.text)];return{type:"genfrac",mode:a.mode,numer:n,denom:i,continued:!1,hasBarLine:r,barSize:h,leftDelim:o,rightDelim:s,size:m}},htmlBuilder:Or,mathmlBuilder:Rr}),_t({type:"infix",names:["\\above"],props:{numArgs:1,argTypes:["size"],infix:!0},handler:function(t,e){var r=t.parser,a=(t.funcName,t.token);return{type:"infix",mode:r.mode,replaceWith:"\\\\abovefrac",size:Ce(e[0],"size").value,token:a}}}),_t({type:"genfrac",names:["\\\\abovefrac"],props:{numArgs:3,argTypes:["math","size","math"]},handler:function(t,e){var r=t.parser,a=(t.funcName,e[0]),n=function(t){if(!t)throw new Error("Expected non-null, but got "+String(t));return t}(Ce(e[1],"infix").size),i=e[2],o=n.number>0;return{type:"genfrac",mode:r.mode,numer:a,denom:i,continued:!1,hasBarLine:o,barSize:n,leftDelim:null,rightDelim:null,size:"auto"}},htmlBuilder:Or,mathmlBuilder:Rr});var Pr=function(t,e){var r,a,n=e.style;"supsub"===t.type?(r=t.sup?oe(t.sup,e.havingStyle(n.sup()),e):oe(t.sub,e.havingStyle(n.sub()),e),a=Ce(t.base,"horizBrace")):a=Ce(t,"horizBrace");var i,o=oe(a.base,e.havingBaseStyle(w.DISPLAY)),s=Be(a,e);if(a.isOver?(i=Dt.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:o},{type:"kern",size:.1},{type:"elem",elem:s}]},e)).children[0].children[0].children[1].classes.push("svg-align"):(i=Dt.makeVList({positionType:"bottom",positionData:o.depth+.1+s.height,children:[{type:"elem",elem:s},{type:"kern",size:.1},{type:"elem",elem:o}]},e)).children[0].children[0].children[0].classes.push("svg-align"),r){var l=Dt.makeSpan(["mord",a.isOver?"mover":"munder"],[i],e);i=a.isOver?Dt.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:l},{type:"kern",size:.2},{type:"elem",elem:r}]},e):Dt.makeVList({positionType:"bottom",positionData:l.depth+.2+r.height+r.depth,children:[{type:"elem",elem:r},{type:"kern",size:.2},{type:"elem",elem:l}]},e)}return Dt.makeSpan(["mord",a.isOver?"mover":"munder"],[i],e)};_t({type:"horizBrace",names:["\\overbrace","\\underbrace"],props:{numArgs:1},handler:function(t,e){var r=t.parser,a=t.funcName;return{type:"horizBrace",mode:r.mode,label:a,isOver:/^\\over/.test(a),base:e[0]}},htmlBuilder:Pr,mathmlBuilder:function(t,e){var r=Te(t.label);return new ue.MathNode(t.isOver?"mover":"munder",[ve(t.base,e),r])}}),_t({type:"href",names:["\\href"],props:{numArgs:2,argTypes:["url","original"],allowedInText:!0},handler:function(t,e){var r=t.parser,a=e[1],n=Ce(e[0],"url").url;return r.settings.isTrusted({command:"\\href",url:n})?{type:"href",mode:r.mode,href:n,body:$t(a)}:r.formatUnsupportedCmd("\\href")},htmlBuilder:function(t,e){var r=ee(t.body,e,!1);return Dt.makeAnchor(t.href,[],r,e)},mathmlBuilder:function(t,e){var r=xe(t.body,e);return r instanceof me||(r=new me("mrow",[r])),r.setAttribute("href",t.href),r}}),_t({type:"href",names:["\\url"],props:{numArgs:1,argTypes:["url"],allowedInText:!0},handler:function(t,e){var r=t.parser,a=Ce(e[0],"url").url;if(!r.settings.isTrusted({command:"\\url",url:a}))return r.formatUnsupportedCmd("\\url");for(var n=[],i=0;i0&&(a=Tt(t.totalheight,e)-r,a=Number(a.toFixed(2)));var n=0;t.width.number>0&&(n=Tt(t.width,e));var i={height:r+a+"em"};n>0&&(i.width=n+"em"),a>0&&(i.verticalAlign=-a+"em");var o=new O(t.src,t.alt,i);return o.height=r,o.depth=a,o},mathmlBuilder:function(t,e){var r=new ue.MathNode("mglyph",[]);r.setAttribute("alt",t.alt);var a=Tt(t.height,e),n=0;if(t.totalheight.number>0&&(n=(n=Tt(t.totalheight,e)-a).toFixed(2),r.setAttribute("valign","-"+n+"em")),r.setAttribute("height",a+n+"em"),t.width.number>0){var i=Tt(t.width,e);r.setAttribute("width",i+"em")}return r.setAttribute("src",t.src),r}}),_t({type:"kern",names:["\\kern","\\mkern","\\hskip","\\mskip"],props:{numArgs:1,argTypes:["size"],allowedInText:!0},handler:function(t,e){var r=t.parser,a=t.funcName,n=Ce(e[0],"size");if(r.settings.strict){var i="m"===a[1],o="mu"===n.value.unit;i?(o||r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+a+" supports only mu units, not "+n.value.unit+" units"),"math"!==r.mode&&r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+a+" works only in math mode")):o&&r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+a+" doesn't support mu units")}return{type:"kern",mode:r.mode,dimension:n.value}},htmlBuilder:function(t,e){return Dt.makeGlue(t.dimension,e)},mathmlBuilder:function(t,e){var r=Tt(t.dimension,e);return new ue.SpaceNode(r)}}),_t({type:"lap",names:["\\mathllap","\\mathrlap","\\mathclap"],props:{numArgs:1,allowedInText:!0},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];return{type:"lap",mode:r.mode,alignment:a.slice(5),body:n}},htmlBuilder:function(t,e){var r;"clap"===t.alignment?(r=Dt.makeSpan([],[oe(t.body,e)]),r=Dt.makeSpan(["inner"],[r],e)):r=Dt.makeSpan(["inner"],[oe(t.body,e)]);var a=Dt.makeSpan(["fix"],[]),n=Dt.makeSpan([t.alignment],[r,a],e),i=Dt.makeSpan(["strut"]);return i.style.height=n.height+n.depth+"em",i.style.verticalAlign=-n.depth+"em",n.children.unshift(i),n=Dt.makeSpan(["thinbox"],[n],e),Dt.makeSpan(["mord","vbox"],[n],e)},mathmlBuilder:function(t,e){var r=new ue.MathNode("mpadded",[ve(t.body,e)]);if("rlap"!==t.alignment){var a="llap"===t.alignment?"-1":"-0.5";r.setAttribute("lspace",a+"width")}return r.setAttribute("width","0px"),r}}),_t({type:"styling",names:["\\(","$"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler:function(t,e){var r=t.funcName,a=t.parser,n=a.mode;a.switchMode("math");var i="\\("===r?"\\)":"$",o=a.parseExpression(!1,i);return a.expect(i),a.switchMode(n),{type:"styling",mode:a.mode,style:"text",body:o}}}),_t({type:"text",names:["\\)","\\]"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler:function(t,e){throw new o("Mismatched "+t.funcName)}});var Hr=function(t,e){switch(e.style.size){case w.DISPLAY.size:return t.display;case w.TEXT.size:return t.text;case w.SCRIPT.size:return t.script;case w.SCRIPTSCRIPT.size:return t.scriptscript;default:return t.text}};_t({type:"mathchoice",names:["\\mathchoice"],props:{numArgs:4},handler:function(t,e){return{type:"mathchoice",mode:t.parser.mode,display:$t(e[0]),text:$t(e[1]),script:$t(e[2]),scriptscript:$t(e[3])}},htmlBuilder:function(t,e){var r=Hr(t,e),a=ee(r,e,!1);return Dt.makeFragment(a)},mathmlBuilder:function(t,e){var r=Hr(t,e);return xe(r,e)}});var Fr=function(t,e,r,a,n,i,o){var s,l,h;if(t=Dt.makeSpan([],[t]),e){var m=oe(e,a.havingStyle(n.sup()),a);l={elem:m,kern:Math.max(a.fontMetrics().bigOpSpacing1,a.fontMetrics().bigOpSpacing3-m.depth)}}if(r){var c=oe(r,a.havingStyle(n.sub()),a);s={elem:c,kern:Math.max(a.fontMetrics().bigOpSpacing2,a.fontMetrics().bigOpSpacing4-c.height)}}if(l&&s){var u=a.fontMetrics().bigOpSpacing5+s.elem.height+s.elem.depth+s.kern+t.depth+o;h=Dt.makeVList({positionType:"bottom",positionData:u,children:[{type:"kern",size:a.fontMetrics().bigOpSpacing5},{type:"elem",elem:s.elem,marginLeft:-i+"em"},{type:"kern",size:s.kern},{type:"elem",elem:t},{type:"kern",size:l.kern},{type:"elem",elem:l.elem,marginLeft:i+"em"},{type:"kern",size:a.fontMetrics().bigOpSpacing5}]},a)}else if(s){var p=t.height-o;h=Dt.makeVList({positionType:"top",positionData:p,children:[{type:"kern",size:a.fontMetrics().bigOpSpacing5},{type:"elem",elem:s.elem,marginLeft:-i+"em"},{type:"kern",size:s.kern},{type:"elem",elem:t}]},a)}else{if(!l)return t;var d=t.depth+o;h=Dt.makeVList({positionType:"bottom",positionData:d,children:[{type:"elem",elem:t},{type:"kern",size:l.kern},{type:"elem",elem:l.elem,marginLeft:i+"em"},{type:"kern",size:a.fontMetrics().bigOpSpacing5}]},a)}return Dt.makeSpan(["mop","op-limits"],[h],a)},Vr=["\\smallint"],Ur=function(t,e){var r,a,n,i=!1;"supsub"===t.type?(r=t.sup,a=t.sub,n=Ce(t.base,"op"),i=!0):n=Ce(t,"op");var o,s=e.style,l=!1;if(s.size===w.DISPLAY.size&&n.symbol&&!c.contains(Vr,n.name)&&(l=!0),n.symbol){var h=l?"Size2-Regular":"Size1-Regular",m="";if("\\oiint"!==n.name&&"\\oiiint"!==n.name||(m=n.name.substr(1),n.name="oiint"===m?"\\iint":"\\iiint"),o=Dt.makeSymbol(n.name,h,"math",e,["mop","op-symbol",l?"large-op":"small-op"]),m.length>0){var u=o.italic,p=Dt.staticSvg(m+"Size"+(l?"2":"1"),e);o=Dt.makeVList({positionType:"individualShift",children:[{type:"elem",elem:o,shift:0},{type:"elem",elem:p,shift:l?.08:0}]},e),n.name="\\"+m,o.classes.unshift("mop"),o.italic=u}}else if(n.body){var d=ee(n.body,e,!0);1===d.length&&d[0]instanceof E?(o=d[0]).classes[0]="mop":o=Dt.makeSpan(["mop"],Dt.tryCombineChars(d),e)}else{for(var f=[],g=1;g0){for(var s=n.body.map(function(t){var e=t.text;return"string"==typeof e?{type:"textord",mode:t.mode,text:e}:t}),l=ee(s,e.withFont("mathrm"),!0),h=0;h=0?s.setAttribute("height","+"+n+"em"):(s.setAttribute("height",n+"em"),s.setAttribute("depth","+"+-n+"em")),s.setAttribute("voffset",n+"em"),s}});var jr=["\\tiny","\\sixptsize","\\scriptsize","\\footnotesize","\\small","\\normalsize","\\large","\\Large","\\LARGE","\\huge","\\Huge"];_t({type:"sizing",names:jr,props:{numArgs:0,allowedInText:!0},handler:function(t,e){var r=t.breakOnTokenText,a=t.funcName,n=t.parser,i=n.parseExpression(!1,r);return{type:"sizing",mode:n.mode,size:jr.indexOf(a)+1,body:i}},htmlBuilder:function(t,e){var r=e.havingSize(t.size);return _r(t.body,r,e)},mathmlBuilder:function(t,e){var r=e.havingSize(t.size),a=ge(t.body,r),n=new ue.MathNode("mstyle",a);return n.setAttribute("mathsize",r.sizeMultiplier+"em"),n}}),_t({type:"smash",names:["\\smash"],props:{numArgs:1,numOptionalArgs:1,allowedInText:!0},handler:function(t,e,r){var a=t.parser,n=!1,i=!1,o=r[0]&&Ce(r[0],"ordgroup");if(o)for(var s="",l=0;lr.height+r.depth+i&&(i=(i+c-r.height-r.depth)/2);var u=l.height-r.height-i-h;r.style.paddingLeft=m+"em";var p=Dt.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:r,wrapperClasses:["svg-align"]},{type:"kern",size:-(r.height+u)},{type:"elem",elem:l},{type:"kern",size:h}]},e);if(t.index){var d=e.havingStyle(w.SCRIPTSCRIPT),f=oe(t.index,d,e),g=.6*(p.height-p.depth),x=Dt.makeVList({positionType:"shift",positionData:-g,children:[{type:"elem",elem:f}]},e),v=Dt.makeSpan(["root"],[x]);return Dt.makeSpan(["mord","sqrt"],[v,p],e)}return Dt.makeSpan(["mord","sqrt"],[p],e)},mathmlBuilder:function(t,e){var r=t.body,a=t.index;return a?new ue.MathNode("mroot",[ve(r,e),ve(a,e)]):new ue.MathNode("msqrt",[ve(r,e)])}});var $r={display:w.DISPLAY,text:w.TEXT,script:w.SCRIPT,scriptscript:w.SCRIPTSCRIPT};_t({type:"styling",names:["\\displaystyle","\\textstyle","\\scriptstyle","\\scriptscriptstyle"],props:{numArgs:0,allowedInText:!0},handler:function(t,e){var r=t.breakOnTokenText,a=t.funcName,n=t.parser,i=n.parseExpression(!0,r),o=a.slice(1,a.length-5);return{type:"styling",mode:n.mode,style:o,body:i}},htmlBuilder:function(t,e){var r=$r[t.style],a=e.havingStyle(r).withFont("");return _r(t.body,a,e)},mathmlBuilder:function(t,e){var r=$r[t.style],a=e.havingStyle(r),n=ge(t.body,a),i=new ue.MathNode("mstyle",n),o={display:["0","true"],text:["0","false"],script:["1","false"],scriptscript:["2","false"]}[t.style];return i.setAttribute("scriptlevel",o[0]),i.setAttribute("displaystyle",o[1]),i}});jt({type:"supsub",htmlBuilder:function(t,e){var r=function(t,e){var r=t.base;return r?"op"===r.type?r.limits&&(e.style.size===w.DISPLAY.size||r.alwaysHandleSupSub)?Ur:null:"operatorname"===r.type?r.alwaysHandleSupSub&&(e.style.size===w.DISPLAY.size||r.limits)?Xr:null:"accent"===r.type?c.isCharacterBox(r.base)?Ie:null:"horizBrace"===r.type&&!t.sub===r.isOver?Pr:null:null}(t,e);if(r)return r(t,e);var a,n,i,o=t.base,s=t.sup,l=t.sub,h=oe(o,e),m=e.fontMetrics(),u=0,p=0,d=o&&c.isCharacterBox(o);if(s){var f=e.havingStyle(e.style.sup());a=oe(s,f,e),d||(u=h.height-f.fontMetrics().supDrop*f.sizeMultiplier/e.sizeMultiplier)}if(l){var g=e.havingStyle(e.style.sub());n=oe(l,g,e),d||(p=h.depth+g.fontMetrics().subDrop*g.sizeMultiplier/e.sizeMultiplier)}i=e.style===w.DISPLAY?m.sup1:e.style.cramped?m.sup3:m.sup2;var x,v=e.sizeMultiplier,b=.5/m.ptPerEm/v+"em",y=null;if(n){var k=t.base&&"op"===t.base.type&&t.base.name&&("\\oiint"===t.base.name||"\\oiiint"===t.base.name);(h instanceof E||k)&&(y=-h.italic+"em")}if(a&&n){u=Math.max(u,i,a.depth+.25*m.xHeight),p=Math.max(p,m.sub2);var S=4*m.defaultRuleThickness;if(u-a.depth-(n.height-p)0&&(u+=M,p-=M)}var z=[{type:"elem",elem:n,shift:p,marginRight:b,marginLeft:y},{type:"elem",elem:a,shift:-u,marginRight:b}];x=Dt.makeVList({positionType:"individualShift",children:z},e)}else if(n){p=Math.max(p,m.sub1,n.height-.8*m.xHeight);var A=[{type:"elem",elem:n,marginLeft:y,marginRight:b}];x=Dt.makeVList({positionType:"shift",positionData:p,children:A},e)}else{if(!a)throw new Error("supsub must have either sup or sub.");u=Math.max(u,i,a.depth+.25*m.xHeight),x=Dt.makeVList({positionType:"shift",positionData:-u,children:[{type:"elem",elem:a,marginRight:b}]},e)}var T=ne(h,"right")||"mord";return Dt.makeSpan([T],[h,Dt.makeSpan(["msupsub"],[x])],e)},mathmlBuilder:function(t,e){var r,a=!1;t.base&&"horizBrace"===t.base.type&&!!t.sup===t.base.isOver&&(a=!0,r=t.base.isOver),!t.base||"op"!==t.base.type&&"operatorname"!==t.base.type||(t.base.parentIsSupSub=!0);var n,i=[ve(t.base,e)];if(t.sub&&i.push(ve(t.sub,e)),t.sup&&i.push(ve(t.sup,e)),a)n=r?"mover":"munder";else if(t.sub)if(t.sup){var o=t.base;n=o&&"op"===o.type&&o.limits&&e.style===w.DISPLAY?"munderover":o&&"operatorname"===o.type&&o.alwaysHandleSupSub&&(e.style===w.DISPLAY||o.limits)?"munderover":"msubsup"}else{var s=t.base;n=s&&"op"===s.type&&s.limits&&(e.style===w.DISPLAY||s.alwaysHandleSupSub)?"munder":s&&"operatorname"===s.type&&s.alwaysHandleSupSub&&(s.limits||e.style===w.DISPLAY)?"munder":"msub"}else{var l=t.base;n=l&&"op"===l.type&&l.limits&&(e.style===w.DISPLAY||l.alwaysHandleSupSub)?"mover":l&&"operatorname"===l.type&&l.alwaysHandleSupSub&&(l.limits||e.style===w.DISPLAY)?"mover":"msup"}return new ue.MathNode(n,i)}}),jt({type:"atom",htmlBuilder:function(t,e){return Dt.mathsym(t.text,t.mode,e,["m"+t.family])},mathmlBuilder:function(t,e){var r=new ue.MathNode("mo",[pe(t.text,t.mode)]);if("bin"===t.family){var a=fe(t,e);"bold-italic"===a&&r.setAttribute("mathvariant",a)}else"punct"===t.family?r.setAttribute("separator","true"):"open"!==t.family&&"close"!==t.family||r.setAttribute("stretchy","false");return r}});var Zr={mi:"italic",mn:"normal",mtext:"normal"};jt({type:"mathord",htmlBuilder:function(t,e){return Dt.makeOrd(t,e,"mathord")},mathmlBuilder:function(t,e){var r=new ue.MathNode("mi",[pe(t.text,t.mode,e)]),a=fe(t,e)||"italic";return a!==Zr[r.type]&&r.setAttribute("mathvariant",a),r}}),jt({type:"textord",htmlBuilder:function(t,e){return Dt.makeOrd(t,e,"textord")},mathmlBuilder:function(t,e){var r,a=pe(t.text,t.mode,e),n=fe(t,e)||"normal";return r="text"===t.mode?new ue.MathNode("mtext",[a]):/[0-9]/.test(t.text)?new ue.MathNode("mn",[a]):"\\prime"===t.text?new ue.MathNode("mo",[a]):new ue.MathNode("mi",[a]),n!==Zr[r.type]&&r.setAttribute("mathvariant",n),r}});var Kr={"\\nobreak":"nobreak","\\allowbreak":"allowbreak"},Jr={" ":{},"\\ ":{},"~":{className:"nobreak"},"\\space":{},"\\nobreakspace":{className:"nobreak"}};jt({type:"spacing",htmlBuilder:function(t,e){if(Jr.hasOwnProperty(t.text)){var r=Jr[t.text].className||"";if("text"===t.mode){var a=Dt.makeOrd(t,e,"textord");return a.classes.push(r),a}return Dt.makeSpan(["mspace",r],[Dt.mathsym(t.text,t.mode,e)],e)}if(Kr.hasOwnProperty(t.text))return Dt.makeSpan(["mspace",Kr[t.text]],[],e);throw new o('Unknown type of space "'+t.text+'"')},mathmlBuilder:function(t,e){if(!Jr.hasOwnProperty(t.text)){if(Kr.hasOwnProperty(t.text))return new ue.MathNode("mspace");throw new o('Unknown type of space "'+t.text+'"')}return new ue.MathNode("mtext",[new ue.TextNode("\xa0")])}});var Qr=function(){var t=new ue.MathNode("mtd",[]);return t.setAttribute("width","50%"),t};jt({type:"tag",mathmlBuilder:function(t,e){var r=new ue.MathNode("mtable",[new ue.MathNode("mtr",[Qr(),new ue.MathNode("mtd",[xe(t.body,e)]),Qr(),new ue.MathNode("mtd",[xe(t.tag,e)])])]);return r.setAttribute("width","100%"),r}});var ta={"\\text":void 0,"\\textrm":"textrm","\\textsf":"textsf","\\texttt":"texttt","\\textnormal":"textrm"},ea={"\\textbf":"textbf","\\textmd":"textmd"},ra={"\\textit":"textit","\\textup":"textup"},aa=function(t,e){var r=t.font;return r?ta[r]?e.withTextFontFamily(ta[r]):ea[r]?e.withTextFontWeight(ea[r]):e.withTextFontShape(ra[r]):e};_t({type:"text",names:["\\text","\\textrm","\\textsf","\\texttt","\\textnormal","\\textbf","\\textmd","\\textit","\\textup"],props:{numArgs:1,argTypes:["text"],greediness:2,allowedInText:!0},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];return{type:"text",mode:r.mode,body:$t(n),font:a}},htmlBuilder:function(t,e){var r=aa(t,e),a=ee(t.body,r,!0);return Dt.makeSpan(["mord","text"],Dt.tryCombineChars(a),r)},mathmlBuilder:function(t,e){var r=aa(t,e);return xe(t.body,r)}}),_t({type:"underline",names:["\\underline"],props:{numArgs:1,allowedInText:!0},handler:function(t,e){return{type:"underline",mode:t.parser.mode,body:e[0]}},htmlBuilder:function(t,e){var r=oe(t.body,e),a=Dt.makeLineSpan("underline-line",e),n=e.fontMetrics().defaultRuleThickness,i=Dt.makeVList({positionType:"top",positionData:r.height,children:[{type:"kern",size:n},{type:"elem",elem:a},{type:"kern",size:3*n},{type:"elem",elem:r}]},e);return Dt.makeSpan(["mord","underline"],[i],e)},mathmlBuilder:function(t,e){var r=new ue.MathNode("mo",[new ue.TextNode("\u203e")]);r.setAttribute("stretchy","true");var a=new ue.MathNode("munder",[ve(t.body,e),r]);return a.setAttribute("accentunder","true"),a}}),_t({type:"verb",names:["\\verb"],props:{numArgs:0,allowedInText:!0},handler:function(t,e,r){throw new o("\\verb ended by end of line instead of matching delimiter")},htmlBuilder:function(t,e){for(var r=na(t),a=[],n=e.havingStyle(e.style.text()),i=0;i0&&(this.undefStack[this.undefStack.length-1][t]=e)}else{var n=this.undefStack[this.undefStack.length-1];n&&!n.hasOwnProperty(t)&&(n[t]=this.current[t])}this.current[t]=e},t}(),ca={},ua=ca;function pa(t,e){ca[t]=e}pa("\\noexpand",function(t){var e=t.popToken();return t.isExpandable(e.text)&&(e.noexpand=!0,e.treatAsRelax=!0),{tokens:[e],numArgs:0}}),pa("\\expandafter",function(t){var e=t.popToken();return t.expandOnce(!0),{tokens:[e],numArgs:0}}),pa("\\@firstoftwo",function(t){return{tokens:t.consumeArgs(2)[0],numArgs:0}}),pa("\\@secondoftwo",function(t){return{tokens:t.consumeArgs(2)[1],numArgs:0}}),pa("\\@ifnextchar",function(t){var e=t.consumeArgs(3);t.consumeSpaces();var r=t.future();return 1===e[0].length&&e[0][0].text===r.text?{tokens:e[1],numArgs:0}:{tokens:e[2],numArgs:0}}),pa("\\@ifstar","\\@ifnextchar *{\\@firstoftwo{#1}}"),pa("\\TextOrMath",function(t){var e=t.consumeArgs(2);return"text"===t.mode?{tokens:e[0],numArgs:0}:{tokens:e[1],numArgs:0}});var da={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,a:10,A:10,b:11,B:11,c:12,C:12,d:13,D:13,e:14,E:14,f:15,F:15};pa("\\char",function(t){var e,r=t.popToken(),a="";if("'"===r.text)e=8,r=t.popToken();else if('"'===r.text)e=16,r=t.popToken();else if("`"===r.text)if("\\"===(r=t.popToken()).text[0])a=r.text.charCodeAt(1);else{if("EOF"===r.text)throw new o("\\char` missing argument");a=r.text.charCodeAt(0)}else e=10;if(e){if(null==(a=da[r.text])||a>=e)throw new o("Invalid base-"+e+" digit "+r.text);for(var n;null!=(n=da[t.future().text])&&n":"\\dotsb","-":"\\dotsb","*":"\\dotsb",":":"\\dotsb","\\DOTSB":"\\dotsb","\\coprod":"\\dotsb","\\bigvee":"\\dotsb","\\bigwedge":"\\dotsb","\\biguplus":"\\dotsb","\\bigcap":"\\dotsb","\\bigcup":"\\dotsb","\\prod":"\\dotsb","\\sum":"\\dotsb","\\bigotimes":"\\dotsb","\\bigoplus":"\\dotsb","\\bigodot":"\\dotsb","\\bigsqcup":"\\dotsb","\\And":"\\dotsb","\\longrightarrow":"\\dotsb","\\Longrightarrow":"\\dotsb","\\longleftarrow":"\\dotsb","\\Longleftarrow":"\\dotsb","\\longleftrightarrow":"\\dotsb","\\Longleftrightarrow":"\\dotsb","\\mapsto":"\\dotsb","\\longmapsto":"\\dotsb","\\hookrightarrow":"\\dotsb","\\doteq":"\\dotsb","\\mathbin":"\\dotsb","\\mathrel":"\\dotsb","\\relbar":"\\dotsb","\\Relbar":"\\dotsb","\\xrightarrow":"\\dotsb","\\xleftarrow":"\\dotsb","\\DOTSI":"\\dotsi","\\int":"\\dotsi","\\oint":"\\dotsi","\\iint":"\\dotsi","\\iiint":"\\dotsi","\\iiiint":"\\dotsi","\\idotsint":"\\dotsi","\\DOTSX":"\\dotsx"};pa("\\dots",function(t){var e="\\dotso",r=t.expandAfterFuture().text;return r in ga?e=ga[r]:"\\not"===r.substr(0,4)?e="\\dotsb":r in j.math&&c.contains(["bin","rel"],j.math[r].group)&&(e="\\dotsb"),e});var xa={")":!0,"]":!0,"\\rbrack":!0,"\\}":!0,"\\rbrace":!0,"\\rangle":!0,"\\rceil":!0,"\\rfloor":!0,"\\rgroup":!0,"\\rmoustache":!0,"\\right":!0,"\\bigr":!0,"\\biggr":!0,"\\Bigr":!0,"\\Biggr":!0,$:!0,";":!0,".":!0,",":!0};pa("\\dotso",function(t){return t.future().text in xa?"\\ldots\\,":"\\ldots"}),pa("\\dotsc",function(t){var e=t.future().text;return e in xa&&","!==e?"\\ldots\\,":"\\ldots"}),pa("\\cdots",function(t){return t.future().text in xa?"\\@cdots\\,":"\\@cdots"}),pa("\\dotsb","\\cdots"),pa("\\dotsm","\\cdots"),pa("\\dotsi","\\!\\cdots"),pa("\\dotsx","\\ldots\\,"),pa("\\DOTSI","\\relax"),pa("\\DOTSB","\\relax"),pa("\\DOTSX","\\relax"),pa("\\tmspace","\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax"),pa("\\,","\\tmspace+{3mu}{.1667em}"),pa("\\thinspace","\\,"),pa("\\>","\\mskip{4mu}"),pa("\\:","\\tmspace+{4mu}{.2222em}"),pa("\\medspace","\\:"),pa("\\;","\\tmspace+{5mu}{.2777em}"),pa("\\thickspace","\\;"),pa("\\!","\\tmspace-{3mu}{.1667em}"),pa("\\negthinspace","\\!"),pa("\\negmedspace","\\tmspace-{4mu}{.2222em}"),pa("\\negthickspace","\\tmspace-{5mu}{.277em}"),pa("\\enspace","\\kern.5em "),pa("\\enskip","\\hskip.5em\\relax"),pa("\\quad","\\hskip1em\\relax"),pa("\\qquad","\\hskip2em\\relax"),pa("\\tag","\\@ifstar\\tag@literal\\tag@paren"),pa("\\tag@paren","\\tag@literal{({#1})}"),pa("\\tag@literal",function(t){if(t.macros.get("\\df@tag"))throw new o("Multiple \\tag");return"\\gdef\\df@tag{\\text{#1}}"}),pa("\\bmod","\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}\\mathbin{\\rm mod}\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}"),pa("\\pod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern8mu}{\\mkern8mu}{\\mkern8mu}(#1)"),pa("\\pmod","\\pod{{\\rm mod}\\mkern6mu#1}"),pa("\\mod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern12mu}{\\mkern12mu}{\\mkern12mu}{\\rm mod}\\,\\,#1"),pa("\\pmb","\\html@mathml{\\@binrel{#1}{\\mathrlap{#1}\\kern0.5px#1}}{\\mathbf{#1}}"),pa("\\\\","\\newline"),pa("\\TeX","\\textrm{\\html@mathml{T\\kern-.1667em\\raisebox{-.5ex}{E}\\kern-.125emX}{TeX}}");var va=F["Main-Regular"]["T".charCodeAt(0)][1]-.7*F["Main-Regular"]["A".charCodeAt(0)][1]+"em";pa("\\LaTeX","\\textrm{\\html@mathml{L\\kern-.36em\\raisebox{"+va+"}{\\scriptstyle A}\\kern-.15em\\TeX}{LaTeX}}"),pa("\\KaTeX","\\textrm{\\html@mathml{K\\kern-.17em\\raisebox{"+va+"}{\\scriptstyle A}\\kern-.15em\\TeX}{KaTeX}}"),pa("\\hspace","\\@ifstar\\@hspacer\\@hspace"),pa("\\@hspace","\\hskip #1\\relax"),pa("\\@hspacer","\\rule{0pt}{0pt}\\hskip #1\\relax"),pa("\\ordinarycolon",":"),pa("\\vcentcolon","\\mathrel{\\mathop\\ordinarycolon}"),pa("\\dblcolon",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-.9mu}\\vcentcolon}}{\\mathop{\\char"2237}}'),pa("\\coloneqq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2254}}'),pa("\\Coloneqq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2237\\char"3d}}'),pa("\\coloneq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"3a\\char"2212}}'),pa("\\Coloneq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"2237\\char"2212}}'),pa("\\eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2255}}'),pa("\\Eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"3d\\char"2237}}'),pa("\\eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2239}}'),pa("\\Eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"2212\\char"2237}}'),pa("\\colonapprox",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"3a\\char"2248}}'),pa("\\Colonapprox",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"2237\\char"2248}}'),pa("\\colonsim",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"3a\\char"223c}}'),pa("\\Colonsim",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"2237\\char"223c}}'),pa("\u2237","\\dblcolon"),pa("\u2239","\\eqcolon"),pa("\u2254","\\coloneqq"),pa("\u2255","\\eqqcolon"),pa("\u2a74","\\Coloneqq"),pa("\\ratio","\\vcentcolon"),pa("\\coloncolon","\\dblcolon"),pa("\\colonequals","\\coloneqq"),pa("\\coloncolonequals","\\Coloneqq"),pa("\\equalscolon","\\eqqcolon"),pa("\\equalscoloncolon","\\Eqqcolon"),pa("\\colonminus","\\coloneq"),pa("\\coloncolonminus","\\Coloneq"),pa("\\minuscolon","\\eqcolon"),pa("\\minuscoloncolon","\\Eqcolon"),pa("\\coloncolonapprox","\\Colonapprox"),pa("\\coloncolonsim","\\Colonsim"),pa("\\simcolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),pa("\\simcoloncolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\dblcolon}"),pa("\\approxcolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),pa("\\approxcoloncolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\dblcolon}"),pa("\\notni","\\html@mathml{\\not\\ni}{\\mathrel{\\char`\u220c}}"),pa("\\limsup","\\DOTSB\\operatorname*{lim\\,sup}"),pa("\\liminf","\\DOTSB\\operatorname*{lim\\,inf}"),pa("\\gvertneqq","\\html@mathml{\\@gvertneqq}{\u2269}"),pa("\\lvertneqq","\\html@mathml{\\@lvertneqq}{\u2268}"),pa("\\ngeqq","\\html@mathml{\\@ngeqq}{\u2271}"),pa("\\ngeqslant","\\html@mathml{\\@ngeqslant}{\u2271}"),pa("\\nleqq","\\html@mathml{\\@nleqq}{\u2270}"),pa("\\nleqslant","\\html@mathml{\\@nleqslant}{\u2270}"),pa("\\nshortmid","\\html@mathml{\\@nshortmid}{\u2224}"),pa("\\nshortparallel","\\html@mathml{\\@nshortparallel}{\u2226}"),pa("\\nsubseteqq","\\html@mathml{\\@nsubseteqq}{\u2288}"),pa("\\nsupseteqq","\\html@mathml{\\@nsupseteqq}{\u2289}"),pa("\\varsubsetneq","\\html@mathml{\\@varsubsetneq}{\u228a}"),pa("\\varsubsetneqq","\\html@mathml{\\@varsubsetneqq}{\u2acb}"),pa("\\varsupsetneq","\\html@mathml{\\@varsupsetneq}{\u228b}"),pa("\\varsupsetneqq","\\html@mathml{\\@varsupsetneqq}{\u2acc}"),pa("\\imath","\\html@mathml{\\@imath}{\u0131}"),pa("\\jmath","\\html@mathml{\\@jmath}{\u0237}"),pa("\\llbracket","\\html@mathml{\\mathopen{[\\mkern-3.2mu[}}{\\mathopen{\\char`\u27e6}}"),pa("\\rrbracket","\\html@mathml{\\mathclose{]\\mkern-3.2mu]}}{\\mathclose{\\char`\u27e7}}"),pa("\u27e6","\\llbracket"),pa("\u27e7","\\rrbracket"),pa("\\lBrace","\\html@mathml{\\mathopen{\\{\\mkern-3.2mu[}}{\\mathopen{\\char`\u2983}}"),pa("\\rBrace","\\html@mathml{\\mathclose{]\\mkern-3.2mu\\}}}{\\mathclose{\\char`\u2984}}"),pa("\u2983","\\lBrace"),pa("\u2984","\\rBrace"),pa("\\minuso","\\mathbin{\\html@mathml{{\\mathrlap{\\mathchoice{\\kern{0.145em}}{\\kern{0.145em}}{\\kern{0.1015em}}{\\kern{0.0725em}}\\circ}{-}}}{\\char`\u29b5}}"),pa("\u29b5","\\minuso"),pa("\\darr","\\downarrow"),pa("\\dArr","\\Downarrow"),pa("\\Darr","\\Downarrow"),pa("\\lang","\\langle"),pa("\\rang","\\rangle"),pa("\\uarr","\\uparrow"),pa("\\uArr","\\Uparrow"),pa("\\Uarr","\\Uparrow"),pa("\\N","\\mathbb{N}"),pa("\\R","\\mathbb{R}"),pa("\\Z","\\mathbb{Z}"),pa("\\alef","\\aleph"),pa("\\alefsym","\\aleph"),pa("\\Alpha","\\mathrm{A}"),pa("\\Beta","\\mathrm{B}"),pa("\\bull","\\bullet"),pa("\\Chi","\\mathrm{X}"),pa("\\clubs","\\clubsuit"),pa("\\cnums","\\mathbb{C}"),pa("\\Complex","\\mathbb{C}"),pa("\\Dagger","\\ddagger"),pa("\\diamonds","\\diamondsuit"),pa("\\empty","\\emptyset"),pa("\\Epsilon","\\mathrm{E}"),pa("\\Eta","\\mathrm{H}"),pa("\\exist","\\exists"),pa("\\harr","\\leftrightarrow"),pa("\\hArr","\\Leftrightarrow"),pa("\\Harr","\\Leftrightarrow"),pa("\\hearts","\\heartsuit"),pa("\\image","\\Im"),pa("\\infin","\\infty"),pa("\\Iota","\\mathrm{I}"),pa("\\isin","\\in"),pa("\\Kappa","\\mathrm{K}"),pa("\\larr","\\leftarrow"),pa("\\lArr","\\Leftarrow"),pa("\\Larr","\\Leftarrow"),pa("\\lrarr","\\leftrightarrow"),pa("\\lrArr","\\Leftrightarrow"),pa("\\Lrarr","\\Leftrightarrow"),pa("\\Mu","\\mathrm{M}"),pa("\\natnums","\\mathbb{N}"),pa("\\Nu","\\mathrm{N}"),pa("\\Omicron","\\mathrm{O}"),pa("\\plusmn","\\pm"),pa("\\rarr","\\rightarrow"),pa("\\rArr","\\Rightarrow"),pa("\\Rarr","\\Rightarrow"),pa("\\real","\\Re"),pa("\\reals","\\mathbb{R}"),pa("\\Reals","\\mathbb{R}"),pa("\\Rho","\\mathrm{P}"),pa("\\sdot","\\cdot"),pa("\\sect","\\S"),pa("\\spades","\\spadesuit"),pa("\\sub","\\subset"),pa("\\sube","\\subseteq"),pa("\\supe","\\supseteq"),pa("\\Tau","\\mathrm{T}"),pa("\\thetasym","\\vartheta"),pa("\\weierp","\\wp"),pa("\\Zeta","\\mathrm{Z}"),pa("\\argmin","\\DOTSB\\operatorname*{arg\\,min}"),pa("\\argmax","\\DOTSB\\operatorname*{arg\\,max}"),pa("\\plim","\\DOTSB\\mathop{\\operatorname{plim}}\\limits"),pa("\\bra","\\mathinner{\\langle{#1}|}"),pa("\\ket","\\mathinner{|{#1}\\rangle}"),pa("\\braket","\\mathinner{\\langle{#1}\\rangle}"),pa("\\Bra","\\left\\langle#1\\right|"),pa("\\Ket","\\left|#1\\right\\rangle"),pa("\\blue","\\textcolor{##6495ed}{#1}"),pa("\\orange","\\textcolor{##ffa500}{#1}"),pa("\\pink","\\textcolor{##ff00af}{#1}"),pa("\\red","\\textcolor{##df0030}{#1}"),pa("\\green","\\textcolor{##28ae7b}{#1}"),pa("\\gray","\\textcolor{gray}{#1}"),pa("\\purple","\\textcolor{##9d38bd}{#1}"),pa("\\blueA","\\textcolor{##ccfaff}{#1}"),pa("\\blueB","\\textcolor{##80f6ff}{#1}"),pa("\\blueC","\\textcolor{##63d9ea}{#1}"),pa("\\blueD","\\textcolor{##11accd}{#1}"),pa("\\blueE","\\textcolor{##0c7f99}{#1}"),pa("\\tealA","\\textcolor{##94fff5}{#1}"),pa("\\tealB","\\textcolor{##26edd5}{#1}"),pa("\\tealC","\\textcolor{##01d1c1}{#1}"),pa("\\tealD","\\textcolor{##01a995}{#1}"),pa("\\tealE","\\textcolor{##208170}{#1}"),pa("\\greenA","\\textcolor{##b6ffb0}{#1}"),pa("\\greenB","\\textcolor{##8af281}{#1}"),pa("\\greenC","\\textcolor{##74cf70}{#1}"),pa("\\greenD","\\textcolor{##1fab54}{#1}"),pa("\\greenE","\\textcolor{##0d923f}{#1}"),pa("\\goldA","\\textcolor{##ffd0a9}{#1}"),pa("\\goldB","\\textcolor{##ffbb71}{#1}"),pa("\\goldC","\\textcolor{##ff9c39}{#1}"),pa("\\goldD","\\textcolor{##e07d10}{#1}"),pa("\\goldE","\\textcolor{##a75a05}{#1}"),pa("\\redA","\\textcolor{##fca9a9}{#1}"),pa("\\redB","\\textcolor{##ff8482}{#1}"),pa("\\redC","\\textcolor{##f9685d}{#1}"),pa("\\redD","\\textcolor{##e84d39}{#1}"),pa("\\redE","\\textcolor{##bc2612}{#1}"),pa("\\maroonA","\\textcolor{##ffbde0}{#1}"),pa("\\maroonB","\\textcolor{##ff92c6}{#1}"),pa("\\maroonC","\\textcolor{##ed5fa6}{#1}"),pa("\\maroonD","\\textcolor{##ca337c}{#1}"),pa("\\maroonE","\\textcolor{##9e034e}{#1}"),pa("\\purpleA","\\textcolor{##ddd7ff}{#1}"),pa("\\purpleB","\\textcolor{##c6b9fc}{#1}"),pa("\\purpleC","\\textcolor{##aa87ff}{#1}"),pa("\\purpleD","\\textcolor{##7854ab}{#1}"),pa("\\purpleE","\\textcolor{##543b78}{#1}"),pa("\\mintA","\\textcolor{##f5f9e8}{#1}"),pa("\\mintB","\\textcolor{##edf2df}{#1}"),pa("\\mintC","\\textcolor{##e0e5cc}{#1}"),pa("\\grayA","\\textcolor{##f6f7f7}{#1}"),pa("\\grayB","\\textcolor{##f0f1f2}{#1}"),pa("\\grayC","\\textcolor{##e3e5e6}{#1}"),pa("\\grayD","\\textcolor{##d6d8da}{#1}"),pa("\\grayE","\\textcolor{##babec2}{#1}"),pa("\\grayF","\\textcolor{##888d93}{#1}"),pa("\\grayG","\\textcolor{##626569}{#1}"),pa("\\grayH","\\textcolor{##3b3e40}{#1}"),pa("\\grayI","\\textcolor{##21242c}{#1}"),pa("\\kaBlue","\\textcolor{##314453}{#1}"),pa("\\kaGreen","\\textcolor{##71B307}{#1}");var ba={"\\relax":!0,"^":!0,_:!0,"\\limits":!0,"\\nolimits":!0},ya=function(){function t(t,e,r){this.settings=void 0,this.expansionCount=void 0,this.lexer=void 0,this.macros=void 0,this.stack=void 0,this.mode=void 0,this.settings=e,this.expansionCount=0,this.feed(t),this.macros=new ma(ua,e.macros),this.mode=r,this.stack=[]}var e=t.prototype;return e.feed=function(t){this.lexer=new ha(t,this.settings)},e.switchMode=function(t){this.mode=t},e.beginGroup=function(){this.macros.beginGroup()},e.endGroup=function(){this.macros.endGroup()},e.future=function(){return 0===this.stack.length&&this.pushToken(this.lexer.lex()),this.stack[this.stack.length-1]},e.popToken=function(){return this.future(),this.stack.pop()},e.pushToken=function(t){this.stack.push(t)},e.pushTokens=function(t){var e;(e=this.stack).push.apply(e,t)},e.consumeSpaces=function(){for(;;){if(" "!==this.future().text)break;this.stack.pop()}},e.consumeArgs=function(t){for(var e=[],r=0;rthis.settings.maxExpand)throw new o("Too many expansions: infinite loop or need to increase maxExpand setting");var n=a.tokens;if(a.numArgs)for(var i=this.consumeArgs(a.numArgs),s=(n=n.slice()).length-1;s>=0;--s){var l=n[s];if("#"===l.text){if(0===s)throw new o("Incomplete placeholder at end of macro body",l);if("#"===(l=n[--s]).text)n.splice(s+1,1);else{if(!/^[1-9]$/.test(l.text))throw new o("Not a valid argument number",l);var h;(h=n).splice.apply(h,[s,2].concat(i[+l.text-1]))}}}return this.pushTokens(n),n},e.expandAfterFuture=function(){return this.expandOnce(),this.future()},e.expandNextToken=function(){for(;;){var t=this.expandOnce();if(t instanceof n){if("\\relax"!==t.text&&!t.treatAsRelax)return this.stack.pop();this.stack.pop()}}throw new Error},e.expandMacro=function(t){return this.macros.has(t)?this.expandTokens([new n(t)]):void 0},e.expandTokens=function(t){var e=[],r=this.stack.length;for(this.pushTokens(t);this.stack.length>r;){var a=this.expandOnce(!0);a instanceof n&&(a.treatAsRelax&&(a.noexpand=!1,a.treatAsRelax=!1),e.push(this.stack.pop()))}return e},e.expandMacroAsText=function(t){var e=this.expandMacro(t);return e?e.map(function(t){return t.text}).join(""):e},e._getExpansion=function(t){var e=this.macros.get(t);if(null==e)return e;var r="function"==typeof e?e(this):e;if("string"==typeof r){var a=0;if(-1!==r.indexOf("#"))for(var n=r.replace(/##/g,"");-1!==n.indexOf("#"+(a+1));)++a;for(var i=new ha(r,this.settings),o=[],s=i.lex();"EOF"!==s.text;)o.push(s),s=i.lex();return o.reverse(),{tokens:o,numArgs:a}}return r},e.isDefined=function(t){return this.macros.has(t)||ia.hasOwnProperty(t)||j.math.hasOwnProperty(t)||j.text.hasOwnProperty(t)||ba.hasOwnProperty(t)},e.isExpandable=function(t){var e=this.macros.get(t);return null!=e?"string"==typeof e||"function"==typeof e||!e.unexpandable:ia.hasOwnProperty(t)},t}(),wa={"\u0301":{text:"\\'",math:"\\acute"},"\u0300":{text:"\\`",math:"\\grave"},"\u0308":{text:'\\"',math:"\\ddot"},"\u0303":{text:"\\~",math:"\\tilde"},"\u0304":{text:"\\=",math:"\\bar"},"\u0306":{text:"\\u",math:"\\breve"},"\u030c":{text:"\\v",math:"\\check"},"\u0302":{text:"\\^",math:"\\hat"},"\u0307":{text:"\\.",math:"\\dot"},"\u030a":{text:"\\r",math:"\\mathring"},"\u030b":{text:"\\H"}},ka={"\xe1":"a\u0301","\xe0":"a\u0300","\xe4":"a\u0308","\u01df":"a\u0308\u0304","\xe3":"a\u0303","\u0101":"a\u0304","\u0103":"a\u0306","\u1eaf":"a\u0306\u0301","\u1eb1":"a\u0306\u0300","\u1eb5":"a\u0306\u0303","\u01ce":"a\u030c","\xe2":"a\u0302","\u1ea5":"a\u0302\u0301","\u1ea7":"a\u0302\u0300","\u1eab":"a\u0302\u0303","\u0227":"a\u0307","\u01e1":"a\u0307\u0304","\xe5":"a\u030a","\u01fb":"a\u030a\u0301","\u1e03":"b\u0307","\u0107":"c\u0301","\u010d":"c\u030c","\u0109":"c\u0302","\u010b":"c\u0307","\u010f":"d\u030c","\u1e0b":"d\u0307","\xe9":"e\u0301","\xe8":"e\u0300","\xeb":"e\u0308","\u1ebd":"e\u0303","\u0113":"e\u0304","\u1e17":"e\u0304\u0301","\u1e15":"e\u0304\u0300","\u0115":"e\u0306","\u011b":"e\u030c","\xea":"e\u0302","\u1ebf":"e\u0302\u0301","\u1ec1":"e\u0302\u0300","\u1ec5":"e\u0302\u0303","\u0117":"e\u0307","\u1e1f":"f\u0307","\u01f5":"g\u0301","\u1e21":"g\u0304","\u011f":"g\u0306","\u01e7":"g\u030c","\u011d":"g\u0302","\u0121":"g\u0307","\u1e27":"h\u0308","\u021f":"h\u030c","\u0125":"h\u0302","\u1e23":"h\u0307","\xed":"i\u0301","\xec":"i\u0300","\xef":"i\u0308","\u1e2f":"i\u0308\u0301","\u0129":"i\u0303","\u012b":"i\u0304","\u012d":"i\u0306","\u01d0":"i\u030c","\xee":"i\u0302","\u01f0":"j\u030c","\u0135":"j\u0302","\u1e31":"k\u0301","\u01e9":"k\u030c","\u013a":"l\u0301","\u013e":"l\u030c","\u1e3f":"m\u0301","\u1e41":"m\u0307","\u0144":"n\u0301","\u01f9":"n\u0300","\xf1":"n\u0303","\u0148":"n\u030c","\u1e45":"n\u0307","\xf3":"o\u0301","\xf2":"o\u0300","\xf6":"o\u0308","\u022b":"o\u0308\u0304","\xf5":"o\u0303","\u1e4d":"o\u0303\u0301","\u1e4f":"o\u0303\u0308","\u022d":"o\u0303\u0304","\u014d":"o\u0304","\u1e53":"o\u0304\u0301","\u1e51":"o\u0304\u0300","\u014f":"o\u0306","\u01d2":"o\u030c","\xf4":"o\u0302","\u1ed1":"o\u0302\u0301","\u1ed3":"o\u0302\u0300","\u1ed7":"o\u0302\u0303","\u022f":"o\u0307","\u0231":"o\u0307\u0304","\u0151":"o\u030b","\u1e55":"p\u0301","\u1e57":"p\u0307","\u0155":"r\u0301","\u0159":"r\u030c","\u1e59":"r\u0307","\u015b":"s\u0301","\u1e65":"s\u0301\u0307","\u0161":"s\u030c","\u1e67":"s\u030c\u0307","\u015d":"s\u0302","\u1e61":"s\u0307","\u1e97":"t\u0308","\u0165":"t\u030c","\u1e6b":"t\u0307","\xfa":"u\u0301","\xf9":"u\u0300","\xfc":"u\u0308","\u01d8":"u\u0308\u0301","\u01dc":"u\u0308\u0300","\u01d6":"u\u0308\u0304","\u01da":"u\u0308\u030c","\u0169":"u\u0303","\u1e79":"u\u0303\u0301","\u016b":"u\u0304","\u1e7b":"u\u0304\u0308","\u016d":"u\u0306","\u01d4":"u\u030c","\xfb":"u\u0302","\u016f":"u\u030a","\u0171":"u\u030b","\u1e7d":"v\u0303","\u1e83":"w\u0301","\u1e81":"w\u0300","\u1e85":"w\u0308","\u0175":"w\u0302","\u1e87":"w\u0307","\u1e98":"w\u030a","\u1e8d":"x\u0308","\u1e8b":"x\u0307","\xfd":"y\u0301","\u1ef3":"y\u0300","\xff":"y\u0308","\u1ef9":"y\u0303","\u0233":"y\u0304","\u0177":"y\u0302","\u1e8f":"y\u0307","\u1e99":"y\u030a","\u017a":"z\u0301","\u017e":"z\u030c","\u1e91":"z\u0302","\u017c":"z\u0307","\xc1":"A\u0301","\xc0":"A\u0300","\xc4":"A\u0308","\u01de":"A\u0308\u0304","\xc3":"A\u0303","\u0100":"A\u0304","\u0102":"A\u0306","\u1eae":"A\u0306\u0301","\u1eb0":"A\u0306\u0300","\u1eb4":"A\u0306\u0303","\u01cd":"A\u030c","\xc2":"A\u0302","\u1ea4":"A\u0302\u0301","\u1ea6":"A\u0302\u0300","\u1eaa":"A\u0302\u0303","\u0226":"A\u0307","\u01e0":"A\u0307\u0304","\xc5":"A\u030a","\u01fa":"A\u030a\u0301","\u1e02":"B\u0307","\u0106":"C\u0301","\u010c":"C\u030c","\u0108":"C\u0302","\u010a":"C\u0307","\u010e":"D\u030c","\u1e0a":"D\u0307","\xc9":"E\u0301","\xc8":"E\u0300","\xcb":"E\u0308","\u1ebc":"E\u0303","\u0112":"E\u0304","\u1e16":"E\u0304\u0301","\u1e14":"E\u0304\u0300","\u0114":"E\u0306","\u011a":"E\u030c","\xca":"E\u0302","\u1ebe":"E\u0302\u0301","\u1ec0":"E\u0302\u0300","\u1ec4":"E\u0302\u0303","\u0116":"E\u0307","\u1e1e":"F\u0307","\u01f4":"G\u0301","\u1e20":"G\u0304","\u011e":"G\u0306","\u01e6":"G\u030c","\u011c":"G\u0302","\u0120":"G\u0307","\u1e26":"H\u0308","\u021e":"H\u030c","\u0124":"H\u0302","\u1e22":"H\u0307","\xcd":"I\u0301","\xcc":"I\u0300","\xcf":"I\u0308","\u1e2e":"I\u0308\u0301","\u0128":"I\u0303","\u012a":"I\u0304","\u012c":"I\u0306","\u01cf":"I\u030c","\xce":"I\u0302","\u0130":"I\u0307","\u0134":"J\u0302","\u1e30":"K\u0301","\u01e8":"K\u030c","\u0139":"L\u0301","\u013d":"L\u030c","\u1e3e":"M\u0301","\u1e40":"M\u0307","\u0143":"N\u0301","\u01f8":"N\u0300","\xd1":"N\u0303","\u0147":"N\u030c","\u1e44":"N\u0307","\xd3":"O\u0301","\xd2":"O\u0300","\xd6":"O\u0308","\u022a":"O\u0308\u0304","\xd5":"O\u0303","\u1e4c":"O\u0303\u0301","\u1e4e":"O\u0303\u0308","\u022c":"O\u0303\u0304","\u014c":"O\u0304","\u1e52":"O\u0304\u0301","\u1e50":"O\u0304\u0300","\u014e":"O\u0306","\u01d1":"O\u030c","\xd4":"O\u0302","\u1ed0":"O\u0302\u0301","\u1ed2":"O\u0302\u0300","\u1ed6":"O\u0302\u0303","\u022e":"O\u0307","\u0230":"O\u0307\u0304","\u0150":"O\u030b","\u1e54":"P\u0301","\u1e56":"P\u0307","\u0154":"R\u0301","\u0158":"R\u030c","\u1e58":"R\u0307","\u015a":"S\u0301","\u1e64":"S\u0301\u0307","\u0160":"S\u030c","\u1e66":"S\u030c\u0307","\u015c":"S\u0302","\u1e60":"S\u0307","\u0164":"T\u030c","\u1e6a":"T\u0307","\xda":"U\u0301","\xd9":"U\u0300","\xdc":"U\u0308","\u01d7":"U\u0308\u0301","\u01db":"U\u0308\u0300","\u01d5":"U\u0308\u0304","\u01d9":"U\u0308\u030c","\u0168":"U\u0303","\u1e78":"U\u0303\u0301","\u016a":"U\u0304","\u1e7a":"U\u0304\u0308","\u016c":"U\u0306","\u01d3":"U\u030c","\xdb":"U\u0302","\u016e":"U\u030a","\u0170":"U\u030b","\u1e7c":"V\u0303","\u1e82":"W\u0301","\u1e80":"W\u0300","\u1e84":"W\u0308","\u0174":"W\u0302","\u1e86":"W\u0307","\u1e8c":"X\u0308","\u1e8a":"X\u0307","\xdd":"Y\u0301","\u1ef2":"Y\u0300","\u0178":"Y\u0308","\u1ef8":"Y\u0303","\u0232":"Y\u0304","\u0176":"Y\u0302","\u1e8e":"Y\u0307","\u0179":"Z\u0301","\u017d":"Z\u030c","\u1e90":"Z\u0302","\u017b":"Z\u0307","\u03ac":"\u03b1\u0301","\u1f70":"\u03b1\u0300","\u1fb1":"\u03b1\u0304","\u1fb0":"\u03b1\u0306","\u03ad":"\u03b5\u0301","\u1f72":"\u03b5\u0300","\u03ae":"\u03b7\u0301","\u1f74":"\u03b7\u0300","\u03af":"\u03b9\u0301","\u1f76":"\u03b9\u0300","\u03ca":"\u03b9\u0308","\u0390":"\u03b9\u0308\u0301","\u1fd2":"\u03b9\u0308\u0300","\u1fd1":"\u03b9\u0304","\u1fd0":"\u03b9\u0306","\u03cc":"\u03bf\u0301","\u1f78":"\u03bf\u0300","\u03cd":"\u03c5\u0301","\u1f7a":"\u03c5\u0300","\u03cb":"\u03c5\u0308","\u03b0":"\u03c5\u0308\u0301","\u1fe2":"\u03c5\u0308\u0300","\u1fe1":"\u03c5\u0304","\u1fe0":"\u03c5\u0306","\u03ce":"\u03c9\u0301","\u1f7c":"\u03c9\u0300","\u038e":"\u03a5\u0301","\u1fea":"\u03a5\u0300","\u03ab":"\u03a5\u0308","\u1fe9":"\u03a5\u0304","\u1fe8":"\u03a5\u0306","\u038f":"\u03a9\u0301","\u1ffa":"\u03a9\u0300"},Sa=function(){function t(t,e){this.mode=void 0,this.gullet=void 0,this.settings=void 0,this.leftrightDepth=void 0,this.nextToken=void 0,this.mode="math",this.gullet=new ya(t,e,this.mode),this.settings=e,this.leftrightDepth=0}var e=t.prototype;return e.expect=function(t,e){if(void 0===e&&(e=!0),this.fetch().text!==t)throw new o("Expected '"+t+"', got '"+this.fetch().text+"'",this.fetch());e&&this.consume()},e.consume=function(){this.nextToken=null},e.fetch=function(){return null==this.nextToken&&(this.nextToken=this.gullet.expandNextToken()),this.nextToken},e.switchMode=function(t){this.mode=t,this.gullet.switchMode(t)},e.parse=function(){this.settings.globalGroup||this.gullet.beginGroup(),this.settings.colorIsTextColor&&this.gullet.macros.set("\\color","\\textcolor");var t=this.parseExpression(!1);return this.expect("EOF"),this.settings.globalGroup||this.gullet.endGroup(),t},e.parseExpression=function(e,r){for(var a=[];;){"math"===this.mode&&this.consumeSpaces();var n=this.fetch();if(-1!==t.endOfExpression.indexOf(n.text))break;if(r&&n.text===r)break;if(e&&ia[n.text]&&ia[n.text].infix)break;var i=this.parseAtom(r);if(!i)break;"internal"!==i.type&&a.push(i)}return"text"===this.mode&&this.formLigatures(a),this.handleInfixNodes(a)},e.handleInfixNodes=function(t){for(var e,r=-1,a=0;a0&&!h||0===s&&!h&&"math"===this.mode,c=this.parseGroupOfType("argument to '"+t+"'",l,h,a,m);if(!c){if(h){i.push(null);continue}throw new o("Expected group after '"+t+"'",this.fetch())}(h?i:n).push(c)}return{args:n,optArgs:i}},e.parseGroupOfType=function(t,e,r,a,n){switch(e){case"color":return n&&this.consumeSpaces(),this.parseColorGroup(r);case"size":return n&&this.consumeSpaces(),this.parseSizeGroup(r);case"url":return this.parseUrlGroup(r,n);case"math":case"text":return this.parseGroup(t,r,a,void 0,e,n);case"hbox":var i=this.parseGroup(t,r,a,void 0,"text",n);return i?{type:"styling",mode:i.mode,body:[i],style:"text"}:i;case"raw":if(n&&this.consumeSpaces(),r&&"{"===this.fetch().text)return null;var s=this.parseStringGroup("raw",r,!0);if(s)return{type:"raw",mode:"text",string:s.text};throw new o("Expected raw group",this.fetch());case"original":case null:case void 0:return this.parseGroup(t,r,a,void 0,void 0,n);default:throw new o("Unknown group type as "+t,this.fetch())}},e.consumeSpaces=function(){for(;" "===this.fetch().text;)this.consume()},e.parseStringGroup=function(t,e,r){var a=e?"[":"{",n=e?"]":"}",i=this.fetch();if(i.text!==a){if(e)return null;if(r&&"EOF"!==i.text&&/[^{}[\]]/.test(i.text))return this.consume(),i}var s=this.mode;this.mode="text",this.expect(a);for(var l,h="",m=this.fetch(),c=0,u=m;(l=this.fetch()).text!==n||r&&c>0;){switch(l.text){case"EOF":throw new o("Unexpected end of input in "+t,m.range(u,h));case a:c++;break;case n:c--}h+=(u=l).text,this.consume()}return this.expect(n),this.mode=s,m.range(u,h)},e.parseRegexGroup=function(t,e){var r=this.mode;this.mode="text";for(var a,n=this.fetch(),i=n,s="";"EOF"!==(a=this.fetch()).text&&t.test(s+a.text);)s+=(i=a).text,this.consume();if(""===s)throw new o("Invalid "+e+": '"+n.text+"'",n);return this.mode=r,n.range(i,s)},e.parseColorGroup=function(t){var e=this.parseStringGroup("color",t);if(!e)return null;var r=/^(#[a-f0-9]{3}|#?[a-f0-9]{6}|[a-z]+)$/i.exec(e.text);if(!r)throw new o("Invalid color: '"+e.text+"'",e);var a=r[0];return/^[0-9a-f]{6}$/i.test(a)&&(a="#"+a),{type:"color-token",mode:this.mode,color:a}},e.parseSizeGroup=function(t){var e,r=!1;if(!(e=t||"{"===this.fetch().text?this.parseStringGroup("size",t):this.parseRegexGroup(/^[-+]? *(?:$|\d+|\d+\.\d*|\.\d*) *[a-z]{0,2} *$/,"size")))return null;t||0!==e.text.length||(e.text="0pt",r=!0);var a=/([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/.exec(e.text);if(!a)throw new o("Invalid size: '"+e.text+"'",e);var n={number:+(a[1]+a[2]),unit:a[3]};if(!At(n))throw new o("Invalid unit: '"+n.unit+"'",e);return{type:"size",mode:this.mode,value:n,isBlank:r}},e.parseUrlGroup=function(t,e){this.gullet.lexer.setCatcode("%",13);var r=this.parseStringGroup("url",t,!0);if(this.gullet.lexer.setCatcode("%",14),!r)return null;var a=r.text.replace(/\\([#$%&~_^{}])/g,"$1");return{type:"url",mode:this.mode,url:a}},e.parseGroup=function(e,r,n,i,s,l){var h=this.mode;s&&this.switchMode(s),l&&this.consumeSpaces();var m,c=this.fetch(),u=c.text;if(r?"["===u:"{"===u||"\\begingroup"===u){this.consume();var p=t.endOfGroup[u];this.gullet.beginGroup();var d=this.parseExpression(!1,p),f=this.fetch();this.expect(p),this.gullet.endGroup(),m={type:"ordgroup",mode:this.mode,loc:a.range(c,f),body:d,semisimple:"\\begingroup"===u||void 0}}else if(r)m=null;else if(null==(m=this.parseFunction(i,e,n)||this.parseSymbol())&&"\\"===u[0]&&!ba.hasOwnProperty(u)){if(this.settings.throwOnError)throw new o("Undefined control sequence: "+u,c);m=this.formatUnsupportedCmd(u),this.consume()}return s&&this.switchMode(h),m},e.formLigatures=function(t){for(var e=t.length-1,r=0;r=0&&this.settings.reportNonstrict("unicodeTextInMathMode",'Latin-1/Unicode text character "'+e[0]+'" used in math mode',t);var l,h=j[this.mode][e].group,m=a.range(t);if(W.hasOwnProperty(h)){var c=h;l={type:"atom",mode:this.mode,family:c,loc:m,text:e}}else l={type:h,mode:this.mode,loc:m,text:e};i=l}else{if(!(e.charCodeAt(0)>=128))return null;this.settings.strict&&(M(e.charCodeAt(0))?"math"===this.mode&&this.settings.reportNonstrict("unicodeTextInMathMode",'Unicode text character "'+e[0]+'" used in math mode',t):this.settings.reportNonstrict("unknownSymbol",'Unrecognized Unicode character "'+e[0]+'" ('+e.charCodeAt(0)+")",t)),i={type:"textord",mode:"text",loc:a.range(t),text:e}}if(this.consume(),s)for(var u=0;u 15) { - left = "…" + input.slice(start - 15, start); - } else { - left = input.slice(0, start); - } - - let right; - - if (end + 15 < input.length) { - right = input.slice(end, end + 15) + "…"; - } else { - right = input.slice(end); - } - - error += left + underlined + right; - } // Some hackery to make ParseError a prototype of Error - // See http://stackoverflow.com/a/8460753 - - - const self = new Error(error); - self.name = "ParseError"; // $FlowFixMe - - self.__proto__ = ParseError.prototype; // $FlowFixMe - - self.position = start; - return self; - } - -} // $FlowFixMe More hackery - - -ParseError.prototype.__proto__ = Error.prototype; - -/** - * This file contains a list of utility functions which are useful in other - * files. - */ - -/** - * Return whether an element is contained in a list - */ -const contains = function contains(list, elem) { - return list.indexOf(elem) !== -1; -}; -/** - * Provide a default value if a setting is undefined - * NOTE: Couldn't use `T` as the output type due to facebook/flow#5022. - */ - - -const deflt = function deflt(setting, defaultIfUndefined) { - return setting === undefined ? defaultIfUndefined : setting; -}; // hyphenate and escape adapted from Facebook's React under Apache 2 license - - -const uppercase = /([A-Z])/g; - -const hyphenate = function hyphenate(str) { - return str.replace(uppercase, "-$1").toLowerCase(); -}; - -const ESCAPE_LOOKUP = { - "&": "&", - ">": ">", - "<": "<", - "\"": """, - "'": "'" -}; -const ESCAPE_REGEX = /[&><"']/g; -/** - * Escapes text to prevent scripting attacks. - */ - -function escape(text) { - return String(text).replace(ESCAPE_REGEX, match => ESCAPE_LOOKUP[match]); -} -/** - * Sometimes we want to pull out the innermost element of a group. In most - * cases, this will just be the group itself, but when ordgroups and colors have - * a single element, we want to pull that out. - */ - - -const getBaseElem = function getBaseElem(group) { - if (group.type === "ordgroup") { - if (group.body.length === 1) { - return getBaseElem(group.body[0]); - } else { - return group; - } - } else if (group.type === "color") { - if (group.body.length === 1) { - return getBaseElem(group.body[0]); - } else { - return group; - } - } else if (group.type === "font") { - return getBaseElem(group.body); - } else { - return group; - } -}; -/** - * TeXbook algorithms often reference "character boxes", which are simply groups - * with a single character in them. To decide if something is a character box, - * we find its innermost group, and see if it is a single character. - */ - - -const isCharacterBox = function isCharacterBox(group) { - const baseElem = getBaseElem(group); // These are all they types of groups which hold single characters - - return baseElem.type === "mathord" || baseElem.type === "textord" || baseElem.type === "atom"; -}; - -const assert = function assert(value) { - if (!value) { - throw new Error('Expected non-null, but got ' + String(value)); - } - - return value; -}; -/** - * Return the protocol of a URL, or "_relative" if the URL does not specify a - * protocol (and thus is relative). - */ - -const protocolFromUrl = function protocolFromUrl(url) { - const protocol = /^\s*([^\\/#]*?)(?::|�*58|�*3a)/i.exec(url); - return protocol != null ? protocol[1] : "_relative"; -}; -var utils = { - contains, - deflt, - escape, - hyphenate, - getBaseElem, - isCharacterBox, - protocolFromUrl -}; - -/* eslint no-console:0 */ - -/** - * The main Settings object - * - * The current options stored are: - * - displayMode: Whether the expression should be typeset as inline math - * (false, the default), meaning that the math starts in - * \textstyle and is placed in an inline-block); or as display - * math (true), meaning that the math starts in \displaystyle - * and is placed in a block with vertical margin. - */ -class Settings { - constructor(options) { - this.displayMode = void 0; - this.output = void 0; - this.leqno = void 0; - this.fleqn = void 0; - this.throwOnError = void 0; - this.errorColor = void 0; - this.macros = void 0; - this.minRuleThickness = void 0; - this.colorIsTextColor = void 0; - this.strict = void 0; - this.trust = void 0; - this.maxSize = void 0; - this.maxExpand = void 0; - this.globalGroup = void 0; - // allow null options - options = options || {}; - this.displayMode = utils.deflt(options.displayMode, false); - this.output = utils.deflt(options.output, "htmlAndMathml"); - this.leqno = utils.deflt(options.leqno, false); - this.fleqn = utils.deflt(options.fleqn, false); - this.throwOnError = utils.deflt(options.throwOnError, true); - this.errorColor = utils.deflt(options.errorColor, "#cc0000"); - this.macros = options.macros || {}; - this.minRuleThickness = Math.max(0, utils.deflt(options.minRuleThickness, 0)); - this.colorIsTextColor = utils.deflt(options.colorIsTextColor, false); - this.strict = utils.deflt(options.strict, "warn"); - this.trust = utils.deflt(options.trust, false); - this.maxSize = Math.max(0, utils.deflt(options.maxSize, Infinity)); - this.maxExpand = Math.max(0, utils.deflt(options.maxExpand, 1000)); - this.globalGroup = utils.deflt(options.globalGroup, false); - } - /** - * Report nonstrict (non-LaTeX-compatible) input. - * Can safely not be called if `this.strict` is false in JavaScript. - */ - - - reportNonstrict(errorCode, errorMsg, token) { - let strict = this.strict; - - if (typeof strict === "function") { - // Allow return value of strict function to be boolean or string - // (or null/undefined, meaning no further processing). - strict = strict(errorCode, errorMsg, token); - } - - if (!strict || strict === "ignore") { - return; - } else if (strict === true || strict === "error") { - throw new ParseError("LaTeX-incompatible input and strict mode is set to 'error': " + `${errorMsg} [${errorCode}]`, token); - } else if (strict === "warn") { - typeof console !== "undefined" && console.warn("LaTeX-incompatible input and strict mode is set to 'warn': " + `${errorMsg} [${errorCode}]`); - } else { - // won't happen in type-safe code - typeof console !== "undefined" && console.warn("LaTeX-incompatible input and strict mode is set to " + `unrecognized '${strict}': ${errorMsg} [${errorCode}]`); - } - } - /** - * Check whether to apply strict (LaTeX-adhering) behavior for unusual - * input (like `\\`). Unlike `nonstrict`, will not throw an error; - * instead, "error" translates to a return value of `true`, while "ignore" - * translates to a return value of `false`. May still print a warning: - * "warn" prints a warning and returns `false`. - * This is for the second category of `errorCode`s listed in the README. - */ - - - useStrictBehavior(errorCode, errorMsg, token) { - let strict = this.strict; - - if (typeof strict === "function") { - // Allow return value of strict function to be boolean or string - // (or null/undefined, meaning no further processing). - // But catch any exceptions thrown by function, treating them - // like "error". - try { - strict = strict(errorCode, errorMsg, token); - } catch (error) { - strict = "error"; - } - } - - if (!strict || strict === "ignore") { - return false; - } else if (strict === true || strict === "error") { - return true; - } else if (strict === "warn") { - typeof console !== "undefined" && console.warn("LaTeX-incompatible input and strict mode is set to 'warn': " + `${errorMsg} [${errorCode}]`); - return false; - } else { - // won't happen in type-safe code - typeof console !== "undefined" && console.warn("LaTeX-incompatible input and strict mode is set to " + `unrecognized '${strict}': ${errorMsg} [${errorCode}]`); - return false; - } - } - /** - * Check whether to test potentially dangerous input, and return - * `true` (trusted) or `false` (untrusted). The sole argument `context` - * should be an object with `command` field specifying the relevant LaTeX - * command (as a string starting with `\`), and any other arguments, etc. - * If `context` has a `url` field, a `protocol` field will automatically - * get added by this function (changing the specified object). - */ - - - isTrusted(context) { - if (context.url && !context.protocol) { - context.protocol = utils.protocolFromUrl(context.url); - } - - const trust = typeof this.trust === "function" ? this.trust(context) : this.trust; - return Boolean(trust); - } - -} - -/** - * This file contains information and classes for the various kinds of styles - * used in TeX. It provides a generic `Style` class, which holds information - * about a specific style. It then provides instances of all the different kinds - * of styles possible, and provides functions to move between them and get - * information about them. - */ - -/** - * The main style class. Contains a unique id for the style, a size (which is - * the same for cramped and uncramped version of a style), and a cramped flag. - */ -class Style { - constructor(id, size, cramped) { - this.id = void 0; - this.size = void 0; - this.cramped = void 0; - this.id = id; - this.size = size; - this.cramped = cramped; - } - /** - * Get the style of a superscript given a base in the current style. - */ - - - sup() { - return styles[sup[this.id]]; - } - /** - * Get the style of a subscript given a base in the current style. - */ - - - sub() { - return styles[sub[this.id]]; - } - /** - * Get the style of a fraction numerator given the fraction in the current - * style. - */ - - - fracNum() { - return styles[fracNum[this.id]]; - } - /** - * Get the style of a fraction denominator given the fraction in the current - * style. - */ - - - fracDen() { - return styles[fracDen[this.id]]; - } - /** - * Get the cramped version of a style (in particular, cramping a cramped style - * doesn't change the style). - */ - - - cramp() { - return styles[cramp[this.id]]; - } - /** - * Get a text or display version of this style. - */ - - - text() { - return styles[text[this.id]]; - } - /** - * Return true if this style is tightly spaced (scriptstyle/scriptscriptstyle) - */ - - - isTight() { - return this.size >= 2; - } - -} // Export an interface for type checking, but don't expose the implementation. -// This way, no more styles can be generated. - - -// IDs of the different styles -const D = 0; -const Dc = 1; -const T = 2; -const Tc = 3; -const S = 4; -const Sc = 5; -const SS = 6; -const SSc = 7; // Instances of the different styles - -const styles = [new Style(D, 0, false), new Style(Dc, 0, true), new Style(T, 1, false), new Style(Tc, 1, true), new Style(S, 2, false), new Style(Sc, 2, true), new Style(SS, 3, false), new Style(SSc, 3, true)]; // Lookup tables for switching from one style to another - -const sup = [S, Sc, S, Sc, SS, SSc, SS, SSc]; -const sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc]; -const fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc]; -const fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc]; -const cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc]; -const text = [D, Dc, T, Tc, T, Tc, T, Tc]; // We only export some of the styles. - -var Style$1 = { - DISPLAY: styles[D], - TEXT: styles[T], - SCRIPT: styles[S], - SCRIPTSCRIPT: styles[SS] -}; - -/* - * This file defines the Unicode scripts and script families that we - * support. To add new scripts or families, just add a new entry to the - * scriptData array below. Adding scripts to the scriptData array allows - * characters from that script to appear in \text{} environments. - */ - -/** - * Each script or script family has a name and an array of blocks. - * Each block is an array of two numbers which specify the start and - * end points (inclusive) of a block of Unicode codepoints. - */ - -/** - * Unicode block data for the families of scripts we support in \text{}. - * Scripts only need to appear here if they do not have font metrics. - */ -const scriptData = [{ - // Latin characters beyond the Latin-1 characters we have metrics for. - // Needed for Czech, Hungarian and Turkish text, for example. - name: 'latin', - blocks: [[0x0100, 0x024f], // Latin Extended-A and Latin Extended-B - [0x0300, 0x036f]] -}, { - // The Cyrillic script used by Russian and related languages. - // A Cyrillic subset used to be supported as explicitly defined - // symbols in symbols.js - name: 'cyrillic', - blocks: [[0x0400, 0x04ff]] -}, { - // The Brahmic scripts of South and Southeast Asia - // Devanagari (0900–097F) - // Bengali (0980–09FF) - // Gurmukhi (0A00–0A7F) - // Gujarati (0A80–0AFF) - // Oriya (0B00–0B7F) - // Tamil (0B80–0BFF) - // Telugu (0C00–0C7F) - // Kannada (0C80–0CFF) - // Malayalam (0D00–0D7F) - // Sinhala (0D80–0DFF) - // Thai (0E00–0E7F) - // Lao (0E80–0EFF) - // Tibetan (0F00–0FFF) - // Myanmar (1000–109F) - name: 'brahmic', - blocks: [[0x0900, 0x109F]] -}, { - name: 'georgian', - blocks: [[0x10A0, 0x10ff]] -}, { - // Chinese and Japanese. - // The "k" in cjk is for Korean, but we've separated Korean out - name: "cjk", - blocks: [[0x3000, 0x30FF], // CJK symbols and punctuation, Hiragana, Katakana - [0x4E00, 0x9FAF], // CJK ideograms - [0xFF00, 0xFF60]] -}, { - // Korean - name: 'hangul', - blocks: [[0xAC00, 0xD7AF]] -}]; -/** - * Given a codepoint, return the name of the script or script family - * it is from, or null if it is not part of a known block - */ - -function scriptFromCodepoint(codepoint) { - for (let i = 0; i < scriptData.length; i++) { - const script = scriptData[i]; - - for (let i = 0; i < script.blocks.length; i++) { - const block = script.blocks[i]; - - if (codepoint >= block[0] && codepoint <= block[1]) { - return script.name; - } - } - } - - return null; -} -/** - * A flattened version of all the supported blocks in a single array. - * This is an optimization to make supportedCodepoint() fast. - */ - -const allBlocks = []; -scriptData.forEach(s => s.blocks.forEach(b => allBlocks.push(...b))); -/** - * Given a codepoint, return true if it falls within one of the - * scripts or script families defined above and false otherwise. - * - * Micro benchmarks shows that this is faster than - * /[\u3000-\u30FF\u4E00-\u9FAF\uFF00-\uFF60\uAC00-\uD7AF\u0900-\u109F]/.test() - * in Firefox, Chrome and Node. - */ - -function supportedCodepoint(codepoint) { - for (let i = 0; i < allBlocks.length; i += 2) { - if (codepoint >= allBlocks[i] && codepoint <= allBlocks[i + 1]) { - return true; - } - } - - return false; -} - -/** - * This file provides support to domTree.js and delimiter.js. - * It's a storehouse of path geometry for SVG images. - */ -// In all paths below, the viewBox-to-em scale is 1000:1. -const hLinePad = 80; // padding above a sqrt viniculum. Prevents image cropping. -// The viniculum of a \sqrt can be made thicker by a KaTeX rendering option. -// Think of variable extraViniculum as two detours in the SVG path. -// The detour begins at the lower left of the area labeled extraViniculum below. -// The detour proceeds one extraViniculum distance up and slightly to the right, -// displacing the radiused corner between surd and viniculum. The radius is -// traversed as usual, then the detour resumes. It goes right, to the end of -// the very long viniculumn, then down one extraViniculum distance, -// after which it resumes regular path geometry for the radical. - -/* viniculum - / - /▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒←extraViniculum - / █████████████████████←0.04em (40 unit) std viniculum thickness - / / - / / - / /\ - / / surd -*/ - -const sqrtMain = function sqrtMain(extraViniculum, hLinePad) { - // sqrtMain path geometry is from glyph U221A in the font KaTeX Main - return `M95,${622 + extraViniculum + hLinePad} -c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14 -c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54 -c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10 -s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429 -c69,-144,104.5,-217.7,106.5,-221 -l${extraViniculum / 2.075} -${extraViniculum} -c5.3,-9.3,12,-14,20,-14 -H400000v${40 + extraViniculum}H845.2724 -s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7 -c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z -M${834 + extraViniculum} ${hLinePad}h400000v${40 + extraViniculum}h-400000z`; -}; - -const sqrtSize1 = function sqrtSize1(extraViniculum, hLinePad) { - // size1 is from glyph U221A in the font KaTeX_Size1-Regular - return `M263,${601 + extraViniculum + hLinePad}c0.7,0,18,39.7,52,119 -c34,79.3,68.167,158.7,102.5,238c34.3,79.3,51.8,119.3,52.5,120 -c340,-704.7,510.7,-1060.3,512,-1067 -l${extraViniculum / 2.084} -${extraViniculum} -c4.7,-7.3,11,-11,19,-11 -H40000v${40 + extraViniculum}H1012.3 -s-271.3,567,-271.3,567c-38.7,80.7,-84,175,-136,283c-52,108,-89.167,185.3,-111.5,232 -c-22.3,46.7,-33.8,70.3,-34.5,71c-4.7,4.7,-12.3,7,-23,7s-12,-1,-12,-1 -s-109,-253,-109,-253c-72.7,-168,-109.3,-252,-110,-252c-10.7,8,-22,16.7,-34,26 -c-22,17.3,-33.3,26,-34,26s-26,-26,-26,-26s76,-59,76,-59s76,-60,76,-60z -M${1001 + extraViniculum} ${hLinePad}h400000v${40 + extraViniculum}h-400000z`; -}; - -const sqrtSize2 = function sqrtSize2(extraViniculum, hLinePad) { - // size2 is from glyph U221A in the font KaTeX_Size2-Regular - return `M983 ${10 + extraViniculum + hLinePad} -l${extraViniculum / 3.13} -${extraViniculum} -c4,-6.7,10,-10,18,-10 H400000v${40 + extraViniculum} -H1013.1s-83.4,268,-264.1,840c-180.7,572,-277,876.3,-289,913c-4.7,4.7,-12.7,7,-24,7 -s-12,0,-12,0c-1.3,-3.3,-3.7,-11.7,-7,-25c-35.3,-125.3,-106.7,-373.3,-214,-744 -c-10,12,-21,25,-33,39s-32,39,-32,39c-6,-5.3,-15,-14,-27,-26s25,-30,25,-30 -c26.7,-32.7,52,-63,76,-91s52,-60,52,-60s208,722,208,722 -c56,-175.3,126.3,-397.3,211,-666c84.7,-268.7,153.8,-488.2,207.5,-658.5 -c53.7,-170.3,84.5,-266.8,92.5,-289.5z -M${1001 + extraViniculum} ${hLinePad}h400000v${40 + extraViniculum}h-400000z`; -}; - -const sqrtSize3 = function sqrtSize3(extraViniculum, hLinePad) { - // size3 is from glyph U221A in the font KaTeX_Size3-Regular - return `M424,${2398 + extraViniculum + hLinePad} -c-1.3,-0.7,-38.5,-172,-111.5,-514c-73,-342,-109.8,-513.3,-110.5,-514 -c0,-2,-10.7,14.3,-32,49c-4.7,7.3,-9.8,15.7,-15.5,25c-5.7,9.3,-9.8,16,-12.5,20 -s-5,7,-5,7c-4,-3.3,-8.3,-7.7,-13,-13s-13,-13,-13,-13s76,-122,76,-122s77,-121,77,-121 -s209,968,209,968c0,-2,84.7,-361.7,254,-1079c169.3,-717.3,254.7,-1077.7,256,-1081 -l${extraViniculum / 4.223} -${extraViniculum}c4,-6.7,10,-10,18,-10 H400000 -v${40 + extraViniculum}H1014.6 -s-87.3,378.7,-272.6,1166c-185.3,787.3,-279.3,1182.3,-282,1185 -c-2,6,-10,9,-24,9 -c-8,0,-12,-0.7,-12,-2z M${1001 + extraViniculum} ${hLinePad} -h400000v${40 + extraViniculum}h-400000z`; -}; - -const sqrtSize4 = function sqrtSize4(extraViniculum, hLinePad) { - // size4 is from glyph U221A in the font KaTeX_Size4-Regular - return `M473,${2713 + extraViniculum + hLinePad} -c339.3,-1799.3,509.3,-2700,510,-2702 l${extraViniculum / 5.298} -${extraViniculum} -c3.3,-7.3,9.3,-11,18,-11 H400000v${40 + extraViniculum}H1017.7 -s-90.5,478,-276.2,1466c-185.7,988,-279.5,1483,-281.5,1485c-2,6,-10,9,-24,9 -c-8,0,-12,-0.7,-12,-2c0,-1.3,-5.3,-32,-16,-92c-50.7,-293.3,-119.7,-693.3,-207,-1200 -c0,-1.3,-5.3,8.7,-16,30c-10.7,21.3,-21.3,42.7,-32,64s-16,33,-16,33s-26,-26,-26,-26 -s76,-153,76,-153s77,-151,77,-151c0.7,0.7,35.7,202,105,604c67.3,400.7,102,602.7,104, -606zM${1001 + extraViniculum} ${hLinePad}h400000v${40 + extraViniculum}H1017.7z`; -}; - -const sqrtTall = function sqrtTall(extraViniculum, hLinePad, viewBoxHeight) { - // sqrtTall is from glyph U23B7 in the font KaTeX_Size4-Regular - // One path edge has a variable length. It runs vertically from the viniculumn - // to a point near (14 units) the bottom of the surd. The viniculum - // is normally 40 units thick. So the length of the line in question is: - const vertSegment = viewBoxHeight - 54 - hLinePad - extraViniculum; - return `M702 ${extraViniculum + hLinePad}H400000${40 + extraViniculum} -H742v${vertSegment}l-4 4-4 4c-.667.7 -2 1.5-4 2.5s-4.167 1.833-6.5 2.5-5.5 1-9.5 1 -h-12l-28-84c-16.667-52-96.667 -294.333-240-727l-212 -643 -85 170 -c-4-3.333-8.333-7.667-13 -13l-13-13l77-155 77-156c66 199.333 139 419.667 -219 661 l218 661zM702 ${hLinePad}H400000v${40 + extraViniculum}H742z`; -}; - -const sqrtPath = function sqrtPath(size, extraViniculum, viewBoxHeight) { - extraViniculum = 1000 * extraViniculum; // Convert from document ems to viewBox. - - let path = ""; - - switch (size) { - case "sqrtMain": - path = sqrtMain(extraViniculum, hLinePad); - break; - - case "sqrtSize1": - path = sqrtSize1(extraViniculum, hLinePad); - break; - - case "sqrtSize2": - path = sqrtSize2(extraViniculum, hLinePad); - break; - - case "sqrtSize3": - path = sqrtSize3(extraViniculum, hLinePad); - break; - - case "sqrtSize4": - path = sqrtSize4(extraViniculum, hLinePad); - break; - - case "sqrtTall": - path = sqrtTall(extraViniculum, hLinePad, viewBoxHeight); - } - - return path; -}; -const path = { - // Two paths that cover gaps in built-up parentheses. - leftParenInner: `M291 0 H417 V300 H291 z`, - rightParenInner: `M457 0 H583 V300 H457 z`, - // The doubleleftarrow geometry is from glyph U+21D0 in the font KaTeX Main - doubleleftarrow: `M262 157 -l10-10c34-36 62.7-77 86-123 3.3-8 5-13.3 5-16 0-5.3-6.7-8-20-8-7.3 - 0-12.2.5-14.5 1.5-2.3 1-4.8 4.5-7.5 10.5-49.3 97.3-121.7 169.3-217 216-28 - 14-57.3 25-88 33-6.7 2-11 3.8-13 5.5-2 1.7-3 4.2-3 7.5s1 5.8 3 7.5 -c2 1.7 6.3 3.5 13 5.5 68 17.3 128.2 47.8 180.5 91.5 52.3 43.7 93.8 96.2 124.5 - 157.5 9.3 8 15.3 12.3 18 13h6c12-.7 18-4 18-10 0-2-1.7-7-5-15-23.3-46-52-87 --86-123l-10-10h399738v-40H218c328 0 0 0 0 0l-10-8c-26.7-20-65.7-43-117-69 2.7 --2 6-3.7 10-5 36.7-16 72.3-37.3 107-64l10-8h399782v-40z -m8 0v40h399730v-40zm0 194v40h399730v-40z`, - // doublerightarrow is from glyph U+21D2 in font KaTeX Main - doublerightarrow: `M399738 392l --10 10c-34 36-62.7 77-86 123-3.3 8-5 13.3-5 16 0 5.3 6.7 8 20 8 7.3 0 12.2-.5 - 14.5-1.5 2.3-1 4.8-4.5 7.5-10.5 49.3-97.3 121.7-169.3 217-216 28-14 57.3-25 88 --33 6.7-2 11-3.8 13-5.5 2-1.7 3-4.2 3-7.5s-1-5.8-3-7.5c-2-1.7-6.3-3.5-13-5.5-68 --17.3-128.2-47.8-180.5-91.5-52.3-43.7-93.8-96.2-124.5-157.5-9.3-8-15.3-12.3-18 --13h-6c-12 .7-18 4-18 10 0 2 1.7 7 5 15 23.3 46 52 87 86 123l10 10H0v40h399782 -c-328 0 0 0 0 0l10 8c26.7 20 65.7 43 117 69-2.7 2-6 3.7-10 5-36.7 16-72.3 37.3 --107 64l-10 8H0v40zM0 157v40h399730v-40zm0 194v40h399730v-40z`, - // leftarrow is from glyph U+2190 in font KaTeX Main - leftarrow: `M400000 241H110l3-3c68.7-52.7 113.7-120 - 135-202 4-14.7 6-23 6-25 0-7.3-7-11-21-11-8 0-13.2.8-15.5 2.5-2.3 1.7-4.2 5.8 --5.5 12.5-1.3 4.7-2.7 10.3-4 17-12 48.7-34.8 92-68.5 130S65.3 228.3 18 247 -c-10 4-16 7.7-18 11 0 8.7 6 14.3 18 17 47.3 18.7 87.8 47 121.5 85S196 441.3 208 - 490c.7 2 1.3 5 2 9s1.2 6.7 1.5 8c.3 1.3 1 3.3 2 6s2.2 4.5 3.5 5.5c1.3 1 3.3 - 1.8 6 2.5s6 1 10 1c14 0 21-3.7 21-11 0-2-2-10.3-6-25-20-79.3-65-146.7-135-202 - l-3-3h399890zM100 241v40h399900v-40z`, - // overbrace is from glyphs U+23A9/23A8/23A7 in font KaTeX_Size4-Regular - leftbrace: `M6 548l-6-6v-35l6-11c56-104 135.3-181.3 238-232 57.3-28.7 117 --45 179-50h399577v120H403c-43.3 7-81 15-113 26-100.7 33-179.7 91-237 174-2.7 - 5-6 9-10 13-.7 1-7.3 1-20 1H6z`, - leftbraceunder: `M0 6l6-6h17c12.688 0 19.313.3 20 1 4 4 7.313 8.3 10 13 - 35.313 51.3 80.813 93.8 136.5 127.5 55.688 33.7 117.188 55.8 184.5 66.5.688 - 0 2 .3 4 1 18.688 2.7 76 4.3 172 5h399450v120H429l-6-1c-124.688-8-235-61.7 --331-161C60.687 138.7 32.312 99.3 7 54L0 41V6z`, - // overgroup is from the MnSymbol package (public domain) - leftgroup: `M400000 80 -H435C64 80 168.3 229.4 21 260c-5.9 1.2-18 0-18 0-2 0-3-1-3-3v-38C76 61 257 0 - 435 0h399565z`, - leftgroupunder: `M400000 262 -H435C64 262 168.3 112.6 21 82c-5.9-1.2-18 0-18 0-2 0-3 1-3 3v38c76 158 257 219 - 435 219h399565z`, - // Harpoons are from glyph U+21BD in font KaTeX Main - leftharpoon: `M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3 --3.3 10.2-9.5 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5 --18.3 3-21-1.3-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7 --196 228-6.7 4.7-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40z`, - leftharpoonplus: `M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3-3.3 10.2-9.5 - 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5-18.3 3-21-1.3 --4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7-196 228-6.7 4.7 --10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40zM0 435v40h400000v-40z -m0 0v40h400000v-40z`, - leftharpoondown: `M7 241c-4 4-6.333 8.667-7 14 0 5.333.667 9 2 11s5.333 - 5.333 12 10c90.667 54 156 130 196 228 3.333 10.667 6.333 16.333 9 17 2 .667 5 - 1 9 1h5c10.667 0 16.667-2 18-6 2-2.667 1-9.667-3-21-32-87.333-82.667-157.667 --152-211l-3-3h399907v-40zM93 281 H400000 v-40L7 241z`, - leftharpoondownplus: `M7 435c-4 4-6.3 8.7-7 14 0 5.3.7 9 2 11s5.3 5.3 12 - 10c90.7 54 156 130 196 228 3.3 10.7 6.3 16.3 9 17 2 .7 5 1 9 1h5c10.7 0 16.7 --2 18-6 2-2.7 1-9.7-3-21-32-87.3-82.7-157.7-152-211l-3-3h399907v-40H7zm93 0 -v40h399900v-40zM0 241v40h399900v-40zm0 0v40h399900v-40z`, - // hook is from glyph U+21A9 in font KaTeX Main - lefthook: `M400000 281 H103s-33-11.2-61-33.5S0 197.3 0 164s14.2-61.2 42.5 --83.5C70.8 58.2 104 47 142 47 c16.7 0 25 6.7 25 20 0 12-8.7 18.7-26 20-40 3.3 --68.7 15.7-86 37-10 12-15 25.3-15 40 0 22.7 9.8 40.7 29.5 54 19.7 13.3 43.5 21 - 71.5 23h399859zM103 281v-40h399897v40z`, - leftlinesegment: `M40 281 V428 H0 V94 H40 V241 H400000 v40z -M40 281 V428 H0 V94 H40 V241 H400000 v40z`, - leftmapsto: `M40 281 V448H0V74H40V241H400000v40z -M40 281 V448H0V74H40V241H400000v40z`, - // tofrom is from glyph U+21C4 in font KaTeX AMS Regular - leftToFrom: `M0 147h400000v40H0zm0 214c68 40 115.7 95.7 143 167h22c15.3 0 23 --.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69-70-101l-7-8h399905v-40H95l7-8 -c28.7-32 52-65.7 70-101 10.7-23.3 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 265.3 - 68 321 0 361zm0-174v-40h399900v40zm100 154v40h399900v-40z`, - longequal: `M0 50 h400000 v40H0z m0 194h40000v40H0z -M0 50 h400000 v40H0z m0 194h40000v40H0z`, - midbrace: `M200428 334 -c-100.7-8.3-195.3-44-280-108-55.3-42-101.7-93-139-153l-9-14c-2.7 4-5.7 8.7-9 14 --53.3 86.7-123.7 153-211 199-66.7 36-137.3 56.3-212 62H0V214h199568c178.3-11.7 - 311.7-78.3 403-201 6-8 9.7-12 11-12 .7-.7 6.7-1 18-1s17.3.3 18 1c1.3 0 5 4 11 - 12 44.7 59.3 101.3 106.3 170 141s145.3 54.3 229 60h199572v120z`, - midbraceunder: `M199572 214 -c100.7 8.3 195.3 44 280 108 55.3 42 101.7 93 139 153l9 14c2.7-4 5.7-8.7 9-14 - 53.3-86.7 123.7-153 211-199 66.7-36 137.3-56.3 212-62h199568v120H200432c-178.3 - 11.7-311.7 78.3-403 201-6 8-9.7 12-11 12-.7.7-6.7 1-18 1s-17.3-.3-18-1c-1.3 0 --5-4-11-12-44.7-59.3-101.3-106.3-170-141s-145.3-54.3-229-60H0V214z`, - oiintSize1: `M512.6 71.6c272.6 0 320.3 106.8 320.3 178.2 0 70.8-47.7 177.6 --320.3 177.6S193.1 320.6 193.1 249.8c0-71.4 46.9-178.2 319.5-178.2z -m368.1 178.2c0-86.4-60.9-215.4-368.1-215.4-306.4 0-367.3 129-367.3 215.4 0 85.8 -60.9 214.8 367.3 214.8 307.2 0 368.1-129 368.1-214.8z`, - oiintSize2: `M757.8 100.1c384.7 0 451.1 137.6 451.1 230 0 91.3-66.4 228.8 --451.1 228.8-386.3 0-452.7-137.5-452.7-228.8 0-92.4 66.4-230 452.7-230z -m502.4 230c0-111.2-82.4-277.2-502.4-277.2s-504 166-504 277.2 -c0 110 84 276 504 276s502.4-166 502.4-276z`, - oiiintSize1: `M681.4 71.6c408.9 0 480.5 106.8 480.5 178.2 0 70.8-71.6 177.6 --480.5 177.6S202.1 320.6 202.1 249.8c0-71.4 70.5-178.2 479.3-178.2z -m525.8 178.2c0-86.4-86.8-215.4-525.7-215.4-437.9 0-524.7 129-524.7 215.4 0 -85.8 86.8 214.8 524.7 214.8 438.9 0 525.7-129 525.7-214.8z`, - oiiintSize2: `M1021.2 53c603.6 0 707.8 165.8 707.8 277.2 0 110-104.2 275.8 --707.8 275.8-606 0-710.2-165.8-710.2-275.8C311 218.8 415.2 53 1021.2 53z -m770.4 277.1c0-131.2-126.4-327.6-770.5-327.6S248.4 198.9 248.4 330.1 -c0 130 128.8 326.4 772.7 326.4s770.5-196.4 770.5-326.4z`, - rightarrow: `M0 241v40h399891c-47.3 35.3-84 78-110 128 --16.7 32-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 - 11 8 0 13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 - 39-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85 --40.5-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5 --12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67 - 151.7 139 205zm0 0v40h399900v-40z`, - rightbrace: `M400000 542l --6 6h-17c-12.7 0-19.3-.3-20-1-4-4-7.3-8.3-10-13-35.3-51.3-80.8-93.8-136.5-127.5 -s-117.2-55.8-184.5-66.5c-.7 0-2-.3-4-1-18.7-2.7-76-4.3-172-5H0V214h399571l6 1 -c124.7 8 235 61.7 331 161 31.3 33.3 59.7 72.7 85 118l7 13v35z`, - rightbraceunder: `M399994 0l6 6v35l-6 11c-56 104-135.3 181.3-238 232-57.3 - 28.7-117 45-179 50H-300V214h399897c43.3-7 81-15 113-26 100.7-33 179.7-91 237 --174 2.7-5 6-9 10-13 .7-1 7.3-1 20-1h17z`, - rightgroup: `M0 80h399565c371 0 266.7 149.4 414 180 5.9 1.2 18 0 18 0 2 0 - 3-1 3-3v-38c-76-158-257-219-435-219H0z`, - rightgroupunder: `M0 262h399565c371 0 266.7-149.4 414-180 5.9-1.2 18 0 18 - 0 2 0 3 1 3 3v38c-76 158-257 219-435 219H0z`, - rightharpoon: `M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3 --3.7-15.3-11-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2 --10.7 0-16.7 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58 - 69.2 92 94.5zm0 0v40h399900v-40z`, - rightharpoonplus: `M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3-3.7-15.3-11 --18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2-10.7 0-16.7 - 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58 69.2 92 94.5z -m0 0v40h399900v-40z m100 194v40h399900v-40zm0 0v40h399900v-40z`, - rightharpoondown: `M399747 511c0 7.3 6.7 11 20 11 8 0 13-.8 15-2.5s4.7-6.8 - 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3 8.5-5.8 9.5 --7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3-64.7 57-92 95 --27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 241v40h399900v-40z`, - rightharpoondownplus: `M399747 705c0 7.3 6.7 11 20 11 8 0 13-.8 - 15-2.5s4.7-6.8 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3 - 8.5-5.8 9.5-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3 --64.7 57-92 95-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 435v40h399900v-40z -m0-194v40h400000v-40zm0 0v40h400000v-40z`, - righthook: `M399859 241c-764 0 0 0 0 0 40-3.3 68.7-15.7 86-37 10-12 15-25.3 - 15-40 0-22.7-9.8-40.7-29.5-54-19.7-13.3-43.5-21-71.5-23-17.3-1.3-26-8-26-20 0 --13.3 8.7-20 26-20 38 0 71 11.2 99 33.5 0 0 7 5.6 21 16.7 14 11.2 21 33.5 21 - 66.8s-14 61.2-42 83.5c-28 22.3-61 33.5-99 33.5L0 241z M0 281v-40h399859v40z`, - rightlinesegment: `M399960 241 V94 h40 V428 h-40 V281 H0 v-40z -M399960 241 V94 h40 V428 h-40 V281 H0 v-40z`, - rightToFrom: `M400000 167c-70.7-42-118-97.7-142-167h-23c-15.3 0-23 .3-23 - 1 0 1.3 5.3 13.7 16 37 18 35.3 41.3 69 70 101l7 8H0v40h399905l-7 8c-28.7 32 --52 65.7-70 101-10.7 23.3-16 35.7-16 37 0 .7 7.7 1 23 1h23c24-69.3 71.3-125 142 --167z M100 147v40h399900v-40zM0 341v40h399900v-40z`, - // twoheadleftarrow is from glyph U+219E in font KaTeX AMS Regular - twoheadleftarrow: `M0 167c68 40 - 115.7 95.7 143 167h22c15.3 0 23-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69 --70-101l-7-8h125l9 7c50.7 39.3 85 86 103 140h46c0-4.7-6.3-18.7-19-42-18-35.3 --40-67.3-66-96l-9-9h399716v-40H284l9-9c26-28.7 48-60.7 66-96 12.7-23.333 19 --37.333 19-42h-46c-18 54-52.3 100.7-103 140l-9 7H95l7-8c28.7-32 52-65.7 70-101 - 10.7-23.333 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 71.3 68 127 0 167z`, - twoheadrightarrow: `M400000 167 -c-68-40-115.7-95.7-143-167h-22c-15.3 0-23 .3-23 1 0 1.3 5.3 13.7 16 37 18 35.3 - 41.3 69 70 101l7 8h-125l-9-7c-50.7-39.3-85-86-103-140h-46c0 4.7 6.3 18.7 19 42 - 18 35.3 40 67.3 66 96l9 9H0v40h399716l-9 9c-26 28.7-48 60.7-66 96-12.7 23.333 --19 37.333-19 42h46c18-54 52.3-100.7 103-140l9-7h125l-7 8c-28.7 32-52 65.7-70 - 101-10.7 23.333-16 35.7-16 37 0 .7 7.7 1 23 1h22c27.3-71.3 75-127 143-167z`, - // tilde1 is a modified version of a glyph from the MnSymbol package - tilde1: `M200 55.538c-77 0-168 73.953-177 73.953-3 0-7 --2.175-9-5.437L2 97c-1-2-2-4-2-6 0-4 2-7 5-9l20-12C116 12 171 0 207 0c86 0 - 114 68 191 68 78 0 168-68 177-68 4 0 7 2 9 5l12 19c1 2.175 2 4.35 2 6.525 0 - 4.35-2 7.613-5 9.788l-19 13.05c-92 63.077-116.937 75.308-183 76.128 --68.267.847-113-73.952-191-73.952z`, - // ditto tilde2, tilde3, & tilde4 - tilde2: `M344 55.266c-142 0-300.638 81.316-311.5 86.418 --8.01 3.762-22.5 10.91-23.5 5.562L1 120c-1-2-1-3-1-4 0-5 3-9 8-10l18.4-9C160.9 - 31.9 283 0 358 0c148 0 188 122 331 122s314-97 326-97c4 0 8 2 10 7l7 21.114 -c1 2.14 1 3.21 1 4.28 0 5.347-3 9.626-7 10.696l-22.3 12.622C852.6 158.372 751 - 181.476 676 181.476c-149 0-189-126.21-332-126.21z`, - tilde3: `M786 59C457 59 32 175.242 13 175.242c-6 0-10-3.457 --11-10.37L.15 138c-1-7 3-12 10-13l19.2-6.4C378.4 40.7 634.3 0 804.3 0c337 0 - 411.8 157 746.8 157 328 0 754-112 773-112 5 0 10 3 11 9l1 14.075c1 8.066-.697 - 16.595-6.697 17.492l-21.052 7.31c-367.9 98.146-609.15 122.696-778.15 122.696 - -338 0-409-156.573-744-156.573z`, - tilde4: `M786 58C457 58 32 177.487 13 177.487c-6 0-10-3.345 --11-10.035L.15 143c-1-7 3-12 10-13l22-6.7C381.2 35 637.15 0 807.15 0c337 0 409 - 177 744 177 328 0 754-127 773-127 5 0 10 3 11 9l1 14.794c1 7.805-3 13.38-9 - 14.495l-20.7 5.574c-366.85 99.79-607.3 139.372-776.3 139.372-338 0-409 - -175.236-744-175.236z`, - // vec is from glyph U+20D7 in font KaTeX Main - vec: `M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 -3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 -10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 --1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 --7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 -H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 -c-16-25.333-24-45-24-59z`, - // widehat1 is a modified version of a glyph from the MnSymbol package - widehat1: `M529 0h5l519 115c5 1 9 5 9 10 0 1-1 2-1 3l-4 22 -c-1 5-5 9-11 9h-2L532 67 19 159h-2c-5 0-9-4-11-9l-5-22c-1-6 2-12 8-13z`, - // ditto widehat2, widehat3, & widehat4 - widehat2: `M1181 0h2l1171 176c6 0 10 5 10 11l-2 23c-1 6-5 10 --11 10h-1L1182 67 15 220h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z`, - widehat3: `M1181 0h2l1171 236c6 0 10 5 10 11l-2 23c-1 6-5 10 --11 10h-1L1182 67 15 280h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z`, - widehat4: `M1181 0h2l1171 296c6 0 10 5 10 11l-2 23c-1 6-5 10 --11 10h-1L1182 67 15 340h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z`, - // widecheck paths are all inverted versions of widehat - widecheck1: `M529,159h5l519,-115c5,-1,9,-5,9,-10c0,-1,-1,-2,-1,-3l-4,-22c-1, --5,-5,-9,-11,-9h-2l-512,92l-513,-92h-2c-5,0,-9,4,-11,9l-5,22c-1,6,2,12,8,13z`, - widecheck2: `M1181,220h2l1171,-176c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10, --11,-10h-1l-1168,153l-1167,-153h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z`, - widecheck3: `M1181,280h2l1171,-236c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10, --11,-10h-1l-1168,213l-1167,-213h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z`, - widecheck4: `M1181,340h2l1171,-296c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10, --11,-10h-1l-1168,273l-1167,-273h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z`, - // The next ten paths support reaction arrows from the mhchem package. - // Arrows for \ce{<-->} are offset from xAxis by 0.22ex, per mhchem in LaTeX - // baraboveleftarrow is mostly from from glyph U+2190 in font KaTeX Main - baraboveleftarrow: `M400000 620h-399890l3 -3c68.7 -52.7 113.7 -120 135 -202 -c4 -14.7 6 -23 6 -25c0 -7.3 -7 -11 -21 -11c-8 0 -13.2 0.8 -15.5 2.5 -c-2.3 1.7 -4.2 5.8 -5.5 12.5c-1.3 4.7 -2.7 10.3 -4 17c-12 48.7 -34.8 92 -68.5 130 -s-74.2 66.3 -121.5 85c-10 4 -16 7.7 -18 11c0 8.7 6 14.3 18 17c47.3 18.7 87.8 47 -121.5 85s56.5 81.3 68.5 130c0.7 2 1.3 5 2 9s1.2 6.7 1.5 8c0.3 1.3 1 3.3 2 6 -s2.2 4.5 3.5 5.5c1.3 1 3.3 1.8 6 2.5s6 1 10 1c14 0 21 -3.7 21 -11 -c0 -2 -2 -10.3 -6 -25c-20 -79.3 -65 -146.7 -135 -202l-3 -3h399890z -M100 620v40h399900v-40z M0 241v40h399900v-40zM0 241v40h399900v-40z`, - // rightarrowabovebar is mostly from glyph U+2192, KaTeX Main - rightarrowabovebar: `M0 241v40h399891c-47.3 35.3-84 78-110 128-16.7 32 --27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 11 8 0 -13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 39 --84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85-40.5 --119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5 --12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67 -151.7 139 205zm96 379h399894v40H0zm0 0h399904v40H0z`, - // The short left harpoon has 0.5em (i.e. 500 units) kern on the left end. - // Ref from mhchem.sty: \rlap{\raisebox{-.22ex}{$\kern0.5em - baraboveshortleftharpoon: `M507,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11 -c1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17 -c2,0.7,5,1,9,1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21 -c-32,-87.3,-82.7,-157.7,-152,-211c0,0,-3,-3,-3,-3l399351,0l0,-40 -c-398570,0,-399437,0,-399437,0z M593 435 v40 H399500 v-40z -M0 281 v-40 H399908 v40z M0 281 v-40 H399908 v40z`, - rightharpoonaboveshortbar: `M0,241 l0,40c399126,0,399993,0,399993,0 -c4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199, --231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6 -c-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z -M0 241 v40 H399908 v-40z M0 475 v-40 H399500 v40z M0 475 v-40 H399500 v40z`, - shortbaraboveleftharpoon: `M7,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11 -c1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17c2,0.7,5,1,9, -1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21c-32,-87.3,-82.7,-157.7, --152,-211c0,0,-3,-3,-3,-3l399907,0l0,-40c-399126,0,-399993,0,-399993,0z -M93 435 v40 H400000 v-40z M500 241 v40 H400000 v-40z M500 241 v40 H400000 v-40z`, - shortrightharpoonabovebar: `M53,241l0,40c398570,0,399437,0,399437,0 -c4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199, --231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6 -c-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z -M500 241 v40 H399408 v-40z M500 435 v40 H400000 v-40z` -}; - -/** - * This node represents a document fragment, which contains elements, but when - * placed into the DOM doesn't have any representation itself. It only contains - * children and doesn't have any DOM node properties. - */ -class DocumentFragment { - // HtmlDomNode - // Never used; needed for satisfying interface. - constructor(children) { - this.children = void 0; - this.classes = void 0; - this.height = void 0; - this.depth = void 0; - this.maxFontSize = void 0; - this.style = void 0; - this.children = children; - this.classes = []; - this.height = 0; - this.depth = 0; - this.maxFontSize = 0; - this.style = {}; - } - - hasClass(className) { - return utils.contains(this.classes, className); - } - /** Convert the fragment into a node. */ - - - toNode() { - const frag = document.createDocumentFragment(); - - for (let i = 0; i < this.children.length; i++) { - frag.appendChild(this.children[i].toNode()); - } - - return frag; - } - /** Convert the fragment into HTML markup. */ - - - toMarkup() { - let markup = ""; // Simply concatenate the markup for the children together. - - for (let i = 0; i < this.children.length; i++) { - markup += this.children[i].toMarkup(); - } - - return markup; - } - /** - * Converts the math node into a string, similar to innerText. Applies to - * MathDomNode's only. - */ - - - toText() { - // To avoid this, we would subclass documentFragment separately for - // MathML, but polyfills for subclassing is expensive per PR 1469. - // $FlowFixMe: Only works for ChildType = MathDomNode. - const toText = child => child.toText(); - - return this.children.map(toText).join(""); - } - -} - -/** - * These objects store the data about the DOM nodes we create, as well as some - * extra data. They can then be transformed into real DOM nodes with the - * `toNode` function or HTML markup using `toMarkup`. They are useful for both - * storing extra properties on the nodes, as well as providing a way to easily - * work with the DOM. - * - * Similar functions for working with MathML nodes exist in mathMLTree.js. - * - * TODO: refactor `span` and `anchor` into common superclass when - * target environments support class inheritance - */ - -/** - * Create an HTML className based on a list of classes. In addition to joining - * with spaces, we also remove empty classes. - */ -const createClass = function createClass(classes) { - return classes.filter(cls => cls).join(" "); -}; - -const initNode = function initNode(classes, options, style) { - this.classes = classes || []; - this.attributes = {}; - this.height = 0; - this.depth = 0; - this.maxFontSize = 0; - this.style = style || {}; - - if (options) { - if (options.style.isTight()) { - this.classes.push("mtight"); - } - - const color = options.getColor(); - - if (color) { - this.style.color = color; - } - } -}; -/** - * Convert into an HTML node - */ - - -const toNode = function toNode(tagName) { - const node = document.createElement(tagName); // Apply the class - - node.className = createClass(this.classes); // Apply inline styles - - for (const style in this.style) { - if (this.style.hasOwnProperty(style)) { - // $FlowFixMe Flow doesn't seem to understand span.style's type. - node.style[style] = this.style[style]; - } - } // Apply attributes - - - for (const attr in this.attributes) { - if (this.attributes.hasOwnProperty(attr)) { - node.setAttribute(attr, this.attributes[attr]); - } - } // Append the children, also as HTML nodes - - - for (let i = 0; i < this.children.length; i++) { - node.appendChild(this.children[i].toNode()); - } - - return node; -}; -/** - * Convert into an HTML markup string - */ - - -const toMarkup = function toMarkup(tagName) { - let markup = `<${tagName}`; // Add the class - - if (this.classes.length) { - markup += ` class="${utils.escape(createClass(this.classes))}"`; - } - - let styles = ""; // Add the styles, after hyphenation - - for (const style in this.style) { - if (this.style.hasOwnProperty(style)) { - styles += `${utils.hyphenate(style)}:${this.style[style]};`; - } - } - - if (styles) { - markup += ` style="${utils.escape(styles)}"`; - } // Add the attributes - - - for (const attr in this.attributes) { - if (this.attributes.hasOwnProperty(attr)) { - markup += ` ${attr}="${utils.escape(this.attributes[attr])}"`; - } - } - - markup += ">"; // Add the markup of the children, also as markup - - for (let i = 0; i < this.children.length; i++) { - markup += this.children[i].toMarkup(); - } - - markup += ``; - return markup; -}; // Making the type below exact with all optional fields doesn't work due to -// - https://github.com/facebook/flow/issues/4582 -// - https://github.com/facebook/flow/issues/5688 -// However, since *all* fields are optional, $Shape<> works as suggested in 5688 -// above. -// This type does not include all CSS properties. Additional properties should -// be added as needed. - - -/** - * This node represents a span node, with a className, a list of children, and - * an inline style. It also contains information about its height, depth, and - * maxFontSize. - * - * Represents two types with different uses: SvgSpan to wrap an SVG and DomSpan - * otherwise. This typesafety is important when HTML builders access a span's - * children. - */ -class Span { - constructor(classes, children, options, style) { - this.children = void 0; - this.attributes = void 0; - this.classes = void 0; - this.height = void 0; - this.depth = void 0; - this.width = void 0; - this.maxFontSize = void 0; - this.style = void 0; - initNode.call(this, classes, options, style); - this.children = children || []; - } - /** - * Sets an arbitrary attribute on the span. Warning: use this wisely. Not - * all browsers support attributes the same, and having too many custom - * attributes is probably bad. - */ - - - setAttribute(attribute, value) { - this.attributes[attribute] = value; - } - - hasClass(className) { - return utils.contains(this.classes, className); - } - - toNode() { - return toNode.call(this, "span"); - } - - toMarkup() { - return toMarkup.call(this, "span"); - } - -} -/** - * This node represents an anchor () element with a hyperlink. See `span` - * for further details. - */ - -class Anchor { - constructor(href, classes, children, options) { - this.children = void 0; - this.attributes = void 0; - this.classes = void 0; - this.height = void 0; - this.depth = void 0; - this.maxFontSize = void 0; - this.style = void 0; - initNode.call(this, classes, options); - this.children = children || []; - this.setAttribute('href', href); - } - - setAttribute(attribute, value) { - this.attributes[attribute] = value; - } - - hasClass(className) { - return utils.contains(this.classes, className); - } - - toNode() { - return toNode.call(this, "a"); - } - - toMarkup() { - return toMarkup.call(this, "a"); - } - -} -/** - * This node represents an image embed () element. - */ - -class Img { - constructor(src, alt, style) { - this.src = void 0; - this.alt = void 0; - this.classes = void 0; - this.height = void 0; - this.depth = void 0; - this.maxFontSize = void 0; - this.style = void 0; - this.alt = alt; - this.src = src; - this.classes = ["mord"]; - this.style = style; - } - - hasClass(className) { - return utils.contains(this.classes, className); - } - - toNode() { - const node = document.createElement("img"); - node.src = this.src; - node.alt = this.alt; - node.className = "mord"; // Apply inline styles - - for (const style in this.style) { - if (this.style.hasOwnProperty(style)) { - // $FlowFixMe - node.style[style] = this.style[style]; - } - } - - return node; - } - - toMarkup() { - let markup = `${this.alt} 0) { - span = document.createElement("span"); - span.style.marginRight = this.italic + "em"; - } - - if (this.classes.length > 0) { - span = span || document.createElement("span"); - span.className = createClass(this.classes); - } - - for (const style in this.style) { - if (this.style.hasOwnProperty(style)) { - span = span || document.createElement("span"); // $FlowFixMe Flow doesn't seem to understand span.style's type. - - span.style[style] = this.style[style]; - } - } - - if (span) { - span.appendChild(node); - return span; - } else { - return node; - } - } - /** - * Creates markup for a symbol node. - */ - - - toMarkup() { - // TODO(alpert): More duplication than I'd like from - // span.prototype.toMarkup and symbolNode.prototype.toNode... - let needsSpan = false; - let markup = " 0) { - styles += "margin-right:" + this.italic + "em;"; - } - - for (const style in this.style) { - if (this.style.hasOwnProperty(style)) { - styles += utils.hyphenate(style) + ":" + this.style[style] + ";"; - } - } - - if (styles) { - needsSpan = true; - markup += " style=\"" + utils.escape(styles) + "\""; - } - - const escaped = utils.escape(this.text); - - if (needsSpan) { - markup += ">"; - markup += escaped; - markup += ""; - return markup; - } else { - return escaped; - } - } - -} -/** - * SVG nodes are used to render stretchy wide elements. - */ - -class SvgNode { - constructor(children, attributes) { - this.children = void 0; - this.attributes = void 0; - this.children = children || []; - this.attributes = attributes || {}; - } - - toNode() { - const svgNS = "http://www.w3.org/2000/svg"; - const node = document.createElementNS(svgNS, "svg"); // Apply attributes - - for (const attr in this.attributes) { - if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { - node.setAttribute(attr, this.attributes[attr]); - } - } - - for (let i = 0; i < this.children.length; i++) { - node.appendChild(this.children[i].toNode()); - } - - return node; - } - - toMarkup() { - let markup = "`; - } else { - return ``; - } - } - -} -class LineNode { - constructor(attributes) { - this.attributes = void 0; - this.attributes = attributes || {}; - } - - toNode() { - const svgNS = "http://www.w3.org/2000/svg"; - const node = document.createElementNS(svgNS, "line"); // Apply attributes - - for (const attr in this.attributes) { - if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { - node.setAttribute(attr, this.attributes[attr]); - } - } - - return node; - } - - toMarkup() { - let markup = " but got ${String(group)}.`); - } -} - -// This file is GENERATED by buildMetrics.sh. DO NOT MODIFY. -var metricMap = { - "AMS-Regular": { - "32": [0, 0, 0, 0, 0.25], - "65": [0, 0.68889, 0, 0, 0.72222], - "66": [0, 0.68889, 0, 0, 0.66667], - "67": [0, 0.68889, 0, 0, 0.72222], - "68": [0, 0.68889, 0, 0, 0.72222], - "69": [0, 0.68889, 0, 0, 0.66667], - "70": [0, 0.68889, 0, 0, 0.61111], - "71": [0, 0.68889, 0, 0, 0.77778], - "72": [0, 0.68889, 0, 0, 0.77778], - "73": [0, 0.68889, 0, 0, 0.38889], - "74": [0.16667, 0.68889, 0, 0, 0.5], - "75": [0, 0.68889, 0, 0, 0.77778], - "76": [0, 0.68889, 0, 0, 0.66667], - "77": [0, 0.68889, 0, 0, 0.94445], - "78": [0, 0.68889, 0, 0, 0.72222], - "79": [0.16667, 0.68889, 0, 0, 0.77778], - "80": [0, 0.68889, 0, 0, 0.61111], - "81": [0.16667, 0.68889, 0, 0, 0.77778], - "82": [0, 0.68889, 0, 0, 0.72222], - "83": [0, 0.68889, 0, 0, 0.55556], - "84": [0, 0.68889, 0, 0, 0.66667], - "85": [0, 0.68889, 0, 0, 0.72222], - "86": [0, 0.68889, 0, 0, 0.72222], - "87": [0, 0.68889, 0, 0, 1.0], - "88": [0, 0.68889, 0, 0, 0.72222], - "89": [0, 0.68889, 0, 0, 0.72222], - "90": [0, 0.68889, 0, 0, 0.66667], - "107": [0, 0.68889, 0, 0, 0.55556], - "160": [0, 0, 0, 0, 0.25], - "165": [0, 0.675, 0.025, 0, 0.75], - "174": [0.15559, 0.69224, 0, 0, 0.94666], - "240": [0, 0.68889, 0, 0, 0.55556], - "295": [0, 0.68889, 0, 0, 0.54028], - "710": [0, 0.825, 0, 0, 2.33334], - "732": [0, 0.9, 0, 0, 2.33334], - "770": [0, 0.825, 0, 0, 2.33334], - "771": [0, 0.9, 0, 0, 2.33334], - "989": [0.08167, 0.58167, 0, 0, 0.77778], - "1008": [0, 0.43056, 0.04028, 0, 0.66667], - "8245": [0, 0.54986, 0, 0, 0.275], - "8463": [0, 0.68889, 0, 0, 0.54028], - "8487": [0, 0.68889, 0, 0, 0.72222], - "8498": [0, 0.68889, 0, 0, 0.55556], - "8502": [0, 0.68889, 0, 0, 0.66667], - "8503": [0, 0.68889, 0, 0, 0.44445], - "8504": [0, 0.68889, 0, 0, 0.66667], - "8513": [0, 0.68889, 0, 0, 0.63889], - "8592": [-0.03598, 0.46402, 0, 0, 0.5], - "8594": [-0.03598, 0.46402, 0, 0, 0.5], - "8602": [-0.13313, 0.36687, 0, 0, 1.0], - "8603": [-0.13313, 0.36687, 0, 0, 1.0], - "8606": [0.01354, 0.52239, 0, 0, 1.0], - "8608": [0.01354, 0.52239, 0, 0, 1.0], - "8610": [0.01354, 0.52239, 0, 0, 1.11111], - "8611": [0.01354, 0.52239, 0, 0, 1.11111], - "8619": [0, 0.54986, 0, 0, 1.0], - "8620": [0, 0.54986, 0, 0, 1.0], - "8621": [-0.13313, 0.37788, 0, 0, 1.38889], - "8622": [-0.13313, 0.36687, 0, 0, 1.0], - "8624": [0, 0.69224, 0, 0, 0.5], - "8625": [0, 0.69224, 0, 0, 0.5], - "8630": [0, 0.43056, 0, 0, 1.0], - "8631": [0, 0.43056, 0, 0, 1.0], - "8634": [0.08198, 0.58198, 0, 0, 0.77778], - "8635": [0.08198, 0.58198, 0, 0, 0.77778], - "8638": [0.19444, 0.69224, 0, 0, 0.41667], - "8639": [0.19444, 0.69224, 0, 0, 0.41667], - "8642": [0.19444, 0.69224, 0, 0, 0.41667], - "8643": [0.19444, 0.69224, 0, 0, 0.41667], - "8644": [0.1808, 0.675, 0, 0, 1.0], - "8646": [0.1808, 0.675, 0, 0, 1.0], - "8647": [0.1808, 0.675, 0, 0, 1.0], - "8648": [0.19444, 0.69224, 0, 0, 0.83334], - "8649": [0.1808, 0.675, 0, 0, 1.0], - "8650": [0.19444, 0.69224, 0, 0, 0.83334], - "8651": [0.01354, 0.52239, 0, 0, 1.0], - "8652": [0.01354, 0.52239, 0, 0, 1.0], - "8653": [-0.13313, 0.36687, 0, 0, 1.0], - "8654": [-0.13313, 0.36687, 0, 0, 1.0], - "8655": [-0.13313, 0.36687, 0, 0, 1.0], - "8666": [0.13667, 0.63667, 0, 0, 1.0], - "8667": [0.13667, 0.63667, 0, 0, 1.0], - "8669": [-0.13313, 0.37788, 0, 0, 1.0], - "8672": [-0.064, 0.437, 0, 0, 1.334], - "8674": [-0.064, 0.437, 0, 0, 1.334], - "8705": [0, 0.825, 0, 0, 0.5], - "8708": [0, 0.68889, 0, 0, 0.55556], - "8709": [0.08167, 0.58167, 0, 0, 0.77778], - "8717": [0, 0.43056, 0, 0, 0.42917], - "8722": [-0.03598, 0.46402, 0, 0, 0.5], - "8724": [0.08198, 0.69224, 0, 0, 0.77778], - "8726": [0.08167, 0.58167, 0, 0, 0.77778], - "8733": [0, 0.69224, 0, 0, 0.77778], - "8736": [0, 0.69224, 0, 0, 0.72222], - "8737": [0, 0.69224, 0, 0, 0.72222], - "8738": [0.03517, 0.52239, 0, 0, 0.72222], - "8739": [0.08167, 0.58167, 0, 0, 0.22222], - "8740": [0.25142, 0.74111, 0, 0, 0.27778], - "8741": [0.08167, 0.58167, 0, 0, 0.38889], - "8742": [0.25142, 0.74111, 0, 0, 0.5], - "8756": [0, 0.69224, 0, 0, 0.66667], - "8757": [0, 0.69224, 0, 0, 0.66667], - "8764": [-0.13313, 0.36687, 0, 0, 0.77778], - "8765": [-0.13313, 0.37788, 0, 0, 0.77778], - "8769": [-0.13313, 0.36687, 0, 0, 0.77778], - "8770": [-0.03625, 0.46375, 0, 0, 0.77778], - "8774": [0.30274, 0.79383, 0, 0, 0.77778], - "8776": [-0.01688, 0.48312, 0, 0, 0.77778], - "8778": [0.08167, 0.58167, 0, 0, 0.77778], - "8782": [0.06062, 0.54986, 0, 0, 0.77778], - "8783": [0.06062, 0.54986, 0, 0, 0.77778], - "8785": [0.08198, 0.58198, 0, 0, 0.77778], - "8786": [0.08198, 0.58198, 0, 0, 0.77778], - "8787": [0.08198, 0.58198, 0, 0, 0.77778], - "8790": [0, 0.69224, 0, 0, 0.77778], - "8791": [0.22958, 0.72958, 0, 0, 0.77778], - "8796": [0.08198, 0.91667, 0, 0, 0.77778], - "8806": [0.25583, 0.75583, 0, 0, 0.77778], - "8807": [0.25583, 0.75583, 0, 0, 0.77778], - "8808": [0.25142, 0.75726, 0, 0, 0.77778], - "8809": [0.25142, 0.75726, 0, 0, 0.77778], - "8812": [0.25583, 0.75583, 0, 0, 0.5], - "8814": [0.20576, 0.70576, 0, 0, 0.77778], - "8815": [0.20576, 0.70576, 0, 0, 0.77778], - "8816": [0.30274, 0.79383, 0, 0, 0.77778], - "8817": [0.30274, 0.79383, 0, 0, 0.77778], - "8818": [0.22958, 0.72958, 0, 0, 0.77778], - "8819": [0.22958, 0.72958, 0, 0, 0.77778], - "8822": [0.1808, 0.675, 0, 0, 0.77778], - "8823": [0.1808, 0.675, 0, 0, 0.77778], - "8828": [0.13667, 0.63667, 0, 0, 0.77778], - "8829": [0.13667, 0.63667, 0, 0, 0.77778], - "8830": [0.22958, 0.72958, 0, 0, 0.77778], - "8831": [0.22958, 0.72958, 0, 0, 0.77778], - "8832": [0.20576, 0.70576, 0, 0, 0.77778], - "8833": [0.20576, 0.70576, 0, 0, 0.77778], - "8840": [0.30274, 0.79383, 0, 0, 0.77778], - "8841": [0.30274, 0.79383, 0, 0, 0.77778], - "8842": [0.13597, 0.63597, 0, 0, 0.77778], - "8843": [0.13597, 0.63597, 0, 0, 0.77778], - "8847": [0.03517, 0.54986, 0, 0, 0.77778], - "8848": [0.03517, 0.54986, 0, 0, 0.77778], - "8858": [0.08198, 0.58198, 0, 0, 0.77778], - "8859": [0.08198, 0.58198, 0, 0, 0.77778], - "8861": [0.08198, 0.58198, 0, 0, 0.77778], - "8862": [0, 0.675, 0, 0, 0.77778], - "8863": [0, 0.675, 0, 0, 0.77778], - "8864": [0, 0.675, 0, 0, 0.77778], - "8865": [0, 0.675, 0, 0, 0.77778], - "8872": [0, 0.69224, 0, 0, 0.61111], - "8873": [0, 0.69224, 0, 0, 0.72222], - "8874": [0, 0.69224, 0, 0, 0.88889], - "8876": [0, 0.68889, 0, 0, 0.61111], - "8877": [0, 0.68889, 0, 0, 0.61111], - "8878": [0, 0.68889, 0, 0, 0.72222], - "8879": [0, 0.68889, 0, 0, 0.72222], - "8882": [0.03517, 0.54986, 0, 0, 0.77778], - "8883": [0.03517, 0.54986, 0, 0, 0.77778], - "8884": [0.13667, 0.63667, 0, 0, 0.77778], - "8885": [0.13667, 0.63667, 0, 0, 0.77778], - "8888": [0, 0.54986, 0, 0, 1.11111], - "8890": [0.19444, 0.43056, 0, 0, 0.55556], - "8891": [0.19444, 0.69224, 0, 0, 0.61111], - "8892": [0.19444, 0.69224, 0, 0, 0.61111], - "8901": [0, 0.54986, 0, 0, 0.27778], - "8903": [0.08167, 0.58167, 0, 0, 0.77778], - "8905": [0.08167, 0.58167, 0, 0, 0.77778], - "8906": [0.08167, 0.58167, 0, 0, 0.77778], - "8907": [0, 0.69224, 0, 0, 0.77778], - "8908": [0, 0.69224, 0, 0, 0.77778], - "8909": [-0.03598, 0.46402, 0, 0, 0.77778], - "8910": [0, 0.54986, 0, 0, 0.76042], - "8911": [0, 0.54986, 0, 0, 0.76042], - "8912": [0.03517, 0.54986, 0, 0, 0.77778], - "8913": [0.03517, 0.54986, 0, 0, 0.77778], - "8914": [0, 0.54986, 0, 0, 0.66667], - "8915": [0, 0.54986, 0, 0, 0.66667], - "8916": [0, 0.69224, 0, 0, 0.66667], - "8918": [0.0391, 0.5391, 0, 0, 0.77778], - "8919": [0.0391, 0.5391, 0, 0, 0.77778], - "8920": [0.03517, 0.54986, 0, 0, 1.33334], - "8921": [0.03517, 0.54986, 0, 0, 1.33334], - "8922": [0.38569, 0.88569, 0, 0, 0.77778], - "8923": [0.38569, 0.88569, 0, 0, 0.77778], - "8926": [0.13667, 0.63667, 0, 0, 0.77778], - "8927": [0.13667, 0.63667, 0, 0, 0.77778], - "8928": [0.30274, 0.79383, 0, 0, 0.77778], - "8929": [0.30274, 0.79383, 0, 0, 0.77778], - "8934": [0.23222, 0.74111, 0, 0, 0.77778], - "8935": [0.23222, 0.74111, 0, 0, 0.77778], - "8936": [0.23222, 0.74111, 0, 0, 0.77778], - "8937": [0.23222, 0.74111, 0, 0, 0.77778], - "8938": [0.20576, 0.70576, 0, 0, 0.77778], - "8939": [0.20576, 0.70576, 0, 0, 0.77778], - "8940": [0.30274, 0.79383, 0, 0, 0.77778], - "8941": [0.30274, 0.79383, 0, 0, 0.77778], - "8994": [0.19444, 0.69224, 0, 0, 0.77778], - "8995": [0.19444, 0.69224, 0, 0, 0.77778], - "9416": [0.15559, 0.69224, 0, 0, 0.90222], - "9484": [0, 0.69224, 0, 0, 0.5], - "9488": [0, 0.69224, 0, 0, 0.5], - "9492": [0, 0.37788, 0, 0, 0.5], - "9496": [0, 0.37788, 0, 0, 0.5], - "9585": [0.19444, 0.68889, 0, 0, 0.88889], - "9586": [0.19444, 0.74111, 0, 0, 0.88889], - "9632": [0, 0.675, 0, 0, 0.77778], - "9633": [0, 0.675, 0, 0, 0.77778], - "9650": [0, 0.54986, 0, 0, 0.72222], - "9651": [0, 0.54986, 0, 0, 0.72222], - "9654": [0.03517, 0.54986, 0, 0, 0.77778], - "9660": [0, 0.54986, 0, 0, 0.72222], - "9661": [0, 0.54986, 0, 0, 0.72222], - "9664": [0.03517, 0.54986, 0, 0, 0.77778], - "9674": [0.11111, 0.69224, 0, 0, 0.66667], - "9733": [0.19444, 0.69224, 0, 0, 0.94445], - "10003": [0, 0.69224, 0, 0, 0.83334], - "10016": [0, 0.69224, 0, 0, 0.83334], - "10731": [0.11111, 0.69224, 0, 0, 0.66667], - "10846": [0.19444, 0.75583, 0, 0, 0.61111], - "10877": [0.13667, 0.63667, 0, 0, 0.77778], - "10878": [0.13667, 0.63667, 0, 0, 0.77778], - "10885": [0.25583, 0.75583, 0, 0, 0.77778], - "10886": [0.25583, 0.75583, 0, 0, 0.77778], - "10887": [0.13597, 0.63597, 0, 0, 0.77778], - "10888": [0.13597, 0.63597, 0, 0, 0.77778], - "10889": [0.26167, 0.75726, 0, 0, 0.77778], - "10890": [0.26167, 0.75726, 0, 0, 0.77778], - "10891": [0.48256, 0.98256, 0, 0, 0.77778], - "10892": [0.48256, 0.98256, 0, 0, 0.77778], - "10901": [0.13667, 0.63667, 0, 0, 0.77778], - "10902": [0.13667, 0.63667, 0, 0, 0.77778], - "10933": [0.25142, 0.75726, 0, 0, 0.77778], - "10934": [0.25142, 0.75726, 0, 0, 0.77778], - "10935": [0.26167, 0.75726, 0, 0, 0.77778], - "10936": [0.26167, 0.75726, 0, 0, 0.77778], - "10937": [0.26167, 0.75726, 0, 0, 0.77778], - "10938": [0.26167, 0.75726, 0, 0, 0.77778], - "10949": [0.25583, 0.75583, 0, 0, 0.77778], - "10950": [0.25583, 0.75583, 0, 0, 0.77778], - "10955": [0.28481, 0.79383, 0, 0, 0.77778], - "10956": [0.28481, 0.79383, 0, 0, 0.77778], - "57350": [0.08167, 0.58167, 0, 0, 0.22222], - "57351": [0.08167, 0.58167, 0, 0, 0.38889], - "57352": [0.08167, 0.58167, 0, 0, 0.77778], - "57353": [0, 0.43056, 0.04028, 0, 0.66667], - "57356": [0.25142, 0.75726, 0, 0, 0.77778], - "57357": [0.25142, 0.75726, 0, 0, 0.77778], - "57358": [0.41951, 0.91951, 0, 0, 0.77778], - "57359": [0.30274, 0.79383, 0, 0, 0.77778], - "57360": [0.30274, 0.79383, 0, 0, 0.77778], - "57361": [0.41951, 0.91951, 0, 0, 0.77778], - "57366": [0.25142, 0.75726, 0, 0, 0.77778], - "57367": [0.25142, 0.75726, 0, 0, 0.77778], - "57368": [0.25142, 0.75726, 0, 0, 0.77778], - "57369": [0.25142, 0.75726, 0, 0, 0.77778], - "57370": [0.13597, 0.63597, 0, 0, 0.77778], - "57371": [0.13597, 0.63597, 0, 0, 0.77778] - }, - "Caligraphic-Regular": { - "32": [0, 0, 0, 0, 0.25], - "65": [0, 0.68333, 0, 0.19445, 0.79847], - "66": [0, 0.68333, 0.03041, 0.13889, 0.65681], - "67": [0, 0.68333, 0.05834, 0.13889, 0.52653], - "68": [0, 0.68333, 0.02778, 0.08334, 0.77139], - "69": [0, 0.68333, 0.08944, 0.11111, 0.52778], - "70": [0, 0.68333, 0.09931, 0.11111, 0.71875], - "71": [0.09722, 0.68333, 0.0593, 0.11111, 0.59487], - "72": [0, 0.68333, 0.00965, 0.11111, 0.84452], - "73": [0, 0.68333, 0.07382, 0, 0.54452], - "74": [0.09722, 0.68333, 0.18472, 0.16667, 0.67778], - "75": [0, 0.68333, 0.01445, 0.05556, 0.76195], - "76": [0, 0.68333, 0, 0.13889, 0.68972], - "77": [0, 0.68333, 0, 0.13889, 1.2009], - "78": [0, 0.68333, 0.14736, 0.08334, 0.82049], - "79": [0, 0.68333, 0.02778, 0.11111, 0.79611], - "80": [0, 0.68333, 0.08222, 0.08334, 0.69556], - "81": [0.09722, 0.68333, 0, 0.11111, 0.81667], - "82": [0, 0.68333, 0, 0.08334, 0.8475], - "83": [0, 0.68333, 0.075, 0.13889, 0.60556], - "84": [0, 0.68333, 0.25417, 0, 0.54464], - "85": [0, 0.68333, 0.09931, 0.08334, 0.62583], - "86": [0, 0.68333, 0.08222, 0, 0.61278], - "87": [0, 0.68333, 0.08222, 0.08334, 0.98778], - "88": [0, 0.68333, 0.14643, 0.13889, 0.7133], - "89": [0.09722, 0.68333, 0.08222, 0.08334, 0.66834], - "90": [0, 0.68333, 0.07944, 0.13889, 0.72473], - "160": [0, 0, 0, 0, 0.25] - }, - "Fraktur-Regular": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69141, 0, 0, 0.29574], - "34": [0, 0.69141, 0, 0, 0.21471], - "38": [0, 0.69141, 0, 0, 0.73786], - "39": [0, 0.69141, 0, 0, 0.21201], - "40": [0.24982, 0.74947, 0, 0, 0.38865], - "41": [0.24982, 0.74947, 0, 0, 0.38865], - "42": [0, 0.62119, 0, 0, 0.27764], - "43": [0.08319, 0.58283, 0, 0, 0.75623], - "44": [0, 0.10803, 0, 0, 0.27764], - "45": [0.08319, 0.58283, 0, 0, 0.75623], - "46": [0, 0.10803, 0, 0, 0.27764], - "47": [0.24982, 0.74947, 0, 0, 0.50181], - "48": [0, 0.47534, 0, 0, 0.50181], - "49": [0, 0.47534, 0, 0, 0.50181], - "50": [0, 0.47534, 0, 0, 0.50181], - "51": [0.18906, 0.47534, 0, 0, 0.50181], - "52": [0.18906, 0.47534, 0, 0, 0.50181], - "53": [0.18906, 0.47534, 0, 0, 0.50181], - "54": [0, 0.69141, 0, 0, 0.50181], - "55": [0.18906, 0.47534, 0, 0, 0.50181], - "56": [0, 0.69141, 0, 0, 0.50181], - "57": [0.18906, 0.47534, 0, 0, 0.50181], - "58": [0, 0.47534, 0, 0, 0.21606], - "59": [0.12604, 0.47534, 0, 0, 0.21606], - "61": [-0.13099, 0.36866, 0, 0, 0.75623], - "63": [0, 0.69141, 0, 0, 0.36245], - "65": [0, 0.69141, 0, 0, 0.7176], - "66": [0, 0.69141, 0, 0, 0.88397], - "67": [0, 0.69141, 0, 0, 0.61254], - "68": [0, 0.69141, 0, 0, 0.83158], - "69": [0, 0.69141, 0, 0, 0.66278], - "70": [0.12604, 0.69141, 0, 0, 0.61119], - "71": [0, 0.69141, 0, 0, 0.78539], - "72": [0.06302, 0.69141, 0, 0, 0.7203], - "73": [0, 0.69141, 0, 0, 0.55448], - "74": [0.12604, 0.69141, 0, 0, 0.55231], - "75": [0, 0.69141, 0, 0, 0.66845], - "76": [0, 0.69141, 0, 0, 0.66602], - "77": [0, 0.69141, 0, 0, 1.04953], - "78": [0, 0.69141, 0, 0, 0.83212], - "79": [0, 0.69141, 0, 0, 0.82699], - "80": [0.18906, 0.69141, 0, 0, 0.82753], - "81": [0.03781, 0.69141, 0, 0, 0.82699], - "82": [0, 0.69141, 0, 0, 0.82807], - "83": [0, 0.69141, 0, 0, 0.82861], - "84": [0, 0.69141, 0, 0, 0.66899], - "85": [0, 0.69141, 0, 0, 0.64576], - "86": [0, 0.69141, 0, 0, 0.83131], - "87": [0, 0.69141, 0, 0, 1.04602], - "88": [0, 0.69141, 0, 0, 0.71922], - "89": [0.18906, 0.69141, 0, 0, 0.83293], - "90": [0.12604, 0.69141, 0, 0, 0.60201], - "91": [0.24982, 0.74947, 0, 0, 0.27764], - "93": [0.24982, 0.74947, 0, 0, 0.27764], - "94": [0, 0.69141, 0, 0, 0.49965], - "97": [0, 0.47534, 0, 0, 0.50046], - "98": [0, 0.69141, 0, 0, 0.51315], - "99": [0, 0.47534, 0, 0, 0.38946], - "100": [0, 0.62119, 0, 0, 0.49857], - "101": [0, 0.47534, 0, 0, 0.40053], - "102": [0.18906, 0.69141, 0, 0, 0.32626], - "103": [0.18906, 0.47534, 0, 0, 0.5037], - "104": [0.18906, 0.69141, 0, 0, 0.52126], - "105": [0, 0.69141, 0, 0, 0.27899], - "106": [0, 0.69141, 0, 0, 0.28088], - "107": [0, 0.69141, 0, 0, 0.38946], - "108": [0, 0.69141, 0, 0, 0.27953], - "109": [0, 0.47534, 0, 0, 0.76676], - "110": [0, 0.47534, 0, 0, 0.52666], - "111": [0, 0.47534, 0, 0, 0.48885], - "112": [0.18906, 0.52396, 0, 0, 0.50046], - "113": [0.18906, 0.47534, 0, 0, 0.48912], - "114": [0, 0.47534, 0, 0, 0.38919], - "115": [0, 0.47534, 0, 0, 0.44266], - "116": [0, 0.62119, 0, 0, 0.33301], - "117": [0, 0.47534, 0, 0, 0.5172], - "118": [0, 0.52396, 0, 0, 0.5118], - "119": [0, 0.52396, 0, 0, 0.77351], - "120": [0.18906, 0.47534, 0, 0, 0.38865], - "121": [0.18906, 0.47534, 0, 0, 0.49884], - "122": [0.18906, 0.47534, 0, 0, 0.39054], - "160": [0, 0, 0, 0, 0.25], - "8216": [0, 0.69141, 0, 0, 0.21471], - "8217": [0, 0.69141, 0, 0, 0.21471], - "58112": [0, 0.62119, 0, 0, 0.49749], - "58113": [0, 0.62119, 0, 0, 0.4983], - "58114": [0.18906, 0.69141, 0, 0, 0.33328], - "58115": [0.18906, 0.69141, 0, 0, 0.32923], - "58116": [0.18906, 0.47534, 0, 0, 0.50343], - "58117": [0, 0.69141, 0, 0, 0.33301], - "58118": [0, 0.62119, 0, 0, 0.33409], - "58119": [0, 0.47534, 0, 0, 0.50073] - }, - "Main-Bold": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0, 0, 0.35], - "34": [0, 0.69444, 0, 0, 0.60278], - "35": [0.19444, 0.69444, 0, 0, 0.95833], - "36": [0.05556, 0.75, 0, 0, 0.575], - "37": [0.05556, 0.75, 0, 0, 0.95833], - "38": [0, 0.69444, 0, 0, 0.89444], - "39": [0, 0.69444, 0, 0, 0.31944], - "40": [0.25, 0.75, 0, 0, 0.44722], - "41": [0.25, 0.75, 0, 0, 0.44722], - "42": [0, 0.75, 0, 0, 0.575], - "43": [0.13333, 0.63333, 0, 0, 0.89444], - "44": [0.19444, 0.15556, 0, 0, 0.31944], - "45": [0, 0.44444, 0, 0, 0.38333], - "46": [0, 0.15556, 0, 0, 0.31944], - "47": [0.25, 0.75, 0, 0, 0.575], - "48": [0, 0.64444, 0, 0, 0.575], - "49": [0, 0.64444, 0, 0, 0.575], - "50": [0, 0.64444, 0, 0, 0.575], - "51": [0, 0.64444, 0, 0, 0.575], - "52": [0, 0.64444, 0, 0, 0.575], - "53": [0, 0.64444, 0, 0, 0.575], - "54": [0, 0.64444, 0, 0, 0.575], - "55": [0, 0.64444, 0, 0, 0.575], - "56": [0, 0.64444, 0, 0, 0.575], - "57": [0, 0.64444, 0, 0, 0.575], - "58": [0, 0.44444, 0, 0, 0.31944], - "59": [0.19444, 0.44444, 0, 0, 0.31944], - "60": [0.08556, 0.58556, 0, 0, 0.89444], - "61": [-0.10889, 0.39111, 0, 0, 0.89444], - "62": [0.08556, 0.58556, 0, 0, 0.89444], - "63": [0, 0.69444, 0, 0, 0.54305], - "64": [0, 0.69444, 0, 0, 0.89444], - "65": [0, 0.68611, 0, 0, 0.86944], - "66": [0, 0.68611, 0, 0, 0.81805], - "67": [0, 0.68611, 0, 0, 0.83055], - "68": [0, 0.68611, 0, 0, 0.88194], - "69": [0, 0.68611, 0, 0, 0.75555], - "70": [0, 0.68611, 0, 0, 0.72361], - "71": [0, 0.68611, 0, 0, 0.90416], - "72": [0, 0.68611, 0, 0, 0.9], - "73": [0, 0.68611, 0, 0, 0.43611], - "74": [0, 0.68611, 0, 0, 0.59444], - "75": [0, 0.68611, 0, 0, 0.90138], - "76": [0, 0.68611, 0, 0, 0.69166], - "77": [0, 0.68611, 0, 0, 1.09166], - "78": [0, 0.68611, 0, 0, 0.9], - "79": [0, 0.68611, 0, 0, 0.86388], - "80": [0, 0.68611, 0, 0, 0.78611], - "81": [0.19444, 0.68611, 0, 0, 0.86388], - "82": [0, 0.68611, 0, 0, 0.8625], - "83": [0, 0.68611, 0, 0, 0.63889], - "84": [0, 0.68611, 0, 0, 0.8], - "85": [0, 0.68611, 0, 0, 0.88472], - "86": [0, 0.68611, 0.01597, 0, 0.86944], - "87": [0, 0.68611, 0.01597, 0, 1.18888], - "88": [0, 0.68611, 0, 0, 0.86944], - "89": [0, 0.68611, 0.02875, 0, 0.86944], - "90": [0, 0.68611, 0, 0, 0.70277], - "91": [0.25, 0.75, 0, 0, 0.31944], - "92": [0.25, 0.75, 0, 0, 0.575], - "93": [0.25, 0.75, 0, 0, 0.31944], - "94": [0, 0.69444, 0, 0, 0.575], - "95": [0.31, 0.13444, 0.03194, 0, 0.575], - "97": [0, 0.44444, 0, 0, 0.55902], - "98": [0, 0.69444, 0, 0, 0.63889], - "99": [0, 0.44444, 0, 0, 0.51111], - "100": [0, 0.69444, 0, 0, 0.63889], - "101": [0, 0.44444, 0, 0, 0.52708], - "102": [0, 0.69444, 0.10903, 0, 0.35139], - "103": [0.19444, 0.44444, 0.01597, 0, 0.575], - "104": [0, 0.69444, 0, 0, 0.63889], - "105": [0, 0.69444, 0, 0, 0.31944], - "106": [0.19444, 0.69444, 0, 0, 0.35139], - "107": [0, 0.69444, 0, 0, 0.60694], - "108": [0, 0.69444, 0, 0, 0.31944], - "109": [0, 0.44444, 0, 0, 0.95833], - "110": [0, 0.44444, 0, 0, 0.63889], - "111": [0, 0.44444, 0, 0, 0.575], - "112": [0.19444, 0.44444, 0, 0, 0.63889], - "113": [0.19444, 0.44444, 0, 0, 0.60694], - "114": [0, 0.44444, 0, 0, 0.47361], - "115": [0, 0.44444, 0, 0, 0.45361], - "116": [0, 0.63492, 0, 0, 0.44722], - "117": [0, 0.44444, 0, 0, 0.63889], - "118": [0, 0.44444, 0.01597, 0, 0.60694], - "119": [0, 0.44444, 0.01597, 0, 0.83055], - "120": [0, 0.44444, 0, 0, 0.60694], - "121": [0.19444, 0.44444, 0.01597, 0, 0.60694], - "122": [0, 0.44444, 0, 0, 0.51111], - "123": [0.25, 0.75, 0, 0, 0.575], - "124": [0.25, 0.75, 0, 0, 0.31944], - "125": [0.25, 0.75, 0, 0, 0.575], - "126": [0.35, 0.34444, 0, 0, 0.575], - "160": [0, 0, 0, 0, 0.25], - "163": [0, 0.69444, 0, 0, 0.86853], - "168": [0, 0.69444, 0, 0, 0.575], - "172": [0, 0.44444, 0, 0, 0.76666], - "176": [0, 0.69444, 0, 0, 0.86944], - "177": [0.13333, 0.63333, 0, 0, 0.89444], - "184": [0.17014, 0, 0, 0, 0.51111], - "198": [0, 0.68611, 0, 0, 1.04166], - "215": [0.13333, 0.63333, 0, 0, 0.89444], - "216": [0.04861, 0.73472, 0, 0, 0.89444], - "223": [0, 0.69444, 0, 0, 0.59722], - "230": [0, 0.44444, 0, 0, 0.83055], - "247": [0.13333, 0.63333, 0, 0, 0.89444], - "248": [0.09722, 0.54167, 0, 0, 0.575], - "305": [0, 0.44444, 0, 0, 0.31944], - "338": [0, 0.68611, 0, 0, 1.16944], - "339": [0, 0.44444, 0, 0, 0.89444], - "567": [0.19444, 0.44444, 0, 0, 0.35139], - "710": [0, 0.69444, 0, 0, 0.575], - "711": [0, 0.63194, 0, 0, 0.575], - "713": [0, 0.59611, 0, 0, 0.575], - "714": [0, 0.69444, 0, 0, 0.575], - "715": [0, 0.69444, 0, 0, 0.575], - "728": [0, 0.69444, 0, 0, 0.575], - "729": [0, 0.69444, 0, 0, 0.31944], - "730": [0, 0.69444, 0, 0, 0.86944], - "732": [0, 0.69444, 0, 0, 0.575], - "733": [0, 0.69444, 0, 0, 0.575], - "915": [0, 0.68611, 0, 0, 0.69166], - "916": [0, 0.68611, 0, 0, 0.95833], - "920": [0, 0.68611, 0, 0, 0.89444], - "923": [0, 0.68611, 0, 0, 0.80555], - "926": [0, 0.68611, 0, 0, 0.76666], - "928": [0, 0.68611, 0, 0, 0.9], - "931": [0, 0.68611, 0, 0, 0.83055], - "933": [0, 0.68611, 0, 0, 0.89444], - "934": [0, 0.68611, 0, 0, 0.83055], - "936": [0, 0.68611, 0, 0, 0.89444], - "937": [0, 0.68611, 0, 0, 0.83055], - "8211": [0, 0.44444, 0.03194, 0, 0.575], - "8212": [0, 0.44444, 0.03194, 0, 1.14999], - "8216": [0, 0.69444, 0, 0, 0.31944], - "8217": [0, 0.69444, 0, 0, 0.31944], - "8220": [0, 0.69444, 0, 0, 0.60278], - "8221": [0, 0.69444, 0, 0, 0.60278], - "8224": [0.19444, 0.69444, 0, 0, 0.51111], - "8225": [0.19444, 0.69444, 0, 0, 0.51111], - "8242": [0, 0.55556, 0, 0, 0.34444], - "8407": [0, 0.72444, 0.15486, 0, 0.575], - "8463": [0, 0.69444, 0, 0, 0.66759], - "8465": [0, 0.69444, 0, 0, 0.83055], - "8467": [0, 0.69444, 0, 0, 0.47361], - "8472": [0.19444, 0.44444, 0, 0, 0.74027], - "8476": [0, 0.69444, 0, 0, 0.83055], - "8501": [0, 0.69444, 0, 0, 0.70277], - "8592": [-0.10889, 0.39111, 0, 0, 1.14999], - "8593": [0.19444, 0.69444, 0, 0, 0.575], - "8594": [-0.10889, 0.39111, 0, 0, 1.14999], - "8595": [0.19444, 0.69444, 0, 0, 0.575], - "8596": [-0.10889, 0.39111, 0, 0, 1.14999], - "8597": [0.25, 0.75, 0, 0, 0.575], - "8598": [0.19444, 0.69444, 0, 0, 1.14999], - "8599": [0.19444, 0.69444, 0, 0, 1.14999], - "8600": [0.19444, 0.69444, 0, 0, 1.14999], - "8601": [0.19444, 0.69444, 0, 0, 1.14999], - "8636": [-0.10889, 0.39111, 0, 0, 1.14999], - "8637": [-0.10889, 0.39111, 0, 0, 1.14999], - "8640": [-0.10889, 0.39111, 0, 0, 1.14999], - "8641": [-0.10889, 0.39111, 0, 0, 1.14999], - "8656": [-0.10889, 0.39111, 0, 0, 1.14999], - "8657": [0.19444, 0.69444, 0, 0, 0.70277], - "8658": [-0.10889, 0.39111, 0, 0, 1.14999], - "8659": [0.19444, 0.69444, 0, 0, 0.70277], - "8660": [-0.10889, 0.39111, 0, 0, 1.14999], - "8661": [0.25, 0.75, 0, 0, 0.70277], - "8704": [0, 0.69444, 0, 0, 0.63889], - "8706": [0, 0.69444, 0.06389, 0, 0.62847], - "8707": [0, 0.69444, 0, 0, 0.63889], - "8709": [0.05556, 0.75, 0, 0, 0.575], - "8711": [0, 0.68611, 0, 0, 0.95833], - "8712": [0.08556, 0.58556, 0, 0, 0.76666], - "8715": [0.08556, 0.58556, 0, 0, 0.76666], - "8722": [0.13333, 0.63333, 0, 0, 0.89444], - "8723": [0.13333, 0.63333, 0, 0, 0.89444], - "8725": [0.25, 0.75, 0, 0, 0.575], - "8726": [0.25, 0.75, 0, 0, 0.575], - "8727": [-0.02778, 0.47222, 0, 0, 0.575], - "8728": [-0.02639, 0.47361, 0, 0, 0.575], - "8729": [-0.02639, 0.47361, 0, 0, 0.575], - "8730": [0.18, 0.82, 0, 0, 0.95833], - "8733": [0, 0.44444, 0, 0, 0.89444], - "8734": [0, 0.44444, 0, 0, 1.14999], - "8736": [0, 0.69224, 0, 0, 0.72222], - "8739": [0.25, 0.75, 0, 0, 0.31944], - "8741": [0.25, 0.75, 0, 0, 0.575], - "8743": [0, 0.55556, 0, 0, 0.76666], - "8744": [0, 0.55556, 0, 0, 0.76666], - "8745": [0, 0.55556, 0, 0, 0.76666], - "8746": [0, 0.55556, 0, 0, 0.76666], - "8747": [0.19444, 0.69444, 0.12778, 0, 0.56875], - "8764": [-0.10889, 0.39111, 0, 0, 0.89444], - "8768": [0.19444, 0.69444, 0, 0, 0.31944], - "8771": [0.00222, 0.50222, 0, 0, 0.89444], - "8776": [0.02444, 0.52444, 0, 0, 0.89444], - "8781": [0.00222, 0.50222, 0, 0, 0.89444], - "8801": [0.00222, 0.50222, 0, 0, 0.89444], - "8804": [0.19667, 0.69667, 0, 0, 0.89444], - "8805": [0.19667, 0.69667, 0, 0, 0.89444], - "8810": [0.08556, 0.58556, 0, 0, 1.14999], - "8811": [0.08556, 0.58556, 0, 0, 1.14999], - "8826": [0.08556, 0.58556, 0, 0, 0.89444], - "8827": [0.08556, 0.58556, 0, 0, 0.89444], - "8834": [0.08556, 0.58556, 0, 0, 0.89444], - "8835": [0.08556, 0.58556, 0, 0, 0.89444], - "8838": [0.19667, 0.69667, 0, 0, 0.89444], - "8839": [0.19667, 0.69667, 0, 0, 0.89444], - "8846": [0, 0.55556, 0, 0, 0.76666], - "8849": [0.19667, 0.69667, 0, 0, 0.89444], - "8850": [0.19667, 0.69667, 0, 0, 0.89444], - "8851": [0, 0.55556, 0, 0, 0.76666], - "8852": [0, 0.55556, 0, 0, 0.76666], - "8853": [0.13333, 0.63333, 0, 0, 0.89444], - "8854": [0.13333, 0.63333, 0, 0, 0.89444], - "8855": [0.13333, 0.63333, 0, 0, 0.89444], - "8856": [0.13333, 0.63333, 0, 0, 0.89444], - "8857": [0.13333, 0.63333, 0, 0, 0.89444], - "8866": [0, 0.69444, 0, 0, 0.70277], - "8867": [0, 0.69444, 0, 0, 0.70277], - "8868": [0, 0.69444, 0, 0, 0.89444], - "8869": [0, 0.69444, 0, 0, 0.89444], - "8900": [-0.02639, 0.47361, 0, 0, 0.575], - "8901": [-0.02639, 0.47361, 0, 0, 0.31944], - "8902": [-0.02778, 0.47222, 0, 0, 0.575], - "8968": [0.25, 0.75, 0, 0, 0.51111], - "8969": [0.25, 0.75, 0, 0, 0.51111], - "8970": [0.25, 0.75, 0, 0, 0.51111], - "8971": [0.25, 0.75, 0, 0, 0.51111], - "8994": [-0.13889, 0.36111, 0, 0, 1.14999], - "8995": [-0.13889, 0.36111, 0, 0, 1.14999], - "9651": [0.19444, 0.69444, 0, 0, 1.02222], - "9657": [-0.02778, 0.47222, 0, 0, 0.575], - "9661": [0.19444, 0.69444, 0, 0, 1.02222], - "9667": [-0.02778, 0.47222, 0, 0, 0.575], - "9711": [0.19444, 0.69444, 0, 0, 1.14999], - "9824": [0.12963, 0.69444, 0, 0, 0.89444], - "9825": [0.12963, 0.69444, 0, 0, 0.89444], - "9826": [0.12963, 0.69444, 0, 0, 0.89444], - "9827": [0.12963, 0.69444, 0, 0, 0.89444], - "9837": [0, 0.75, 0, 0, 0.44722], - "9838": [0.19444, 0.69444, 0, 0, 0.44722], - "9839": [0.19444, 0.69444, 0, 0, 0.44722], - "10216": [0.25, 0.75, 0, 0, 0.44722], - "10217": [0.25, 0.75, 0, 0, 0.44722], - "10815": [0, 0.68611, 0, 0, 0.9], - "10927": [0.19667, 0.69667, 0, 0, 0.89444], - "10928": [0.19667, 0.69667, 0, 0, 0.89444], - "57376": [0.19444, 0.69444, 0, 0, 0] - }, - "Main-BoldItalic": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0.11417, 0, 0.38611], - "34": [0, 0.69444, 0.07939, 0, 0.62055], - "35": [0.19444, 0.69444, 0.06833, 0, 0.94444], - "37": [0.05556, 0.75, 0.12861, 0, 0.94444], - "38": [0, 0.69444, 0.08528, 0, 0.88555], - "39": [0, 0.69444, 0.12945, 0, 0.35555], - "40": [0.25, 0.75, 0.15806, 0, 0.47333], - "41": [0.25, 0.75, 0.03306, 0, 0.47333], - "42": [0, 0.75, 0.14333, 0, 0.59111], - "43": [0.10333, 0.60333, 0.03306, 0, 0.88555], - "44": [0.19444, 0.14722, 0, 0, 0.35555], - "45": [0, 0.44444, 0.02611, 0, 0.41444], - "46": [0, 0.14722, 0, 0, 0.35555], - "47": [0.25, 0.75, 0.15806, 0, 0.59111], - "48": [0, 0.64444, 0.13167, 0, 0.59111], - "49": [0, 0.64444, 0.13167, 0, 0.59111], - "50": [0, 0.64444, 0.13167, 0, 0.59111], - "51": [0, 0.64444, 0.13167, 0, 0.59111], - "52": [0.19444, 0.64444, 0.13167, 0, 0.59111], - "53": [0, 0.64444, 0.13167, 0, 0.59111], - "54": [0, 0.64444, 0.13167, 0, 0.59111], - "55": [0.19444, 0.64444, 0.13167, 0, 0.59111], - "56": [0, 0.64444, 0.13167, 0, 0.59111], - "57": [0, 0.64444, 0.13167, 0, 0.59111], - "58": [0, 0.44444, 0.06695, 0, 0.35555], - "59": [0.19444, 0.44444, 0.06695, 0, 0.35555], - "61": [-0.10889, 0.39111, 0.06833, 0, 0.88555], - "63": [0, 0.69444, 0.11472, 0, 0.59111], - "64": [0, 0.69444, 0.09208, 0, 0.88555], - "65": [0, 0.68611, 0, 0, 0.86555], - "66": [0, 0.68611, 0.0992, 0, 0.81666], - "67": [0, 0.68611, 0.14208, 0, 0.82666], - "68": [0, 0.68611, 0.09062, 0, 0.87555], - "69": [0, 0.68611, 0.11431, 0, 0.75666], - "70": [0, 0.68611, 0.12903, 0, 0.72722], - "71": [0, 0.68611, 0.07347, 0, 0.89527], - "72": [0, 0.68611, 0.17208, 0, 0.8961], - "73": [0, 0.68611, 0.15681, 0, 0.47166], - "74": [0, 0.68611, 0.145, 0, 0.61055], - "75": [0, 0.68611, 0.14208, 0, 0.89499], - "76": [0, 0.68611, 0, 0, 0.69777], - "77": [0, 0.68611, 0.17208, 0, 1.07277], - "78": [0, 0.68611, 0.17208, 0, 0.8961], - "79": [0, 0.68611, 0.09062, 0, 0.85499], - "80": [0, 0.68611, 0.0992, 0, 0.78721], - "81": [0.19444, 0.68611, 0.09062, 0, 0.85499], - "82": [0, 0.68611, 0.02559, 0, 0.85944], - "83": [0, 0.68611, 0.11264, 0, 0.64999], - "84": [0, 0.68611, 0.12903, 0, 0.7961], - "85": [0, 0.68611, 0.17208, 0, 0.88083], - "86": [0, 0.68611, 0.18625, 0, 0.86555], - "87": [0, 0.68611, 0.18625, 0, 1.15999], - "88": [0, 0.68611, 0.15681, 0, 0.86555], - "89": [0, 0.68611, 0.19803, 0, 0.86555], - "90": [0, 0.68611, 0.14208, 0, 0.70888], - "91": [0.25, 0.75, 0.1875, 0, 0.35611], - "93": [0.25, 0.75, 0.09972, 0, 0.35611], - "94": [0, 0.69444, 0.06709, 0, 0.59111], - "95": [0.31, 0.13444, 0.09811, 0, 0.59111], - "97": [0, 0.44444, 0.09426, 0, 0.59111], - "98": [0, 0.69444, 0.07861, 0, 0.53222], - "99": [0, 0.44444, 0.05222, 0, 0.53222], - "100": [0, 0.69444, 0.10861, 0, 0.59111], - "101": [0, 0.44444, 0.085, 0, 0.53222], - "102": [0.19444, 0.69444, 0.21778, 0, 0.4], - "103": [0.19444, 0.44444, 0.105, 0, 0.53222], - "104": [0, 0.69444, 0.09426, 0, 0.59111], - "105": [0, 0.69326, 0.11387, 0, 0.35555], - "106": [0.19444, 0.69326, 0.1672, 0, 0.35555], - "107": [0, 0.69444, 0.11111, 0, 0.53222], - "108": [0, 0.69444, 0.10861, 0, 0.29666], - "109": [0, 0.44444, 0.09426, 0, 0.94444], - "110": [0, 0.44444, 0.09426, 0, 0.64999], - "111": [0, 0.44444, 0.07861, 0, 0.59111], - "112": [0.19444, 0.44444, 0.07861, 0, 0.59111], - "113": [0.19444, 0.44444, 0.105, 0, 0.53222], - "114": [0, 0.44444, 0.11111, 0, 0.50167], - "115": [0, 0.44444, 0.08167, 0, 0.48694], - "116": [0, 0.63492, 0.09639, 0, 0.385], - "117": [0, 0.44444, 0.09426, 0, 0.62055], - "118": [0, 0.44444, 0.11111, 0, 0.53222], - "119": [0, 0.44444, 0.11111, 0, 0.76777], - "120": [0, 0.44444, 0.12583, 0, 0.56055], - "121": [0.19444, 0.44444, 0.105, 0, 0.56166], - "122": [0, 0.44444, 0.13889, 0, 0.49055], - "126": [0.35, 0.34444, 0.11472, 0, 0.59111], - "160": [0, 0, 0, 0, 0.25], - "168": [0, 0.69444, 0.11473, 0, 0.59111], - "176": [0, 0.69444, 0, 0, 0.94888], - "184": [0.17014, 0, 0, 0, 0.53222], - "198": [0, 0.68611, 0.11431, 0, 1.02277], - "216": [0.04861, 0.73472, 0.09062, 0, 0.88555], - "223": [0.19444, 0.69444, 0.09736, 0, 0.665], - "230": [0, 0.44444, 0.085, 0, 0.82666], - "248": [0.09722, 0.54167, 0.09458, 0, 0.59111], - "305": [0, 0.44444, 0.09426, 0, 0.35555], - "338": [0, 0.68611, 0.11431, 0, 1.14054], - "339": [0, 0.44444, 0.085, 0, 0.82666], - "567": [0.19444, 0.44444, 0.04611, 0, 0.385], - "710": [0, 0.69444, 0.06709, 0, 0.59111], - "711": [0, 0.63194, 0.08271, 0, 0.59111], - "713": [0, 0.59444, 0.10444, 0, 0.59111], - "714": [0, 0.69444, 0.08528, 0, 0.59111], - "715": [0, 0.69444, 0, 0, 0.59111], - "728": [0, 0.69444, 0.10333, 0, 0.59111], - "729": [0, 0.69444, 0.12945, 0, 0.35555], - "730": [0, 0.69444, 0, 0, 0.94888], - "732": [0, 0.69444, 0.11472, 0, 0.59111], - "733": [0, 0.69444, 0.11472, 0, 0.59111], - "915": [0, 0.68611, 0.12903, 0, 0.69777], - "916": [0, 0.68611, 0, 0, 0.94444], - "920": [0, 0.68611, 0.09062, 0, 0.88555], - "923": [0, 0.68611, 0, 0, 0.80666], - "926": [0, 0.68611, 0.15092, 0, 0.76777], - "928": [0, 0.68611, 0.17208, 0, 0.8961], - "931": [0, 0.68611, 0.11431, 0, 0.82666], - "933": [0, 0.68611, 0.10778, 0, 0.88555], - "934": [0, 0.68611, 0.05632, 0, 0.82666], - "936": [0, 0.68611, 0.10778, 0, 0.88555], - "937": [0, 0.68611, 0.0992, 0, 0.82666], - "8211": [0, 0.44444, 0.09811, 0, 0.59111], - "8212": [0, 0.44444, 0.09811, 0, 1.18221], - "8216": [0, 0.69444, 0.12945, 0, 0.35555], - "8217": [0, 0.69444, 0.12945, 0, 0.35555], - "8220": [0, 0.69444, 0.16772, 0, 0.62055], - "8221": [0, 0.69444, 0.07939, 0, 0.62055] - }, - "Main-Italic": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0.12417, 0, 0.30667], - "34": [0, 0.69444, 0.06961, 0, 0.51444], - "35": [0.19444, 0.69444, 0.06616, 0, 0.81777], - "37": [0.05556, 0.75, 0.13639, 0, 0.81777], - "38": [0, 0.69444, 0.09694, 0, 0.76666], - "39": [0, 0.69444, 0.12417, 0, 0.30667], - "40": [0.25, 0.75, 0.16194, 0, 0.40889], - "41": [0.25, 0.75, 0.03694, 0, 0.40889], - "42": [0, 0.75, 0.14917, 0, 0.51111], - "43": [0.05667, 0.56167, 0.03694, 0, 0.76666], - "44": [0.19444, 0.10556, 0, 0, 0.30667], - "45": [0, 0.43056, 0.02826, 0, 0.35778], - "46": [0, 0.10556, 0, 0, 0.30667], - "47": [0.25, 0.75, 0.16194, 0, 0.51111], - "48": [0, 0.64444, 0.13556, 0, 0.51111], - "49": [0, 0.64444, 0.13556, 0, 0.51111], - "50": [0, 0.64444, 0.13556, 0, 0.51111], - "51": [0, 0.64444, 0.13556, 0, 0.51111], - "52": [0.19444, 0.64444, 0.13556, 0, 0.51111], - "53": [0, 0.64444, 0.13556, 0, 0.51111], - "54": [0, 0.64444, 0.13556, 0, 0.51111], - "55": [0.19444, 0.64444, 0.13556, 0, 0.51111], - "56": [0, 0.64444, 0.13556, 0, 0.51111], - "57": [0, 0.64444, 0.13556, 0, 0.51111], - "58": [0, 0.43056, 0.0582, 0, 0.30667], - "59": [0.19444, 0.43056, 0.0582, 0, 0.30667], - "61": [-0.13313, 0.36687, 0.06616, 0, 0.76666], - "63": [0, 0.69444, 0.1225, 0, 0.51111], - "64": [0, 0.69444, 0.09597, 0, 0.76666], - "65": [0, 0.68333, 0, 0, 0.74333], - "66": [0, 0.68333, 0.10257, 0, 0.70389], - "67": [0, 0.68333, 0.14528, 0, 0.71555], - "68": [0, 0.68333, 0.09403, 0, 0.755], - "69": [0, 0.68333, 0.12028, 0, 0.67833], - "70": [0, 0.68333, 0.13305, 0, 0.65277], - "71": [0, 0.68333, 0.08722, 0, 0.77361], - "72": [0, 0.68333, 0.16389, 0, 0.74333], - "73": [0, 0.68333, 0.15806, 0, 0.38555], - "74": [0, 0.68333, 0.14028, 0, 0.525], - "75": [0, 0.68333, 0.14528, 0, 0.76888], - "76": [0, 0.68333, 0, 0, 0.62722], - "77": [0, 0.68333, 0.16389, 0, 0.89666], - "78": [0, 0.68333, 0.16389, 0, 0.74333], - "79": [0, 0.68333, 0.09403, 0, 0.76666], - "80": [0, 0.68333, 0.10257, 0, 0.67833], - "81": [0.19444, 0.68333, 0.09403, 0, 0.76666], - "82": [0, 0.68333, 0.03868, 0, 0.72944], - "83": [0, 0.68333, 0.11972, 0, 0.56222], - "84": [0, 0.68333, 0.13305, 0, 0.71555], - "85": [0, 0.68333, 0.16389, 0, 0.74333], - "86": [0, 0.68333, 0.18361, 0, 0.74333], - "87": [0, 0.68333, 0.18361, 0, 0.99888], - "88": [0, 0.68333, 0.15806, 0, 0.74333], - "89": [0, 0.68333, 0.19383, 0, 0.74333], - "90": [0, 0.68333, 0.14528, 0, 0.61333], - "91": [0.25, 0.75, 0.1875, 0, 0.30667], - "93": [0.25, 0.75, 0.10528, 0, 0.30667], - "94": [0, 0.69444, 0.06646, 0, 0.51111], - "95": [0.31, 0.12056, 0.09208, 0, 0.51111], - "97": [0, 0.43056, 0.07671, 0, 0.51111], - "98": [0, 0.69444, 0.06312, 0, 0.46], - "99": [0, 0.43056, 0.05653, 0, 0.46], - "100": [0, 0.69444, 0.10333, 0, 0.51111], - "101": [0, 0.43056, 0.07514, 0, 0.46], - "102": [0.19444, 0.69444, 0.21194, 0, 0.30667], - "103": [0.19444, 0.43056, 0.08847, 0, 0.46], - "104": [0, 0.69444, 0.07671, 0, 0.51111], - "105": [0, 0.65536, 0.1019, 0, 0.30667], - "106": [0.19444, 0.65536, 0.14467, 0, 0.30667], - "107": [0, 0.69444, 0.10764, 0, 0.46], - "108": [0, 0.69444, 0.10333, 0, 0.25555], - "109": [0, 0.43056, 0.07671, 0, 0.81777], - "110": [0, 0.43056, 0.07671, 0, 0.56222], - "111": [0, 0.43056, 0.06312, 0, 0.51111], - "112": [0.19444, 0.43056, 0.06312, 0, 0.51111], - "113": [0.19444, 0.43056, 0.08847, 0, 0.46], - "114": [0, 0.43056, 0.10764, 0, 0.42166], - "115": [0, 0.43056, 0.08208, 0, 0.40889], - "116": [0, 0.61508, 0.09486, 0, 0.33222], - "117": [0, 0.43056, 0.07671, 0, 0.53666], - "118": [0, 0.43056, 0.10764, 0, 0.46], - "119": [0, 0.43056, 0.10764, 0, 0.66444], - "120": [0, 0.43056, 0.12042, 0, 0.46389], - "121": [0.19444, 0.43056, 0.08847, 0, 0.48555], - "122": [0, 0.43056, 0.12292, 0, 0.40889], - "126": [0.35, 0.31786, 0.11585, 0, 0.51111], - "160": [0, 0, 0, 0, 0.25], - "168": [0, 0.66786, 0.10474, 0, 0.51111], - "176": [0, 0.69444, 0, 0, 0.83129], - "184": [0.17014, 0, 0, 0, 0.46], - "198": [0, 0.68333, 0.12028, 0, 0.88277], - "216": [0.04861, 0.73194, 0.09403, 0, 0.76666], - "223": [0.19444, 0.69444, 0.10514, 0, 0.53666], - "230": [0, 0.43056, 0.07514, 0, 0.71555], - "248": [0.09722, 0.52778, 0.09194, 0, 0.51111], - "338": [0, 0.68333, 0.12028, 0, 0.98499], - "339": [0, 0.43056, 0.07514, 0, 0.71555], - "710": [0, 0.69444, 0.06646, 0, 0.51111], - "711": [0, 0.62847, 0.08295, 0, 0.51111], - "713": [0, 0.56167, 0.10333, 0, 0.51111], - "714": [0, 0.69444, 0.09694, 0, 0.51111], - "715": [0, 0.69444, 0, 0, 0.51111], - "728": [0, 0.69444, 0.10806, 0, 0.51111], - "729": [0, 0.66786, 0.11752, 0, 0.30667], - "730": [0, 0.69444, 0, 0, 0.83129], - "732": [0, 0.66786, 0.11585, 0, 0.51111], - "733": [0, 0.69444, 0.1225, 0, 0.51111], - "915": [0, 0.68333, 0.13305, 0, 0.62722], - "916": [0, 0.68333, 0, 0, 0.81777], - "920": [0, 0.68333, 0.09403, 0, 0.76666], - "923": [0, 0.68333, 0, 0, 0.69222], - "926": [0, 0.68333, 0.15294, 0, 0.66444], - "928": [0, 0.68333, 0.16389, 0, 0.74333], - "931": [0, 0.68333, 0.12028, 0, 0.71555], - "933": [0, 0.68333, 0.11111, 0, 0.76666], - "934": [0, 0.68333, 0.05986, 0, 0.71555], - "936": [0, 0.68333, 0.11111, 0, 0.76666], - "937": [0, 0.68333, 0.10257, 0, 0.71555], - "8211": [0, 0.43056, 0.09208, 0, 0.51111], - "8212": [0, 0.43056, 0.09208, 0, 1.02222], - "8216": [0, 0.69444, 0.12417, 0, 0.30667], - "8217": [0, 0.69444, 0.12417, 0, 0.30667], - "8220": [0, 0.69444, 0.1685, 0, 0.51444], - "8221": [0, 0.69444, 0.06961, 0, 0.51444], - "8463": [0, 0.68889, 0, 0, 0.54028] - }, - "Main-Regular": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0, 0, 0.27778], - "34": [0, 0.69444, 0, 0, 0.5], - "35": [0.19444, 0.69444, 0, 0, 0.83334], - "36": [0.05556, 0.75, 0, 0, 0.5], - "37": [0.05556, 0.75, 0, 0, 0.83334], - "38": [0, 0.69444, 0, 0, 0.77778], - "39": [0, 0.69444, 0, 0, 0.27778], - "40": [0.25, 0.75, 0, 0, 0.38889], - "41": [0.25, 0.75, 0, 0, 0.38889], - "42": [0, 0.75, 0, 0, 0.5], - "43": [0.08333, 0.58333, 0, 0, 0.77778], - "44": [0.19444, 0.10556, 0, 0, 0.27778], - "45": [0, 0.43056, 0, 0, 0.33333], - "46": [0, 0.10556, 0, 0, 0.27778], - "47": [0.25, 0.75, 0, 0, 0.5], - "48": [0, 0.64444, 0, 0, 0.5], - "49": [0, 0.64444, 0, 0, 0.5], - "50": [0, 0.64444, 0, 0, 0.5], - "51": [0, 0.64444, 0, 0, 0.5], - "52": [0, 0.64444, 0, 0, 0.5], - "53": [0, 0.64444, 0, 0, 0.5], - "54": [0, 0.64444, 0, 0, 0.5], - "55": [0, 0.64444, 0, 0, 0.5], - "56": [0, 0.64444, 0, 0, 0.5], - "57": [0, 0.64444, 0, 0, 0.5], - "58": [0, 0.43056, 0, 0, 0.27778], - "59": [0.19444, 0.43056, 0, 0, 0.27778], - "60": [0.0391, 0.5391, 0, 0, 0.77778], - "61": [-0.13313, 0.36687, 0, 0, 0.77778], - "62": [0.0391, 0.5391, 0, 0, 0.77778], - "63": [0, 0.69444, 0, 0, 0.47222], - "64": [0, 0.69444, 0, 0, 0.77778], - "65": [0, 0.68333, 0, 0, 0.75], - "66": [0, 0.68333, 0, 0, 0.70834], - "67": [0, 0.68333, 0, 0, 0.72222], - "68": [0, 0.68333, 0, 0, 0.76389], - "69": [0, 0.68333, 0, 0, 0.68056], - "70": [0, 0.68333, 0, 0, 0.65278], - "71": [0, 0.68333, 0, 0, 0.78472], - "72": [0, 0.68333, 0, 0, 0.75], - "73": [0, 0.68333, 0, 0, 0.36111], - "74": [0, 0.68333, 0, 0, 0.51389], - "75": [0, 0.68333, 0, 0, 0.77778], - "76": [0, 0.68333, 0, 0, 0.625], - "77": [0, 0.68333, 0, 0, 0.91667], - "78": [0, 0.68333, 0, 0, 0.75], - "79": [0, 0.68333, 0, 0, 0.77778], - "80": [0, 0.68333, 0, 0, 0.68056], - "81": [0.19444, 0.68333, 0, 0, 0.77778], - "82": [0, 0.68333, 0, 0, 0.73611], - "83": [0, 0.68333, 0, 0, 0.55556], - "84": [0, 0.68333, 0, 0, 0.72222], - "85": [0, 0.68333, 0, 0, 0.75], - "86": [0, 0.68333, 0.01389, 0, 0.75], - "87": [0, 0.68333, 0.01389, 0, 1.02778], - "88": [0, 0.68333, 0, 0, 0.75], - "89": [0, 0.68333, 0.025, 0, 0.75], - "90": [0, 0.68333, 0, 0, 0.61111], - "91": [0.25, 0.75, 0, 0, 0.27778], - "92": [0.25, 0.75, 0, 0, 0.5], - "93": [0.25, 0.75, 0, 0, 0.27778], - "94": [0, 0.69444, 0, 0, 0.5], - "95": [0.31, 0.12056, 0.02778, 0, 0.5], - "97": [0, 0.43056, 0, 0, 0.5], - "98": [0, 0.69444, 0, 0, 0.55556], - "99": [0, 0.43056, 0, 0, 0.44445], - "100": [0, 0.69444, 0, 0, 0.55556], - "101": [0, 0.43056, 0, 0, 0.44445], - "102": [0, 0.69444, 0.07778, 0, 0.30556], - "103": [0.19444, 0.43056, 0.01389, 0, 0.5], - "104": [0, 0.69444, 0, 0, 0.55556], - "105": [0, 0.66786, 0, 0, 0.27778], - "106": [0.19444, 0.66786, 0, 0, 0.30556], - "107": [0, 0.69444, 0, 0, 0.52778], - "108": [0, 0.69444, 0, 0, 0.27778], - "109": [0, 0.43056, 0, 0, 0.83334], - "110": [0, 0.43056, 0, 0, 0.55556], - "111": [0, 0.43056, 0, 0, 0.5], - "112": [0.19444, 0.43056, 0, 0, 0.55556], - "113": [0.19444, 0.43056, 0, 0, 0.52778], - "114": [0, 0.43056, 0, 0, 0.39167], - "115": [0, 0.43056, 0, 0, 0.39445], - "116": [0, 0.61508, 0, 0, 0.38889], - "117": [0, 0.43056, 0, 0, 0.55556], - "118": [0, 0.43056, 0.01389, 0, 0.52778], - "119": [0, 0.43056, 0.01389, 0, 0.72222], - "120": [0, 0.43056, 0, 0, 0.52778], - "121": [0.19444, 0.43056, 0.01389, 0, 0.52778], - "122": [0, 0.43056, 0, 0, 0.44445], - "123": [0.25, 0.75, 0, 0, 0.5], - "124": [0.25, 0.75, 0, 0, 0.27778], - "125": [0.25, 0.75, 0, 0, 0.5], - "126": [0.35, 0.31786, 0, 0, 0.5], - "160": [0, 0, 0, 0, 0.25], - "163": [0, 0.69444, 0, 0, 0.76909], - "167": [0.19444, 0.69444, 0, 0, 0.44445], - "168": [0, 0.66786, 0, 0, 0.5], - "172": [0, 0.43056, 0, 0, 0.66667], - "176": [0, 0.69444, 0, 0, 0.75], - "177": [0.08333, 0.58333, 0, 0, 0.77778], - "182": [0.19444, 0.69444, 0, 0, 0.61111], - "184": [0.17014, 0, 0, 0, 0.44445], - "198": [0, 0.68333, 0, 0, 0.90278], - "215": [0.08333, 0.58333, 0, 0, 0.77778], - "216": [0.04861, 0.73194, 0, 0, 0.77778], - "223": [0, 0.69444, 0, 0, 0.5], - "230": [0, 0.43056, 0, 0, 0.72222], - "247": [0.08333, 0.58333, 0, 0, 0.77778], - "248": [0.09722, 0.52778, 0, 0, 0.5], - "305": [0, 0.43056, 0, 0, 0.27778], - "338": [0, 0.68333, 0, 0, 1.01389], - "339": [0, 0.43056, 0, 0, 0.77778], - "567": [0.19444, 0.43056, 0, 0, 0.30556], - "710": [0, 0.69444, 0, 0, 0.5], - "711": [0, 0.62847, 0, 0, 0.5], - "713": [0, 0.56778, 0, 0, 0.5], - "714": [0, 0.69444, 0, 0, 0.5], - "715": [0, 0.69444, 0, 0, 0.5], - "728": [0, 0.69444, 0, 0, 0.5], - "729": [0, 0.66786, 0, 0, 0.27778], - "730": [0, 0.69444, 0, 0, 0.75], - "732": [0, 0.66786, 0, 0, 0.5], - "733": [0, 0.69444, 0, 0, 0.5], - "915": [0, 0.68333, 0, 0, 0.625], - "916": [0, 0.68333, 0, 0, 0.83334], - "920": [0, 0.68333, 0, 0, 0.77778], - "923": [0, 0.68333, 0, 0, 0.69445], - "926": [0, 0.68333, 0, 0, 0.66667], - "928": [0, 0.68333, 0, 0, 0.75], - "931": [0, 0.68333, 0, 0, 0.72222], - "933": [0, 0.68333, 0, 0, 0.77778], - "934": [0, 0.68333, 0, 0, 0.72222], - "936": [0, 0.68333, 0, 0, 0.77778], - "937": [0, 0.68333, 0, 0, 0.72222], - "8211": [0, 0.43056, 0.02778, 0, 0.5], - "8212": [0, 0.43056, 0.02778, 0, 1.0], - "8216": [0, 0.69444, 0, 0, 0.27778], - "8217": [0, 0.69444, 0, 0, 0.27778], - "8220": [0, 0.69444, 0, 0, 0.5], - "8221": [0, 0.69444, 0, 0, 0.5], - "8224": [0.19444, 0.69444, 0, 0, 0.44445], - "8225": [0.19444, 0.69444, 0, 0, 0.44445], - "8230": [0, 0.12, 0, 0, 1.172], - "8242": [0, 0.55556, 0, 0, 0.275], - "8407": [0, 0.71444, 0.15382, 0, 0.5], - "8463": [0, 0.68889, 0, 0, 0.54028], - "8465": [0, 0.69444, 0, 0, 0.72222], - "8467": [0, 0.69444, 0, 0.11111, 0.41667], - "8472": [0.19444, 0.43056, 0, 0.11111, 0.63646], - "8476": [0, 0.69444, 0, 0, 0.72222], - "8501": [0, 0.69444, 0, 0, 0.61111], - "8592": [-0.13313, 0.36687, 0, 0, 1.0], - "8593": [0.19444, 0.69444, 0, 0, 0.5], - "8594": [-0.13313, 0.36687, 0, 0, 1.0], - "8595": [0.19444, 0.69444, 0, 0, 0.5], - "8596": [-0.13313, 0.36687, 0, 0, 1.0], - "8597": [0.25, 0.75, 0, 0, 0.5], - "8598": [0.19444, 0.69444, 0, 0, 1.0], - "8599": [0.19444, 0.69444, 0, 0, 1.0], - "8600": [0.19444, 0.69444, 0, 0, 1.0], - "8601": [0.19444, 0.69444, 0, 0, 1.0], - "8614": [0.011, 0.511, 0, 0, 1.0], - "8617": [0.011, 0.511, 0, 0, 1.126], - "8618": [0.011, 0.511, 0, 0, 1.126], - "8636": [-0.13313, 0.36687, 0, 0, 1.0], - "8637": [-0.13313, 0.36687, 0, 0, 1.0], - "8640": [-0.13313, 0.36687, 0, 0, 1.0], - "8641": [-0.13313, 0.36687, 0, 0, 1.0], - "8652": [0.011, 0.671, 0, 0, 1.0], - "8656": [-0.13313, 0.36687, 0, 0, 1.0], - "8657": [0.19444, 0.69444, 0, 0, 0.61111], - "8658": [-0.13313, 0.36687, 0, 0, 1.0], - "8659": [0.19444, 0.69444, 0, 0, 0.61111], - "8660": [-0.13313, 0.36687, 0, 0, 1.0], - "8661": [0.25, 0.75, 0, 0, 0.61111], - "8704": [0, 0.69444, 0, 0, 0.55556], - "8706": [0, 0.69444, 0.05556, 0.08334, 0.5309], - "8707": [0, 0.69444, 0, 0, 0.55556], - "8709": [0.05556, 0.75, 0, 0, 0.5], - "8711": [0, 0.68333, 0, 0, 0.83334], - "8712": [0.0391, 0.5391, 0, 0, 0.66667], - "8715": [0.0391, 0.5391, 0, 0, 0.66667], - "8722": [0.08333, 0.58333, 0, 0, 0.77778], - "8723": [0.08333, 0.58333, 0, 0, 0.77778], - "8725": [0.25, 0.75, 0, 0, 0.5], - "8726": [0.25, 0.75, 0, 0, 0.5], - "8727": [-0.03472, 0.46528, 0, 0, 0.5], - "8728": [-0.05555, 0.44445, 0, 0, 0.5], - "8729": [-0.05555, 0.44445, 0, 0, 0.5], - "8730": [0.2, 0.8, 0, 0, 0.83334], - "8733": [0, 0.43056, 0, 0, 0.77778], - "8734": [0, 0.43056, 0, 0, 1.0], - "8736": [0, 0.69224, 0, 0, 0.72222], - "8739": [0.25, 0.75, 0, 0, 0.27778], - "8741": [0.25, 0.75, 0, 0, 0.5], - "8743": [0, 0.55556, 0, 0, 0.66667], - "8744": [0, 0.55556, 0, 0, 0.66667], - "8745": [0, 0.55556, 0, 0, 0.66667], - "8746": [0, 0.55556, 0, 0, 0.66667], - "8747": [0.19444, 0.69444, 0.11111, 0, 0.41667], - "8764": [-0.13313, 0.36687, 0, 0, 0.77778], - "8768": [0.19444, 0.69444, 0, 0, 0.27778], - "8771": [-0.03625, 0.46375, 0, 0, 0.77778], - "8773": [-0.022, 0.589, 0, 0, 1.0], - "8776": [-0.01688, 0.48312, 0, 0, 0.77778], - "8781": [-0.03625, 0.46375, 0, 0, 0.77778], - "8784": [-0.133, 0.67, 0, 0, 0.778], - "8801": [-0.03625, 0.46375, 0, 0, 0.77778], - "8804": [0.13597, 0.63597, 0, 0, 0.77778], - "8805": [0.13597, 0.63597, 0, 0, 0.77778], - "8810": [0.0391, 0.5391, 0, 0, 1.0], - "8811": [0.0391, 0.5391, 0, 0, 1.0], - "8826": [0.0391, 0.5391, 0, 0, 0.77778], - "8827": [0.0391, 0.5391, 0, 0, 0.77778], - "8834": [0.0391, 0.5391, 0, 0, 0.77778], - "8835": [0.0391, 0.5391, 0, 0, 0.77778], - "8838": [0.13597, 0.63597, 0, 0, 0.77778], - "8839": [0.13597, 0.63597, 0, 0, 0.77778], - "8846": [0, 0.55556, 0, 0, 0.66667], - "8849": [0.13597, 0.63597, 0, 0, 0.77778], - "8850": [0.13597, 0.63597, 0, 0, 0.77778], - "8851": [0, 0.55556, 0, 0, 0.66667], - "8852": [0, 0.55556, 0, 0, 0.66667], - "8853": [0.08333, 0.58333, 0, 0, 0.77778], - "8854": [0.08333, 0.58333, 0, 0, 0.77778], - "8855": [0.08333, 0.58333, 0, 0, 0.77778], - "8856": [0.08333, 0.58333, 0, 0, 0.77778], - "8857": [0.08333, 0.58333, 0, 0, 0.77778], - "8866": [0, 0.69444, 0, 0, 0.61111], - "8867": [0, 0.69444, 0, 0, 0.61111], - "8868": [0, 0.69444, 0, 0, 0.77778], - "8869": [0, 0.69444, 0, 0, 0.77778], - "8872": [0.249, 0.75, 0, 0, 0.867], - "8900": [-0.05555, 0.44445, 0, 0, 0.5], - "8901": [-0.05555, 0.44445, 0, 0, 0.27778], - "8902": [-0.03472, 0.46528, 0, 0, 0.5], - "8904": [0.005, 0.505, 0, 0, 0.9], - "8942": [0.03, 0.9, 0, 0, 0.278], - "8943": [-0.19, 0.31, 0, 0, 1.172], - "8945": [-0.1, 0.82, 0, 0, 1.282], - "8968": [0.25, 0.75, 0, 0, 0.44445], - "8969": [0.25, 0.75, 0, 0, 0.44445], - "8970": [0.25, 0.75, 0, 0, 0.44445], - "8971": [0.25, 0.75, 0, 0, 0.44445], - "8994": [-0.14236, 0.35764, 0, 0, 1.0], - "8995": [-0.14236, 0.35764, 0, 0, 1.0], - "9136": [0.244, 0.744, 0, 0, 0.412], - "9137": [0.244, 0.744, 0, 0, 0.412], - "9651": [0.19444, 0.69444, 0, 0, 0.88889], - "9657": [-0.03472, 0.46528, 0, 0, 0.5], - "9661": [0.19444, 0.69444, 0, 0, 0.88889], - "9667": [-0.03472, 0.46528, 0, 0, 0.5], - "9711": [0.19444, 0.69444, 0, 0, 1.0], - "9824": [0.12963, 0.69444, 0, 0, 0.77778], - "9825": [0.12963, 0.69444, 0, 0, 0.77778], - "9826": [0.12963, 0.69444, 0, 0, 0.77778], - "9827": [0.12963, 0.69444, 0, 0, 0.77778], - "9837": [0, 0.75, 0, 0, 0.38889], - "9838": [0.19444, 0.69444, 0, 0, 0.38889], - "9839": [0.19444, 0.69444, 0, 0, 0.38889], - "10216": [0.25, 0.75, 0, 0, 0.38889], - "10217": [0.25, 0.75, 0, 0, 0.38889], - "10222": [0.244, 0.744, 0, 0, 0.412], - "10223": [0.244, 0.744, 0, 0, 0.412], - "10229": [0.011, 0.511, 0, 0, 1.609], - "10230": [0.011, 0.511, 0, 0, 1.638], - "10231": [0.011, 0.511, 0, 0, 1.859], - "10232": [0.024, 0.525, 0, 0, 1.609], - "10233": [0.024, 0.525, 0, 0, 1.638], - "10234": [0.024, 0.525, 0, 0, 1.858], - "10236": [0.011, 0.511, 0, 0, 1.638], - "10815": [0, 0.68333, 0, 0, 0.75], - "10927": [0.13597, 0.63597, 0, 0, 0.77778], - "10928": [0.13597, 0.63597, 0, 0, 0.77778], - "57376": [0.19444, 0.69444, 0, 0, 0] - }, - "Math-BoldItalic": { - "32": [0, 0, 0, 0, 0.25], - "48": [0, 0.44444, 0, 0, 0.575], - "49": [0, 0.44444, 0, 0, 0.575], - "50": [0, 0.44444, 0, 0, 0.575], - "51": [0.19444, 0.44444, 0, 0, 0.575], - "52": [0.19444, 0.44444, 0, 0, 0.575], - "53": [0.19444, 0.44444, 0, 0, 0.575], - "54": [0, 0.64444, 0, 0, 0.575], - "55": [0.19444, 0.44444, 0, 0, 0.575], - "56": [0, 0.64444, 0, 0, 0.575], - "57": [0.19444, 0.44444, 0, 0, 0.575], - "65": [0, 0.68611, 0, 0, 0.86944], - "66": [0, 0.68611, 0.04835, 0, 0.8664], - "67": [0, 0.68611, 0.06979, 0, 0.81694], - "68": [0, 0.68611, 0.03194, 0, 0.93812], - "69": [0, 0.68611, 0.05451, 0, 0.81007], - "70": [0, 0.68611, 0.15972, 0, 0.68889], - "71": [0, 0.68611, 0, 0, 0.88673], - "72": [0, 0.68611, 0.08229, 0, 0.98229], - "73": [0, 0.68611, 0.07778, 0, 0.51111], - "74": [0, 0.68611, 0.10069, 0, 0.63125], - "75": [0, 0.68611, 0.06979, 0, 0.97118], - "76": [0, 0.68611, 0, 0, 0.75555], - "77": [0, 0.68611, 0.11424, 0, 1.14201], - "78": [0, 0.68611, 0.11424, 0, 0.95034], - "79": [0, 0.68611, 0.03194, 0, 0.83666], - "80": [0, 0.68611, 0.15972, 0, 0.72309], - "81": [0.19444, 0.68611, 0, 0, 0.86861], - "82": [0, 0.68611, 0.00421, 0, 0.87235], - "83": [0, 0.68611, 0.05382, 0, 0.69271], - "84": [0, 0.68611, 0.15972, 0, 0.63663], - "85": [0, 0.68611, 0.11424, 0, 0.80027], - "86": [0, 0.68611, 0.25555, 0, 0.67778], - "87": [0, 0.68611, 0.15972, 0, 1.09305], - "88": [0, 0.68611, 0.07778, 0, 0.94722], - "89": [0, 0.68611, 0.25555, 0, 0.67458], - "90": [0, 0.68611, 0.06979, 0, 0.77257], - "97": [0, 0.44444, 0, 0, 0.63287], - "98": [0, 0.69444, 0, 0, 0.52083], - "99": [0, 0.44444, 0, 0, 0.51342], - "100": [0, 0.69444, 0, 0, 0.60972], - "101": [0, 0.44444, 0, 0, 0.55361], - "102": [0.19444, 0.69444, 0.11042, 0, 0.56806], - "103": [0.19444, 0.44444, 0.03704, 0, 0.5449], - "104": [0, 0.69444, 0, 0, 0.66759], - "105": [0, 0.69326, 0, 0, 0.4048], - "106": [0.19444, 0.69326, 0.0622, 0, 0.47083], - "107": [0, 0.69444, 0.01852, 0, 0.6037], - "108": [0, 0.69444, 0.0088, 0, 0.34815], - "109": [0, 0.44444, 0, 0, 1.0324], - "110": [0, 0.44444, 0, 0, 0.71296], - "111": [0, 0.44444, 0, 0, 0.58472], - "112": [0.19444, 0.44444, 0, 0, 0.60092], - "113": [0.19444, 0.44444, 0.03704, 0, 0.54213], - "114": [0, 0.44444, 0.03194, 0, 0.5287], - "115": [0, 0.44444, 0, 0, 0.53125], - "116": [0, 0.63492, 0, 0, 0.41528], - "117": [0, 0.44444, 0, 0, 0.68102], - "118": [0, 0.44444, 0.03704, 0, 0.56666], - "119": [0, 0.44444, 0.02778, 0, 0.83148], - "120": [0, 0.44444, 0, 0, 0.65903], - "121": [0.19444, 0.44444, 0.03704, 0, 0.59028], - "122": [0, 0.44444, 0.04213, 0, 0.55509], - "160": [0, 0, 0, 0, 0.25], - "915": [0, 0.68611, 0.15972, 0, 0.65694], - "916": [0, 0.68611, 0, 0, 0.95833], - "920": [0, 0.68611, 0.03194, 0, 0.86722], - "923": [0, 0.68611, 0, 0, 0.80555], - "926": [0, 0.68611, 0.07458, 0, 0.84125], - "928": [0, 0.68611, 0.08229, 0, 0.98229], - "931": [0, 0.68611, 0.05451, 0, 0.88507], - "933": [0, 0.68611, 0.15972, 0, 0.67083], - "934": [0, 0.68611, 0, 0, 0.76666], - "936": [0, 0.68611, 0.11653, 0, 0.71402], - "937": [0, 0.68611, 0.04835, 0, 0.8789], - "945": [0, 0.44444, 0, 0, 0.76064], - "946": [0.19444, 0.69444, 0.03403, 0, 0.65972], - "947": [0.19444, 0.44444, 0.06389, 0, 0.59003], - "948": [0, 0.69444, 0.03819, 0, 0.52222], - "949": [0, 0.44444, 0, 0, 0.52882], - "950": [0.19444, 0.69444, 0.06215, 0, 0.50833], - "951": [0.19444, 0.44444, 0.03704, 0, 0.6], - "952": [0, 0.69444, 0.03194, 0, 0.5618], - "953": [0, 0.44444, 0, 0, 0.41204], - "954": [0, 0.44444, 0, 0, 0.66759], - "955": [0, 0.69444, 0, 0, 0.67083], - "956": [0.19444, 0.44444, 0, 0, 0.70787], - "957": [0, 0.44444, 0.06898, 0, 0.57685], - "958": [0.19444, 0.69444, 0.03021, 0, 0.50833], - "959": [0, 0.44444, 0, 0, 0.58472], - "960": [0, 0.44444, 0.03704, 0, 0.68241], - "961": [0.19444, 0.44444, 0, 0, 0.6118], - "962": [0.09722, 0.44444, 0.07917, 0, 0.42361], - "963": [0, 0.44444, 0.03704, 0, 0.68588], - "964": [0, 0.44444, 0.13472, 0, 0.52083], - "965": [0, 0.44444, 0.03704, 0, 0.63055], - "966": [0.19444, 0.44444, 0, 0, 0.74722], - "967": [0.19444, 0.44444, 0, 0, 0.71805], - "968": [0.19444, 0.69444, 0.03704, 0, 0.75833], - "969": [0, 0.44444, 0.03704, 0, 0.71782], - "977": [0, 0.69444, 0, 0, 0.69155], - "981": [0.19444, 0.69444, 0, 0, 0.7125], - "982": [0, 0.44444, 0.03194, 0, 0.975], - "1009": [0.19444, 0.44444, 0, 0, 0.6118], - "1013": [0, 0.44444, 0, 0, 0.48333], - "57649": [0, 0.44444, 0, 0, 0.39352], - "57911": [0.19444, 0.44444, 0, 0, 0.43889] - }, - "Math-Italic": { - "32": [0, 0, 0, 0, 0.25], - "48": [0, 0.43056, 0, 0, 0.5], - "49": [0, 0.43056, 0, 0, 0.5], - "50": [0, 0.43056, 0, 0, 0.5], - "51": [0.19444, 0.43056, 0, 0, 0.5], - "52": [0.19444, 0.43056, 0, 0, 0.5], - "53": [0.19444, 0.43056, 0, 0, 0.5], - "54": [0, 0.64444, 0, 0, 0.5], - "55": [0.19444, 0.43056, 0, 0, 0.5], - "56": [0, 0.64444, 0, 0, 0.5], - "57": [0.19444, 0.43056, 0, 0, 0.5], - "65": [0, 0.68333, 0, 0.13889, 0.75], - "66": [0, 0.68333, 0.05017, 0.08334, 0.75851], - "67": [0, 0.68333, 0.07153, 0.08334, 0.71472], - "68": [0, 0.68333, 0.02778, 0.05556, 0.82792], - "69": [0, 0.68333, 0.05764, 0.08334, 0.7382], - "70": [0, 0.68333, 0.13889, 0.08334, 0.64306], - "71": [0, 0.68333, 0, 0.08334, 0.78625], - "72": [0, 0.68333, 0.08125, 0.05556, 0.83125], - "73": [0, 0.68333, 0.07847, 0.11111, 0.43958], - "74": [0, 0.68333, 0.09618, 0.16667, 0.55451], - "75": [0, 0.68333, 0.07153, 0.05556, 0.84931], - "76": [0, 0.68333, 0, 0.02778, 0.68056], - "77": [0, 0.68333, 0.10903, 0.08334, 0.97014], - "78": [0, 0.68333, 0.10903, 0.08334, 0.80347], - "79": [0, 0.68333, 0.02778, 0.08334, 0.76278], - "80": [0, 0.68333, 0.13889, 0.08334, 0.64201], - "81": [0.19444, 0.68333, 0, 0.08334, 0.79056], - "82": [0, 0.68333, 0.00773, 0.08334, 0.75929], - "83": [0, 0.68333, 0.05764, 0.08334, 0.6132], - "84": [0, 0.68333, 0.13889, 0.08334, 0.58438], - "85": [0, 0.68333, 0.10903, 0.02778, 0.68278], - "86": [0, 0.68333, 0.22222, 0, 0.58333], - "87": [0, 0.68333, 0.13889, 0, 0.94445], - "88": [0, 0.68333, 0.07847, 0.08334, 0.82847], - "89": [0, 0.68333, 0.22222, 0, 0.58056], - "90": [0, 0.68333, 0.07153, 0.08334, 0.68264], - "97": [0, 0.43056, 0, 0, 0.52859], - "98": [0, 0.69444, 0, 0, 0.42917], - "99": [0, 0.43056, 0, 0.05556, 0.43276], - "100": [0, 0.69444, 0, 0.16667, 0.52049], - "101": [0, 0.43056, 0, 0.05556, 0.46563], - "102": [0.19444, 0.69444, 0.10764, 0.16667, 0.48959], - "103": [0.19444, 0.43056, 0.03588, 0.02778, 0.47697], - "104": [0, 0.69444, 0, 0, 0.57616], - "105": [0, 0.65952, 0, 0, 0.34451], - "106": [0.19444, 0.65952, 0.05724, 0, 0.41181], - "107": [0, 0.69444, 0.03148, 0, 0.5206], - "108": [0, 0.69444, 0.01968, 0.08334, 0.29838], - "109": [0, 0.43056, 0, 0, 0.87801], - "110": [0, 0.43056, 0, 0, 0.60023], - "111": [0, 0.43056, 0, 0.05556, 0.48472], - "112": [0.19444, 0.43056, 0, 0.08334, 0.50313], - "113": [0.19444, 0.43056, 0.03588, 0.08334, 0.44641], - "114": [0, 0.43056, 0.02778, 0.05556, 0.45116], - "115": [0, 0.43056, 0, 0.05556, 0.46875], - "116": [0, 0.61508, 0, 0.08334, 0.36111], - "117": [0, 0.43056, 0, 0.02778, 0.57246], - "118": [0, 0.43056, 0.03588, 0.02778, 0.48472], - "119": [0, 0.43056, 0.02691, 0.08334, 0.71592], - "120": [0, 0.43056, 0, 0.02778, 0.57153], - "121": [0.19444, 0.43056, 0.03588, 0.05556, 0.49028], - "122": [0, 0.43056, 0.04398, 0.05556, 0.46505], - "160": [0, 0, 0, 0, 0.25], - "915": [0, 0.68333, 0.13889, 0.08334, 0.61528], - "916": [0, 0.68333, 0, 0.16667, 0.83334], - "920": [0, 0.68333, 0.02778, 0.08334, 0.76278], - "923": [0, 0.68333, 0, 0.16667, 0.69445], - "926": [0, 0.68333, 0.07569, 0.08334, 0.74236], - "928": [0, 0.68333, 0.08125, 0.05556, 0.83125], - "931": [0, 0.68333, 0.05764, 0.08334, 0.77986], - "933": [0, 0.68333, 0.13889, 0.05556, 0.58333], - "934": [0, 0.68333, 0, 0.08334, 0.66667], - "936": [0, 0.68333, 0.11, 0.05556, 0.61222], - "937": [0, 0.68333, 0.05017, 0.08334, 0.7724], - "945": [0, 0.43056, 0.0037, 0.02778, 0.6397], - "946": [0.19444, 0.69444, 0.05278, 0.08334, 0.56563], - "947": [0.19444, 0.43056, 0.05556, 0, 0.51773], - "948": [0, 0.69444, 0.03785, 0.05556, 0.44444], - "949": [0, 0.43056, 0, 0.08334, 0.46632], - "950": [0.19444, 0.69444, 0.07378, 0.08334, 0.4375], - "951": [0.19444, 0.43056, 0.03588, 0.05556, 0.49653], - "952": [0, 0.69444, 0.02778, 0.08334, 0.46944], - "953": [0, 0.43056, 0, 0.05556, 0.35394], - "954": [0, 0.43056, 0, 0, 0.57616], - "955": [0, 0.69444, 0, 0, 0.58334], - "956": [0.19444, 0.43056, 0, 0.02778, 0.60255], - "957": [0, 0.43056, 0.06366, 0.02778, 0.49398], - "958": [0.19444, 0.69444, 0.04601, 0.11111, 0.4375], - "959": [0, 0.43056, 0, 0.05556, 0.48472], - "960": [0, 0.43056, 0.03588, 0, 0.57003], - "961": [0.19444, 0.43056, 0, 0.08334, 0.51702], - "962": [0.09722, 0.43056, 0.07986, 0.08334, 0.36285], - "963": [0, 0.43056, 0.03588, 0, 0.57141], - "964": [0, 0.43056, 0.1132, 0.02778, 0.43715], - "965": [0, 0.43056, 0.03588, 0.02778, 0.54028], - "966": [0.19444, 0.43056, 0, 0.08334, 0.65417], - "967": [0.19444, 0.43056, 0, 0.05556, 0.62569], - "968": [0.19444, 0.69444, 0.03588, 0.11111, 0.65139], - "969": [0, 0.43056, 0.03588, 0, 0.62245], - "977": [0, 0.69444, 0, 0.08334, 0.59144], - "981": [0.19444, 0.69444, 0, 0.08334, 0.59583], - "982": [0, 0.43056, 0.02778, 0, 0.82813], - "1009": [0.19444, 0.43056, 0, 0.08334, 0.51702], - "1013": [0, 0.43056, 0, 0.05556, 0.4059], - "57649": [0, 0.43056, 0, 0.02778, 0.32246], - "57911": [0.19444, 0.43056, 0, 0.08334, 0.38403] - }, - "SansSerif-Bold": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0, 0, 0.36667], - "34": [0, 0.69444, 0, 0, 0.55834], - "35": [0.19444, 0.69444, 0, 0, 0.91667], - "36": [0.05556, 0.75, 0, 0, 0.55], - "37": [0.05556, 0.75, 0, 0, 1.02912], - "38": [0, 0.69444, 0, 0, 0.83056], - "39": [0, 0.69444, 0, 0, 0.30556], - "40": [0.25, 0.75, 0, 0, 0.42778], - "41": [0.25, 0.75, 0, 0, 0.42778], - "42": [0, 0.75, 0, 0, 0.55], - "43": [0.11667, 0.61667, 0, 0, 0.85556], - "44": [0.10556, 0.13056, 0, 0, 0.30556], - "45": [0, 0.45833, 0, 0, 0.36667], - "46": [0, 0.13056, 0, 0, 0.30556], - "47": [0.25, 0.75, 0, 0, 0.55], - "48": [0, 0.69444, 0, 0, 0.55], - "49": [0, 0.69444, 0, 0, 0.55], - "50": [0, 0.69444, 0, 0, 0.55], - "51": [0, 0.69444, 0, 0, 0.55], - "52": [0, 0.69444, 0, 0, 0.55], - "53": [0, 0.69444, 0, 0, 0.55], - "54": [0, 0.69444, 0, 0, 0.55], - "55": [0, 0.69444, 0, 0, 0.55], - "56": [0, 0.69444, 0, 0, 0.55], - "57": [0, 0.69444, 0, 0, 0.55], - "58": [0, 0.45833, 0, 0, 0.30556], - "59": [0.10556, 0.45833, 0, 0, 0.30556], - "61": [-0.09375, 0.40625, 0, 0, 0.85556], - "63": [0, 0.69444, 0, 0, 0.51945], - "64": [0, 0.69444, 0, 0, 0.73334], - "65": [0, 0.69444, 0, 0, 0.73334], - "66": [0, 0.69444, 0, 0, 0.73334], - "67": [0, 0.69444, 0, 0, 0.70278], - "68": [0, 0.69444, 0, 0, 0.79445], - "69": [0, 0.69444, 0, 0, 0.64167], - "70": [0, 0.69444, 0, 0, 0.61111], - "71": [0, 0.69444, 0, 0, 0.73334], - "72": [0, 0.69444, 0, 0, 0.79445], - "73": [0, 0.69444, 0, 0, 0.33056], - "74": [0, 0.69444, 0, 0, 0.51945], - "75": [0, 0.69444, 0, 0, 0.76389], - "76": [0, 0.69444, 0, 0, 0.58056], - "77": [0, 0.69444, 0, 0, 0.97778], - "78": [0, 0.69444, 0, 0, 0.79445], - "79": [0, 0.69444, 0, 0, 0.79445], - "80": [0, 0.69444, 0, 0, 0.70278], - "81": [0.10556, 0.69444, 0, 0, 0.79445], - "82": [0, 0.69444, 0, 0, 0.70278], - "83": [0, 0.69444, 0, 0, 0.61111], - "84": [0, 0.69444, 0, 0, 0.73334], - "85": [0, 0.69444, 0, 0, 0.76389], - "86": [0, 0.69444, 0.01528, 0, 0.73334], - "87": [0, 0.69444, 0.01528, 0, 1.03889], - "88": [0, 0.69444, 0, 0, 0.73334], - "89": [0, 0.69444, 0.0275, 0, 0.73334], - "90": [0, 0.69444, 0, 0, 0.67223], - "91": [0.25, 0.75, 0, 0, 0.34306], - "93": [0.25, 0.75, 0, 0, 0.34306], - "94": [0, 0.69444, 0, 0, 0.55], - "95": [0.35, 0.10833, 0.03056, 0, 0.55], - "97": [0, 0.45833, 0, 0, 0.525], - "98": [0, 0.69444, 0, 0, 0.56111], - "99": [0, 0.45833, 0, 0, 0.48889], - "100": [0, 0.69444, 0, 0, 0.56111], - "101": [0, 0.45833, 0, 0, 0.51111], - "102": [0, 0.69444, 0.07639, 0, 0.33611], - "103": [0.19444, 0.45833, 0.01528, 0, 0.55], - "104": [0, 0.69444, 0, 0, 0.56111], - "105": [0, 0.69444, 0, 0, 0.25556], - "106": [0.19444, 0.69444, 0, 0, 0.28611], - "107": [0, 0.69444, 0, 0, 0.53056], - "108": [0, 0.69444, 0, 0, 0.25556], - "109": [0, 0.45833, 0, 0, 0.86667], - "110": [0, 0.45833, 0, 0, 0.56111], - "111": [0, 0.45833, 0, 0, 0.55], - "112": [0.19444, 0.45833, 0, 0, 0.56111], - "113": [0.19444, 0.45833, 0, 0, 0.56111], - "114": [0, 0.45833, 0.01528, 0, 0.37222], - "115": [0, 0.45833, 0, 0, 0.42167], - "116": [0, 0.58929, 0, 0, 0.40417], - "117": [0, 0.45833, 0, 0, 0.56111], - "118": [0, 0.45833, 0.01528, 0, 0.5], - "119": [0, 0.45833, 0.01528, 0, 0.74445], - "120": [0, 0.45833, 0, 0, 0.5], - "121": [0.19444, 0.45833, 0.01528, 0, 0.5], - "122": [0, 0.45833, 0, 0, 0.47639], - "126": [0.35, 0.34444, 0, 0, 0.55], - "160": [0, 0, 0, 0, 0.25], - "168": [0, 0.69444, 0, 0, 0.55], - "176": [0, 0.69444, 0, 0, 0.73334], - "180": [0, 0.69444, 0, 0, 0.55], - "184": [0.17014, 0, 0, 0, 0.48889], - "305": [0, 0.45833, 0, 0, 0.25556], - "567": [0.19444, 0.45833, 0, 0, 0.28611], - "710": [0, 0.69444, 0, 0, 0.55], - "711": [0, 0.63542, 0, 0, 0.55], - "713": [0, 0.63778, 0, 0, 0.55], - "728": [0, 0.69444, 0, 0, 0.55], - "729": [0, 0.69444, 0, 0, 0.30556], - "730": [0, 0.69444, 0, 0, 0.73334], - "732": [0, 0.69444, 0, 0, 0.55], - "733": [0, 0.69444, 0, 0, 0.55], - "915": [0, 0.69444, 0, 0, 0.58056], - "916": [0, 0.69444, 0, 0, 0.91667], - "920": [0, 0.69444, 0, 0, 0.85556], - "923": [0, 0.69444, 0, 0, 0.67223], - "926": [0, 0.69444, 0, 0, 0.73334], - "928": [0, 0.69444, 0, 0, 0.79445], - "931": [0, 0.69444, 0, 0, 0.79445], - "933": [0, 0.69444, 0, 0, 0.85556], - "934": [0, 0.69444, 0, 0, 0.79445], - "936": [0, 0.69444, 0, 0, 0.85556], - "937": [0, 0.69444, 0, 0, 0.79445], - "8211": [0, 0.45833, 0.03056, 0, 0.55], - "8212": [0, 0.45833, 0.03056, 0, 1.10001], - "8216": [0, 0.69444, 0, 0, 0.30556], - "8217": [0, 0.69444, 0, 0, 0.30556], - "8220": [0, 0.69444, 0, 0, 0.55834], - "8221": [0, 0.69444, 0, 0, 0.55834] - }, - "SansSerif-Italic": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0.05733, 0, 0.31945], - "34": [0, 0.69444, 0.00316, 0, 0.5], - "35": [0.19444, 0.69444, 0.05087, 0, 0.83334], - "36": [0.05556, 0.75, 0.11156, 0, 0.5], - "37": [0.05556, 0.75, 0.03126, 0, 0.83334], - "38": [0, 0.69444, 0.03058, 0, 0.75834], - "39": [0, 0.69444, 0.07816, 0, 0.27778], - "40": [0.25, 0.75, 0.13164, 0, 0.38889], - "41": [0.25, 0.75, 0.02536, 0, 0.38889], - "42": [0, 0.75, 0.11775, 0, 0.5], - "43": [0.08333, 0.58333, 0.02536, 0, 0.77778], - "44": [0.125, 0.08333, 0, 0, 0.27778], - "45": [0, 0.44444, 0.01946, 0, 0.33333], - "46": [0, 0.08333, 0, 0, 0.27778], - "47": [0.25, 0.75, 0.13164, 0, 0.5], - "48": [0, 0.65556, 0.11156, 0, 0.5], - "49": [0, 0.65556, 0.11156, 0, 0.5], - "50": [0, 0.65556, 0.11156, 0, 0.5], - "51": [0, 0.65556, 0.11156, 0, 0.5], - "52": [0, 0.65556, 0.11156, 0, 0.5], - "53": [0, 0.65556, 0.11156, 0, 0.5], - "54": [0, 0.65556, 0.11156, 0, 0.5], - "55": [0, 0.65556, 0.11156, 0, 0.5], - "56": [0, 0.65556, 0.11156, 0, 0.5], - "57": [0, 0.65556, 0.11156, 0, 0.5], - "58": [0, 0.44444, 0.02502, 0, 0.27778], - "59": [0.125, 0.44444, 0.02502, 0, 0.27778], - "61": [-0.13, 0.37, 0.05087, 0, 0.77778], - "63": [0, 0.69444, 0.11809, 0, 0.47222], - "64": [0, 0.69444, 0.07555, 0, 0.66667], - "65": [0, 0.69444, 0, 0, 0.66667], - "66": [0, 0.69444, 0.08293, 0, 0.66667], - "67": [0, 0.69444, 0.11983, 0, 0.63889], - "68": [0, 0.69444, 0.07555, 0, 0.72223], - "69": [0, 0.69444, 0.11983, 0, 0.59722], - "70": [0, 0.69444, 0.13372, 0, 0.56945], - "71": [0, 0.69444, 0.11983, 0, 0.66667], - "72": [0, 0.69444, 0.08094, 0, 0.70834], - "73": [0, 0.69444, 0.13372, 0, 0.27778], - "74": [0, 0.69444, 0.08094, 0, 0.47222], - "75": [0, 0.69444, 0.11983, 0, 0.69445], - "76": [0, 0.69444, 0, 0, 0.54167], - "77": [0, 0.69444, 0.08094, 0, 0.875], - "78": [0, 0.69444, 0.08094, 0, 0.70834], - "79": [0, 0.69444, 0.07555, 0, 0.73611], - "80": [0, 0.69444, 0.08293, 0, 0.63889], - "81": [0.125, 0.69444, 0.07555, 0, 0.73611], - "82": [0, 0.69444, 0.08293, 0, 0.64584], - "83": [0, 0.69444, 0.09205, 0, 0.55556], - "84": [0, 0.69444, 0.13372, 0, 0.68056], - "85": [0, 0.69444, 0.08094, 0, 0.6875], - "86": [0, 0.69444, 0.1615, 0, 0.66667], - "87": [0, 0.69444, 0.1615, 0, 0.94445], - "88": [0, 0.69444, 0.13372, 0, 0.66667], - "89": [0, 0.69444, 0.17261, 0, 0.66667], - "90": [0, 0.69444, 0.11983, 0, 0.61111], - "91": [0.25, 0.75, 0.15942, 0, 0.28889], - "93": [0.25, 0.75, 0.08719, 0, 0.28889], - "94": [0, 0.69444, 0.0799, 0, 0.5], - "95": [0.35, 0.09444, 0.08616, 0, 0.5], - "97": [0, 0.44444, 0.00981, 0, 0.48056], - "98": [0, 0.69444, 0.03057, 0, 0.51667], - "99": [0, 0.44444, 0.08336, 0, 0.44445], - "100": [0, 0.69444, 0.09483, 0, 0.51667], - "101": [0, 0.44444, 0.06778, 0, 0.44445], - "102": [0, 0.69444, 0.21705, 0, 0.30556], - "103": [0.19444, 0.44444, 0.10836, 0, 0.5], - "104": [0, 0.69444, 0.01778, 0, 0.51667], - "105": [0, 0.67937, 0.09718, 0, 0.23889], - "106": [0.19444, 0.67937, 0.09162, 0, 0.26667], - "107": [0, 0.69444, 0.08336, 0, 0.48889], - "108": [0, 0.69444, 0.09483, 0, 0.23889], - "109": [0, 0.44444, 0.01778, 0, 0.79445], - "110": [0, 0.44444, 0.01778, 0, 0.51667], - "111": [0, 0.44444, 0.06613, 0, 0.5], - "112": [0.19444, 0.44444, 0.0389, 0, 0.51667], - "113": [0.19444, 0.44444, 0.04169, 0, 0.51667], - "114": [0, 0.44444, 0.10836, 0, 0.34167], - "115": [0, 0.44444, 0.0778, 0, 0.38333], - "116": [0, 0.57143, 0.07225, 0, 0.36111], - "117": [0, 0.44444, 0.04169, 0, 0.51667], - "118": [0, 0.44444, 0.10836, 0, 0.46111], - "119": [0, 0.44444, 0.10836, 0, 0.68334], - "120": [0, 0.44444, 0.09169, 0, 0.46111], - "121": [0.19444, 0.44444, 0.10836, 0, 0.46111], - "122": [0, 0.44444, 0.08752, 0, 0.43472], - "126": [0.35, 0.32659, 0.08826, 0, 0.5], - "160": [0, 0, 0, 0, 0.25], - "168": [0, 0.67937, 0.06385, 0, 0.5], - "176": [0, 0.69444, 0, 0, 0.73752], - "184": [0.17014, 0, 0, 0, 0.44445], - "305": [0, 0.44444, 0.04169, 0, 0.23889], - "567": [0.19444, 0.44444, 0.04169, 0, 0.26667], - "710": [0, 0.69444, 0.0799, 0, 0.5], - "711": [0, 0.63194, 0.08432, 0, 0.5], - "713": [0, 0.60889, 0.08776, 0, 0.5], - "714": [0, 0.69444, 0.09205, 0, 0.5], - "715": [0, 0.69444, 0, 0, 0.5], - "728": [0, 0.69444, 0.09483, 0, 0.5], - "729": [0, 0.67937, 0.07774, 0, 0.27778], - "730": [0, 0.69444, 0, 0, 0.73752], - "732": [0, 0.67659, 0.08826, 0, 0.5], - "733": [0, 0.69444, 0.09205, 0, 0.5], - "915": [0, 0.69444, 0.13372, 0, 0.54167], - "916": [0, 0.69444, 0, 0, 0.83334], - "920": [0, 0.69444, 0.07555, 0, 0.77778], - "923": [0, 0.69444, 0, 0, 0.61111], - "926": [0, 0.69444, 0.12816, 0, 0.66667], - "928": [0, 0.69444, 0.08094, 0, 0.70834], - "931": [0, 0.69444, 0.11983, 0, 0.72222], - "933": [0, 0.69444, 0.09031, 0, 0.77778], - "934": [0, 0.69444, 0.04603, 0, 0.72222], - "936": [0, 0.69444, 0.09031, 0, 0.77778], - "937": [0, 0.69444, 0.08293, 0, 0.72222], - "8211": [0, 0.44444, 0.08616, 0, 0.5], - "8212": [0, 0.44444, 0.08616, 0, 1.0], - "8216": [0, 0.69444, 0.07816, 0, 0.27778], - "8217": [0, 0.69444, 0.07816, 0, 0.27778], - "8220": [0, 0.69444, 0.14205, 0, 0.5], - "8221": [0, 0.69444, 0.00316, 0, 0.5] - }, - "SansSerif-Regular": { - "32": [0, 0, 0, 0, 0.25], - "33": [0, 0.69444, 0, 0, 0.31945], - "34": [0, 0.69444, 0, 0, 0.5], - "35": [0.19444, 0.69444, 0, 0, 0.83334], - "36": [0.05556, 0.75, 0, 0, 0.5], - "37": [0.05556, 0.75, 0, 0, 0.83334], - "38": [0, 0.69444, 0, 0, 0.75834], - "39": [0, 0.69444, 0, 0, 0.27778], - "40": [0.25, 0.75, 0, 0, 0.38889], - "41": [0.25, 0.75, 0, 0, 0.38889], - "42": [0, 0.75, 0, 0, 0.5], - "43": [0.08333, 0.58333, 0, 0, 0.77778], - "44": [0.125, 0.08333, 0, 0, 0.27778], - "45": [0, 0.44444, 0, 0, 0.33333], - "46": [0, 0.08333, 0, 0, 0.27778], - "47": [0.25, 0.75, 0, 0, 0.5], - "48": [0, 0.65556, 0, 0, 0.5], - "49": [0, 0.65556, 0, 0, 0.5], - "50": [0, 0.65556, 0, 0, 0.5], - "51": [0, 0.65556, 0, 0, 0.5], - "52": [0, 0.65556, 0, 0, 0.5], - "53": [0, 0.65556, 0, 0, 0.5], - "54": [0, 0.65556, 0, 0, 0.5], - "55": [0, 0.65556, 0, 0, 0.5], - "56": [0, 0.65556, 0, 0, 0.5], - "57": [0, 0.65556, 0, 0, 0.5], - "58": [0, 0.44444, 0, 0, 0.27778], - "59": [0.125, 0.44444, 0, 0, 0.27778], - "61": [-0.13, 0.37, 0, 0, 0.77778], - "63": [0, 0.69444, 0, 0, 0.47222], - "64": [0, 0.69444, 0, 0, 0.66667], - "65": [0, 0.69444, 0, 0, 0.66667], - "66": [0, 0.69444, 0, 0, 0.66667], - "67": [0, 0.69444, 0, 0, 0.63889], - "68": [0, 0.69444, 0, 0, 0.72223], - "69": [0, 0.69444, 0, 0, 0.59722], - "70": [0, 0.69444, 0, 0, 0.56945], - "71": [0, 0.69444, 0, 0, 0.66667], - "72": [0, 0.69444, 0, 0, 0.70834], - "73": [0, 0.69444, 0, 0, 0.27778], - "74": [0, 0.69444, 0, 0, 0.47222], - "75": [0, 0.69444, 0, 0, 0.69445], - "76": [0, 0.69444, 0, 0, 0.54167], - "77": [0, 0.69444, 0, 0, 0.875], - "78": [0, 0.69444, 0, 0, 0.70834], - "79": [0, 0.69444, 0, 0, 0.73611], - "80": [0, 0.69444, 0, 0, 0.63889], - "81": [0.125, 0.69444, 0, 0, 0.73611], - "82": [0, 0.69444, 0, 0, 0.64584], - "83": [0, 0.69444, 0, 0, 0.55556], - "84": [0, 0.69444, 0, 0, 0.68056], - "85": [0, 0.69444, 0, 0, 0.6875], - "86": [0, 0.69444, 0.01389, 0, 0.66667], - "87": [0, 0.69444, 0.01389, 0, 0.94445], - "88": [0, 0.69444, 0, 0, 0.66667], - "89": [0, 0.69444, 0.025, 0, 0.66667], - "90": [0, 0.69444, 0, 0, 0.61111], - "91": [0.25, 0.75, 0, 0, 0.28889], - "93": [0.25, 0.75, 0, 0, 0.28889], - "94": [0, 0.69444, 0, 0, 0.5], - "95": [0.35, 0.09444, 0.02778, 0, 0.5], - "97": [0, 0.44444, 0, 0, 0.48056], - "98": [0, 0.69444, 0, 0, 0.51667], - "99": [0, 0.44444, 0, 0, 0.44445], - "100": [0, 0.69444, 0, 0, 0.51667], - "101": [0, 0.44444, 0, 0, 0.44445], - "102": [0, 0.69444, 0.06944, 0, 0.30556], - "103": [0.19444, 0.44444, 0.01389, 0, 0.5], - "104": [0, 0.69444, 0, 0, 0.51667], - "105": [0, 0.67937, 0, 0, 0.23889], - "106": [0.19444, 0.67937, 0, 0, 0.26667], - "107": [0, 0.69444, 0, 0, 0.48889], - "108": [0, 0.69444, 0, 0, 0.23889], - "109": [0, 0.44444, 0, 0, 0.79445], - "110": [0, 0.44444, 0, 0, 0.51667], - "111": [0, 0.44444, 0, 0, 0.5], - "112": [0.19444, 0.44444, 0, 0, 0.51667], - "113": [0.19444, 0.44444, 0, 0, 0.51667], - "114": [0, 0.44444, 0.01389, 0, 0.34167], - "115": [0, 0.44444, 0, 0, 0.38333], - "116": [0, 0.57143, 0, 0, 0.36111], - "117": [0, 0.44444, 0, 0, 0.51667], - "118": [0, 0.44444, 0.01389, 0, 0.46111], - "119": [0, 0.44444, 0.01389, 0, 0.68334], - "120": [0, 0.44444, 0, 0, 0.46111], - "121": [0.19444, 0.44444, 0.01389, 0, 0.46111], - "122": [0, 0.44444, 0, 0, 0.43472], - "126": [0.35, 0.32659, 0, 0, 0.5], - "160": [0, 0, 0, 0, 0.25], - "168": [0, 0.67937, 0, 0, 0.5], - "176": [0, 0.69444, 0, 0, 0.66667], - "184": [0.17014, 0, 0, 0, 0.44445], - "305": [0, 0.44444, 0, 0, 0.23889], - "567": [0.19444, 0.44444, 0, 0, 0.26667], - "710": [0, 0.69444, 0, 0, 0.5], - "711": [0, 0.63194, 0, 0, 0.5], - "713": [0, 0.60889, 0, 0, 0.5], - "714": [0, 0.69444, 0, 0, 0.5], - "715": [0, 0.69444, 0, 0, 0.5], - "728": [0, 0.69444, 0, 0, 0.5], - "729": [0, 0.67937, 0, 0, 0.27778], - "730": [0, 0.69444, 0, 0, 0.66667], - "732": [0, 0.67659, 0, 0, 0.5], - "733": [0, 0.69444, 0, 0, 0.5], - "915": [0, 0.69444, 0, 0, 0.54167], - "916": [0, 0.69444, 0, 0, 0.83334], - "920": [0, 0.69444, 0, 0, 0.77778], - "923": [0, 0.69444, 0, 0, 0.61111], - "926": [0, 0.69444, 0, 0, 0.66667], - "928": [0, 0.69444, 0, 0, 0.70834], - "931": [0, 0.69444, 0, 0, 0.72222], - "933": [0, 0.69444, 0, 0, 0.77778], - "934": [0, 0.69444, 0, 0, 0.72222], - "936": [0, 0.69444, 0, 0, 0.77778], - "937": [0, 0.69444, 0, 0, 0.72222], - "8211": [0, 0.44444, 0.02778, 0, 0.5], - "8212": [0, 0.44444, 0.02778, 0, 1.0], - "8216": [0, 0.69444, 0, 0, 0.27778], - "8217": [0, 0.69444, 0, 0, 0.27778], - "8220": [0, 0.69444, 0, 0, 0.5], - "8221": [0, 0.69444, 0, 0, 0.5] - }, - "Script-Regular": { - "32": [0, 0, 0, 0, 0.25], - "65": [0, 0.7, 0.22925, 0, 0.80253], - "66": [0, 0.7, 0.04087, 0, 0.90757], - "67": [0, 0.7, 0.1689, 0, 0.66619], - "68": [0, 0.7, 0.09371, 0, 0.77443], - "69": [0, 0.7, 0.18583, 0, 0.56162], - "70": [0, 0.7, 0.13634, 0, 0.89544], - "71": [0, 0.7, 0.17322, 0, 0.60961], - "72": [0, 0.7, 0.29694, 0, 0.96919], - "73": [0, 0.7, 0.19189, 0, 0.80907], - "74": [0.27778, 0.7, 0.19189, 0, 1.05159], - "75": [0, 0.7, 0.31259, 0, 0.91364], - "76": [0, 0.7, 0.19189, 0, 0.87373], - "77": [0, 0.7, 0.15981, 0, 1.08031], - "78": [0, 0.7, 0.3525, 0, 0.9015], - "79": [0, 0.7, 0.08078, 0, 0.73787], - "80": [0, 0.7, 0.08078, 0, 1.01262], - "81": [0, 0.7, 0.03305, 0, 0.88282], - "82": [0, 0.7, 0.06259, 0, 0.85], - "83": [0, 0.7, 0.19189, 0, 0.86767], - "84": [0, 0.7, 0.29087, 0, 0.74697], - "85": [0, 0.7, 0.25815, 0, 0.79996], - "86": [0, 0.7, 0.27523, 0, 0.62204], - "87": [0, 0.7, 0.27523, 0, 0.80532], - "88": [0, 0.7, 0.26006, 0, 0.94445], - "89": [0, 0.7, 0.2939, 0, 0.70961], - "90": [0, 0.7, 0.24037, 0, 0.8212], - "160": [0, 0, 0, 0, 0.25] - }, - "Size1-Regular": { - "32": [0, 0, 0, 0, 0.25], - "40": [0.35001, 0.85, 0, 0, 0.45834], - "41": [0.35001, 0.85, 0, 0, 0.45834], - "47": [0.35001, 0.85, 0, 0, 0.57778], - "91": [0.35001, 0.85, 0, 0, 0.41667], - "92": [0.35001, 0.85, 0, 0, 0.57778], - "93": [0.35001, 0.85, 0, 0, 0.41667], - "123": [0.35001, 0.85, 0, 0, 0.58334], - "125": [0.35001, 0.85, 0, 0, 0.58334], - "160": [0, 0, 0, 0, 0.25], - "710": [0, 0.72222, 0, 0, 0.55556], - "732": [0, 0.72222, 0, 0, 0.55556], - "770": [0, 0.72222, 0, 0, 0.55556], - "771": [0, 0.72222, 0, 0, 0.55556], - "8214": [-0.00099, 0.601, 0, 0, 0.77778], - "8593": [1e-05, 0.6, 0, 0, 0.66667], - "8595": [1e-05, 0.6, 0, 0, 0.66667], - "8657": [1e-05, 0.6, 0, 0, 0.77778], - "8659": [1e-05, 0.6, 0, 0, 0.77778], - "8719": [0.25001, 0.75, 0, 0, 0.94445], - "8720": [0.25001, 0.75, 0, 0, 0.94445], - "8721": [0.25001, 0.75, 0, 0, 1.05556], - "8730": [0.35001, 0.85, 0, 0, 1.0], - "8739": [-0.00599, 0.606, 0, 0, 0.33333], - "8741": [-0.00599, 0.606, 0, 0, 0.55556], - "8747": [0.30612, 0.805, 0.19445, 0, 0.47222], - "8748": [0.306, 0.805, 0.19445, 0, 0.47222], - "8749": [0.306, 0.805, 0.19445, 0, 0.47222], - "8750": [0.30612, 0.805, 0.19445, 0, 0.47222], - "8896": [0.25001, 0.75, 0, 0, 0.83334], - "8897": [0.25001, 0.75, 0, 0, 0.83334], - "8898": [0.25001, 0.75, 0, 0, 0.83334], - "8899": [0.25001, 0.75, 0, 0, 0.83334], - "8968": [0.35001, 0.85, 0, 0, 0.47222], - "8969": [0.35001, 0.85, 0, 0, 0.47222], - "8970": [0.35001, 0.85, 0, 0, 0.47222], - "8971": [0.35001, 0.85, 0, 0, 0.47222], - "9168": [-0.00099, 0.601, 0, 0, 0.66667], - "10216": [0.35001, 0.85, 0, 0, 0.47222], - "10217": [0.35001, 0.85, 0, 0, 0.47222], - "10752": [0.25001, 0.75, 0, 0, 1.11111], - "10753": [0.25001, 0.75, 0, 0, 1.11111], - "10754": [0.25001, 0.75, 0, 0, 1.11111], - "10756": [0.25001, 0.75, 0, 0, 0.83334], - "10758": [0.25001, 0.75, 0, 0, 0.83334] - }, - "Size2-Regular": { - "32": [0, 0, 0, 0, 0.25], - "40": [0.65002, 1.15, 0, 0, 0.59722], - "41": [0.65002, 1.15, 0, 0, 0.59722], - "47": [0.65002, 1.15, 0, 0, 0.81111], - "91": [0.65002, 1.15, 0, 0, 0.47222], - "92": [0.65002, 1.15, 0, 0, 0.81111], - "93": [0.65002, 1.15, 0, 0, 0.47222], - "123": [0.65002, 1.15, 0, 0, 0.66667], - "125": [0.65002, 1.15, 0, 0, 0.66667], - "160": [0, 0, 0, 0, 0.25], - "710": [0, 0.75, 0, 0, 1.0], - "732": [0, 0.75, 0, 0, 1.0], - "770": [0, 0.75, 0, 0, 1.0], - "771": [0, 0.75, 0, 0, 1.0], - "8719": [0.55001, 1.05, 0, 0, 1.27778], - "8720": [0.55001, 1.05, 0, 0, 1.27778], - "8721": [0.55001, 1.05, 0, 0, 1.44445], - "8730": [0.65002, 1.15, 0, 0, 1.0], - "8747": [0.86225, 1.36, 0.44445, 0, 0.55556], - "8748": [0.862, 1.36, 0.44445, 0, 0.55556], - "8749": [0.862, 1.36, 0.44445, 0, 0.55556], - "8750": [0.86225, 1.36, 0.44445, 0, 0.55556], - "8896": [0.55001, 1.05, 0, 0, 1.11111], - "8897": [0.55001, 1.05, 0, 0, 1.11111], - "8898": [0.55001, 1.05, 0, 0, 1.11111], - "8899": [0.55001, 1.05, 0, 0, 1.11111], - "8968": [0.65002, 1.15, 0, 0, 0.52778], - "8969": [0.65002, 1.15, 0, 0, 0.52778], - "8970": [0.65002, 1.15, 0, 0, 0.52778], - "8971": [0.65002, 1.15, 0, 0, 0.52778], - "10216": [0.65002, 1.15, 0, 0, 0.61111], - "10217": [0.65002, 1.15, 0, 0, 0.61111], - "10752": [0.55001, 1.05, 0, 0, 1.51112], - "10753": [0.55001, 1.05, 0, 0, 1.51112], - "10754": [0.55001, 1.05, 0, 0, 1.51112], - "10756": [0.55001, 1.05, 0, 0, 1.11111], - "10758": [0.55001, 1.05, 0, 0, 1.11111] - }, - "Size3-Regular": { - "32": [0, 0, 0, 0, 0.25], - "40": [0.95003, 1.45, 0, 0, 0.73611], - "41": [0.95003, 1.45, 0, 0, 0.73611], - "47": [0.95003, 1.45, 0, 0, 1.04445], - "91": [0.95003, 1.45, 0, 0, 0.52778], - "92": [0.95003, 1.45, 0, 0, 1.04445], - "93": [0.95003, 1.45, 0, 0, 0.52778], - "123": [0.95003, 1.45, 0, 0, 0.75], - "125": [0.95003, 1.45, 0, 0, 0.75], - "160": [0, 0, 0, 0, 0.25], - "710": [0, 0.75, 0, 0, 1.44445], - "732": [0, 0.75, 0, 0, 1.44445], - "770": [0, 0.75, 0, 0, 1.44445], - "771": [0, 0.75, 0, 0, 1.44445], - "8730": [0.95003, 1.45, 0, 0, 1.0], - "8968": [0.95003, 1.45, 0, 0, 0.58334], - "8969": [0.95003, 1.45, 0, 0, 0.58334], - "8970": [0.95003, 1.45, 0, 0, 0.58334], - "8971": [0.95003, 1.45, 0, 0, 0.58334], - "10216": [0.95003, 1.45, 0, 0, 0.75], - "10217": [0.95003, 1.45, 0, 0, 0.75] - }, - "Size4-Regular": { - "32": [0, 0, 0, 0, 0.25], - "40": [1.25003, 1.75, 0, 0, 0.79167], - "41": [1.25003, 1.75, 0, 0, 0.79167], - "47": [1.25003, 1.75, 0, 0, 1.27778], - "91": [1.25003, 1.75, 0, 0, 0.58334], - "92": [1.25003, 1.75, 0, 0, 1.27778], - "93": [1.25003, 1.75, 0, 0, 0.58334], - "123": [1.25003, 1.75, 0, 0, 0.80556], - "125": [1.25003, 1.75, 0, 0, 0.80556], - "160": [0, 0, 0, 0, 0.25], - "710": [0, 0.825, 0, 0, 1.8889], - "732": [0, 0.825, 0, 0, 1.8889], - "770": [0, 0.825, 0, 0, 1.8889], - "771": [0, 0.825, 0, 0, 1.8889], - "8730": [1.25003, 1.75, 0, 0, 1.0], - "8968": [1.25003, 1.75, 0, 0, 0.63889], - "8969": [1.25003, 1.75, 0, 0, 0.63889], - "8970": [1.25003, 1.75, 0, 0, 0.63889], - "8971": [1.25003, 1.75, 0, 0, 0.63889], - "9115": [0.64502, 1.155, 0, 0, 0.875], - "9116": [1e-05, 0.6, 0, 0, 0.875], - "9117": [0.64502, 1.155, 0, 0, 0.875], - "9118": [0.64502, 1.155, 0, 0, 0.875], - "9119": [1e-05, 0.6, 0, 0, 0.875], - "9120": [0.64502, 1.155, 0, 0, 0.875], - "9121": [0.64502, 1.155, 0, 0, 0.66667], - "9122": [-0.00099, 0.601, 0, 0, 0.66667], - "9123": [0.64502, 1.155, 0, 0, 0.66667], - "9124": [0.64502, 1.155, 0, 0, 0.66667], - "9125": [-0.00099, 0.601, 0, 0, 0.66667], - "9126": [0.64502, 1.155, 0, 0, 0.66667], - "9127": [1e-05, 0.9, 0, 0, 0.88889], - "9128": [0.65002, 1.15, 0, 0, 0.88889], - "9129": [0.90001, 0, 0, 0, 0.88889], - "9130": [0, 0.3, 0, 0, 0.88889], - "9131": [1e-05, 0.9, 0, 0, 0.88889], - "9132": [0.65002, 1.15, 0, 0, 0.88889], - "9133": [0.90001, 0, 0, 0, 0.88889], - "9143": [0.88502, 0.915, 0, 0, 1.05556], - "10216": [1.25003, 1.75, 0, 0, 0.80556], - "10217": [1.25003, 1.75, 0, 0, 0.80556], - "57344": [-0.00499, 0.605, 0, 0, 1.05556], - "57345": [-0.00499, 0.605, 0, 0, 1.05556], - "57680": [0, 0.12, 0, 0, 0.45], - "57681": [0, 0.12, 0, 0, 0.45], - "57682": [0, 0.12, 0, 0, 0.45], - "57683": [0, 0.12, 0, 0, 0.45] - }, - "Typewriter-Regular": { - "32": [0, 0, 0, 0, 0.525], - "33": [0, 0.61111, 0, 0, 0.525], - "34": [0, 0.61111, 0, 0, 0.525], - "35": [0, 0.61111, 0, 0, 0.525], - "36": [0.08333, 0.69444, 0, 0, 0.525], - "37": [0.08333, 0.69444, 0, 0, 0.525], - "38": [0, 0.61111, 0, 0, 0.525], - "39": [0, 0.61111, 0, 0, 0.525], - "40": [0.08333, 0.69444, 0, 0, 0.525], - "41": [0.08333, 0.69444, 0, 0, 0.525], - "42": [0, 0.52083, 0, 0, 0.525], - "43": [-0.08056, 0.53055, 0, 0, 0.525], - "44": [0.13889, 0.125, 0, 0, 0.525], - "45": [-0.08056, 0.53055, 0, 0, 0.525], - "46": [0, 0.125, 0, 0, 0.525], - "47": [0.08333, 0.69444, 0, 0, 0.525], - "48": [0, 0.61111, 0, 0, 0.525], - "49": [0, 0.61111, 0, 0, 0.525], - "50": [0, 0.61111, 0, 0, 0.525], - "51": [0, 0.61111, 0, 0, 0.525], - "52": [0, 0.61111, 0, 0, 0.525], - "53": [0, 0.61111, 0, 0, 0.525], - "54": [0, 0.61111, 0, 0, 0.525], - "55": [0, 0.61111, 0, 0, 0.525], - "56": [0, 0.61111, 0, 0, 0.525], - "57": [0, 0.61111, 0, 0, 0.525], - "58": [0, 0.43056, 0, 0, 0.525], - "59": [0.13889, 0.43056, 0, 0, 0.525], - "60": [-0.05556, 0.55556, 0, 0, 0.525], - "61": [-0.19549, 0.41562, 0, 0, 0.525], - "62": [-0.05556, 0.55556, 0, 0, 0.525], - "63": [0, 0.61111, 0, 0, 0.525], - "64": [0, 0.61111, 0, 0, 0.525], - "65": [0, 0.61111, 0, 0, 0.525], - "66": [0, 0.61111, 0, 0, 0.525], - "67": [0, 0.61111, 0, 0, 0.525], - "68": [0, 0.61111, 0, 0, 0.525], - "69": [0, 0.61111, 0, 0, 0.525], - "70": [0, 0.61111, 0, 0, 0.525], - "71": [0, 0.61111, 0, 0, 0.525], - "72": [0, 0.61111, 0, 0, 0.525], - "73": [0, 0.61111, 0, 0, 0.525], - "74": [0, 0.61111, 0, 0, 0.525], - "75": [0, 0.61111, 0, 0, 0.525], - "76": [0, 0.61111, 0, 0, 0.525], - "77": [0, 0.61111, 0, 0, 0.525], - "78": [0, 0.61111, 0, 0, 0.525], - "79": [0, 0.61111, 0, 0, 0.525], - "80": [0, 0.61111, 0, 0, 0.525], - "81": [0.13889, 0.61111, 0, 0, 0.525], - "82": [0, 0.61111, 0, 0, 0.525], - "83": [0, 0.61111, 0, 0, 0.525], - "84": [0, 0.61111, 0, 0, 0.525], - "85": [0, 0.61111, 0, 0, 0.525], - "86": [0, 0.61111, 0, 0, 0.525], - "87": [0, 0.61111, 0, 0, 0.525], - "88": [0, 0.61111, 0, 0, 0.525], - "89": [0, 0.61111, 0, 0, 0.525], - "90": [0, 0.61111, 0, 0, 0.525], - "91": [0.08333, 0.69444, 0, 0, 0.525], - "92": [0.08333, 0.69444, 0, 0, 0.525], - "93": [0.08333, 0.69444, 0, 0, 0.525], - "94": [0, 0.61111, 0, 0, 0.525], - "95": [0.09514, 0, 0, 0, 0.525], - "96": [0, 0.61111, 0, 0, 0.525], - "97": [0, 0.43056, 0, 0, 0.525], - "98": [0, 0.61111, 0, 0, 0.525], - "99": [0, 0.43056, 0, 0, 0.525], - "100": [0, 0.61111, 0, 0, 0.525], - "101": [0, 0.43056, 0, 0, 0.525], - "102": [0, 0.61111, 0, 0, 0.525], - "103": [0.22222, 0.43056, 0, 0, 0.525], - "104": [0, 0.61111, 0, 0, 0.525], - "105": [0, 0.61111, 0, 0, 0.525], - "106": [0.22222, 0.61111, 0, 0, 0.525], - "107": [0, 0.61111, 0, 0, 0.525], - "108": [0, 0.61111, 0, 0, 0.525], - "109": [0, 0.43056, 0, 0, 0.525], - "110": [0, 0.43056, 0, 0, 0.525], - "111": [0, 0.43056, 0, 0, 0.525], - "112": [0.22222, 0.43056, 0, 0, 0.525], - "113": [0.22222, 0.43056, 0, 0, 0.525], - "114": [0, 0.43056, 0, 0, 0.525], - "115": [0, 0.43056, 0, 0, 0.525], - "116": [0, 0.55358, 0, 0, 0.525], - "117": [0, 0.43056, 0, 0, 0.525], - "118": [0, 0.43056, 0, 0, 0.525], - "119": [0, 0.43056, 0, 0, 0.525], - "120": [0, 0.43056, 0, 0, 0.525], - "121": [0.22222, 0.43056, 0, 0, 0.525], - "122": [0, 0.43056, 0, 0, 0.525], - "123": [0.08333, 0.69444, 0, 0, 0.525], - "124": [0.08333, 0.69444, 0, 0, 0.525], - "125": [0.08333, 0.69444, 0, 0, 0.525], - "126": [0, 0.61111, 0, 0, 0.525], - "127": [0, 0.61111, 0, 0, 0.525], - "160": [0, 0, 0, 0, 0.525], - "176": [0, 0.61111, 0, 0, 0.525], - "184": [0.19445, 0, 0, 0, 0.525], - "305": [0, 0.43056, 0, 0, 0.525], - "567": [0.22222, 0.43056, 0, 0, 0.525], - "711": [0, 0.56597, 0, 0, 0.525], - "713": [0, 0.56555, 0, 0, 0.525], - "714": [0, 0.61111, 0, 0, 0.525], - "715": [0, 0.61111, 0, 0, 0.525], - "728": [0, 0.61111, 0, 0, 0.525], - "730": [0, 0.61111, 0, 0, 0.525], - "770": [0, 0.61111, 0, 0, 0.525], - "771": [0, 0.61111, 0, 0, 0.525], - "776": [0, 0.61111, 0, 0, 0.525], - "915": [0, 0.61111, 0, 0, 0.525], - "916": [0, 0.61111, 0, 0, 0.525], - "920": [0, 0.61111, 0, 0, 0.525], - "923": [0, 0.61111, 0, 0, 0.525], - "926": [0, 0.61111, 0, 0, 0.525], - "928": [0, 0.61111, 0, 0, 0.525], - "931": [0, 0.61111, 0, 0, 0.525], - "933": [0, 0.61111, 0, 0, 0.525], - "934": [0, 0.61111, 0, 0, 0.525], - "936": [0, 0.61111, 0, 0, 0.525], - "937": [0, 0.61111, 0, 0, 0.525], - "8216": [0, 0.61111, 0, 0, 0.525], - "8217": [0, 0.61111, 0, 0, 0.525], - "8242": [0, 0.61111, 0, 0, 0.525], - "9251": [0.11111, 0.21944, 0, 0, 0.525] - } -}; - -/** - * This file contains metrics regarding fonts and individual symbols. The sigma - * and xi variables, as well as the metricMap map contain data extracted from - * TeX, TeX font metrics, and the TTF files. These data are then exposed via the - * `metrics` variable and the getCharacterMetrics function. - */ -// In TeX, there are actually three sets of dimensions, one for each of -// textstyle (size index 5 and higher: >=9pt), scriptstyle (size index 3 and 4: -// 7-8pt), and scriptscriptstyle (size index 1 and 2: 5-6pt). These are -// provided in the arrays below, in that order. -// -// The font metrics are stored in fonts cmsy10, cmsy7, and cmsy5 respectively. -// This was determined by running the following script: -// -// latex -interaction=nonstopmode \ -// '\documentclass{article}\usepackage{amsmath}\begin{document}' \ -// '$a$ \expandafter\show\the\textfont2' \ -// '\expandafter\show\the\scriptfont2' \ -// '\expandafter\show\the\scriptscriptfont2' \ -// '\stop' -// -// The metrics themselves were retrieved using the following commands: -// -// tftopl cmsy10 -// tftopl cmsy7 -// tftopl cmsy5 -// -// The output of each of these commands is quite lengthy. The only part we -// care about is the FONTDIMEN section. Each value is measured in EMs. -const sigmasAndXis = { - slant: [0.250, 0.250, 0.250], - // sigma1 - space: [0.000, 0.000, 0.000], - // sigma2 - stretch: [0.000, 0.000, 0.000], - // sigma3 - shrink: [0.000, 0.000, 0.000], - // sigma4 - xHeight: [0.431, 0.431, 0.431], - // sigma5 - quad: [1.000, 1.171, 1.472], - // sigma6 - extraSpace: [0.000, 0.000, 0.000], - // sigma7 - num1: [0.677, 0.732, 0.925], - // sigma8 - num2: [0.394, 0.384, 0.387], - // sigma9 - num3: [0.444, 0.471, 0.504], - // sigma10 - denom1: [0.686, 0.752, 1.025], - // sigma11 - denom2: [0.345, 0.344, 0.532], - // sigma12 - sup1: [0.413, 0.503, 0.504], - // sigma13 - sup2: [0.363, 0.431, 0.404], - // sigma14 - sup3: [0.289, 0.286, 0.294], - // sigma15 - sub1: [0.150, 0.143, 0.200], - // sigma16 - sub2: [0.247, 0.286, 0.400], - // sigma17 - supDrop: [0.386, 0.353, 0.494], - // sigma18 - subDrop: [0.050, 0.071, 0.100], - // sigma19 - delim1: [2.390, 1.700, 1.980], - // sigma20 - delim2: [1.010, 1.157, 1.420], - // sigma21 - axisHeight: [0.250, 0.250, 0.250], - // sigma22 - // These font metrics are extracted from TeX by using tftopl on cmex10.tfm; - // they correspond to the font parameters of the extension fonts (family 3). - // See the TeXbook, page 441. In AMSTeX, the extension fonts scale; to - // match cmex7, we'd use cmex7.tfm values for script and scriptscript - // values. - defaultRuleThickness: [0.04, 0.049, 0.049], - // xi8; cmex7: 0.049 - bigOpSpacing1: [0.111, 0.111, 0.111], - // xi9 - bigOpSpacing2: [0.166, 0.166, 0.166], - // xi10 - bigOpSpacing3: [0.2, 0.2, 0.2], - // xi11 - bigOpSpacing4: [0.6, 0.611, 0.611], - // xi12; cmex7: 0.611 - bigOpSpacing5: [0.1, 0.143, 0.143], - // xi13; cmex7: 0.143 - // The \sqrt rule width is taken from the height of the surd character. - // Since we use the same font at all sizes, this thickness doesn't scale. - sqrtRuleThickness: [0.04, 0.04, 0.04], - // This value determines how large a pt is, for metrics which are defined - // in terms of pts. - // This value is also used in katex.less; if you change it make sure the - // values match. - ptPerEm: [10.0, 10.0, 10.0], - // The space between adjacent `|` columns in an array definition. From - // `\showthe\doublerulesep` in LaTeX. Equals 2.0 / ptPerEm. - doubleRuleSep: [0.2, 0.2, 0.2], - // The width of separator lines in {array} environments. From - // `\showthe\arrayrulewidth` in LaTeX. Equals 0.4 / ptPerEm. - arrayRuleWidth: [0.04, 0.04, 0.04], - // Two values from LaTeX source2e: - fboxsep: [0.3, 0.3, 0.3], - // 3 pt / ptPerEm - fboxrule: [0.04, 0.04, 0.04] // 0.4 pt / ptPerEm - -}; // This map contains a mapping from font name and character code to character -// should have Latin-1 and Cyrillic characters, but may not depending on the -// operating system. The metrics do not account for extra height from the -// accents. In the case of Cyrillic characters which have both ascenders and -// descenders we prefer approximations with ascenders, primarily to prevent -// the fraction bar or root line from intersecting the glyph. -// TODO(kevinb) allow union of multiple glyph metrics for better accuracy. - -const extraCharacterMap = { - // Latin-1 - 'Å': 'A', - 'Ç': 'C', - 'Ð': 'D', - 'Þ': 'o', - 'å': 'a', - 'ç': 'c', - 'ð': 'd', - 'þ': 'o', - // Cyrillic - 'А': 'A', - 'Б': 'B', - 'В': 'B', - 'Г': 'F', - 'Д': 'A', - 'Е': 'E', - 'Ж': 'K', - 'З': '3', - 'И': 'N', - 'Й': 'N', - 'К': 'K', - 'Л': 'N', - 'М': 'M', - 'Н': 'H', - 'О': 'O', - 'П': 'N', - 'Р': 'P', - 'С': 'C', - 'Т': 'T', - 'У': 'y', - 'Ф': 'O', - 'Х': 'X', - 'Ц': 'U', - 'Ч': 'h', - 'Ш': 'W', - 'Щ': 'W', - 'Ъ': 'B', - 'Ы': 'X', - 'Ь': 'B', - 'Э': '3', - 'Ю': 'X', - 'Я': 'R', - 'а': 'a', - 'б': 'b', - 'в': 'a', - 'г': 'r', - 'д': 'y', - 'е': 'e', - 'ж': 'm', - 'з': 'e', - 'и': 'n', - 'й': 'n', - 'к': 'n', - 'л': 'n', - 'м': 'm', - 'н': 'n', - 'о': 'o', - 'п': 'n', - 'р': 'p', - 'с': 'c', - 'т': 'o', - 'у': 'y', - 'ф': 'b', - 'х': 'x', - 'ц': 'n', - 'ч': 'n', - 'ш': 'w', - 'щ': 'w', - 'ъ': 'a', - 'ы': 'm', - 'ь': 'a', - 'э': 'e', - 'ю': 'm', - 'я': 'r' -}; - -/** - * This function adds new font metrics to default metricMap - * It can also override existing metrics - */ -function setFontMetrics(fontName, metrics) { - metricMap[fontName] = metrics; -} -/** - * This function is a convenience function for looking up information in the - * metricMap table. It takes a character as a string, and a font. - * - * Note: the `width` property may be undefined if fontMetricsData.js wasn't - * built using `Make extended_metrics`. - */ - -function getCharacterMetrics(character, font, mode) { - if (!metricMap[font]) { - throw new Error(`Font metrics not found for font: ${font}.`); - } - - let ch = character.charCodeAt(0); - let metrics = metricMap[font][ch]; - - if (!metrics && character[0] in extraCharacterMap) { - ch = extraCharacterMap[character[0]].charCodeAt(0); - metrics = metricMap[font][ch]; - } - - if (!metrics && mode === 'text') { - // We don't typically have font metrics for Asian scripts. - // But since we support them in text mode, we need to return - // some sort of metrics. - // So if the character is in a script we support but we - // don't have metrics for it, just use the metrics for - // the Latin capital letter M. This is close enough because - // we (currently) only care about the height of the glyph - // not its width. - if (supportedCodepoint(ch)) { - metrics = metricMap[font][77]; // 77 is the charcode for 'M' - } - } - - if (metrics) { - return { - depth: metrics[0], - height: metrics[1], - italic: metrics[2], - skew: metrics[3], - width: metrics[4] - }; - } -} -const fontMetricsBySizeIndex = {}; -/** - * Get the font metrics for a given size. - */ - -function getGlobalMetrics(size) { - let sizeIndex; - - if (size >= 5) { - sizeIndex = 0; - } else if (size >= 3) { - sizeIndex = 1; - } else { - sizeIndex = 2; - } - - if (!fontMetricsBySizeIndex[sizeIndex]) { - const metrics = fontMetricsBySizeIndex[sizeIndex] = { - cssEmPerMu: sigmasAndXis.quad[sizeIndex] / 18 - }; - - for (const key in sigmasAndXis) { - if (sigmasAndXis.hasOwnProperty(key)) { - metrics[key] = sigmasAndXis[key][sizeIndex]; - } - } - } - - return fontMetricsBySizeIndex[sizeIndex]; -} - -/** - * This file holds a list of all no-argument functions and single-character - * symbols (like 'a' or ';'). - * - * For each of the symbols, there are three properties they can have: - * - font (required): the font to be used for this symbol. Either "main" (the - normal font), or "ams" (the ams fonts). - * - group (required): the ParseNode group type the symbol should have (i.e. - "textord", "mathord", etc). - See https://github.com/KaTeX/KaTeX/wiki/Examining-TeX#group-types - * - replace: the character that this symbol or function should be - * replaced with (i.e. "\phi" has a replace value of "\u03d5", the phi - * character in the main font). - * - * The outermost map in the table indicates what mode the symbols should be - * accepted in (e.g. "math" or "text"). - */ -// Some of these have a "-token" suffix since these are also used as `ParseNode` -// types for raw text tokens, and we want to avoid conflicts with higher-level -// `ParseNode` types. These `ParseNode`s are constructed within `Parser` by -// looking up the `symbols` map. -const ATOMS = { - "bin": 1, - "close": 1, - "inner": 1, - "open": 1, - "punct": 1, - "rel": 1 -}; -const NON_ATOMS = { - "accent-token": 1, - "mathord": 1, - "op-token": 1, - "spacing": 1, - "textord": 1 -}; -const symbols = { - "math": {}, - "text": {} -}; -/** `acceptUnicodeChar = true` is only applicable if `replace` is set. */ - -function defineSymbol(mode, font, group, replace, name, acceptUnicodeChar) { - symbols[mode][name] = { - font, - group, - replace - }; - - if (acceptUnicodeChar && replace) { - symbols[mode][replace] = symbols[mode][name]; - } -} // Some abbreviations for commonly used strings. -// This helps minify the code, and also spotting typos using jshint. -// modes: - -const math = "math"; -const text$1 = "text"; // fonts: - -const main = "main"; -const ams = "ams"; // groups: - -const accent = "accent-token"; -const bin = "bin"; -const close = "close"; -const inner = "inner"; -const mathord = "mathord"; -const op = "op-token"; -const open = "open"; -const punct = "punct"; -const rel = "rel"; -const spacing = "spacing"; -const textord = "textord"; // Now comes the symbol table -// Relation Symbols - -defineSymbol(math, main, rel, "\u2261", "\\equiv", true); -defineSymbol(math, main, rel, "\u227a", "\\prec", true); -defineSymbol(math, main, rel, "\u227b", "\\succ", true); -defineSymbol(math, main, rel, "\u223c", "\\sim", true); -defineSymbol(math, main, rel, "\u22a5", "\\perp"); -defineSymbol(math, main, rel, "\u2aaf", "\\preceq", true); -defineSymbol(math, main, rel, "\u2ab0", "\\succeq", true); -defineSymbol(math, main, rel, "\u2243", "\\simeq", true); -defineSymbol(math, main, rel, "\u2223", "\\mid", true); -defineSymbol(math, main, rel, "\u226a", "\\ll", true); -defineSymbol(math, main, rel, "\u226b", "\\gg", true); -defineSymbol(math, main, rel, "\u224d", "\\asymp", true); -defineSymbol(math, main, rel, "\u2225", "\\parallel"); -defineSymbol(math, main, rel, "\u22c8", "\\bowtie", true); -defineSymbol(math, main, rel, "\u2323", "\\smile", true); -defineSymbol(math, main, rel, "\u2291", "\\sqsubseteq", true); -defineSymbol(math, main, rel, "\u2292", "\\sqsupseteq", true); -defineSymbol(math, main, rel, "\u2250", "\\doteq", true); -defineSymbol(math, main, rel, "\u2322", "\\frown", true); -defineSymbol(math, main, rel, "\u220b", "\\ni", true); -defineSymbol(math, main, rel, "\u221d", "\\propto", true); -defineSymbol(math, main, rel, "\u22a2", "\\vdash", true); -defineSymbol(math, main, rel, "\u22a3", "\\dashv", true); -defineSymbol(math, main, rel, "\u220b", "\\owns"); // Punctuation - -defineSymbol(math, main, punct, "\u002e", "\\ldotp"); -defineSymbol(math, main, punct, "\u22c5", "\\cdotp"); // Misc Symbols - -defineSymbol(math, main, textord, "\u0023", "\\#"); -defineSymbol(text$1, main, textord, "\u0023", "\\#"); -defineSymbol(math, main, textord, "\u0026", "\\&"); -defineSymbol(text$1, main, textord, "\u0026", "\\&"); -defineSymbol(math, main, textord, "\u2135", "\\aleph", true); -defineSymbol(math, main, textord, "\u2200", "\\forall", true); -defineSymbol(math, main, textord, "\u210f", "\\hbar", true); -defineSymbol(math, main, textord, "\u2203", "\\exists", true); -defineSymbol(math, main, textord, "\u2207", "\\nabla", true); -defineSymbol(math, main, textord, "\u266d", "\\flat", true); -defineSymbol(math, main, textord, "\u2113", "\\ell", true); -defineSymbol(math, main, textord, "\u266e", "\\natural", true); -defineSymbol(math, main, textord, "\u2663", "\\clubsuit", true); -defineSymbol(math, main, textord, "\u2118", "\\wp", true); -defineSymbol(math, main, textord, "\u266f", "\\sharp", true); -defineSymbol(math, main, textord, "\u2662", "\\diamondsuit", true); -defineSymbol(math, main, textord, "\u211c", "\\Re", true); -defineSymbol(math, main, textord, "\u2661", "\\heartsuit", true); -defineSymbol(math, main, textord, "\u2111", "\\Im", true); -defineSymbol(math, main, textord, "\u2660", "\\spadesuit", true); -defineSymbol(text$1, main, textord, "\u00a7", "\\S", true); -defineSymbol(text$1, main, textord, "\u00b6", "\\P", true); // Math and Text - -defineSymbol(math, main, textord, "\u2020", "\\dag"); -defineSymbol(text$1, main, textord, "\u2020", "\\dag"); -defineSymbol(text$1, main, textord, "\u2020", "\\textdagger"); -defineSymbol(math, main, textord, "\u2021", "\\ddag"); -defineSymbol(text$1, main, textord, "\u2021", "\\ddag"); -defineSymbol(text$1, main, textord, "\u2021", "\\textdaggerdbl"); // Large Delimiters - -defineSymbol(math, main, close, "\u23b1", "\\rmoustache", true); -defineSymbol(math, main, open, "\u23b0", "\\lmoustache", true); -defineSymbol(math, main, close, "\u27ef", "\\rgroup", true); -defineSymbol(math, main, open, "\u27ee", "\\lgroup", true); // Binary Operators - -defineSymbol(math, main, bin, "\u2213", "\\mp", true); -defineSymbol(math, main, bin, "\u2296", "\\ominus", true); -defineSymbol(math, main, bin, "\u228e", "\\uplus", true); -defineSymbol(math, main, bin, "\u2293", "\\sqcap", true); -defineSymbol(math, main, bin, "\u2217", "\\ast"); -defineSymbol(math, main, bin, "\u2294", "\\sqcup", true); -defineSymbol(math, main, bin, "\u25ef", "\\bigcirc"); -defineSymbol(math, main, bin, "\u2219", "\\bullet"); -defineSymbol(math, main, bin, "\u2021", "\\ddagger"); -defineSymbol(math, main, bin, "\u2240", "\\wr", true); -defineSymbol(math, main, bin, "\u2a3f", "\\amalg"); -defineSymbol(math, main, bin, "\u0026", "\\And"); // from amsmath -// Arrow Symbols - -defineSymbol(math, main, rel, "\u27f5", "\\longleftarrow", true); -defineSymbol(math, main, rel, "\u21d0", "\\Leftarrow", true); -defineSymbol(math, main, rel, "\u27f8", "\\Longleftarrow", true); -defineSymbol(math, main, rel, "\u27f6", "\\longrightarrow", true); -defineSymbol(math, main, rel, "\u21d2", "\\Rightarrow", true); -defineSymbol(math, main, rel, "\u27f9", "\\Longrightarrow", true); -defineSymbol(math, main, rel, "\u2194", "\\leftrightarrow", true); -defineSymbol(math, main, rel, "\u27f7", "\\longleftrightarrow", true); -defineSymbol(math, main, rel, "\u21d4", "\\Leftrightarrow", true); -defineSymbol(math, main, rel, "\u27fa", "\\Longleftrightarrow", true); -defineSymbol(math, main, rel, "\u21a6", "\\mapsto", true); -defineSymbol(math, main, rel, "\u27fc", "\\longmapsto", true); -defineSymbol(math, main, rel, "\u2197", "\\nearrow", true); -defineSymbol(math, main, rel, "\u21a9", "\\hookleftarrow", true); -defineSymbol(math, main, rel, "\u21aa", "\\hookrightarrow", true); -defineSymbol(math, main, rel, "\u2198", "\\searrow", true); -defineSymbol(math, main, rel, "\u21bc", "\\leftharpoonup", true); -defineSymbol(math, main, rel, "\u21c0", "\\rightharpoonup", true); -defineSymbol(math, main, rel, "\u2199", "\\swarrow", true); -defineSymbol(math, main, rel, "\u21bd", "\\leftharpoondown", true); -defineSymbol(math, main, rel, "\u21c1", "\\rightharpoondown", true); -defineSymbol(math, main, rel, "\u2196", "\\nwarrow", true); -defineSymbol(math, main, rel, "\u21cc", "\\rightleftharpoons", true); // AMS Negated Binary Relations - -defineSymbol(math, ams, rel, "\u226e", "\\nless", true); // Symbol names proceeded by "@" each have a corresponding macro. - -defineSymbol(math, ams, rel, "\ue010", "\\@nleqslant"); -defineSymbol(math, ams, rel, "\ue011", "\\@nleqq"); -defineSymbol(math, ams, rel, "\u2a87", "\\lneq", true); -defineSymbol(math, ams, rel, "\u2268", "\\lneqq", true); -defineSymbol(math, ams, rel, "\ue00c", "\\@lvertneqq"); -defineSymbol(math, ams, rel, "\u22e6", "\\lnsim", true); -defineSymbol(math, ams, rel, "\u2a89", "\\lnapprox", true); -defineSymbol(math, ams, rel, "\u2280", "\\nprec", true); // unicode-math maps \u22e0 to \npreccurlyeq. We'll use the AMS synonym. - -defineSymbol(math, ams, rel, "\u22e0", "\\npreceq", true); -defineSymbol(math, ams, rel, "\u22e8", "\\precnsim", true); -defineSymbol(math, ams, rel, "\u2ab9", "\\precnapprox", true); -defineSymbol(math, ams, rel, "\u2241", "\\nsim", true); -defineSymbol(math, ams, rel, "\ue006", "\\@nshortmid"); -defineSymbol(math, ams, rel, "\u2224", "\\nmid", true); -defineSymbol(math, ams, rel, "\u22ac", "\\nvdash", true); -defineSymbol(math, ams, rel, "\u22ad", "\\nvDash", true); -defineSymbol(math, ams, rel, "\u22ea", "\\ntriangleleft"); -defineSymbol(math, ams, rel, "\u22ec", "\\ntrianglelefteq", true); -defineSymbol(math, ams, rel, "\u228a", "\\subsetneq", true); -defineSymbol(math, ams, rel, "\ue01a", "\\@varsubsetneq"); -defineSymbol(math, ams, rel, "\u2acb", "\\subsetneqq", true); -defineSymbol(math, ams, rel, "\ue017", "\\@varsubsetneqq"); -defineSymbol(math, ams, rel, "\u226f", "\\ngtr", true); -defineSymbol(math, ams, rel, "\ue00f", "\\@ngeqslant"); -defineSymbol(math, ams, rel, "\ue00e", "\\@ngeqq"); -defineSymbol(math, ams, rel, "\u2a88", "\\gneq", true); -defineSymbol(math, ams, rel, "\u2269", "\\gneqq", true); -defineSymbol(math, ams, rel, "\ue00d", "\\@gvertneqq"); -defineSymbol(math, ams, rel, "\u22e7", "\\gnsim", true); -defineSymbol(math, ams, rel, "\u2a8a", "\\gnapprox", true); -defineSymbol(math, ams, rel, "\u2281", "\\nsucc", true); // unicode-math maps \u22e1 to \nsucccurlyeq. We'll use the AMS synonym. - -defineSymbol(math, ams, rel, "\u22e1", "\\nsucceq", true); -defineSymbol(math, ams, rel, "\u22e9", "\\succnsim", true); -defineSymbol(math, ams, rel, "\u2aba", "\\succnapprox", true); // unicode-math maps \u2246 to \simneqq. We'll use the AMS synonym. - -defineSymbol(math, ams, rel, "\u2246", "\\ncong", true); -defineSymbol(math, ams, rel, "\ue007", "\\@nshortparallel"); -defineSymbol(math, ams, rel, "\u2226", "\\nparallel", true); -defineSymbol(math, ams, rel, "\u22af", "\\nVDash", true); -defineSymbol(math, ams, rel, "\u22eb", "\\ntriangleright"); -defineSymbol(math, ams, rel, "\u22ed", "\\ntrianglerighteq", true); -defineSymbol(math, ams, rel, "\ue018", "\\@nsupseteqq"); -defineSymbol(math, ams, rel, "\u228b", "\\supsetneq", true); -defineSymbol(math, ams, rel, "\ue01b", "\\@varsupsetneq"); -defineSymbol(math, ams, rel, "\u2acc", "\\supsetneqq", true); -defineSymbol(math, ams, rel, "\ue019", "\\@varsupsetneqq"); -defineSymbol(math, ams, rel, "\u22ae", "\\nVdash", true); -defineSymbol(math, ams, rel, "\u2ab5", "\\precneqq", true); -defineSymbol(math, ams, rel, "\u2ab6", "\\succneqq", true); -defineSymbol(math, ams, rel, "\ue016", "\\@nsubseteqq"); -defineSymbol(math, ams, bin, "\u22b4", "\\unlhd"); -defineSymbol(math, ams, bin, "\u22b5", "\\unrhd"); // AMS Negated Arrows - -defineSymbol(math, ams, rel, "\u219a", "\\nleftarrow", true); -defineSymbol(math, ams, rel, "\u219b", "\\nrightarrow", true); -defineSymbol(math, ams, rel, "\u21cd", "\\nLeftarrow", true); -defineSymbol(math, ams, rel, "\u21cf", "\\nRightarrow", true); -defineSymbol(math, ams, rel, "\u21ae", "\\nleftrightarrow", true); -defineSymbol(math, ams, rel, "\u21ce", "\\nLeftrightarrow", true); // AMS Misc - -defineSymbol(math, ams, rel, "\u25b3", "\\vartriangle"); -defineSymbol(math, ams, textord, "\u210f", "\\hslash"); -defineSymbol(math, ams, textord, "\u25bd", "\\triangledown"); -defineSymbol(math, ams, textord, "\u25ca", "\\lozenge"); -defineSymbol(math, ams, textord, "\u24c8", "\\circledS"); -defineSymbol(math, ams, textord, "\u00ae", "\\circledR"); -defineSymbol(text$1, ams, textord, "\u00ae", "\\circledR"); -defineSymbol(math, ams, textord, "\u2221", "\\measuredangle", true); -defineSymbol(math, ams, textord, "\u2204", "\\nexists"); -defineSymbol(math, ams, textord, "\u2127", "\\mho"); -defineSymbol(math, ams, textord, "\u2132", "\\Finv", true); -defineSymbol(math, ams, textord, "\u2141", "\\Game", true); -defineSymbol(math, ams, textord, "\u2035", "\\backprime"); -defineSymbol(math, ams, textord, "\u25b2", "\\blacktriangle"); -defineSymbol(math, ams, textord, "\u25bc", "\\blacktriangledown"); -defineSymbol(math, ams, textord, "\u25a0", "\\blacksquare"); -defineSymbol(math, ams, textord, "\u29eb", "\\blacklozenge"); -defineSymbol(math, ams, textord, "\u2605", "\\bigstar"); -defineSymbol(math, ams, textord, "\u2222", "\\sphericalangle", true); -defineSymbol(math, ams, textord, "\u2201", "\\complement", true); // unicode-math maps U+F0 to \matheth. We map to AMS function \eth - -defineSymbol(math, ams, textord, "\u00f0", "\\eth", true); -defineSymbol(text$1, main, textord, "\u00f0", "\u00f0"); -defineSymbol(math, ams, textord, "\u2571", "\\diagup"); -defineSymbol(math, ams, textord, "\u2572", "\\diagdown"); -defineSymbol(math, ams, textord, "\u25a1", "\\square"); -defineSymbol(math, ams, textord, "\u25a1", "\\Box"); -defineSymbol(math, ams, textord, "\u25ca", "\\Diamond"); // unicode-math maps U+A5 to \mathyen. We map to AMS function \yen - -defineSymbol(math, ams, textord, "\u00a5", "\\yen", true); -defineSymbol(text$1, ams, textord, "\u00a5", "\\yen", true); -defineSymbol(math, ams, textord, "\u2713", "\\checkmark", true); -defineSymbol(text$1, ams, textord, "\u2713", "\\checkmark"); // AMS Hebrew - -defineSymbol(math, ams, textord, "\u2136", "\\beth", true); -defineSymbol(math, ams, textord, "\u2138", "\\daleth", true); -defineSymbol(math, ams, textord, "\u2137", "\\gimel", true); // AMS Greek - -defineSymbol(math, ams, textord, "\u03dd", "\\digamma", true); -defineSymbol(math, ams, textord, "\u03f0", "\\varkappa"); // AMS Delimiters - -defineSymbol(math, ams, open, "\u250c", "\\@ulcorner", true); -defineSymbol(math, ams, close, "\u2510", "\\@urcorner", true); -defineSymbol(math, ams, open, "\u2514", "\\@llcorner", true); -defineSymbol(math, ams, close, "\u2518", "\\@lrcorner", true); // AMS Binary Relations - -defineSymbol(math, ams, rel, "\u2266", "\\leqq", true); -defineSymbol(math, ams, rel, "\u2a7d", "\\leqslant", true); -defineSymbol(math, ams, rel, "\u2a95", "\\eqslantless", true); -defineSymbol(math, ams, rel, "\u2272", "\\lesssim", true); -defineSymbol(math, ams, rel, "\u2a85", "\\lessapprox", true); -defineSymbol(math, ams, rel, "\u224a", "\\approxeq", true); -defineSymbol(math, ams, bin, "\u22d6", "\\lessdot"); -defineSymbol(math, ams, rel, "\u22d8", "\\lll", true); -defineSymbol(math, ams, rel, "\u2276", "\\lessgtr", true); -defineSymbol(math, ams, rel, "\u22da", "\\lesseqgtr", true); -defineSymbol(math, ams, rel, "\u2a8b", "\\lesseqqgtr", true); -defineSymbol(math, ams, rel, "\u2251", "\\doteqdot"); -defineSymbol(math, ams, rel, "\u2253", "\\risingdotseq", true); -defineSymbol(math, ams, rel, "\u2252", "\\fallingdotseq", true); -defineSymbol(math, ams, rel, "\u223d", "\\backsim", true); -defineSymbol(math, ams, rel, "\u22cd", "\\backsimeq", true); -defineSymbol(math, ams, rel, "\u2ac5", "\\subseteqq", true); -defineSymbol(math, ams, rel, "\u22d0", "\\Subset", true); -defineSymbol(math, ams, rel, "\u228f", "\\sqsubset", true); -defineSymbol(math, ams, rel, "\u227c", "\\preccurlyeq", true); -defineSymbol(math, ams, rel, "\u22de", "\\curlyeqprec", true); -defineSymbol(math, ams, rel, "\u227e", "\\precsim", true); -defineSymbol(math, ams, rel, "\u2ab7", "\\precapprox", true); -defineSymbol(math, ams, rel, "\u22b2", "\\vartriangleleft"); -defineSymbol(math, ams, rel, "\u22b4", "\\trianglelefteq"); -defineSymbol(math, ams, rel, "\u22a8", "\\vDash", true); -defineSymbol(math, ams, rel, "\u22aa", "\\Vvdash", true); -defineSymbol(math, ams, rel, "\u2323", "\\smallsmile"); -defineSymbol(math, ams, rel, "\u2322", "\\smallfrown"); -defineSymbol(math, ams, rel, "\u224f", "\\bumpeq", true); -defineSymbol(math, ams, rel, "\u224e", "\\Bumpeq", true); -defineSymbol(math, ams, rel, "\u2267", "\\geqq", true); -defineSymbol(math, ams, rel, "\u2a7e", "\\geqslant", true); -defineSymbol(math, ams, rel, "\u2a96", "\\eqslantgtr", true); -defineSymbol(math, ams, rel, "\u2273", "\\gtrsim", true); -defineSymbol(math, ams, rel, "\u2a86", "\\gtrapprox", true); -defineSymbol(math, ams, bin, "\u22d7", "\\gtrdot"); -defineSymbol(math, ams, rel, "\u22d9", "\\ggg", true); -defineSymbol(math, ams, rel, "\u2277", "\\gtrless", true); -defineSymbol(math, ams, rel, "\u22db", "\\gtreqless", true); -defineSymbol(math, ams, rel, "\u2a8c", "\\gtreqqless", true); -defineSymbol(math, ams, rel, "\u2256", "\\eqcirc", true); -defineSymbol(math, ams, rel, "\u2257", "\\circeq", true); -defineSymbol(math, ams, rel, "\u225c", "\\triangleq", true); -defineSymbol(math, ams, rel, "\u223c", "\\thicksim"); -defineSymbol(math, ams, rel, "\u2248", "\\thickapprox"); -defineSymbol(math, ams, rel, "\u2ac6", "\\supseteqq", true); -defineSymbol(math, ams, rel, "\u22d1", "\\Supset", true); -defineSymbol(math, ams, rel, "\u2290", "\\sqsupset", true); -defineSymbol(math, ams, rel, "\u227d", "\\succcurlyeq", true); -defineSymbol(math, ams, rel, "\u22df", "\\curlyeqsucc", true); -defineSymbol(math, ams, rel, "\u227f", "\\succsim", true); -defineSymbol(math, ams, rel, "\u2ab8", "\\succapprox", true); -defineSymbol(math, ams, rel, "\u22b3", "\\vartriangleright"); -defineSymbol(math, ams, rel, "\u22b5", "\\trianglerighteq"); -defineSymbol(math, ams, rel, "\u22a9", "\\Vdash", true); -defineSymbol(math, ams, rel, "\u2223", "\\shortmid"); -defineSymbol(math, ams, rel, "\u2225", "\\shortparallel"); -defineSymbol(math, ams, rel, "\u226c", "\\between", true); -defineSymbol(math, ams, rel, "\u22d4", "\\pitchfork", true); -defineSymbol(math, ams, rel, "\u221d", "\\varpropto"); -defineSymbol(math, ams, rel, "\u25c0", "\\blacktriangleleft"); // unicode-math says that \therefore is a mathord atom. -// We kept the amssymb atom type, which is rel. - -defineSymbol(math, ams, rel, "\u2234", "\\therefore", true); -defineSymbol(math, ams, rel, "\u220d", "\\backepsilon"); -defineSymbol(math, ams, rel, "\u25b6", "\\blacktriangleright"); // unicode-math says that \because is a mathord atom. -// We kept the amssymb atom type, which is rel. - -defineSymbol(math, ams, rel, "\u2235", "\\because", true); -defineSymbol(math, ams, rel, "\u22d8", "\\llless"); -defineSymbol(math, ams, rel, "\u22d9", "\\gggtr"); -defineSymbol(math, ams, bin, "\u22b2", "\\lhd"); -defineSymbol(math, ams, bin, "\u22b3", "\\rhd"); -defineSymbol(math, ams, rel, "\u2242", "\\eqsim", true); -defineSymbol(math, main, rel, "\u22c8", "\\Join"); -defineSymbol(math, ams, rel, "\u2251", "\\Doteq", true); // AMS Binary Operators - -defineSymbol(math, ams, bin, "\u2214", "\\dotplus", true); -defineSymbol(math, ams, bin, "\u2216", "\\smallsetminus"); -defineSymbol(math, ams, bin, "\u22d2", "\\Cap", true); -defineSymbol(math, ams, bin, "\u22d3", "\\Cup", true); -defineSymbol(math, ams, bin, "\u2a5e", "\\doublebarwedge", true); -defineSymbol(math, ams, bin, "\u229f", "\\boxminus", true); -defineSymbol(math, ams, bin, "\u229e", "\\boxplus", true); -defineSymbol(math, ams, bin, "\u22c7", "\\divideontimes", true); -defineSymbol(math, ams, bin, "\u22c9", "\\ltimes", true); -defineSymbol(math, ams, bin, "\u22ca", "\\rtimes", true); -defineSymbol(math, ams, bin, "\u22cb", "\\leftthreetimes", true); -defineSymbol(math, ams, bin, "\u22cc", "\\rightthreetimes", true); -defineSymbol(math, ams, bin, "\u22cf", "\\curlywedge", true); -defineSymbol(math, ams, bin, "\u22ce", "\\curlyvee", true); -defineSymbol(math, ams, bin, "\u229d", "\\circleddash", true); -defineSymbol(math, ams, bin, "\u229b", "\\circledast", true); -defineSymbol(math, ams, bin, "\u22c5", "\\centerdot"); -defineSymbol(math, ams, bin, "\u22ba", "\\intercal", true); -defineSymbol(math, ams, bin, "\u22d2", "\\doublecap"); -defineSymbol(math, ams, bin, "\u22d3", "\\doublecup"); -defineSymbol(math, ams, bin, "\u22a0", "\\boxtimes", true); // AMS Arrows -// Note: unicode-math maps \u21e2 to their own function \rightdasharrow. -// We'll map it to AMS function \dashrightarrow. It produces the same atom. - -defineSymbol(math, ams, rel, "\u21e2", "\\dashrightarrow", true); // unicode-math maps \u21e0 to \leftdasharrow. We'll use the AMS synonym. - -defineSymbol(math, ams, rel, "\u21e0", "\\dashleftarrow", true); -defineSymbol(math, ams, rel, "\u21c7", "\\leftleftarrows", true); -defineSymbol(math, ams, rel, "\u21c6", "\\leftrightarrows", true); -defineSymbol(math, ams, rel, "\u21da", "\\Lleftarrow", true); -defineSymbol(math, ams, rel, "\u219e", "\\twoheadleftarrow", true); -defineSymbol(math, ams, rel, "\u21a2", "\\leftarrowtail", true); -defineSymbol(math, ams, rel, "\u21ab", "\\looparrowleft", true); -defineSymbol(math, ams, rel, "\u21cb", "\\leftrightharpoons", true); -defineSymbol(math, ams, rel, "\u21b6", "\\curvearrowleft", true); // unicode-math maps \u21ba to \acwopencirclearrow. We'll use the AMS synonym. - -defineSymbol(math, ams, rel, "\u21ba", "\\circlearrowleft", true); -defineSymbol(math, ams, rel, "\u21b0", "\\Lsh", true); -defineSymbol(math, ams, rel, "\u21c8", "\\upuparrows", true); -defineSymbol(math, ams, rel, "\u21bf", "\\upharpoonleft", true); -defineSymbol(math, ams, rel, "\u21c3", "\\downharpoonleft", true); -defineSymbol(math, ams, rel, "\u22b8", "\\multimap", true); -defineSymbol(math, ams, rel, "\u21ad", "\\leftrightsquigarrow", true); -defineSymbol(math, ams, rel, "\u21c9", "\\rightrightarrows", true); -defineSymbol(math, ams, rel, "\u21c4", "\\rightleftarrows", true); -defineSymbol(math, ams, rel, "\u21a0", "\\twoheadrightarrow", true); -defineSymbol(math, ams, rel, "\u21a3", "\\rightarrowtail", true); -defineSymbol(math, ams, rel, "\u21ac", "\\looparrowright", true); -defineSymbol(math, ams, rel, "\u21b7", "\\curvearrowright", true); // unicode-math maps \u21bb to \cwopencirclearrow. We'll use the AMS synonym. - -defineSymbol(math, ams, rel, "\u21bb", "\\circlearrowright", true); -defineSymbol(math, ams, rel, "\u21b1", "\\Rsh", true); -defineSymbol(math, ams, rel, "\u21ca", "\\downdownarrows", true); -defineSymbol(math, ams, rel, "\u21be", "\\upharpoonright", true); -defineSymbol(math, ams, rel, "\u21c2", "\\downharpoonright", true); -defineSymbol(math, ams, rel, "\u21dd", "\\rightsquigarrow", true); -defineSymbol(math, ams, rel, "\u21dd", "\\leadsto"); -defineSymbol(math, ams, rel, "\u21db", "\\Rrightarrow", true); -defineSymbol(math, ams, rel, "\u21be", "\\restriction"); -defineSymbol(math, main, textord, "\u2018", "`"); -defineSymbol(math, main, textord, "$", "\\$"); -defineSymbol(text$1, main, textord, "$", "\\$"); -defineSymbol(text$1, main, textord, "$", "\\textdollar"); -defineSymbol(math, main, textord, "%", "\\%"); -defineSymbol(text$1, main, textord, "%", "\\%"); -defineSymbol(math, main, textord, "_", "\\_"); -defineSymbol(text$1, main, textord, "_", "\\_"); -defineSymbol(text$1, main, textord, "_", "\\textunderscore"); -defineSymbol(math, main, textord, "\u2220", "\\angle", true); -defineSymbol(math, main, textord, "\u221e", "\\infty", true); -defineSymbol(math, main, textord, "\u2032", "\\prime"); -defineSymbol(math, main, textord, "\u25b3", "\\triangle"); -defineSymbol(math, main, textord, "\u0393", "\\Gamma", true); -defineSymbol(math, main, textord, "\u0394", "\\Delta", true); -defineSymbol(math, main, textord, "\u0398", "\\Theta", true); -defineSymbol(math, main, textord, "\u039b", "\\Lambda", true); -defineSymbol(math, main, textord, "\u039e", "\\Xi", true); -defineSymbol(math, main, textord, "\u03a0", "\\Pi", true); -defineSymbol(math, main, textord, "\u03a3", "\\Sigma", true); -defineSymbol(math, main, textord, "\u03a5", "\\Upsilon", true); -defineSymbol(math, main, textord, "\u03a6", "\\Phi", true); -defineSymbol(math, main, textord, "\u03a8", "\\Psi", true); -defineSymbol(math, main, textord, "\u03a9", "\\Omega", true); -defineSymbol(math, main, textord, "A", "\u0391"); -defineSymbol(math, main, textord, "B", "\u0392"); -defineSymbol(math, main, textord, "E", "\u0395"); -defineSymbol(math, main, textord, "Z", "\u0396"); -defineSymbol(math, main, textord, "H", "\u0397"); -defineSymbol(math, main, textord, "I", "\u0399"); -defineSymbol(math, main, textord, "K", "\u039A"); -defineSymbol(math, main, textord, "M", "\u039C"); -defineSymbol(math, main, textord, "N", "\u039D"); -defineSymbol(math, main, textord, "O", "\u039F"); -defineSymbol(math, main, textord, "P", "\u03A1"); -defineSymbol(math, main, textord, "T", "\u03A4"); -defineSymbol(math, main, textord, "X", "\u03A7"); -defineSymbol(math, main, textord, "\u00ac", "\\neg", true); -defineSymbol(math, main, textord, "\u00ac", "\\lnot"); -defineSymbol(math, main, textord, "\u22a4", "\\top"); -defineSymbol(math, main, textord, "\u22a5", "\\bot"); -defineSymbol(math, main, textord, "\u2205", "\\emptyset"); -defineSymbol(math, ams, textord, "\u2205", "\\varnothing"); -defineSymbol(math, main, mathord, "\u03b1", "\\alpha", true); -defineSymbol(math, main, mathord, "\u03b2", "\\beta", true); -defineSymbol(math, main, mathord, "\u03b3", "\\gamma", true); -defineSymbol(math, main, mathord, "\u03b4", "\\delta", true); -defineSymbol(math, main, mathord, "\u03f5", "\\epsilon", true); -defineSymbol(math, main, mathord, "\u03b6", "\\zeta", true); -defineSymbol(math, main, mathord, "\u03b7", "\\eta", true); -defineSymbol(math, main, mathord, "\u03b8", "\\theta", true); -defineSymbol(math, main, mathord, "\u03b9", "\\iota", true); -defineSymbol(math, main, mathord, "\u03ba", "\\kappa", true); -defineSymbol(math, main, mathord, "\u03bb", "\\lambda", true); -defineSymbol(math, main, mathord, "\u03bc", "\\mu", true); -defineSymbol(math, main, mathord, "\u03bd", "\\nu", true); -defineSymbol(math, main, mathord, "\u03be", "\\xi", true); -defineSymbol(math, main, mathord, "\u03bf", "\\omicron", true); -defineSymbol(math, main, mathord, "\u03c0", "\\pi", true); -defineSymbol(math, main, mathord, "\u03c1", "\\rho", true); -defineSymbol(math, main, mathord, "\u03c3", "\\sigma", true); -defineSymbol(math, main, mathord, "\u03c4", "\\tau", true); -defineSymbol(math, main, mathord, "\u03c5", "\\upsilon", true); -defineSymbol(math, main, mathord, "\u03d5", "\\phi", true); -defineSymbol(math, main, mathord, "\u03c7", "\\chi", true); -defineSymbol(math, main, mathord, "\u03c8", "\\psi", true); -defineSymbol(math, main, mathord, "\u03c9", "\\omega", true); -defineSymbol(math, main, mathord, "\u03b5", "\\varepsilon", true); -defineSymbol(math, main, mathord, "\u03d1", "\\vartheta", true); -defineSymbol(math, main, mathord, "\u03d6", "\\varpi", true); -defineSymbol(math, main, mathord, "\u03f1", "\\varrho", true); -defineSymbol(math, main, mathord, "\u03c2", "\\varsigma", true); -defineSymbol(math, main, mathord, "\u03c6", "\\varphi", true); -defineSymbol(math, main, bin, "\u2217", "*"); -defineSymbol(math, main, bin, "+", "+"); -defineSymbol(math, main, bin, "\u2212", "-"); -defineSymbol(math, main, bin, "\u22c5", "\\cdot", true); -defineSymbol(math, main, bin, "\u2218", "\\circ"); -defineSymbol(math, main, bin, "\u00f7", "\\div", true); -defineSymbol(math, main, bin, "\u00b1", "\\pm", true); -defineSymbol(math, main, bin, "\u00d7", "\\times", true); -defineSymbol(math, main, bin, "\u2229", "\\cap", true); -defineSymbol(math, main, bin, "\u222a", "\\cup", true); -defineSymbol(math, main, bin, "\u2216", "\\setminus"); -defineSymbol(math, main, bin, "\u2227", "\\land"); -defineSymbol(math, main, bin, "\u2228", "\\lor"); -defineSymbol(math, main, bin, "\u2227", "\\wedge", true); -defineSymbol(math, main, bin, "\u2228", "\\vee", true); -defineSymbol(math, main, textord, "\u221a", "\\surd"); -defineSymbol(math, main, open, "\u27e8", "\\langle", true); -defineSymbol(math, main, open, "\u2223", "\\lvert"); -defineSymbol(math, main, open, "\u2225", "\\lVert"); -defineSymbol(math, main, close, "?", "?"); -defineSymbol(math, main, close, "!", "!"); -defineSymbol(math, main, close, "\u27e9", "\\rangle", true); -defineSymbol(math, main, close, "\u2223", "\\rvert"); -defineSymbol(math, main, close, "\u2225", "\\rVert"); -defineSymbol(math, main, rel, "=", "="); -defineSymbol(math, main, rel, ":", ":"); -defineSymbol(math, main, rel, "\u2248", "\\approx", true); -defineSymbol(math, main, rel, "\u2245", "\\cong", true); -defineSymbol(math, main, rel, "\u2265", "\\ge"); -defineSymbol(math, main, rel, "\u2265", "\\geq", true); -defineSymbol(math, main, rel, "\u2190", "\\gets"); -defineSymbol(math, main, rel, ">", "\\gt", true); -defineSymbol(math, main, rel, "\u2208", "\\in", true); -defineSymbol(math, main, rel, "\ue020", "\\@not"); -defineSymbol(math, main, rel, "\u2282", "\\subset", true); -defineSymbol(math, main, rel, "\u2283", "\\supset", true); -defineSymbol(math, main, rel, "\u2286", "\\subseteq", true); -defineSymbol(math, main, rel, "\u2287", "\\supseteq", true); -defineSymbol(math, ams, rel, "\u2288", "\\nsubseteq", true); -defineSymbol(math, ams, rel, "\u2289", "\\nsupseteq", true); -defineSymbol(math, main, rel, "\u22a8", "\\models"); -defineSymbol(math, main, rel, "\u2190", "\\leftarrow", true); -defineSymbol(math, main, rel, "\u2264", "\\le"); -defineSymbol(math, main, rel, "\u2264", "\\leq", true); -defineSymbol(math, main, rel, "<", "\\lt", true); -defineSymbol(math, main, rel, "\u2192", "\\rightarrow", true); -defineSymbol(math, main, rel, "\u2192", "\\to"); -defineSymbol(math, ams, rel, "\u2271", "\\ngeq", true); -defineSymbol(math, ams, rel, "\u2270", "\\nleq", true); -defineSymbol(math, main, spacing, "\u00a0", "\\ "); -defineSymbol(math, main, spacing, "\u00a0", "~"); -defineSymbol(math, main, spacing, "\u00a0", "\\space"); // Ref: LaTeX Source 2e: \DeclareRobustCommand{\nobreakspace}{% - -defineSymbol(math, main, spacing, "\u00a0", "\\nobreakspace"); -defineSymbol(text$1, main, spacing, "\u00a0", "\\ "); -defineSymbol(text$1, main, spacing, "\u00a0", " "); -defineSymbol(text$1, main, spacing, "\u00a0", "~"); -defineSymbol(text$1, main, spacing, "\u00a0", "\\space"); -defineSymbol(text$1, main, spacing, "\u00a0", "\\nobreakspace"); -defineSymbol(math, main, spacing, null, "\\nobreak"); -defineSymbol(math, main, spacing, null, "\\allowbreak"); -defineSymbol(math, main, punct, ",", ","); -defineSymbol(math, main, punct, ";", ";"); -defineSymbol(math, ams, bin, "\u22bc", "\\barwedge", true); -defineSymbol(math, ams, bin, "\u22bb", "\\veebar", true); -defineSymbol(math, main, bin, "\u2299", "\\odot", true); -defineSymbol(math, main, bin, "\u2295", "\\oplus", true); -defineSymbol(math, main, bin, "\u2297", "\\otimes", true); -defineSymbol(math, main, textord, "\u2202", "\\partial", true); -defineSymbol(math, main, bin, "\u2298", "\\oslash", true); -defineSymbol(math, ams, bin, "\u229a", "\\circledcirc", true); -defineSymbol(math, ams, bin, "\u22a1", "\\boxdot", true); -defineSymbol(math, main, bin, "\u25b3", "\\bigtriangleup"); -defineSymbol(math, main, bin, "\u25bd", "\\bigtriangledown"); -defineSymbol(math, main, bin, "\u2020", "\\dagger"); -defineSymbol(math, main, bin, "\u22c4", "\\diamond"); -defineSymbol(math, main, bin, "\u22c6", "\\star"); -defineSymbol(math, main, bin, "\u25c3", "\\triangleleft"); -defineSymbol(math, main, bin, "\u25b9", "\\triangleright"); -defineSymbol(math, main, open, "{", "\\{"); -defineSymbol(text$1, main, textord, "{", "\\{"); -defineSymbol(text$1, main, textord, "{", "\\textbraceleft"); -defineSymbol(math, main, close, "}", "\\}"); -defineSymbol(text$1, main, textord, "}", "\\}"); -defineSymbol(text$1, main, textord, "}", "\\textbraceright"); -defineSymbol(math, main, open, "{", "\\lbrace"); -defineSymbol(math, main, close, "}", "\\rbrace"); -defineSymbol(math, main, open, "[", "\\lbrack", true); -defineSymbol(text$1, main, textord, "[", "\\lbrack", true); -defineSymbol(math, main, close, "]", "\\rbrack", true); -defineSymbol(text$1, main, textord, "]", "\\rbrack", true); -defineSymbol(math, main, open, "(", "\\lparen", true); -defineSymbol(math, main, close, ")", "\\rparen", true); -defineSymbol(text$1, main, textord, "<", "\\textless", true); // in T1 fontenc - -defineSymbol(text$1, main, textord, ">", "\\textgreater", true); // in T1 fontenc - -defineSymbol(math, main, open, "\u230a", "\\lfloor", true); -defineSymbol(math, main, close, "\u230b", "\\rfloor", true); -defineSymbol(math, main, open, "\u2308", "\\lceil", true); -defineSymbol(math, main, close, "\u2309", "\\rceil", true); -defineSymbol(math, main, textord, "\\", "\\backslash"); -defineSymbol(math, main, textord, "\u2223", "|"); -defineSymbol(math, main, textord, "\u2223", "\\vert"); -defineSymbol(text$1, main, textord, "|", "\\textbar", true); // in T1 fontenc - -defineSymbol(math, main, textord, "\u2225", "\\|"); -defineSymbol(math, main, textord, "\u2225", "\\Vert"); -defineSymbol(text$1, main, textord, "\u2225", "\\textbardbl"); -defineSymbol(text$1, main, textord, "~", "\\textasciitilde"); -defineSymbol(text$1, main, textord, "\\", "\\textbackslash"); -defineSymbol(text$1, main, textord, "^", "\\textasciicircum"); -defineSymbol(math, main, rel, "\u2191", "\\uparrow", true); -defineSymbol(math, main, rel, "\u21d1", "\\Uparrow", true); -defineSymbol(math, main, rel, "\u2193", "\\downarrow", true); -defineSymbol(math, main, rel, "\u21d3", "\\Downarrow", true); -defineSymbol(math, main, rel, "\u2195", "\\updownarrow", true); -defineSymbol(math, main, rel, "\u21d5", "\\Updownarrow", true); -defineSymbol(math, main, op, "\u2210", "\\coprod"); -defineSymbol(math, main, op, "\u22c1", "\\bigvee"); -defineSymbol(math, main, op, "\u22c0", "\\bigwedge"); -defineSymbol(math, main, op, "\u2a04", "\\biguplus"); -defineSymbol(math, main, op, "\u22c2", "\\bigcap"); -defineSymbol(math, main, op, "\u22c3", "\\bigcup"); -defineSymbol(math, main, op, "\u222b", "\\int"); -defineSymbol(math, main, op, "\u222b", "\\intop"); -defineSymbol(math, main, op, "\u222c", "\\iint"); -defineSymbol(math, main, op, "\u222d", "\\iiint"); -defineSymbol(math, main, op, "\u220f", "\\prod"); -defineSymbol(math, main, op, "\u2211", "\\sum"); -defineSymbol(math, main, op, "\u2a02", "\\bigotimes"); -defineSymbol(math, main, op, "\u2a01", "\\bigoplus"); -defineSymbol(math, main, op, "\u2a00", "\\bigodot"); -defineSymbol(math, main, op, "\u222e", "\\oint"); -defineSymbol(math, main, op, "\u2a06", "\\bigsqcup"); -defineSymbol(math, main, op, "\u222b", "\\smallint"); -defineSymbol(text$1, main, inner, "\u2026", "\\textellipsis"); -defineSymbol(math, main, inner, "\u2026", "\\mathellipsis"); -defineSymbol(text$1, main, inner, "\u2026", "\\ldots", true); -defineSymbol(math, main, inner, "\u2026", "\\ldots", true); -defineSymbol(math, main, inner, "\u22ef", "\\@cdots", true); -defineSymbol(math, main, inner, "\u22f1", "\\ddots", true); -defineSymbol(math, main, textord, "\u22ee", "\\varvdots"); // \vdots is a macro - -defineSymbol(math, main, accent, "\u02ca", "\\acute"); -defineSymbol(math, main, accent, "\u02cb", "\\grave"); -defineSymbol(math, main, accent, "\u00a8", "\\ddot"); -defineSymbol(math, main, accent, "\u007e", "\\tilde"); -defineSymbol(math, main, accent, "\u02c9", "\\bar"); -defineSymbol(math, main, accent, "\u02d8", "\\breve"); -defineSymbol(math, main, accent, "\u02c7", "\\check"); -defineSymbol(math, main, accent, "\u005e", "\\hat"); -defineSymbol(math, main, accent, "\u20d7", "\\vec"); -defineSymbol(math, main, accent, "\u02d9", "\\dot"); -defineSymbol(math, main, accent, "\u02da", "\\mathring"); // \imath and \jmath should be invariant to \mathrm, \mathbf, etc., so use PUA - -defineSymbol(math, main, mathord, "\ue131", "\\@imath"); -defineSymbol(math, main, mathord, "\ue237", "\\@jmath"); -defineSymbol(math, main, textord, "\u0131", "\u0131"); -defineSymbol(math, main, textord, "\u0237", "\u0237"); -defineSymbol(text$1, main, textord, "\u0131", "\\i", true); -defineSymbol(text$1, main, textord, "\u0237", "\\j", true); -defineSymbol(text$1, main, textord, "\u00df", "\\ss", true); -defineSymbol(text$1, main, textord, "\u00e6", "\\ae", true); -defineSymbol(text$1, main, textord, "\u0153", "\\oe", true); -defineSymbol(text$1, main, textord, "\u00f8", "\\o", true); -defineSymbol(text$1, main, textord, "\u00c6", "\\AE", true); -defineSymbol(text$1, main, textord, "\u0152", "\\OE", true); -defineSymbol(text$1, main, textord, "\u00d8", "\\O", true); -defineSymbol(text$1, main, accent, "\u02ca", "\\'"); // acute - -defineSymbol(text$1, main, accent, "\u02cb", "\\`"); // grave - -defineSymbol(text$1, main, accent, "\u02c6", "\\^"); // circumflex - -defineSymbol(text$1, main, accent, "\u02dc", "\\~"); // tilde - -defineSymbol(text$1, main, accent, "\u02c9", "\\="); // macron - -defineSymbol(text$1, main, accent, "\u02d8", "\\u"); // breve - -defineSymbol(text$1, main, accent, "\u02d9", "\\."); // dot above - -defineSymbol(text$1, main, accent, "\u02da", "\\r"); // ring above - -defineSymbol(text$1, main, accent, "\u02c7", "\\v"); // caron - -defineSymbol(text$1, main, accent, "\u00a8", '\\"'); // diaeresis - -defineSymbol(text$1, main, accent, "\u02dd", "\\H"); // double acute - -defineSymbol(text$1, main, accent, "\u25ef", "\\textcircled"); // \bigcirc glyph -// These ligatures are detected and created in Parser.js's `formLigatures`. - -const ligatures = { - "--": true, - "---": true, - "``": true, - "''": true -}; -defineSymbol(text$1, main, textord, "\u2013", "--", true); -defineSymbol(text$1, main, textord, "\u2013", "\\textendash"); -defineSymbol(text$1, main, textord, "\u2014", "---", true); -defineSymbol(text$1, main, textord, "\u2014", "\\textemdash"); -defineSymbol(text$1, main, textord, "\u2018", "`", true); -defineSymbol(text$1, main, textord, "\u2018", "\\textquoteleft"); -defineSymbol(text$1, main, textord, "\u2019", "'", true); -defineSymbol(text$1, main, textord, "\u2019", "\\textquoteright"); -defineSymbol(text$1, main, textord, "\u201c", "``", true); -defineSymbol(text$1, main, textord, "\u201c", "\\textquotedblleft"); -defineSymbol(text$1, main, textord, "\u201d", "''", true); -defineSymbol(text$1, main, textord, "\u201d", "\\textquotedblright"); // \degree from gensymb package - -defineSymbol(math, main, textord, "\u00b0", "\\degree", true); -defineSymbol(text$1, main, textord, "\u00b0", "\\degree"); // \textdegree from inputenc package - -defineSymbol(text$1, main, textord, "\u00b0", "\\textdegree", true); // TODO: In LaTeX, \pounds can generate a different character in text and math -// mode, but among our fonts, only Main-Regular defines this character "163". - -defineSymbol(math, main, textord, "\u00a3", "\\pounds"); -defineSymbol(math, main, textord, "\u00a3", "\\mathsterling", true); -defineSymbol(text$1, main, textord, "\u00a3", "\\pounds"); -defineSymbol(text$1, main, textord, "\u00a3", "\\textsterling", true); -defineSymbol(math, ams, textord, "\u2720", "\\maltese"); -defineSymbol(text$1, ams, textord, "\u2720", "\\maltese"); // There are lots of symbols which are the same, so we add them in afterwards. -// All of these are textords in math mode - -const mathTextSymbols = "0123456789/@.\""; - -for (let i = 0; i < mathTextSymbols.length; i++) { - const ch = mathTextSymbols.charAt(i); - defineSymbol(math, main, textord, ch, ch); -} // All of these are textords in text mode - - -const textSymbols = "0123456789!@*()-=+\";:?/.,"; - -for (let i = 0; i < textSymbols.length; i++) { - const ch = textSymbols.charAt(i); - defineSymbol(text$1, main, textord, ch, ch); -} // All of these are textords in text mode, and mathords in math mode - - -const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - -for (let i = 0; i < letters.length; i++) { - const ch = letters.charAt(i); - defineSymbol(math, main, mathord, ch, ch); - defineSymbol(text$1, main, textord, ch, ch); -} // Blackboard bold and script letters in Unicode range - - -defineSymbol(math, ams, textord, "C", "\u2102"); // blackboard bold - -defineSymbol(text$1, ams, textord, "C", "\u2102"); -defineSymbol(math, ams, textord, "H", "\u210D"); -defineSymbol(text$1, ams, textord, "H", "\u210D"); -defineSymbol(math, ams, textord, "N", "\u2115"); -defineSymbol(text$1, ams, textord, "N", "\u2115"); -defineSymbol(math, ams, textord, "P", "\u2119"); -defineSymbol(text$1, ams, textord, "P", "\u2119"); -defineSymbol(math, ams, textord, "Q", "\u211A"); -defineSymbol(text$1, ams, textord, "Q", "\u211A"); -defineSymbol(math, ams, textord, "R", "\u211D"); -defineSymbol(text$1, ams, textord, "R", "\u211D"); -defineSymbol(math, ams, textord, "Z", "\u2124"); -defineSymbol(text$1, ams, textord, "Z", "\u2124"); -defineSymbol(math, main, mathord, "h", "\u210E"); // italic h, Planck constant - -defineSymbol(text$1, main, mathord, "h", "\u210E"); // The next loop loads wide (surrogate pair) characters. -// We support some letters in the Unicode range U+1D400 to U+1D7FF, -// Mathematical Alphanumeric Symbols. -// Some editors do not deal well with wide characters. So don't write the -// string into this file. Instead, create the string from the surrogate pair. - -let wideChar = ""; - -for (let i = 0; i < letters.length; i++) { - const ch = letters.charAt(i); // The hex numbers in the next line are a surrogate pair. - // 0xD835 is the high surrogate for all letters in the range we support. - // 0xDC00 is the low surrogate for bold A. - - wideChar = String.fromCharCode(0xD835, 0xDC00 + i); // A-Z a-z bold - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); - wideChar = String.fromCharCode(0xD835, 0xDC34 + i); // A-Z a-z italic - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); - wideChar = String.fromCharCode(0xD835, 0xDC68 + i); // A-Z a-z bold italic - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); - wideChar = String.fromCharCode(0xD835, 0xDD04 + i); // A-Z a-z Fractur - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); - wideChar = String.fromCharCode(0xD835, 0xDDA0 + i); // A-Z a-z sans-serif - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); - wideChar = String.fromCharCode(0xD835, 0xDDD4 + i); // A-Z a-z sans bold - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); - wideChar = String.fromCharCode(0xD835, 0xDE08 + i); // A-Z a-z sans italic - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); - wideChar = String.fromCharCode(0xD835, 0xDE70 + i); // A-Z a-z monospace - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); - - if (i < 26) { - // KaTeX fonts have only capital letters for blackboard bold and script. - // See exception for k below. - wideChar = String.fromCharCode(0xD835, 0xDD38 + i); // A-Z double struck - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); - wideChar = String.fromCharCode(0xD835, 0xDC9C + i); // A-Z script - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); - } // TODO: Add bold script when it is supported by a KaTeX font. - -} // "k" is the only double struck lower case letter in the KaTeX fonts. - - -wideChar = String.fromCharCode(0xD835, 0xDD5C); // k double struck - -defineSymbol(math, main, mathord, "k", wideChar); -defineSymbol(text$1, main, textord, "k", wideChar); // Next, some wide character numerals - -for (let i = 0; i < 10; i++) { - const ch = i.toString(); - wideChar = String.fromCharCode(0xD835, 0xDFCE + i); // 0-9 bold - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); - wideChar = String.fromCharCode(0xD835, 0xDFE2 + i); // 0-9 sans serif - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); - wideChar = String.fromCharCode(0xD835, 0xDFEC + i); // 0-9 bold sans - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); - wideChar = String.fromCharCode(0xD835, 0xDFF6 + i); // 0-9 monospace - - defineSymbol(math, main, mathord, ch, wideChar); - defineSymbol(text$1, main, textord, ch, wideChar); -} // We add these Latin-1 letters as symbols for backwards-compatibility, -// but they are not actually in the font, nor are they supported by the -// Unicode accent mechanism, so they fall back to Times font and look ugly. -// TODO(edemaine): Fix this. - - -const extraLatin = "\u00c7\u00d0\u00de\u00e7\u00fe"; - -for (let i = 0; i < extraLatin.length; i++) { - const ch = extraLatin.charAt(i); - defineSymbol(math, main, mathord, ch, ch); - defineSymbol(text$1, main, textord, ch, ch); -} - -/** - * This file provides support for Unicode range U+1D400 to U+1D7FF, - * Mathematical Alphanumeric Symbols. - * - * Function wideCharacterFont takes a wide character as input and returns - * the font information necessary to render it properly. - */ -/** - * Data below is from https://www.unicode.org/charts/PDF/U1D400.pdf - * That document sorts characters into groups by font type, say bold or italic. - * - * In the arrays below, each subarray consists three elements: - * * The CSS class of that group when in math mode. - * * The CSS class of that group when in text mode. - * * The font name, so that KaTeX can get font metrics. - */ - -const wideLatinLetterData = [["mathbf", "textbf", "Main-Bold"], // A-Z bold upright -["mathbf", "textbf", "Main-Bold"], // a-z bold upright -["mathnormal", "textit", "Math-Italic"], // A-Z italic -["mathnormal", "textit", "Math-Italic"], // a-z italic -["boldsymbol", "boldsymbol", "Main-BoldItalic"], // A-Z bold italic -["boldsymbol", "boldsymbol", "Main-BoldItalic"], // a-z bold italic -// Map fancy A-Z letters to script, not calligraphic. -// This aligns with unicode-math and math fonts (except Cambria Math). -["mathscr", "textscr", "Script-Regular"], // A-Z script -["", "", ""], // a-z script. No font -["", "", ""], // A-Z bold script. No font -["", "", ""], // a-z bold script. No font -["mathfrak", "textfrak", "Fraktur-Regular"], // A-Z Fraktur -["mathfrak", "textfrak", "Fraktur-Regular"], // a-z Fraktur -["mathbb", "textbb", "AMS-Regular"], // A-Z double-struck -["mathbb", "textbb", "AMS-Regular"], // k double-struck -["", "", ""], // A-Z bold Fraktur No font metrics -["", "", ""], // a-z bold Fraktur. No font. -["mathsf", "textsf", "SansSerif-Regular"], // A-Z sans-serif -["mathsf", "textsf", "SansSerif-Regular"], // a-z sans-serif -["mathboldsf", "textboldsf", "SansSerif-Bold"], // A-Z bold sans-serif -["mathboldsf", "textboldsf", "SansSerif-Bold"], // a-z bold sans-serif -["mathitsf", "textitsf", "SansSerif-Italic"], // A-Z italic sans-serif -["mathitsf", "textitsf", "SansSerif-Italic"], // a-z italic sans-serif -["", "", ""], // A-Z bold italic sans. No font -["", "", ""], // a-z bold italic sans. No font -["mathtt", "texttt", "Typewriter-Regular"], // A-Z monospace -["mathtt", "texttt", "Typewriter-Regular"]]; -const wideNumeralData = [["mathbf", "textbf", "Main-Bold"], // 0-9 bold -["", "", ""], // 0-9 double-struck. No KaTeX font. -["mathsf", "textsf", "SansSerif-Regular"], // 0-9 sans-serif -["mathboldsf", "textboldsf", "SansSerif-Bold"], // 0-9 bold sans-serif -["mathtt", "texttt", "Typewriter-Regular"]]; -const wideCharacterFont = function wideCharacterFont(wideChar, mode) { - // IE doesn't support codePointAt(). So work with the surrogate pair. - const H = wideChar.charCodeAt(0); // high surrogate - - const L = wideChar.charCodeAt(1); // low surrogate - - const codePoint = (H - 0xD800) * 0x400 + (L - 0xDC00) + 0x10000; - const j = mode === "math" ? 0 : 1; // column index for CSS class. - - if (0x1D400 <= codePoint && codePoint < 0x1D6A4) { - // wideLatinLetterData contains exactly 26 chars on each row. - // So we can calculate the relevant row. No traverse necessary. - const i = Math.floor((codePoint - 0x1D400) / 26); - return [wideLatinLetterData[i][2], wideLatinLetterData[i][j]]; - } else if (0x1D7CE <= codePoint && codePoint <= 0x1D7FF) { - // Numerals, ten per row. - const i = Math.floor((codePoint - 0x1D7CE) / 10); - return [wideNumeralData[i][2], wideNumeralData[i][j]]; - } else if (codePoint === 0x1D6A5 || codePoint === 0x1D6A6) { - // dotless i or j - return [wideLatinLetterData[0][2], wideLatinLetterData[0][j]]; - } else if (0x1D6A6 < codePoint && codePoint < 0x1D7CE) { - // Greek letters. Not supported, yet. - return ["", ""]; - } else { - // We don't support any wide characters outside 1D400–1D7FF. - throw new ParseError("Unsupported character: " + wideChar); - } -}; - -/** - * This file contains information about the options that the Parser carries - * around with it while parsing. Data is held in an `Options` object, and when - * recursing, a new `Options` object can be created with the `.with*` and - * `.reset` functions. - */ -const sizeStyleMap = [// Each element contains [textsize, scriptsize, scriptscriptsize]. -// The size mappings are taken from TeX with \normalsize=10pt. -[1, 1, 1], // size1: [5, 5, 5] \tiny -[2, 1, 1], // size2: [6, 5, 5] -[3, 1, 1], // size3: [7, 5, 5] \scriptsize -[4, 2, 1], // size4: [8, 6, 5] \footnotesize -[5, 2, 1], // size5: [9, 6, 5] \small -[6, 3, 1], // size6: [10, 7, 5] \normalsize -[7, 4, 2], // size7: [12, 8, 6] \large -[8, 6, 3], // size8: [14.4, 10, 7] \Large -[9, 7, 6], // size9: [17.28, 12, 10] \LARGE -[10, 8, 7], // size10: [20.74, 14.4, 12] \huge -[11, 10, 9]]; -const sizeMultipliers = [// fontMetrics.js:getGlobalMetrics also uses size indexes, so if -// you change size indexes, change that function. -0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2, 1.44, 1.728, 2.074, 2.488]; - -const sizeAtStyle = function sizeAtStyle(size, style) { - return style.size < 2 ? size : sizeStyleMap[size - 1][style.size - 1]; -}; // In these types, "" (empty string) means "no change". - - -/** - * This is the main options class. It contains the current style, size, color, - * and font. - * - * Options objects should not be modified. To create a new Options with - * different properties, call a `.having*` method. - */ -class Options { - // A font family applies to a group of fonts (i.e. SansSerif), while a font - // represents a specific font (i.e. SansSerif Bold). - // See: https://tex.stackexchange.com/questions/22350/difference-between-textrm-and-mathrm - - /** - * The base size index. - */ - constructor(data) { - this.style = void 0; - this.color = void 0; - this.size = void 0; - this.textSize = void 0; - this.phantom = void 0; - this.font = void 0; - this.fontFamily = void 0; - this.fontWeight = void 0; - this.fontShape = void 0; - this.sizeMultiplier = void 0; - this.maxSize = void 0; - this.minRuleThickness = void 0; - this._fontMetrics = void 0; - this.style = data.style; - this.color = data.color; - this.size = data.size || Options.BASESIZE; - this.textSize = data.textSize || this.size; - this.phantom = !!data.phantom; - this.font = data.font || ""; - this.fontFamily = data.fontFamily || ""; - this.fontWeight = data.fontWeight || ''; - this.fontShape = data.fontShape || ''; - this.sizeMultiplier = sizeMultipliers[this.size - 1]; - this.maxSize = data.maxSize; - this.minRuleThickness = data.minRuleThickness; - this._fontMetrics = undefined; - } - /** - * Returns a new options object with the same properties as "this". Properties - * from "extension" will be copied to the new options object. - */ - - - extend(extension) { - const data = { - style: this.style, - size: this.size, - textSize: this.textSize, - color: this.color, - phantom: this.phantom, - font: this.font, - fontFamily: this.fontFamily, - fontWeight: this.fontWeight, - fontShape: this.fontShape, - maxSize: this.maxSize, - minRuleThickness: this.minRuleThickness - }; - - for (const key in extension) { - if (extension.hasOwnProperty(key)) { - data[key] = extension[key]; - } - } - - return new Options(data); - } - /** - * Return an options object with the given style. If `this.style === style`, - * returns `this`. - */ - - - havingStyle(style) { - if (this.style === style) { - return this; - } else { - return this.extend({ - style: style, - size: sizeAtStyle(this.textSize, style) - }); - } - } - /** - * Return an options object with a cramped version of the current style. If - * the current style is cramped, returns `this`. - */ - - - havingCrampedStyle() { - return this.havingStyle(this.style.cramp()); - } - /** - * Return an options object with the given size and in at least `\textstyle`. - * Returns `this` if appropriate. - */ - - - havingSize(size) { - if (this.size === size && this.textSize === size) { - return this; - } else { - return this.extend({ - style: this.style.text(), - size: size, - textSize: size, - sizeMultiplier: sizeMultipliers[size - 1] - }); - } - } - /** - * Like `this.havingSize(BASESIZE).havingStyle(style)`. If `style` is omitted, - * changes to at least `\textstyle`. - */ - - - havingBaseStyle(style) { - style = style || this.style.text(); - const wantSize = sizeAtStyle(Options.BASESIZE, style); - - if (this.size === wantSize && this.textSize === Options.BASESIZE && this.style === style) { - return this; - } else { - return this.extend({ - style: style, - size: wantSize - }); - } - } - /** - * Remove the effect of sizing changes such as \Huge. - * Keep the effect of the current style, such as \scriptstyle. - */ - - - havingBaseSizing() { - let size; - - switch (this.style.id) { - case 4: - case 5: - size = 3; // normalsize in scriptstyle - - break; - - case 6: - case 7: - size = 1; // normalsize in scriptscriptstyle - - break; - - default: - size = 6; - // normalsize in textstyle or displaystyle - } - - return this.extend({ - style: this.style.text(), - size: size - }); - } - /** - * Create a new options object with the given color. - */ - - - withColor(color) { - return this.extend({ - color: color - }); - } - /** - * Create a new options object with "phantom" set to true. - */ - - - withPhantom() { - return this.extend({ - phantom: true - }); - } - /** - * Creates a new options object with the given math font or old text font. - * @type {[type]} - */ - - - withFont(font) { - return this.extend({ - font - }); - } - /** - * Create a new options objects with the given fontFamily. - */ - - - withTextFontFamily(fontFamily) { - return this.extend({ - fontFamily, - font: "" - }); - } - /** - * Creates a new options object with the given font weight - */ - - - withTextFontWeight(fontWeight) { - return this.extend({ - fontWeight, - font: "" - }); - } - /** - * Creates a new options object with the given font weight - */ - - - withTextFontShape(fontShape) { - return this.extend({ - fontShape, - font: "" - }); - } - /** - * Return the CSS sizing classes required to switch from enclosing options - * `oldOptions` to `this`. Returns an array of classes. - */ - - - sizingClasses(oldOptions) { - if (oldOptions.size !== this.size) { - return ["sizing", "reset-size" + oldOptions.size, "size" + this.size]; - } else { - return []; - } - } - /** - * Return the CSS sizing classes required to switch to the base size. Like - * `this.havingSize(BASESIZE).sizingClasses(this)`. - */ - - - baseSizingClasses() { - if (this.size !== Options.BASESIZE) { - return ["sizing", "reset-size" + this.size, "size" + Options.BASESIZE]; - } else { - return []; - } - } - /** - * Return the font metrics for this size. - */ - - - fontMetrics() { - if (!this._fontMetrics) { - this._fontMetrics = getGlobalMetrics(this.size); - } - - return this._fontMetrics; - } - /** - * Gets the CSS color of the current options object - */ - - - getColor() { - if (this.phantom) { - return "transparent"; - } else { - return this.color; - } - } - -} - -Options.BASESIZE = 6; - -/** - * This file does conversion between units. In particular, it provides - * calculateSize to convert other units into ems. - */ -// Thus, multiplying a length by this number converts the length from units -// into pts. Dividing the result by ptPerEm gives the number of ems -// *assuming* a font size of ptPerEm (normal size, normal style). - -const ptPerUnit = { - // https://en.wikibooks.org/wiki/LaTeX/Lengths and - // https://tex.stackexchange.com/a/8263 - "pt": 1, - // TeX point - "mm": 7227 / 2540, - // millimeter - "cm": 7227 / 254, - // centimeter - "in": 72.27, - // inch - "bp": 803 / 800, - // big (PostScript) points - "pc": 12, - // pica - "dd": 1238 / 1157, - // didot - "cc": 14856 / 1157, - // cicero (12 didot) - "nd": 685 / 642, - // new didot - "nc": 1370 / 107, - // new cicero (12 new didot) - "sp": 1 / 65536, - // scaled point (TeX's internal smallest unit) - // https://tex.stackexchange.com/a/41371 - "px": 803 / 800 // \pdfpxdimen defaults to 1 bp in pdfTeX and LuaTeX - -}; // Dictionary of relative units, for fast validity testing. - -const relativeUnit = { - "ex": true, - "em": true, - "mu": true -}; - -/** - * Determine whether the specified unit (either a string defining the unit - * or a "size" parse node containing a unit field) is valid. - */ -const validUnit = function validUnit(unit) { - if (typeof unit !== "string") { - unit = unit.unit; - } - - return unit in ptPerUnit || unit in relativeUnit || unit === "ex"; -}; -/* - * Convert a "size" parse node (with numeric "number" and string "unit" fields, - * as parsed by functions.js argType "size") into a CSS em value for the - * current style/scale. `options` gives the current options. - */ - -const calculateSize = function calculateSize(sizeValue, options) { - let scale; - - if (sizeValue.unit in ptPerUnit) { - // Absolute units - scale = ptPerUnit[sizeValue.unit] // Convert unit to pt - / options.fontMetrics().ptPerEm // Convert pt to CSS em - / options.sizeMultiplier; // Unscale to make absolute units - } else if (sizeValue.unit === "mu") { - // `mu` units scale with scriptstyle/scriptscriptstyle. - scale = options.fontMetrics().cssEmPerMu; - } else { - // Other relative units always refer to the *textstyle* font - // in the current size. - let unitOptions; - - if (options.style.isTight()) { - // isTight() means current style is script/scriptscript. - unitOptions = options.havingStyle(options.style.text()); - } else { - unitOptions = options; - } // TODO: In TeX these units are relative to the quad of the current - // *text* font, e.g. cmr10. KaTeX instead uses values from the - // comparably-sized *Computer Modern symbol* font. At 10pt, these - // match. At 7pt and 5pt, they differ: cmr7=1.138894, cmsy7=1.170641; - // cmr5=1.361133, cmsy5=1.472241. Consider $\scriptsize a\kern1emb$. - // TeX \showlists shows a kern of 1.13889 * fontsize; - // KaTeX shows a kern of 1.171 * fontsize. - - - if (sizeValue.unit === "ex") { - scale = unitOptions.fontMetrics().xHeight; - } else if (sizeValue.unit === "em") { - scale = unitOptions.fontMetrics().quad; - } else { - throw new ParseError("Invalid unit: '" + sizeValue.unit + "'"); - } - - if (unitOptions !== options) { - scale *= unitOptions.sizeMultiplier / options.sizeMultiplier; - } - } - - return Math.min(sizeValue.number * scale, options.maxSize); -}; - -/* eslint no-console:0 */ - -/** - * Looks up the given symbol in fontMetrics, after applying any symbol - * replacements defined in symbol.js - */ -const lookupSymbol = function lookupSymbol(value, // TODO(#963): Use a union type for this. -fontName, mode) { - // Replace the value with its replaced value from symbol.js - if (symbols[mode][value] && symbols[mode][value].replace) { - value = symbols[mode][value].replace; - } - - return { - value: value, - metrics: getCharacterMetrics(value, fontName, mode) - }; -}; -/** - * Makes a symbolNode after translation via the list of symbols in symbols.js. - * Correctly pulls out metrics for the character, and optionally takes a list of - * classes to be attached to the node. - * - * TODO: make argument order closer to makeSpan - * TODO: add a separate argument for math class (e.g. `mop`, `mbin`), which - * should if present come first in `classes`. - * TODO(#953): Make `options` mandatory and always pass it in. - */ - - -const makeSymbol = function makeSymbol(value, fontName, mode, options, classes) { - const lookup = lookupSymbol(value, fontName, mode); - const metrics = lookup.metrics; - value = lookup.value; - let symbolNode; - - if (metrics) { - let italic = metrics.italic; - - if (mode === "text" || options && options.font === "mathit") { - italic = 0; - } - - symbolNode = new SymbolNode(value, metrics.height, metrics.depth, italic, metrics.skew, metrics.width, classes); - } else { - // TODO(emily): Figure out a good way to only print this in development - typeof console !== "undefined" && console.warn("No character metrics " + `for '${value}' in style '${fontName}' and mode '${mode}'`); - symbolNode = new SymbolNode(value, 0, 0, 0, 0, 0, classes); - } - - if (options) { - symbolNode.maxFontSize = options.sizeMultiplier; - - if (options.style.isTight()) { - symbolNode.classes.push("mtight"); - } - - const color = options.getColor(); - - if (color) { - symbolNode.style.color = color; - } - } - - return symbolNode; -}; -/** - * Makes a symbol in Main-Regular or AMS-Regular. - * Used for rel, bin, open, close, inner, and punct. - */ - - -const mathsym = function mathsym(value, mode, options, classes) { - if (classes === void 0) { - classes = []; - } - - // Decide what font to render the symbol in by its entry in the symbols - // table. - // Have a special case for when the value = \ because the \ is used as a - // textord in unsupported command errors but cannot be parsed as a regular - // text ordinal and is therefore not present as a symbol in the symbols - // table for text, as well as a special case for boldsymbol because it - // can be used for bold + and - - if (options.font === "boldsymbol" && lookupSymbol(value, "Main-Bold", mode).metrics) { - return makeSymbol(value, "Main-Bold", mode, options, classes.concat(["mathbf"])); - } else if (value === "\\" || symbols[mode][value].font === "main") { - return makeSymbol(value, "Main-Regular", mode, options, classes); - } else { - return makeSymbol(value, "AMS-Regular", mode, options, classes.concat(["amsrm"])); - } -}; -/** - * Determines which of the two font names (Main-Bold and Math-BoldItalic) and - * corresponding style tags (mathbf or boldsymbol) to use for font "boldsymbol", - * depending on the symbol. Use this function instead of fontMap for font - * "boldsymbol". - */ - - -const boldsymbol = function boldsymbol(value, mode, options, classes, type) { - if (type !== "textord" && lookupSymbol(value, "Math-BoldItalic", mode).metrics) { - return { - fontName: "Math-BoldItalic", - fontClass: "boldsymbol" - }; - } else { - // Some glyphs do not exist in Math-BoldItalic so we need to use - // Main-Bold instead. - return { - fontName: "Main-Bold", - fontClass: "mathbf" - }; - } -}; -/** - * Makes either a mathord or textord in the correct font and color. - */ - - -const makeOrd = function makeOrd(group, options, type) { - const mode = group.mode; - const text = group.text; - const classes = ["mord"]; // Math mode or Old font (i.e. \rm) - - const isFont = mode === "math" || mode === "text" && options.font; - const fontOrFamily = isFont ? options.font : options.fontFamily; - - if (text.charCodeAt(0) === 0xD835) { - // surrogate pairs get special treatment - const _wideCharacterFont = wideCharacterFont(text, mode), - wideFontName = _wideCharacterFont[0], - wideFontClass = _wideCharacterFont[1]; - - return makeSymbol(text, wideFontName, mode, options, classes.concat(wideFontClass)); - } else if (fontOrFamily) { - let fontName; - let fontClasses; - - if (fontOrFamily === "boldsymbol") { - const fontData = boldsymbol(text, mode, options, classes, type); - fontName = fontData.fontName; - fontClasses = [fontData.fontClass]; - } else if (isFont) { - fontName = fontMap[fontOrFamily].fontName; - fontClasses = [fontOrFamily]; - } else { - fontName = retrieveTextFontName(fontOrFamily, options.fontWeight, options.fontShape); - fontClasses = [fontOrFamily, options.fontWeight, options.fontShape]; - } - - if (lookupSymbol(text, fontName, mode).metrics) { - return makeSymbol(text, fontName, mode, options, classes.concat(fontClasses)); - } else if (ligatures.hasOwnProperty(text) && fontName.substr(0, 10) === "Typewriter") { - // Deconstruct ligatures in monospace fonts (\texttt, \tt). - const parts = []; - - for (let i = 0; i < text.length; i++) { - parts.push(makeSymbol(text[i], fontName, mode, options, classes.concat(fontClasses))); - } - - return makeFragment(parts); - } - } // Makes a symbol in the default font for mathords and textords. - - - if (type === "mathord") { - return makeSymbol(text, "Math-Italic", mode, options, classes.concat(["mathnormal"])); - } else if (type === "textord") { - const font = symbols[mode][text] && symbols[mode][text].font; - - if (font === "ams") { - const fontName = retrieveTextFontName("amsrm", options.fontWeight, options.fontShape); - return makeSymbol(text, fontName, mode, options, classes.concat("amsrm", options.fontWeight, options.fontShape)); - } else if (font === "main" || !font) { - const fontName = retrieveTextFontName("textrm", options.fontWeight, options.fontShape); - return makeSymbol(text, fontName, mode, options, classes.concat(options.fontWeight, options.fontShape)); - } else { - // fonts added by plugins - const fontName = retrieveTextFontName(font, options.fontWeight, options.fontShape); // We add font name as a css class - - return makeSymbol(text, fontName, mode, options, classes.concat(fontName, options.fontWeight, options.fontShape)); - } - } else { - throw new Error("unexpected type: " + type + " in makeOrd"); - } -}; -/** - * Returns true if subsequent symbolNodes have the same classes, skew, maxFont, - * and styles. - */ - - -const canCombine = (prev, next) => { - if (createClass(prev.classes) !== createClass(next.classes) || prev.skew !== next.skew || prev.maxFontSize !== next.maxFontSize) { - return false; - } - - for (const style in prev.style) { - if (prev.style.hasOwnProperty(style) && prev.style[style] !== next.style[style]) { - return false; - } - } - - for (const style in next.style) { - if (next.style.hasOwnProperty(style) && prev.style[style] !== next.style[style]) { - return false; - } - } - - return true; -}; -/** - * Combine consecutive domTree.symbolNodes into a single symbolNode. - * Note: this function mutates the argument. - */ - - -const tryCombineChars = chars => { - for (let i = 0; i < chars.length - 1; i++) { - const prev = chars[i]; - const next = chars[i + 1]; - - if (prev instanceof SymbolNode && next instanceof SymbolNode && canCombine(prev, next)) { - prev.text += next.text; - prev.height = Math.max(prev.height, next.height); - prev.depth = Math.max(prev.depth, next.depth); // Use the last character's italic correction since we use - // it to add padding to the right of the span created from - // the combined characters. - - prev.italic = next.italic; - chars.splice(i + 1, 1); - i--; - } - } - - return chars; -}; -/** - * Calculate the height, depth, and maxFontSize of an element based on its - * children. - */ - - -const sizeElementFromChildren = function sizeElementFromChildren(elem) { - let height = 0; - let depth = 0; - let maxFontSize = 0; - - for (let i = 0; i < elem.children.length; i++) { - const child = elem.children[i]; - - if (child.height > height) { - height = child.height; - } - - if (child.depth > depth) { - depth = child.depth; - } - - if (child.maxFontSize > maxFontSize) { - maxFontSize = child.maxFontSize; - } - } - - elem.height = height; - elem.depth = depth; - elem.maxFontSize = maxFontSize; -}; -/** - * Makes a span with the given list of classes, list of children, and options. - * - * TODO(#953): Ensure that `options` is always provided (currently some call - * sites don't pass it) and make the type below mandatory. - * TODO: add a separate argument for math class (e.g. `mop`, `mbin`), which - * should if present come first in `classes`. - */ - - -const makeSpan = function makeSpan(classes, children, options, style) { - const span = new Span(classes, children, options, style); - sizeElementFromChildren(span); - return span; -}; // SVG one is simpler -- doesn't require height, depth, max-font setting. -// This is also a separate method for typesafety. - - -const makeSvgSpan = (classes, children, options, style) => new Span(classes, children, options, style); - -const makeLineSpan = function makeLineSpan(className, options, thickness) { - const line = makeSpan([className], [], options); - line.height = Math.max(thickness || options.fontMetrics().defaultRuleThickness, options.minRuleThickness); - line.style.borderBottomWidth = line.height + "em"; - line.maxFontSize = 1.0; - return line; -}; -/** - * Makes an anchor with the given href, list of classes, list of children, - * and options. - */ - - -const makeAnchor = function makeAnchor(href, classes, children, options) { - const anchor = new Anchor(href, classes, children, options); - sizeElementFromChildren(anchor); - return anchor; -}; -/** - * Makes a document fragment with the given list of children. - */ - - -const makeFragment = function makeFragment(children) { - const fragment = new DocumentFragment(children); - sizeElementFromChildren(fragment); - return fragment; -}; -/** - * Wraps group in a span if it's a document fragment, allowing to apply classes - * and styles - */ - - -const wrapFragment = function wrapFragment(group, options) { - if (group instanceof DocumentFragment) { - return makeSpan([], [group], options); - } - - return group; -}; // These are exact object types to catch typos in the names of the optional fields. - - -// Computes the updated `children` list and the overall depth. -// -// This helper function for makeVList makes it easier to enforce type safety by -// allowing early exits (returns) in the logic. -const getVListChildrenAndDepth = function getVListChildrenAndDepth(params) { - if (params.positionType === "individualShift") { - const oldChildren = params.children; - const children = [oldChildren[0]]; // Add in kerns to the list of params.children to get each element to be - // shifted to the correct specified shift - - const depth = -oldChildren[0].shift - oldChildren[0].elem.depth; - let currPos = depth; - - for (let i = 1; i < oldChildren.length; i++) { - const diff = -oldChildren[i].shift - currPos - oldChildren[i].elem.depth; - const size = diff - (oldChildren[i - 1].elem.height + oldChildren[i - 1].elem.depth); - currPos = currPos + diff; - children.push({ - type: "kern", - size - }); - children.push(oldChildren[i]); - } - - return { - children, - depth - }; - } - - let depth; - - if (params.positionType === "top") { - // We always start at the bottom, so calculate the bottom by adding up - // all the sizes - let bottom = params.positionData; - - for (let i = 0; i < params.children.length; i++) { - const child = params.children[i]; - bottom -= child.type === "kern" ? child.size : child.elem.height + child.elem.depth; - } - - depth = bottom; - } else if (params.positionType === "bottom") { - depth = -params.positionData; - } else { - const firstChild = params.children[0]; - - if (firstChild.type !== "elem") { - throw new Error('First child must have type "elem".'); - } - - if (params.positionType === "shift") { - depth = -firstChild.elem.depth - params.positionData; - } else if (params.positionType === "firstBaseline") { - depth = -firstChild.elem.depth; - } else { - throw new Error(`Invalid positionType ${params.positionType}.`); - } - } - - return { - children: params.children, - depth - }; -}; -/** - * Makes a vertical list by stacking elements and kerns on top of each other. - * Allows for many different ways of specifying the positioning method. - * - * See VListParam documentation above. - */ - - -const makeVList = function makeVList(params, options) { - const _getVListChildrenAndD = getVListChildrenAndDepth(params), - children = _getVListChildrenAndD.children, - depth = _getVListChildrenAndD.depth; // Create a strut that is taller than any list item. The strut is added to - // each item, where it will determine the item's baseline. Since it has - // `overflow:hidden`, the strut's top edge will sit on the item's line box's - // top edge and the strut's bottom edge will sit on the item's baseline, - // with no additional line-height spacing. This allows the item baseline to - // be positioned precisely without worrying about font ascent and - // line-height. - - - let pstrutSize = 0; - - for (let i = 0; i < children.length; i++) { - const child = children[i]; - - if (child.type === "elem") { - const elem = child.elem; - pstrutSize = Math.max(pstrutSize, elem.maxFontSize, elem.height); - } - } - - pstrutSize += 2; - const pstrut = makeSpan(["pstrut"], []); - pstrut.style.height = pstrutSize + "em"; // Create a new list of actual children at the correct offsets - - const realChildren = []; - let minPos = depth; - let maxPos = depth; - let currPos = depth; - - for (let i = 0; i < children.length; i++) { - const child = children[i]; - - if (child.type === "kern") { - currPos += child.size; - } else { - const elem = child.elem; - const classes = child.wrapperClasses || []; - const style = child.wrapperStyle || {}; - const childWrap = makeSpan(classes, [pstrut, elem], undefined, style); - childWrap.style.top = -pstrutSize - currPos - elem.depth + "em"; - - if (child.marginLeft) { - childWrap.style.marginLeft = child.marginLeft; - } - - if (child.marginRight) { - childWrap.style.marginRight = child.marginRight; - } - - realChildren.push(childWrap); - currPos += elem.height + elem.depth; - } - - minPos = Math.min(minPos, currPos); - maxPos = Math.max(maxPos, currPos); - } // The vlist contents go in a table-cell with `vertical-align:bottom`. - // This cell's bottom edge will determine the containing table's baseline - // without overly expanding the containing line-box. - - - const vlist = makeSpan(["vlist"], realChildren); - vlist.style.height = maxPos + "em"; // A second row is used if necessary to represent the vlist's depth. - - let rows; - - if (minPos < 0) { - // We will define depth in an empty span with display: table-cell. - // It should render with the height that we define. But Chrome, in - // contenteditable mode only, treats that span as if it contains some - // text content. And that min-height over-rides our desired height. - // So we put another empty span inside the depth strut span. - const emptySpan = makeSpan([], []); - const depthStrut = makeSpan(["vlist"], [emptySpan]); - depthStrut.style.height = -minPos + "em"; // Safari wants the first row to have inline content; otherwise it - // puts the bottom of the *second* row on the baseline. - - const topStrut = makeSpan(["vlist-s"], [new SymbolNode("\u200b")]); - rows = [makeSpan(["vlist-r"], [vlist, topStrut]), makeSpan(["vlist-r"], [depthStrut])]; - } else { - rows = [makeSpan(["vlist-r"], [vlist])]; - } - - const vtable = makeSpan(["vlist-t"], rows); - - if (rows.length === 2) { - vtable.classes.push("vlist-t2"); - } - - vtable.height = maxPos; - vtable.depth = -minPos; - return vtable; -}; // Glue is a concept from TeX which is a flexible space between elements in -// either a vertical or horizontal list. In KaTeX, at least for now, it's -// static space between elements in a horizontal layout. - - -const makeGlue = (measurement, options) => { - // Make an empty span for the space - const rule = makeSpan(["mspace"], [], options); - const size = calculateSize(measurement, options); - rule.style.marginRight = `${size}em`; - return rule; -}; // Takes font options, and returns the appropriate fontLookup name - - -const retrieveTextFontName = function retrieveTextFontName(fontFamily, fontWeight, fontShape) { - let baseFontName = ""; - - switch (fontFamily) { - case "amsrm": - baseFontName = "AMS"; - break; - - case "textrm": - baseFontName = "Main"; - break; - - case "textsf": - baseFontName = "SansSerif"; - break; - - case "texttt": - baseFontName = "Typewriter"; - break; - - default: - baseFontName = fontFamily; - // use fonts added by a plugin - } - - let fontStylesName; - - if (fontWeight === "textbf" && fontShape === "textit") { - fontStylesName = "BoldItalic"; - } else if (fontWeight === "textbf") { - fontStylesName = "Bold"; - } else if (fontWeight === "textit") { - fontStylesName = "Italic"; - } else { - fontStylesName = "Regular"; - } - - return `${baseFontName}-${fontStylesName}`; -}; -/** - * Maps TeX font commands to objects containing: - * - variant: string used for "mathvariant" attribute in buildMathML.js - * - fontName: the "style" parameter to fontMetrics.getCharacterMetrics - */ -// A map between tex font commands an MathML mathvariant attribute values - - -const fontMap = { - // styles - "mathbf": { - variant: "bold", - fontName: "Main-Bold" - }, - "mathrm": { - variant: "normal", - fontName: "Main-Regular" - }, - "textit": { - variant: "italic", - fontName: "Main-Italic" - }, - "mathit": { - variant: "italic", - fontName: "Main-Italic" - }, - "mathnormal": { - variant: "italic", - fontName: "Math-Italic" - }, - // "boldsymbol" is missing because they require the use of multiple fonts: - // Math-BoldItalic and Main-Bold. This is handled by a special case in - // makeOrd which ends up calling boldsymbol. - // families - "mathbb": { - variant: "double-struck", - fontName: "AMS-Regular" - }, - "mathcal": { - variant: "script", - fontName: "Caligraphic-Regular" - }, - "mathfrak": { - variant: "fraktur", - fontName: "Fraktur-Regular" - }, - "mathscr": { - variant: "script", - fontName: "Script-Regular" - }, - "mathsf": { - variant: "sans-serif", - fontName: "SansSerif-Regular" - }, - "mathtt": { - variant: "monospace", - fontName: "Typewriter-Regular" - } -}; -const svgData = { - // path, width, height - vec: ["vec", 0.471, 0.714], - // values from the font glyph - oiintSize1: ["oiintSize1", 0.957, 0.499], - // oval to overlay the integrand - oiintSize2: ["oiintSize2", 1.472, 0.659], - oiiintSize1: ["oiiintSize1", 1.304, 0.499], - oiiintSize2: ["oiiintSize2", 1.98, 0.659], - leftParenInner: ["leftParenInner", 0.875, 0.3], - rightParenInner: ["rightParenInner", 0.875, 0.3] -}; - -const staticSvg = function staticSvg(value, options) { - // Create a span with inline SVG for the element. - const _svgData$value = svgData[value], - pathName = _svgData$value[0], - width = _svgData$value[1], - height = _svgData$value[2]; - const path = new PathNode(pathName); - const svgNode = new SvgNode([path], { - "width": width + "em", - "height": height + "em", - // Override CSS rule `.katex svg { width: 100% }` - "style": "width:" + width + "em", - "viewBox": "0 0 " + 1000 * width + " " + 1000 * height, - "preserveAspectRatio": "xMinYMin" - }); - const span = makeSvgSpan(["overlay"], [svgNode], options); - span.height = height; - span.style.height = height + "em"; - span.style.width = width + "em"; - return span; -}; - -var buildCommon = { - fontMap, - makeSymbol, - mathsym, - makeSpan, - makeSvgSpan, - makeLineSpan, - makeAnchor, - makeFragment, - wrapFragment, - makeVList, - makeOrd, - makeGlue, - staticSvg, - svgData, - tryCombineChars -}; - -/** - * Describes spaces between different classes of atoms. - */ -const thinspace = { - number: 3, - unit: "mu" -}; -const mediumspace = { - number: 4, - unit: "mu" -}; -const thickspace = { - number: 5, - unit: "mu" -}; // Making the type below exact with all optional fields doesn't work due to -// - https://github.com/facebook/flow/issues/4582 -// - https://github.com/facebook/flow/issues/5688 -// However, since *all* fields are optional, $Shape<> works as suggested in 5688 -// above. - -// Spacing relationships for display and text styles -const spacings = { - mord: { - mop: thinspace, - mbin: mediumspace, - mrel: thickspace, - minner: thinspace - }, - mop: { - mord: thinspace, - mop: thinspace, - mrel: thickspace, - minner: thinspace - }, - mbin: { - mord: mediumspace, - mop: mediumspace, - mopen: mediumspace, - minner: mediumspace - }, - mrel: { - mord: thickspace, - mop: thickspace, - mopen: thickspace, - minner: thickspace - }, - mopen: {}, - mclose: { - mop: thinspace, - mbin: mediumspace, - mrel: thickspace, - minner: thinspace - }, - mpunct: { - mord: thinspace, - mop: thinspace, - mrel: thickspace, - mopen: thinspace, - mclose: thinspace, - mpunct: thinspace, - minner: thinspace - }, - minner: { - mord: thinspace, - mop: thinspace, - mbin: mediumspace, - mrel: thickspace, - mopen: thinspace, - mpunct: thinspace, - minner: thinspace - } -}; // Spacing relationships for script and scriptscript styles - -const tightSpacings = { - mord: { - mop: thinspace - }, - mop: { - mord: thinspace, - mop: thinspace - }, - mbin: {}, - mrel: {}, - mopen: {}, - mclose: { - mop: thinspace - }, - mpunct: {}, - minner: { - mop: thinspace - } -}; - -/** Context provided to function handlers for error messages. */ -// Note: reverse the order of the return type union will cause a flow error. -// See https://github.com/facebook/flow/issues/3663. -// More general version of `HtmlBuilder` for nodes (e.g. \sum, accent types) -// whose presence impacts super/subscripting. In this case, ParseNode<"supsub"> -// delegates its HTML building to the HtmlBuilder corresponding to these nodes. - -/** - * Final function spec for use at parse time. - * This is almost identical to `FunctionPropSpec`, except it - * 1. includes the function handler, and - * 2. requires all arguments except argTypes. - * It is generated by `defineFunction()` below. - */ - -/** - * All registered functions. - * `functions.js` just exports this same dictionary again and makes it public. - * `Parser.js` requires this dictionary. - */ -const _functions = {}; -/** - * All HTML builders. Should be only used in the `define*` and the `build*ML` - * functions. - */ - -const _htmlGroupBuilders = {}; -/** - * All MathML builders. Should be only used in the `define*` and the `build*ML` - * functions. - */ - -const _mathmlGroupBuilders = {}; -function defineFunction(_ref) { - let type = _ref.type, - names = _ref.names, - props = _ref.props, - handler = _ref.handler, - htmlBuilder = _ref.htmlBuilder, - mathmlBuilder = _ref.mathmlBuilder; - // Set default values of functions - const data = { - type, - numArgs: props.numArgs, - argTypes: props.argTypes, - greediness: props.greediness === undefined ? 1 : props.greediness, - allowedInText: !!props.allowedInText, - allowedInMath: props.allowedInMath === undefined ? true : props.allowedInMath, - numOptionalArgs: props.numOptionalArgs || 0, - infix: !!props.infix, - handler: handler - }; - - for (let i = 0; i < names.length; ++i) { - _functions[names[i]] = data; - } - - if (type) { - if (htmlBuilder) { - _htmlGroupBuilders[type] = htmlBuilder; - } - - if (mathmlBuilder) { - _mathmlGroupBuilders[type] = mathmlBuilder; - } - } -} -/** - * Use this to register only the HTML and MathML builders for a function (e.g. - * if the function's ParseNode is generated in Parser.js rather than via a - * stand-alone handler provided to `defineFunction`). - */ - -function defineFunctionBuilders(_ref2) { - let type = _ref2.type, - htmlBuilder = _ref2.htmlBuilder, - mathmlBuilder = _ref2.mathmlBuilder; - defineFunction({ - type, - names: [], - props: { - numArgs: 0 - }, - - handler() { - throw new Error('Should never be called.'); - }, - - htmlBuilder, - mathmlBuilder - }); -} // Since the corresponding buildHTML/buildMathML function expects a -// list of elements, we normalize for different kinds of arguments - -const ordargument = function ordargument(arg) { - return arg.type === "ordgroup" ? arg.body : [arg]; -}; - -/** - * This file does the main work of building a domTree structure from a parse - * tree. The entry point is the `buildHTML` function, which takes a parse tree. - * Then, the buildExpression, buildGroup, and various groupBuilders functions - * are called, to produce a final HTML tree. - */ -const makeSpan$1 = buildCommon.makeSpan; // Binary atoms (first class `mbin`) change into ordinary atoms (`mord`) -// depending on their surroundings. See TeXbook pg. 442-446, Rules 5 and 6, -// and the text before Rule 19. - -const binLeftCanceller = ["leftmost", "mbin", "mopen", "mrel", "mop", "mpunct"]; -const binRightCanceller = ["rightmost", "mrel", "mclose", "mpunct"]; -const styleMap = { - "display": Style$1.DISPLAY, - "text": Style$1.TEXT, - "script": Style$1.SCRIPT, - "scriptscript": Style$1.SCRIPTSCRIPT -}; -const DomEnum = { - mord: "mord", - mop: "mop", - mbin: "mbin", - mrel: "mrel", - mopen: "mopen", - mclose: "mclose", - mpunct: "mpunct", - minner: "minner" -}; - -/** - * Take a list of nodes, build them in order, and return a list of the built - * nodes. documentFragments are flattened into their contents, so the - * returned list contains no fragments. `isRealGroup` is true if `expression` - * is a real group (no atoms will be added on either side), as opposed to - * a partial group (e.g. one created by \color). `surrounding` is an array - * consisting type of nodes that will be added to the left and right. - */ -const buildExpression = function buildExpression(expression, options, isRealGroup, surrounding) { - if (surrounding === void 0) { - surrounding = [null, null]; - } - - // Parse expressions into `groups`. - const groups = []; - - for (let i = 0; i < expression.length; i++) { - const output = buildGroup(expression[i], options); - - if (output instanceof DocumentFragment) { - const children = output.children; - groups.push(...children); - } else { - groups.push(output); - } - } // If `expression` is a partial group, let the parent handle spacings - // to avoid processing groups multiple times. - - - if (!isRealGroup) { - return groups; - } - - let glueOptions = options; - - if (expression.length === 1) { - const node = expression[0]; - - if (node.type === "sizing") { - glueOptions = options.havingSize(node.size); - } else if (node.type === "styling") { - glueOptions = options.havingStyle(styleMap[node.style]); - } - } // Dummy spans for determining spacings between surrounding atoms. - // If `expression` has no atoms on the left or right, class "leftmost" - // or "rightmost", respectively, is used to indicate it. - - - const dummyPrev = makeSpan$1([surrounding[0] || "leftmost"], [], options); - const dummyNext = makeSpan$1([surrounding[1] || "rightmost"], [], options); // TODO: These code assumes that a node's math class is the first element - // of its `classes` array. A later cleanup should ensure this, for - // instance by changing the signature of `makeSpan`. - // Before determining what spaces to insert, perform bin cancellation. - // Binary operators change to ordinary symbols in some contexts. - - const isRoot = isRealGroup === "root"; - traverseNonSpaceNodes(groups, (node, prev) => { - const prevType = prev.classes[0]; - const type = node.classes[0]; - - if (prevType === "mbin" && utils.contains(binRightCanceller, type)) { - prev.classes[0] = "mord"; - } else if (type === "mbin" && utils.contains(binLeftCanceller, prevType)) { - node.classes[0] = "mord"; - } - }, { - node: dummyPrev - }, dummyNext, isRoot); - traverseNonSpaceNodes(groups, (node, prev) => { - const prevType = getTypeOfDomTree(prev); - const type = getTypeOfDomTree(node); // 'mtight' indicates that the node is script or scriptscript style. - - const space = prevType && type ? node.hasClass("mtight") ? tightSpacings[prevType][type] : spacings[prevType][type] : null; - - if (space) { - // Insert glue (spacing) after the `prev`. - return buildCommon.makeGlue(space, glueOptions); - } - }, { - node: dummyPrev - }, dummyNext, isRoot); - return groups; -}; // Depth-first traverse non-space `nodes`, calling `callback` with the current and -// previous node as arguments, optionally returning a node to insert after the -// previous node. `prev` is an object with the previous node and `insertAfter` -// function to insert after it. `next` is a node that will be added to the right. -// Used for bin cancellation and inserting spacings. - -const traverseNonSpaceNodes = function traverseNonSpaceNodes(nodes, callback, prev, next, isRoot) { - if (next) { - // temporarily append the right node, if exists - nodes.push(next); - } - - let i = 0; - - for (; i < nodes.length; i++) { - const node = nodes[i]; - const partialGroup = checkPartialGroup(node); - - if (partialGroup) { - // Recursive DFS - // $FlowFixMe: make nodes a $ReadOnlyArray by returning a new array - traverseNonSpaceNodes(partialGroup.children, callback, prev, null, isRoot); - continue; - } // Ignore explicit spaces (e.g., \;, \,) when determining what implicit - // spacing should go between atoms of different classes - - - const nonspace = !node.hasClass("mspace"); - - if (nonspace) { - const result = callback(node, prev.node); - - if (result) { - if (prev.insertAfter) { - prev.insertAfter(result); - } else { - // insert at front - nodes.unshift(result); - i++; - } - } - } - - if (nonspace) { - prev.node = node; - } else if (isRoot && node.hasClass("newline")) { - prev.node = makeSpan$1(["leftmost"]); // treat like beginning of line - } - - prev.insertAfter = (index => n => { - nodes.splice(index + 1, 0, n); - i++; - })(i); - } - - if (next) { - nodes.pop(); - } -}; // Check if given node is a partial group, i.e., does not affect spacing around. - - -const checkPartialGroup = function checkPartialGroup(node) { - if (node instanceof DocumentFragment || node instanceof Anchor || node instanceof Span && node.hasClass("enclosing")) { - return node; - } - - return null; -}; // Return the outermost node of a domTree. - - -const getOutermostNode = function getOutermostNode(node, side) { - const partialGroup = checkPartialGroup(node); - - if (partialGroup) { - const children = partialGroup.children; - - if (children.length) { - if (side === "right") { - return getOutermostNode(children[children.length - 1], "right"); - } else if (side === "left") { - return getOutermostNode(children[0], "left"); - } - } - } - - return node; -}; // Return math atom class (mclass) of a domTree. -// If `side` is given, it will get the type of the outermost node at given side. - - -const getTypeOfDomTree = function getTypeOfDomTree(node, side) { - if (!node) { - return null; - } - - if (side) { - node = getOutermostNode(node, side); - } // This makes a lot of assumptions as to where the type of atom - // appears. We should do a better job of enforcing this. - - - return DomEnum[node.classes[0]] || null; -}; -const makeNullDelimiter = function makeNullDelimiter(options, classes) { - const moreClasses = ["nulldelimiter"].concat(options.baseSizingClasses()); - return makeSpan$1(classes.concat(moreClasses)); -}; -/** - * buildGroup is the function that takes a group and calls the correct groupType - * function for it. It also handles the interaction of size and style changes - * between parents and children. - */ - -const buildGroup = function buildGroup(group, options, baseOptions) { - if (!group) { - return makeSpan$1(); - } - - if (_htmlGroupBuilders[group.type]) { - // Call the groupBuilders function - let groupNode = _htmlGroupBuilders[group.type](group, options); // If the size changed between the parent and the current group, account - // for that size difference. - - if (baseOptions && options.size !== baseOptions.size) { - groupNode = makeSpan$1(options.sizingClasses(baseOptions), [groupNode], options); - const multiplier = options.sizeMultiplier / baseOptions.sizeMultiplier; - groupNode.height *= multiplier; - groupNode.depth *= multiplier; - } - - return groupNode; - } else { - throw new ParseError("Got group of unknown type: '" + group.type + "'"); - } -}; -/** - * Combine an array of HTML DOM nodes (e.g., the output of `buildExpression`) - * into an unbreakable HTML node of class .base, with proper struts to - * guarantee correct vertical extent. `buildHTML` calls this repeatedly to - * make up the entire expression as a sequence of unbreakable units. - */ - -function buildHTMLUnbreakable(children, options) { - // Compute height and depth of this chunk. - const body = makeSpan$1(["base"], children, options); // Add strut, which ensures that the top of the HTML element falls at - // the height of the expression, and the bottom of the HTML element - // falls at the depth of the expression. - - const strut = makeSpan$1(["strut"]); - strut.style.height = body.height + body.depth + "em"; - strut.style.verticalAlign = -body.depth + "em"; - body.children.unshift(strut); - return body; -} -/** - * Take an entire parse tree, and build it into an appropriate set of HTML - * nodes. - */ - - -function buildHTML(tree, options) { - // Strip off outer tag wrapper for processing below. - let tag = null; - - if (tree.length === 1 && tree[0].type === "tag") { - tag = tree[0].tag; - tree = tree[0].body; - } // Build the expression contained in the tree - - - const expression = buildExpression(tree, options, "root"); - const children = []; // Create one base node for each chunk between potential line breaks. - // The TeXBook [p.173] says "A formula will be broken only after a - // relation symbol like $=$ or $<$ or $\rightarrow$, or after a binary - // operation symbol like $+$ or $-$ or $\times$, where the relation or - // binary operation is on the ``outer level'' of the formula (i.e., not - // enclosed in {...} and not part of an \over construction)." - - let parts = []; - - for (let i = 0; i < expression.length; i++) { - parts.push(expression[i]); - - if (expression[i].hasClass("mbin") || expression[i].hasClass("mrel") || expression[i].hasClass("allowbreak")) { - // Put any post-operator glue on same line as operator. - // Watch for \nobreak along the way, and stop at \newline. - let nobreak = false; - - while (i < expression.length - 1 && expression[i + 1].hasClass("mspace") && !expression[i + 1].hasClass("newline")) { - i++; - parts.push(expression[i]); - - if (expression[i].hasClass("nobreak")) { - nobreak = true; - } - } // Don't allow break if \nobreak among the post-operator glue. - - - if (!nobreak) { - children.push(buildHTMLUnbreakable(parts, options)); - parts = []; - } - } else if (expression[i].hasClass("newline")) { - // Write the line except the newline - parts.pop(); - - if (parts.length > 0) { - children.push(buildHTMLUnbreakable(parts, options)); - parts = []; - } // Put the newline at the top level - - - children.push(expression[i]); - } - } - - if (parts.length > 0) { - children.push(buildHTMLUnbreakable(parts, options)); - } // Now, if there was a tag, build it too and append it as a final child. - - - let tagChild; - - if (tag) { - tagChild = buildHTMLUnbreakable(buildExpression(tag, options, true)); - tagChild.classes = ["tag"]; - children.push(tagChild); - } - - const htmlNode = makeSpan$1(["katex-html"], children); - htmlNode.setAttribute("aria-hidden", "true"); // Adjust the strut of the tag to be the maximum height of all children - // (the height of the enclosing htmlNode) for proper vertical alignment. - - if (tagChild) { - const strut = tagChild.children[0]; - strut.style.height = htmlNode.height + htmlNode.depth + "em"; - strut.style.verticalAlign = -htmlNode.depth + "em"; - } - - return htmlNode; -} - -/** - * These objects store data about MathML nodes. This is the MathML equivalent - * of the types in domTree.js. Since MathML handles its own rendering, and - * since we're mainly using MathML to improve accessibility, we don't manage - * any of the styling state that the plain DOM nodes do. - * - * The `toNode` and `toMarkup` functions work similarly to how they do in - * domTree.js, creating namespaced DOM nodes and HTML text markup respectively. - */ -function newDocumentFragment(children) { - return new DocumentFragment(children); -} -/** - * This node represents a general purpose MathML node of any type. The - * constructor requires the type of node to create (for example, `"mo"` or - * `"mspace"`, corresponding to `` and `` tags). - */ - -class MathNode { - constructor(type, children) { - this.type = void 0; - this.attributes = void 0; - this.children = void 0; - this.type = type; - this.attributes = {}; - this.children = children || []; - } - /** - * Sets an attribute on a MathML node. MathML depends on attributes to convey a - * semantic content, so this is used heavily. - */ - - - setAttribute(name, value) { - this.attributes[name] = value; - } - /** - * Gets an attribute on a MathML node. - */ - - - getAttribute(name) { - return this.attributes[name]; - } - /** - * Converts the math node into a MathML-namespaced DOM element. - */ - - - toNode() { - const node = document.createElementNS("http://www.w3.org/1998/Math/MathML", this.type); - - for (const attr in this.attributes) { - if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { - node.setAttribute(attr, this.attributes[attr]); - } - } - - for (let i = 0; i < this.children.length; i++) { - node.appendChild(this.children[i].toNode()); - } - - return node; - } - /** - * Converts the math node into an HTML markup string. - */ - - - toMarkup() { - let markup = "<" + this.type; // Add the attributes - - for (const attr in this.attributes) { - if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { - markup += " " + attr + "=\""; - markup += utils.escape(this.attributes[attr]); - markup += "\""; - } - } - - markup += ">"; - - for (let i = 0; i < this.children.length; i++) { - markup += this.children[i].toMarkup(); - } - - markup += ""; - return markup; - } - /** - * Converts the math node into a string, similar to innerText, but escaped. - */ - - - toText() { - return this.children.map(child => child.toText()).join(""); - } - -} -/** - * This node represents a piece of text. - */ - -class TextNode { - constructor(text) { - this.text = void 0; - this.text = text; - } - /** - * Converts the text node into a DOM text node. - */ - - - toNode() { - return document.createTextNode(this.text); - } - /** - * Converts the text node into escaped HTML markup - * (representing the text itself). - */ - - - toMarkup() { - return utils.escape(this.toText()); - } - /** - * Converts the text node into a string - * (representing the text itself). - */ - - - toText() { - return this.text; - } - -} -/** - * This node represents a space, but may render as or as text, - * depending on the width. - */ - -class SpaceNode { - /** - * Create a Space node with width given in CSS ems. - */ - constructor(width) { - this.width = void 0; - this.character = void 0; - this.width = width; // See https://www.w3.org/TR/2000/WD-MathML2-20000328/chapter6.html - // for a table of space-like characters. We use Unicode - // representations instead of &LongNames; as it's not clear how to - // make the latter via document.createTextNode. - - if (width >= 0.05555 && width <= 0.05556) { - this.character = "\u200a"; //   - } else if (width >= 0.1666 && width <= 0.1667) { - this.character = "\u2009"; //   - } else if (width >= 0.2222 && width <= 0.2223) { - this.character = "\u2005"; //   - } else if (width >= 0.2777 && width <= 0.2778) { - this.character = "\u2005\u200a"; //    - } else if (width >= -0.05556 && width <= -0.05555) { - this.character = "\u200a\u2063"; // ​ - } else if (width >= -0.1667 && width <= -0.1666) { - this.character = "\u2009\u2063"; // ​ - } else if (width >= -0.2223 && width <= -0.2222) { - this.character = "\u205f\u2063"; // ​ - } else if (width >= -0.2778 && width <= -0.2777) { - this.character = "\u2005\u2063"; // ​ - } else { - this.character = null; - } - } - /** - * Converts the math node into a MathML-namespaced DOM element. - */ - - - toNode() { - if (this.character) { - return document.createTextNode(this.character); - } else { - const node = document.createElementNS("http://www.w3.org/1998/Math/MathML", "mspace"); - node.setAttribute("width", this.width + "em"); - return node; - } - } - /** - * Converts the math node into an HTML markup string. - */ - - - toMarkup() { - if (this.character) { - return `${this.character}`; - } else { - return ``; - } - } - /** - * Converts the math node into a string, similar to innerText. - */ - - - toText() { - if (this.character) { - return this.character; - } else { - return " "; - } - } - -} - -var mathMLTree = { - MathNode, - TextNode, - SpaceNode, - newDocumentFragment -}; - -/** - * This file converts a parse tree into a corresponding MathML tree. The main - * entry point is the `buildMathML` function, which takes a parse tree from the - * parser. - */ - -/** - * Takes a symbol and converts it into a MathML text node after performing - * optional replacement from symbols.js. - */ -const makeText = function makeText(text, mode, options) { - if (symbols[mode][text] && symbols[mode][text].replace && text.charCodeAt(0) !== 0xD835 && !(ligatures.hasOwnProperty(text) && options && (options.fontFamily && options.fontFamily.substr(4, 2) === "tt" || options.font && options.font.substr(4, 2) === "tt"))) { - text = symbols[mode][text].replace; - } - - return new mathMLTree.TextNode(text); -}; -/** - * Wrap the given array of nodes in an node if needed, i.e., - * unless the array has length 1. Always returns a single node. - */ - -const makeRow = function makeRow(body) { - if (body.length === 1) { - return body[0]; - } else { - return new mathMLTree.MathNode("mrow", body); - } -}; -/** - * Returns the math variant as a string or null if none is required. - */ - -const getVariant = function getVariant(group, options) { - // Handle \text... font specifiers as best we can. - // MathML has a limited list of allowable mathvariant specifiers; see - // https://www.w3.org/TR/MathML3/chapter3.html#presm.commatt - if (options.fontFamily === "texttt") { - return "monospace"; - } else if (options.fontFamily === "textsf") { - if (options.fontShape === "textit" && options.fontWeight === "textbf") { - return "sans-serif-bold-italic"; - } else if (options.fontShape === "textit") { - return "sans-serif-italic"; - } else if (options.fontWeight === "textbf") { - return "bold-sans-serif"; - } else { - return "sans-serif"; - } - } else if (options.fontShape === "textit" && options.fontWeight === "textbf") { - return "bold-italic"; - } else if (options.fontShape === "textit") { - return "italic"; - } else if (options.fontWeight === "textbf") { - return "bold"; - } - - const font = options.font; - - if (!font || font === "mathnormal") { - return null; - } - - const mode = group.mode; - - if (font === "mathit") { - return "italic"; - } else if (font === "boldsymbol") { - return group.type === "textord" ? "bold" : "bold-italic"; - } else if (font === "mathbf") { - return "bold"; - } else if (font === "mathbb") { - return "double-struck"; - } else if (font === "mathfrak") { - return "fraktur"; - } else if (font === "mathscr" || font === "mathcal") { - // MathML makes no distinction between script and caligrahpic - return "script"; - } else if (font === "mathsf") { - return "sans-serif"; - } else if (font === "mathtt") { - return "monospace"; - } - - let text = group.text; - - if (utils.contains(["\\imath", "\\jmath"], text)) { - return null; - } - - if (symbols[mode][text] && symbols[mode][text].replace) { - text = symbols[mode][text].replace; - } - - const fontName = buildCommon.fontMap[font].fontName; - - if (getCharacterMetrics(text, fontName, mode)) { - return buildCommon.fontMap[font].variant; - } - - return null; -}; -/** - * Takes a list of nodes, builds them, and returns a list of the generated - * MathML nodes. Also combine consecutive outputs into a single - * tag. - */ - -const buildExpression$1 = function buildExpression(expression, options, isOrdgroup) { - if (expression.length === 1) { - const group = buildGroup$1(expression[0], options); - - if (isOrdgroup && group instanceof MathNode && group.type === "mo") { - // When TeX writers want to suppress spacing on an operator, - // they often put the operator by itself inside braces. - group.setAttribute("lspace", "0em"); - group.setAttribute("rspace", "0em"); - } - - return [group]; - } - - const groups = []; - let lastGroup; - - for (let i = 0; i < expression.length; i++) { - const group = buildGroup$1(expression[i], options); - - if (group instanceof MathNode && lastGroup instanceof MathNode) { - // Concatenate adjacent s - if (group.type === 'mtext' && lastGroup.type === 'mtext' && group.getAttribute('mathvariant') === lastGroup.getAttribute('mathvariant')) { - lastGroup.children.push(...group.children); - continue; // Concatenate adjacent s - } else if (group.type === 'mn' && lastGroup.type === 'mn') { - lastGroup.children.push(...group.children); - continue; // Concatenate ... followed by . - } else if (group.type === 'mi' && group.children.length === 1 && lastGroup.type === 'mn') { - const child = group.children[0]; - - if (child instanceof TextNode && child.text === '.') { - lastGroup.children.push(...group.children); - continue; - } - } else if (lastGroup.type === 'mi' && lastGroup.children.length === 1) { - const lastChild = lastGroup.children[0]; - - if (lastChild instanceof TextNode && lastChild.text === '\u0338' && (group.type === 'mo' || group.type === 'mi' || group.type === 'mn')) { - const child = group.children[0]; - - if (child instanceof TextNode && child.text.length > 0) { - // Overlay with combining character long solidus - child.text = child.text.slice(0, 1) + "\u0338" + child.text.slice(1); - groups.pop(); - } - } - } - } - - groups.push(group); - lastGroup = group; - } - - return groups; -}; -/** - * Equivalent to buildExpression, but wraps the elements in an - * if there's more than one. Returns a single node instead of an array. - */ - -const buildExpressionRow = function buildExpressionRow(expression, options, isOrdgroup) { - return makeRow(buildExpression$1(expression, options, isOrdgroup)); -}; -/** - * Takes a group from the parser and calls the appropriate groupBuilders function - * on it to produce a MathML node. - */ - -const buildGroup$1 = function buildGroup(group, options) { - if (!group) { - return new mathMLTree.MathNode("mrow"); - } - - if (_mathmlGroupBuilders[group.type]) { - // Call the groupBuilders function - const result = _mathmlGroupBuilders[group.type](group, options); - return result; - } else { - throw new ParseError("Got group of unknown type: '" + group.type + "'"); - } -}; -/** - * Takes a full parse tree and settings and builds a MathML representation of - * it. In particular, we put the elements from building the parse tree into a - * tag so we can also include that TeX source as an annotation. - * - * Note that we actually return a domTree element with a `` inside it so - * we can do appropriate styling. - */ - -function buildMathML(tree, texExpression, options, isDisplayMode, forMathmlOnly) { - const expression = buildExpression$1(tree, options); // Wrap up the expression in an mrow so it is presented in the semantics - // tag correctly, unless it's a single or . - - let wrapper; - - if (expression.length === 1 && expression[0] instanceof MathNode && utils.contains(["mrow", "mtable"], expression[0].type)) { - wrapper = expression[0]; - } else { - wrapper = new mathMLTree.MathNode("mrow", expression); - } // Build a TeX annotation of the source - - - const annotation = new mathMLTree.MathNode("annotation", [new mathMLTree.TextNode(texExpression)]); - annotation.setAttribute("encoding", "application/x-tex"); - const semantics = new mathMLTree.MathNode("semantics", [wrapper, annotation]); - const math = new mathMLTree.MathNode("math", [semantics]); - math.setAttribute("xmlns", "http://www.w3.org/1998/Math/MathML"); - - if (isDisplayMode) { - math.setAttribute("display", "block"); - } // You can't style nodes, so we wrap the node in a span. - // NOTE: The span class is not typed to have nodes as children, and - // we don't want to make the children type more generic since the children - // of span are expected to have more fields in `buildHtml` contexts. - - - const wrapperClass = forMathmlOnly ? "katex" : "katex-mathml"; // $FlowFixMe - - return buildCommon.makeSpan([wrapperClass], [math]); -} - -const optionsFromSettings = function optionsFromSettings(settings) { - return new Options({ - style: settings.displayMode ? Style$1.DISPLAY : Style$1.TEXT, - maxSize: settings.maxSize, - minRuleThickness: settings.minRuleThickness - }); -}; - -const displayWrap = function displayWrap(node, settings) { - if (settings.displayMode) { - const classes = ["katex-display"]; - - if (settings.leqno) { - classes.push("leqno"); - } - - if (settings.fleqn) { - classes.push("fleqn"); - } - - node = buildCommon.makeSpan(classes, [node]); - } - - return node; -}; - -const buildTree = function buildTree(tree, expression, settings) { - const options = optionsFromSettings(settings); - let katexNode; - - if (settings.output === "mathml") { - return buildMathML(tree, expression, options, settings.displayMode, true); - } else if (settings.output === "html") { - const htmlNode = buildHTML(tree, options); - katexNode = buildCommon.makeSpan(["katex"], [htmlNode]); - } else { - const mathMLNode = buildMathML(tree, expression, options, settings.displayMode, false); - const htmlNode = buildHTML(tree, options); - katexNode = buildCommon.makeSpan(["katex"], [mathMLNode, htmlNode]); - } - - return displayWrap(katexNode, settings); -}; -const buildHTMLTree = function buildHTMLTree(tree, expression, settings) { - const options = optionsFromSettings(settings); - const htmlNode = buildHTML(tree, options); - const katexNode = buildCommon.makeSpan(["katex"], [htmlNode]); - return displayWrap(katexNode, settings); -}; - -/** - * This file provides support to buildMathML.js and buildHTML.js - * for stretchy wide elements rendered from SVG files - * and other CSS trickery. - */ -const stretchyCodePoint = { - widehat: "^", - widecheck: "ˇ", - widetilde: "~", - utilde: "~", - overleftarrow: "\u2190", - underleftarrow: "\u2190", - xleftarrow: "\u2190", - overrightarrow: "\u2192", - underrightarrow: "\u2192", - xrightarrow: "\u2192", - underbrace: "\u23df", - overbrace: "\u23de", - overgroup: "\u23e0", - undergroup: "\u23e1", - overleftrightarrow: "\u2194", - underleftrightarrow: "\u2194", - xleftrightarrow: "\u2194", - Overrightarrow: "\u21d2", - xRightarrow: "\u21d2", - overleftharpoon: "\u21bc", - xleftharpoonup: "\u21bc", - overrightharpoon: "\u21c0", - xrightharpoonup: "\u21c0", - xLeftarrow: "\u21d0", - xLeftrightarrow: "\u21d4", - xhookleftarrow: "\u21a9", - xhookrightarrow: "\u21aa", - xmapsto: "\u21a6", - xrightharpoondown: "\u21c1", - xleftharpoondown: "\u21bd", - xrightleftharpoons: "\u21cc", - xleftrightharpoons: "\u21cb", - xtwoheadleftarrow: "\u219e", - xtwoheadrightarrow: "\u21a0", - xlongequal: "=", - xtofrom: "\u21c4", - xrightleftarrows: "\u21c4", - xrightequilibrium: "\u21cc", - // Not a perfect match. - xleftequilibrium: "\u21cb" // None better available. - -}; - -const mathMLnode = function mathMLnode(label) { - const node = new mathMLTree.MathNode("mo", [new mathMLTree.TextNode(stretchyCodePoint[label.substr(1)])]); - node.setAttribute("stretchy", "true"); - return node; -}; // Many of the KaTeX SVG images have been adapted from glyphs in KaTeX fonts. -// Copyright (c) 2009-2010, Design Science, Inc. () -// Copyright (c) 2014-2017 Khan Academy () -// Licensed under the SIL Open Font License, Version 1.1. -// See \nhttp://scripts.sil.org/OFL -// Very Long SVGs -// Many of the KaTeX stretchy wide elements use a long SVG image and an -// overflow: hidden tactic to achieve a stretchy image while avoiding -// distortion of arrowheads or brace corners. -// The SVG typically contains a very long (400 em) arrow. -// The SVG is in a container span that has overflow: hidden, so the span -// acts like a window that exposes only part of the SVG. -// The SVG always has a longer, thinner aspect ratio than the container span. -// After the SVG fills 100% of the height of the container span, -// there is a long arrow shaft left over. That left-over shaft is not shown. -// Instead, it is sliced off because the span's CSS has overflow: hidden. -// Thus, the reader sees an arrow that matches the subject matter width -// without distortion. -// Some functions, such as \cancel, need to vary their aspect ratio. These -// functions do not get the overflow SVG treatment. -// Second Brush Stroke -// Low resolution monitors struggle to display images in fine detail. -// So browsers apply anti-aliasing. A long straight arrow shaft therefore -// will sometimes appear as if it has a blurred edge. -// To mitigate this, these SVG files contain a second "brush-stroke" on the -// arrow shafts. That is, a second long thin rectangular SVG path has been -// written directly on top of each arrow shaft. This reinforcement causes -// some of the screen pixels to display as black instead of the anti-aliased -// gray pixel that a single path would generate. So we get arrow shafts -// whose edges appear to be sharper. -// In the katexImagesData object just below, the dimensions all -// correspond to path geometry inside the relevant SVG. -// For example, \overrightarrow uses the same arrowhead as glyph U+2192 -// from the KaTeX Main font. The scaling factor is 1000. -// That is, inside the font, that arrowhead is 522 units tall, which -// corresponds to 0.522 em inside the document. - - -const katexImagesData = { - // path(s), minWidth, height, align - overrightarrow: [["rightarrow"], 0.888, 522, "xMaxYMin"], - overleftarrow: [["leftarrow"], 0.888, 522, "xMinYMin"], - underrightarrow: [["rightarrow"], 0.888, 522, "xMaxYMin"], - underleftarrow: [["leftarrow"], 0.888, 522, "xMinYMin"], - xrightarrow: [["rightarrow"], 1.469, 522, "xMaxYMin"], - xleftarrow: [["leftarrow"], 1.469, 522, "xMinYMin"], - Overrightarrow: [["doublerightarrow"], 0.888, 560, "xMaxYMin"], - xRightarrow: [["doublerightarrow"], 1.526, 560, "xMaxYMin"], - xLeftarrow: [["doubleleftarrow"], 1.526, 560, "xMinYMin"], - overleftharpoon: [["leftharpoon"], 0.888, 522, "xMinYMin"], - xleftharpoonup: [["leftharpoon"], 0.888, 522, "xMinYMin"], - xleftharpoondown: [["leftharpoondown"], 0.888, 522, "xMinYMin"], - overrightharpoon: [["rightharpoon"], 0.888, 522, "xMaxYMin"], - xrightharpoonup: [["rightharpoon"], 0.888, 522, "xMaxYMin"], - xrightharpoondown: [["rightharpoondown"], 0.888, 522, "xMaxYMin"], - xlongequal: [["longequal"], 0.888, 334, "xMinYMin"], - xtwoheadleftarrow: [["twoheadleftarrow"], 0.888, 334, "xMinYMin"], - xtwoheadrightarrow: [["twoheadrightarrow"], 0.888, 334, "xMaxYMin"], - overleftrightarrow: [["leftarrow", "rightarrow"], 0.888, 522], - overbrace: [["leftbrace", "midbrace", "rightbrace"], 1.6, 548], - underbrace: [["leftbraceunder", "midbraceunder", "rightbraceunder"], 1.6, 548], - underleftrightarrow: [["leftarrow", "rightarrow"], 0.888, 522], - xleftrightarrow: [["leftarrow", "rightarrow"], 1.75, 522], - xLeftrightarrow: [["doubleleftarrow", "doublerightarrow"], 1.75, 560], - xrightleftharpoons: [["leftharpoondownplus", "rightharpoonplus"], 1.75, 716], - xleftrightharpoons: [["leftharpoonplus", "rightharpoondownplus"], 1.75, 716], - xhookleftarrow: [["leftarrow", "righthook"], 1.08, 522], - xhookrightarrow: [["lefthook", "rightarrow"], 1.08, 522], - overlinesegment: [["leftlinesegment", "rightlinesegment"], 0.888, 522], - underlinesegment: [["leftlinesegment", "rightlinesegment"], 0.888, 522], - overgroup: [["leftgroup", "rightgroup"], 0.888, 342], - undergroup: [["leftgroupunder", "rightgroupunder"], 0.888, 342], - xmapsto: [["leftmapsto", "rightarrow"], 1.5, 522], - xtofrom: [["leftToFrom", "rightToFrom"], 1.75, 528], - // The next three arrows are from the mhchem package. - // In mhchem.sty, min-length is 2.0em. But these arrows might appear in the - // document as \xrightarrow or \xrightleftharpoons. Those have - // min-length = 1.75em, so we set min-length on these next three to match. - xrightleftarrows: [["baraboveleftarrow", "rightarrowabovebar"], 1.75, 901], - xrightequilibrium: [["baraboveshortleftharpoon", "rightharpoonaboveshortbar"], 1.75, 716], - xleftequilibrium: [["shortbaraboveleftharpoon", "shortrightharpoonabovebar"], 1.75, 716] -}; - -const groupLength = function groupLength(arg) { - if (arg.type === "ordgroup") { - return arg.body.length; - } else { - return 1; - } -}; - -const svgSpan = function svgSpan(group, options) { - // Create a span with inline SVG for the element. - function buildSvgSpan_() { - let viewBoxWidth = 400000; // default - - const label = group.label.substr(1); - - if (utils.contains(["widehat", "widecheck", "widetilde", "utilde"], label)) { - // Each type in the `if` statement corresponds to one of the ParseNode - // types below. This narrowing is required to access `grp.base`. - const grp = group; // There are four SVG images available for each function. - // Choose a taller image when there are more characters. - - const numChars = groupLength(grp.base); - let viewBoxHeight; - let pathName; - let height; - - if (numChars > 5) { - if (label === "widehat" || label === "widecheck") { - viewBoxHeight = 420; - viewBoxWidth = 2364; - height = 0.42; - pathName = label + "4"; - } else { - viewBoxHeight = 312; - viewBoxWidth = 2340; - height = 0.34; - pathName = "tilde4"; - } - } else { - const imgIndex = [1, 1, 2, 2, 3, 3][numChars]; - - if (label === "widehat" || label === "widecheck") { - viewBoxWidth = [0, 1062, 2364, 2364, 2364][imgIndex]; - viewBoxHeight = [0, 239, 300, 360, 420][imgIndex]; - height = [0, 0.24, 0.3, 0.3, 0.36, 0.42][imgIndex]; - pathName = label + imgIndex; - } else { - viewBoxWidth = [0, 600, 1033, 2339, 2340][imgIndex]; - viewBoxHeight = [0, 260, 286, 306, 312][imgIndex]; - height = [0, 0.26, 0.286, 0.3, 0.306, 0.34][imgIndex]; - pathName = "tilde" + imgIndex; - } - } - - const path = new PathNode(pathName); - const svgNode = new SvgNode([path], { - "width": "100%", - "height": height + "em", - "viewBox": `0 0 ${viewBoxWidth} ${viewBoxHeight}`, - "preserveAspectRatio": "none" - }); - return { - span: buildCommon.makeSvgSpan([], [svgNode], options), - minWidth: 0, - height - }; - } else { - const spans = []; - const data = katexImagesData[label]; - const paths = data[0], - minWidth = data[1], - viewBoxHeight = data[2]; - const height = viewBoxHeight / 1000; - const numSvgChildren = paths.length; - let widthClasses; - let aligns; - - if (numSvgChildren === 1) { - // $FlowFixMe: All these cases must be of the 4-tuple type. - const align1 = data[3]; - widthClasses = ["hide-tail"]; - aligns = [align1]; - } else if (numSvgChildren === 2) { - widthClasses = ["halfarrow-left", "halfarrow-right"]; - aligns = ["xMinYMin", "xMaxYMin"]; - } else if (numSvgChildren === 3) { - widthClasses = ["brace-left", "brace-center", "brace-right"]; - aligns = ["xMinYMin", "xMidYMin", "xMaxYMin"]; - } else { - throw new Error(`Correct katexImagesData or update code here to support - ${numSvgChildren} children.`); - } - - for (let i = 0; i < numSvgChildren; i++) { - const path = new PathNode(paths[i]); - const svgNode = new SvgNode([path], { - "width": "400em", - "height": height + "em", - "viewBox": `0 0 ${viewBoxWidth} ${viewBoxHeight}`, - "preserveAspectRatio": aligns[i] + " slice" - }); - const span = buildCommon.makeSvgSpan([widthClasses[i]], [svgNode], options); - - if (numSvgChildren === 1) { - return { - span, - minWidth, - height - }; - } else { - span.style.height = height + "em"; - spans.push(span); - } - } - - return { - span: buildCommon.makeSpan(["stretchy"], spans, options), - minWidth, - height - }; - } - } // buildSvgSpan_() - - - const _buildSvgSpan_ = buildSvgSpan_(), - span = _buildSvgSpan_.span, - minWidth = _buildSvgSpan_.minWidth, - height = _buildSvgSpan_.height; // Note that we are returning span.depth = 0. - // Any adjustments relative to the baseline must be done in buildHTML. - - - span.height = height; - span.style.height = height + "em"; - - if (minWidth > 0) { - span.style.minWidth = minWidth + "em"; - } - - return span; -}; - -const encloseSpan = function encloseSpan(inner, label, pad, options) { - // Return an image span for \cancel, \bcancel, \xcancel, or \fbox - let img; - const totalHeight = inner.height + inner.depth + 2 * pad; - - if (/fbox|color/.test(label)) { - img = buildCommon.makeSpan(["stretchy", label], [], options); - - if (label === "fbox") { - const color = options.color && options.getColor(); - - if (color) { - img.style.borderColor = color; - } - } - } else { - // \cancel, \bcancel, or \xcancel - // Since \cancel's SVG is inline and it omits the viewBox attribute, - // its stroke-width will not vary with span area. - const lines = []; - - if (/^[bx]cancel$/.test(label)) { - lines.push(new LineNode({ - "x1": "0", - "y1": "0", - "x2": "100%", - "y2": "100%", - "stroke-width": "0.046em" - })); - } - - if (/^x?cancel$/.test(label)) { - lines.push(new LineNode({ - "x1": "0", - "y1": "100%", - "x2": "100%", - "y2": "0", - "stroke-width": "0.046em" - })); - } - - const svgNode = new SvgNode(lines, { - "width": "100%", - "height": totalHeight + "em" - }); - img = buildCommon.makeSvgSpan([], [svgNode], options); - } - - img.height = totalHeight; - img.style.height = totalHeight + "em"; - return img; -}; - -var stretchy = { - encloseSpan, - mathMLnode, - svgSpan -}; - -/** - * Asserts that the node is of the given type and returns it with stricter - * typing. Throws if the node's type does not match. - */ -function assertNodeType(node, type) { - if (!node || node.type !== type) { - throw new Error(`Expected node of type ${type}, but got ` + (node ? `node of type ${node.type}` : String(node))); - } - - return node; -} -/** - * Returns the node more strictly typed iff it is of the given type. Otherwise, - * returns null. - */ - -function assertSymbolNodeType(node) { - const typedNode = checkSymbolNodeType(node); - - if (!typedNode) { - throw new Error(`Expected node of symbol group type, but got ` + (node ? `node of type ${node.type}` : String(node))); - } - - return typedNode; -} -/** - * Returns the node more strictly typed iff it is of the given type. Otherwise, - * returns null. - */ - -function checkSymbolNodeType(node) { - if (node && (node.type === "atom" || NON_ATOMS.hasOwnProperty(node.type))) { - // $FlowFixMe - return node; - } - - return null; -} - -// NOTE: Unlike most `htmlBuilder`s, this one handles not only "accent", but -const htmlBuilder = (grp, options) => { - // Accents are handled in the TeXbook pg. 443, rule 12. - let base; - let group; - let supSubGroup; - - if (grp && grp.type === "supsub") { - // If our base is a character box, and we have superscripts and - // subscripts, the supsub will defer to us. In particular, we want - // to attach the superscripts and subscripts to the inner body (so - // that the position of the superscripts and subscripts won't be - // affected by the height of the accent). We accomplish this by - // sticking the base of the accent into the base of the supsub, and - // rendering that, while keeping track of where the accent is. - // The real accent group is the base of the supsub group - group = assertNodeType(grp.base, "accent"); // The character box is the base of the accent group - - base = group.base; // Stick the character box into the base of the supsub group - - grp.base = base; // Rerender the supsub group with its new base, and store that - // result. - - supSubGroup = assertSpan(buildGroup(grp, options)); // reset original base - - grp.base = group; - } else { - group = assertNodeType(grp, "accent"); - base = group.base; - } // Build the base group - - - const body = buildGroup(base, options.havingCrampedStyle()); // Does the accent need to shift for the skew of a character? - - const mustShift = group.isShifty && utils.isCharacterBox(base); // Calculate the skew of the accent. This is based on the line "If the - // nucleus is not a single character, let s = 0; otherwise set s to the - // kern amount for the nucleus followed by the \skewchar of its font." - // Note that our skew metrics are just the kern between each character - // and the skewchar. - - let skew = 0; - - if (mustShift) { - // If the base is a character box, then we want the skew of the - // innermost character. To do that, we find the innermost character: - const baseChar = utils.getBaseElem(base); // Then, we render its group to get the symbol inside it - - const baseGroup = buildGroup(baseChar, options.havingCrampedStyle()); // Finally, we pull the skew off of the symbol. - - skew = assertSymbolDomNode(baseGroup).skew; // Note that we now throw away baseGroup, because the layers we - // removed with getBaseElem might contain things like \color which - // we can't get rid of. - // TODO(emily): Find a better way to get the skew - } // calculate the amount of space between the body and the accent - - - let clearance = Math.min(body.height, options.fontMetrics().xHeight); // Build the accent - - let accentBody; - - if (!group.isStretchy) { - let accent; - let width; - - if (group.label === "\\vec") { - // Before version 0.9, \vec used the combining font glyph U+20D7. - // But browsers, especially Safari, are not consistent in how they - // render combining characters when not preceded by a character. - // So now we use an SVG. - // If Safari reforms, we should consider reverting to the glyph. - accent = buildCommon.staticSvg("vec", options); - width = buildCommon.svgData.vec[1]; - } else { - accent = buildCommon.makeOrd({ - mode: group.mode, - text: group.label - }, options, "textord"); - accent = assertSymbolDomNode(accent); // Remove the italic correction of the accent, because it only serves to - // shift the accent over to a place we don't want. - - accent.italic = 0; - width = accent.width; - } - - accentBody = buildCommon.makeSpan(["accent-body"], [accent]); // "Full" accents expand the width of the resulting symbol to be - // at least the width of the accent, and overlap directly onto the - // character without any vertical offset. - - const accentFull = group.label === "\\textcircled"; - - if (accentFull) { - accentBody.classes.push('accent-full'); - clearance = body.height; - } // Shift the accent over by the skew. - - - let left = skew; // CSS defines `.katex .accent .accent-body:not(.accent-full) { width: 0 }` - // so that the accent doesn't contribute to the bounding box. - // We need to shift the character by its width (effectively half - // its width) to compensate. - - if (!accentFull) { - left -= width / 2; - } - - accentBody.style.left = left + "em"; // \textcircled uses the \bigcirc glyph, so it needs some - // vertical adjustment to match LaTeX. - - if (group.label === "\\textcircled") { - accentBody.style.top = ".2em"; - } - - accentBody = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: body - }, { - type: "kern", - size: -clearance - }, { - type: "elem", - elem: accentBody - }] - }, options); - } else { - accentBody = stretchy.svgSpan(group, options); - accentBody = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: body - }, { - type: "elem", - elem: accentBody, - wrapperClasses: ["svg-align"], - wrapperStyle: skew > 0 ? { - width: `calc(100% - ${2 * skew}em)`, - marginLeft: `${2 * skew}em` - } : undefined - }] - }, options); - } - - const accentWrap = buildCommon.makeSpan(["mord", "accent"], [accentBody], options); - - if (supSubGroup) { - // Here, we replace the "base" child of the supsub with our newly - // generated accent. - supSubGroup.children[0] = accentWrap; // Since we don't rerun the height calculation after replacing the - // accent, we manually recalculate height. - - supSubGroup.height = Math.max(accentWrap.height, supSubGroup.height); // Accents should always be ords, even when their innards are not. - - supSubGroup.classes[0] = "mord"; - return supSubGroup; - } else { - return accentWrap; - } -}; - -const mathmlBuilder = (group, options) => { - const accentNode = group.isStretchy ? stretchy.mathMLnode(group.label) : new mathMLTree.MathNode("mo", [makeText(group.label, group.mode)]); - const node = new mathMLTree.MathNode("mover", [buildGroup$1(group.base, options), accentNode]); - node.setAttribute("accent", "true"); - return node; -}; - -const NON_STRETCHY_ACCENT_REGEX = new RegExp(["\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve", "\\check", "\\hat", "\\vec", "\\dot", "\\mathring"].map(accent => `\\${accent}`).join("|")); // Accents - -defineFunction({ - type: "accent", - names: ["\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve", "\\check", "\\hat", "\\vec", "\\dot", "\\mathring", "\\widecheck", "\\widehat", "\\widetilde", "\\overrightarrow", "\\overleftarrow", "\\Overrightarrow", "\\overleftrightarrow", "\\overgroup", "\\overlinesegment", "\\overleftharpoon", "\\overrightharpoon"], - props: { - numArgs: 1 - }, - handler: (context, args) => { - const base = args[0]; - const isStretchy = !NON_STRETCHY_ACCENT_REGEX.test(context.funcName); - const isShifty = !isStretchy || context.funcName === "\\widehat" || context.funcName === "\\widetilde" || context.funcName === "\\widecheck"; - return { - type: "accent", - mode: context.parser.mode, - label: context.funcName, - isStretchy: isStretchy, - isShifty: isShifty, - base: base - }; - }, - htmlBuilder, - mathmlBuilder -}); // Text-mode accents - -defineFunction({ - type: "accent", - names: ["\\'", "\\`", "\\^", "\\~", "\\=", "\\u", "\\.", '\\"', "\\r", "\\H", "\\v", "\\textcircled"], - props: { - numArgs: 1, - allowedInText: true, - allowedInMath: false - }, - handler: (context, args) => { - const base = args[0]; - return { - type: "accent", - mode: context.parser.mode, - label: context.funcName, - isStretchy: false, - isShifty: true, - base: base - }; - }, - htmlBuilder, - mathmlBuilder -}); - -// Horizontal overlap functions -defineFunction({ - type: "accentUnder", - names: ["\\underleftarrow", "\\underrightarrow", "\\underleftrightarrow", "\\undergroup", "\\underlinesegment", "\\utilde"], - props: { - numArgs: 1 - }, - handler: (_ref, args) => { - let parser = _ref.parser, - funcName = _ref.funcName; - const base = args[0]; - return { - type: "accentUnder", - mode: parser.mode, - label: funcName, - base: base - }; - }, - htmlBuilder: (group, options) => { - // Treat under accents much like underlines. - const innerGroup = buildGroup(group.base, options); - const accentBody = stretchy.svgSpan(group, options); - const kern = group.label === "\\utilde" ? 0.12 : 0; // Generate the vlist, with the appropriate kerns - - const vlist = buildCommon.makeVList({ - positionType: "top", - positionData: innerGroup.height, - children: [{ - type: "elem", - elem: accentBody, - wrapperClasses: ["svg-align"] - }, { - type: "kern", - size: kern - }, { - type: "elem", - elem: innerGroup - }] - }, options); - return buildCommon.makeSpan(["mord", "accentunder"], [vlist], options); - }, - mathmlBuilder: (group, options) => { - const accentNode = stretchy.mathMLnode(group.label); - const node = new mathMLTree.MathNode("munder", [buildGroup$1(group.base, options), accentNode]); - node.setAttribute("accentunder", "true"); - return node; - } -}); - -// Helper function -const paddedNode = group => { - const node = new mathMLTree.MathNode("mpadded", group ? [group] : []); - node.setAttribute("width", "+0.6em"); - node.setAttribute("lspace", "0.3em"); - return node; -}; // Stretchy arrows with an optional argument - - -defineFunction({ - type: "xArrow", - names: ["\\xleftarrow", "\\xrightarrow", "\\xLeftarrow", "\\xRightarrow", "\\xleftrightarrow", "\\xLeftrightarrow", "\\xhookleftarrow", "\\xhookrightarrow", "\\xmapsto", "\\xrightharpoondown", "\\xrightharpoonup", "\\xleftharpoondown", "\\xleftharpoonup", "\\xrightleftharpoons", "\\xleftrightharpoons", "\\xlongequal", "\\xtwoheadrightarrow", "\\xtwoheadleftarrow", "\\xtofrom", // The next 3 functions are here to support the mhchem extension. - // Direct use of these functions is discouraged and may break someday. - "\\xrightleftarrows", "\\xrightequilibrium", "\\xleftequilibrium"], - props: { - numArgs: 1, - numOptionalArgs: 1 - }, - - handler(_ref, args, optArgs) { - let parser = _ref.parser, - funcName = _ref.funcName; - return { - type: "xArrow", - mode: parser.mode, - label: funcName, - body: args[0], - below: optArgs[0] - }; - }, - - // Flow is unable to correctly infer the type of `group`, even though it's - // unamibiguously determined from the passed-in `type` above. - htmlBuilder(group, options) { - const style = options.style; // Build the argument groups in the appropriate style. - // Ref: amsmath.dtx: \hbox{$\scriptstyle\mkern#3mu{#6}\mkern#4mu$}% - // Some groups can return document fragments. Handle those by wrapping - // them in a span. - - let newOptions = options.havingStyle(style.sup()); - const upperGroup = buildCommon.wrapFragment(buildGroup(group.body, newOptions, options), options); - upperGroup.classes.push("x-arrow-pad"); - let lowerGroup; - - if (group.below) { - // Build the lower group - newOptions = options.havingStyle(style.sub()); - lowerGroup = buildCommon.wrapFragment(buildGroup(group.below, newOptions, options), options); - lowerGroup.classes.push("x-arrow-pad"); - } - - const arrowBody = stretchy.svgSpan(group, options); // Re shift: Note that stretchy.svgSpan returned arrowBody.depth = 0. - // The point we want on the math axis is at 0.5 * arrowBody.height. - - const arrowShift = -options.fontMetrics().axisHeight + 0.5 * arrowBody.height; // 2 mu kern. Ref: amsmath.dtx: #7\if0#2\else\mkern#2mu\fi - - let upperShift = -options.fontMetrics().axisHeight - 0.5 * arrowBody.height - 0.111; // 0.111 em = 2 mu - - if (upperGroup.depth > 0.25 || group.label === "\\xleftequilibrium") { - upperShift -= upperGroup.depth; // shift up if depth encroaches - } // Generate the vlist - - - let vlist; - - if (lowerGroup) { - const lowerShift = -options.fontMetrics().axisHeight + lowerGroup.height + 0.5 * arrowBody.height + 0.111; - vlist = buildCommon.makeVList({ - positionType: "individualShift", - children: [{ - type: "elem", - elem: upperGroup, - shift: upperShift - }, { - type: "elem", - elem: arrowBody, - shift: arrowShift - }, { - type: "elem", - elem: lowerGroup, - shift: lowerShift - }] - }, options); - } else { - vlist = buildCommon.makeVList({ - positionType: "individualShift", - children: [{ - type: "elem", - elem: upperGroup, - shift: upperShift - }, { - type: "elem", - elem: arrowBody, - shift: arrowShift - }] - }, options); - } // $FlowFixMe: Replace this with passing "svg-align" into makeVList. - - - vlist.children[0].children[0].children[1].classes.push("svg-align"); - return buildCommon.makeSpan(["mrel", "x-arrow"], [vlist], options); - }, - - mathmlBuilder(group, options) { - const arrowNode = stretchy.mathMLnode(group.label); - let node; - - if (group.body) { - const upperNode = paddedNode(buildGroup$1(group.body, options)); - - if (group.below) { - const lowerNode = paddedNode(buildGroup$1(group.below, options)); - node = new mathMLTree.MathNode("munderover", [arrowNode, lowerNode, upperNode]); - } else { - node = new mathMLTree.MathNode("mover", [arrowNode, upperNode]); - } - } else if (group.below) { - const lowerNode = paddedNode(buildGroup$1(group.below, options)); - node = new mathMLTree.MathNode("munder", [arrowNode, lowerNode]); - } else { - // This should never happen. - // Parser.js throws an error if there is no argument. - node = paddedNode(); - node = new mathMLTree.MathNode("mover", [arrowNode, node]); - } - - return node; - } - -}); - -// {123} and converts into symbol with code 123. It is used by the *macro* -// \char defined in macros.js. - -defineFunction({ - type: "textord", - names: ["\\@char"], - props: { - numArgs: 1, - allowedInText: true - }, - - handler(_ref, args) { - let parser = _ref.parser; - const arg = assertNodeType(args[0], "ordgroup"); - const group = arg.body; - let number = ""; - - for (let i = 0; i < group.length; i++) { - const node = assertNodeType(group[i], "textord"); - number += node.text; - } - - const code = parseInt(number); - - if (isNaN(code)) { - throw new ParseError(`\\@char has non-numeric argument ${number}`); - } - - return { - type: "textord", - mode: parser.mode, - text: String.fromCharCode(code) - }; - } - -}); - -const htmlBuilder$1 = (group, options) => { - const elements = buildExpression(group.body, options.withColor(group.color), false); // \color isn't supposed to affect the type of the elements it contains. - // To accomplish this, we wrap the results in a fragment, so the inner - // elements will be able to directly interact with their neighbors. For - // example, `\color{red}{2 +} 3` has the same spacing as `2 + 3` - - return buildCommon.makeFragment(elements); -}; - -const mathmlBuilder$1 = (group, options) => { - const inner = buildExpression$1(group.body, options.withColor(group.color)); - const node = new mathMLTree.MathNode("mstyle", inner); - node.setAttribute("mathcolor", group.color); - return node; -}; - -defineFunction({ - type: "color", - names: ["\\textcolor"], - props: { - numArgs: 2, - allowedInText: true, - greediness: 3, - argTypes: ["color", "original"] - }, - - handler(_ref, args) { - let parser = _ref.parser; - const color = assertNodeType(args[0], "color-token").color; - const body = args[1]; - return { - type: "color", - mode: parser.mode, - color, - body: ordargument(body) - }; - }, - - htmlBuilder: htmlBuilder$1, - mathmlBuilder: mathmlBuilder$1 -}); -defineFunction({ - type: "color", - names: ["\\color"], - props: { - numArgs: 1, - allowedInText: true, - greediness: 3, - argTypes: ["color"] - }, - - handler(_ref2, args) { - let parser = _ref2.parser, - breakOnTokenText = _ref2.breakOnTokenText; - const color = assertNodeType(args[0], "color-token").color; // Set macro \current@color in current namespace to store the current - // color, mimicking the behavior of color.sty. - // This is currently used just to correctly color a \right - // that follows a \color command. - - parser.gullet.macros.set("\\current@color", color); // Parse out the implicit body that should be colored. - - const body = parser.parseExpression(true, breakOnTokenText); - return { - type: "color", - mode: parser.mode, - color, - body - }; - }, - - htmlBuilder: htmlBuilder$1, - mathmlBuilder: mathmlBuilder$1 -}); - -// Row breaks within tabular environments, and line breaks at top level -// same signature, we implement them as one megafunction, with newRow -// indicating whether we're in the \cr case, and newLine indicating whether -// to break the line in the \newline case. - -defineFunction({ - type: "cr", - names: ["\\cr", "\\newline"], - props: { - numArgs: 0, - numOptionalArgs: 1, - argTypes: ["size"], - allowedInText: true - }, - - handler(_ref, args, optArgs) { - let parser = _ref.parser, - funcName = _ref.funcName; - const size = optArgs[0]; - const newRow = funcName === "\\cr"; - let newLine = false; - - if (!newRow) { - if (parser.settings.displayMode && parser.settings.useStrictBehavior("newLineInDisplayMode", "In LaTeX, \\\\ or \\newline " + "does nothing in display mode")) { - newLine = false; - } else { - newLine = true; - } - } - - return { - type: "cr", - mode: parser.mode, - newLine, - newRow, - size: size && assertNodeType(size, "size").value - }; - }, - - // The following builders are called only at the top level, - // not within tabular/array environments. - htmlBuilder(group, options) { - if (group.newRow) { - throw new ParseError("\\cr valid only within a tabular/array environment"); - } - - const span = buildCommon.makeSpan(["mspace"], [], options); - - if (group.newLine) { - span.classes.push("newline"); - - if (group.size) { - span.style.marginTop = calculateSize(group.size, options) + "em"; - } - } - - return span; - }, - - mathmlBuilder(group, options) { - const node = new mathMLTree.MathNode("mspace"); - - if (group.newLine) { - node.setAttribute("linebreak", "newline"); - - if (group.size) { - node.setAttribute("height", calculateSize(group.size, options) + "em"); - } - } - - return node; - } - -}); - -const globalMap = { - "\\global": "\\global", - "\\long": "\\\\globallong", - "\\\\globallong": "\\\\globallong", - "\\def": "\\gdef", - "\\gdef": "\\gdef", - "\\edef": "\\xdef", - "\\xdef": "\\xdef", - "\\let": "\\\\globallet", - "\\futurelet": "\\\\globalfuture" -}; - -const checkControlSequence = tok => { - const name = tok.text; - - if (/^(?:[\\{}$&#^_]|EOF)$/.test(name)) { - throw new ParseError("Expected a control sequence", tok); - } - - return name; -}; - -const getRHS = parser => { - let tok = parser.gullet.popToken(); - - if (tok.text === "=") { - // consume optional equals - tok = parser.gullet.popToken(); - - if (tok.text === " ") { - // consume one optional space - tok = parser.gullet.popToken(); - } - } - - return tok; -}; - -const letCommand = (parser, name, tok, global) => { - let macro = parser.gullet.macros.get(tok.text); - - if (macro == null) { - // don't expand it later even if a macro with the same name is defined - // e.g., \let\foo=\frac \def\frac{\relax} \frac12 - tok.noexpand = true; - macro = { - tokens: [tok], - numArgs: 0, - // reproduce the same behavior in expansion - unexpandable: !parser.gullet.isExpandable(tok.text) - }; - } - - parser.gullet.macros.set(name, macro, global); -}; // -> | -// -> |\global -// -> | -// -> \global|\long|\outer - - -defineFunction({ - type: "internal", - names: ["\\global", "\\long", "\\\\globallong"], - props: { - numArgs: 0, - allowedInText: true - }, - - handler(_ref) { - let parser = _ref.parser, - funcName = _ref.funcName; - parser.consumeSpaces(); - const token = parser.fetch(); - - if (globalMap[token.text]) { - // KaTeX doesn't have \par, so ignore \long - if (funcName === "\\global" || funcName === "\\\\globallong") { - token.text = globalMap[token.text]; - } - - return assertNodeType(parser.parseFunction(), "internal"); - } - - throw new ParseError(`Invalid token after macro prefix`, token); - } - -}); // Basic support for macro definitions: \def, \gdef, \edef, \xdef -// -> -// -> \def|\gdef|\edef|\xdef -// -> - -defineFunction({ - type: "internal", - names: ["\\def", "\\gdef", "\\edef", "\\xdef"], - props: { - numArgs: 0, - allowedInText: true - }, - - handler(_ref2) { - let parser = _ref2.parser, - funcName = _ref2.funcName; - let arg = parser.gullet.consumeArgs(1)[0]; - - if (arg.length !== 1) { - throw new ParseError("\\gdef's first argument must be a macro name"); - } - - const name = arg[0].text; // Count argument specifiers, and check they are in the order #1 #2 ... - - let numArgs = 0; - arg = parser.gullet.consumeArgs(1)[0]; - - while (arg.length === 1 && arg[0].text === "#") { - arg = parser.gullet.consumeArgs(1)[0]; - - if (arg.length !== 1) { - throw new ParseError(`Invalid argument number length "${arg.length}"`); - } - - if (!/^[1-9]$/.test(arg[0].text)) { - throw new ParseError(`Invalid argument number "${arg[0].text}"`); - } - - numArgs++; - - if (parseInt(arg[0].text) !== numArgs) { - throw new ParseError(`Argument number "${arg[0].text}" out of order`); - } - - arg = parser.gullet.consumeArgs(1)[0]; - } - - if (funcName === "\\edef" || funcName === "\\xdef") { - arg = parser.gullet.expandTokens(arg); - arg.reverse(); // to fit in with stack order - } // Final arg is the expansion of the macro - - - parser.gullet.macros.set(name, { - tokens: arg, - numArgs - }, funcName === globalMap[funcName]); - return { - type: "internal", - mode: parser.mode - }; - } - -}); // -> -// -> \futurelet -// | \let -// -> |= - -defineFunction({ - type: "internal", - names: ["\\let", "\\\\globallet"], - props: { - numArgs: 0, - allowedInText: true - }, - - handler(_ref3) { - let parser = _ref3.parser, - funcName = _ref3.funcName; - const name = checkControlSequence(parser.gullet.popToken()); - parser.gullet.consumeSpaces(); - const tok = getRHS(parser); - letCommand(parser, name, tok, funcName === "\\\\globallet"); - return { - type: "internal", - mode: parser.mode - }; - } - -}); // ref: https://www.tug.org/TUGboat/tb09-3/tb22bechtolsheim.pdf - -defineFunction({ - type: "internal", - names: ["\\futurelet", "\\\\globalfuture"], - props: { - numArgs: 0, - allowedInText: true - }, - - handler(_ref4) { - let parser = _ref4.parser, - funcName = _ref4.funcName; - const name = checkControlSequence(parser.gullet.popToken()); - const middle = parser.gullet.popToken(); - const tok = parser.gullet.popToken(); - letCommand(parser, name, tok, funcName === "\\\\globalfuture"); - parser.gullet.pushToken(tok); - parser.gullet.pushToken(middle); - return { - type: "internal", - mode: parser.mode - }; - } - -}); - -/** - * This file deals with creating delimiters of various sizes. The TeXbook - * discusses these routines on page 441-442, in the "Another subroutine sets box - * x to a specified variable delimiter" paragraph. - * - * There are three main routines here. `makeSmallDelim` makes a delimiter in the - * normal font, but in either text, script, or scriptscript style. - * `makeLargeDelim` makes a delimiter in textstyle, but in one of the Size1, - * Size2, Size3, or Size4 fonts. `makeStackedDelim` makes a delimiter out of - * smaller pieces that are stacked on top of one another. - * - * The functions take a parameter `center`, which determines if the delimiter - * should be centered around the axis. - * - * Then, there are three exposed functions. `sizedDelim` makes a delimiter in - * one of the given sizes. This is used for things like `\bigl`. - * `customSizedDelim` makes a delimiter with a given total height+depth. It is - * called in places like `\sqrt`. `leftRightDelim` makes an appropriate - * delimiter which surrounds an expression of a given height an depth. It is - * used in `\left` and `\right`. - */ - -/** - * Get the metrics for a given symbol and font, after transformation (i.e. - * after following replacement from symbols.js) - */ -const getMetrics = function getMetrics(symbol, font, mode) { - const replace = symbols.math[symbol] && symbols.math[symbol].replace; - const metrics = getCharacterMetrics(replace || symbol, font, mode); - - if (!metrics) { - throw new Error(`Unsupported symbol ${symbol} and font size ${font}.`); - } - - return metrics; -}; -/** - * Puts a delimiter span in a given style, and adds appropriate height, depth, - * and maxFontSizes. - */ - - -const styleWrap = function styleWrap(delim, toStyle, options, classes) { - const newOptions = options.havingBaseStyle(toStyle); - const span = buildCommon.makeSpan(classes.concat(newOptions.sizingClasses(options)), [delim], options); - const delimSizeMultiplier = newOptions.sizeMultiplier / options.sizeMultiplier; - span.height *= delimSizeMultiplier; - span.depth *= delimSizeMultiplier; - span.maxFontSize = newOptions.sizeMultiplier; - return span; -}; - -const centerSpan = function centerSpan(span, options, style) { - const newOptions = options.havingBaseStyle(style); - const shift = (1 - options.sizeMultiplier / newOptions.sizeMultiplier) * options.fontMetrics().axisHeight; - span.classes.push("delimcenter"); - span.style.top = shift + "em"; - span.height -= shift; - span.depth += shift; -}; -/** - * Makes a small delimiter. This is a delimiter that comes in the Main-Regular - * font, but is restyled to either be in textstyle, scriptstyle, or - * scriptscriptstyle. - */ - - -const makeSmallDelim = function makeSmallDelim(delim, style, center, options, mode, classes) { - const text = buildCommon.makeSymbol(delim, "Main-Regular", mode, options); - const span = styleWrap(text, style, options, classes); - - if (center) { - centerSpan(span, options, style); - } - - return span; -}; -/** - * Builds a symbol in the given font size (note size is an integer) - */ - - -const mathrmSize = function mathrmSize(value, size, mode, options) { - return buildCommon.makeSymbol(value, "Size" + size + "-Regular", mode, options); -}; -/** - * Makes a large delimiter. This is a delimiter that comes in the Size1, Size2, - * Size3, or Size4 fonts. It is always rendered in textstyle. - */ - - -const makeLargeDelim = function makeLargeDelim(delim, size, center, options, mode, classes) { - const inner = mathrmSize(delim, size, mode, options); - const span = styleWrap(buildCommon.makeSpan(["delimsizing", "size" + size], [inner], options), Style$1.TEXT, options, classes); - - if (center) { - centerSpan(span, options, Style$1.TEXT); - } - - return span; -}; -/** - * Make an inner span with the given offset and in the given font. This is used - * in `makeStackedDelim` to make the stacking pieces for the delimiter. - */ - - -const makeInner = function makeInner(symbol, font, mode) { - let sizeClass; // Apply the correct CSS class to choose the right font. - - if (font === "Size1-Regular") { - sizeClass = "delim-size1"; - } else - /* if (font === "Size4-Regular") */ - { - sizeClass = "delim-size4"; - } - - const inner = buildCommon.makeSpan(["delimsizinginner", sizeClass], [buildCommon.makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]); // Since this will be passed into `makeVList` in the end, wrap the element - // in the appropriate tag that VList uses. - - return { - type: "elem", - elem: inner - }; -}; // Helper for makeStackedDelim - - -const lap = { - type: "kern", - size: -0.005 -}; -/** - * Make a stacked delimiter out of a given delimiter, with the total height at - * least `heightTotal`. This routine is mentioned on page 442 of the TeXbook. - */ - -const makeStackedDelim = function makeStackedDelim(delim, heightTotal, center, options, mode, classes) { - // There are four parts, the top, an optional middle, a repeated part, and a - // bottom. - let top; - let middle; - let repeat; - let bottom; - top = repeat = bottom = delim; - middle = null; // Also keep track of what font the delimiters are in - - let font = "Size1-Regular"; // We set the parts and font based on the symbol. Note that we use - // '\u23d0' instead of '|' and '\u2016' instead of '\\|' for the - // repeats of the arrows - - if (delim === "\\uparrow") { - repeat = bottom = "\u23d0"; - } else if (delim === "\\Uparrow") { - repeat = bottom = "\u2016"; - } else if (delim === "\\downarrow") { - top = repeat = "\u23d0"; - } else if (delim === "\\Downarrow") { - top = repeat = "\u2016"; - } else if (delim === "\\updownarrow") { - top = "\\uparrow"; - repeat = "\u23d0"; - bottom = "\\downarrow"; - } else if (delim === "\\Updownarrow") { - top = "\\Uparrow"; - repeat = "\u2016"; - bottom = "\\Downarrow"; - } else if (delim === "[" || delim === "\\lbrack") { - top = "\u23a1"; - repeat = "\u23a2"; - bottom = "\u23a3"; - font = "Size4-Regular"; - } else if (delim === "]" || delim === "\\rbrack") { - top = "\u23a4"; - repeat = "\u23a5"; - bottom = "\u23a6"; - font = "Size4-Regular"; - } else if (delim === "\\lfloor" || delim === "\u230a") { - repeat = top = "\u23a2"; - bottom = "\u23a3"; - font = "Size4-Regular"; - } else if (delim === "\\lceil" || delim === "\u2308") { - top = "\u23a1"; - repeat = bottom = "\u23a2"; - font = "Size4-Regular"; - } else if (delim === "\\rfloor" || delim === "\u230b") { - repeat = top = "\u23a5"; - bottom = "\u23a6"; - font = "Size4-Regular"; - } else if (delim === "\\rceil" || delim === "\u2309") { - top = "\u23a4"; - repeat = bottom = "\u23a5"; - font = "Size4-Regular"; - } else if (delim === "(" || delim === "\\lparen") { - top = "\u239b"; - repeat = "\u239c"; - bottom = "\u239d"; - font = "Size4-Regular"; - } else if (delim === ")" || delim === "\\rparen") { - top = "\u239e"; - repeat = "\u239f"; - bottom = "\u23a0"; - font = "Size4-Regular"; - } else if (delim === "\\{" || delim === "\\lbrace") { - top = "\u23a7"; - middle = "\u23a8"; - bottom = "\u23a9"; - repeat = "\u23aa"; - font = "Size4-Regular"; - } else if (delim === "\\}" || delim === "\\rbrace") { - top = "\u23ab"; - middle = "\u23ac"; - bottom = "\u23ad"; - repeat = "\u23aa"; - font = "Size4-Regular"; - } else if (delim === "\\lgroup" || delim === "\u27ee") { - top = "\u23a7"; - bottom = "\u23a9"; - repeat = "\u23aa"; - font = "Size4-Regular"; - } else if (delim === "\\rgroup" || delim === "\u27ef") { - top = "\u23ab"; - bottom = "\u23ad"; - repeat = "\u23aa"; - font = "Size4-Regular"; - } else if (delim === "\\lmoustache" || delim === "\u23b0") { - top = "\u23a7"; - bottom = "\u23ad"; - repeat = "\u23aa"; - font = "Size4-Regular"; - } else if (delim === "\\rmoustache" || delim === "\u23b1") { - top = "\u23ab"; - bottom = "\u23a9"; - repeat = "\u23aa"; - font = "Size4-Regular"; - } // Get the metrics of the four sections - - - const topMetrics = getMetrics(top, font, mode); - const topHeightTotal = topMetrics.height + topMetrics.depth; - const repeatMetrics = getMetrics(repeat, font, mode); - const repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth; - const bottomMetrics = getMetrics(bottom, font, mode); - const bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth; - let middleHeightTotal = 0; - let middleFactor = 1; - - if (middle !== null) { - const middleMetrics = getMetrics(middle, font, mode); - middleHeightTotal = middleMetrics.height + middleMetrics.depth; - middleFactor = 2; // repeat symmetrically above and below middle - } // Calculate the minimal height that the delimiter can have. - // It is at least the size of the top, bottom, and optional middle combined. - - - const minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal; // Compute the number of copies of the repeat symbol we will need - - const repeatCount = Math.max(0, Math.ceil((heightTotal - minHeight) / (middleFactor * repeatHeightTotal))); // Compute the total height of the delimiter including all the symbols - - const realHeightTotal = minHeight + repeatCount * middleFactor * repeatHeightTotal; // The center of the delimiter is placed at the center of the axis. Note - // that in this context, "center" means that the delimiter should be - // centered around the axis in the current style, while normally it is - // centered around the axis in textstyle. - - let axisHeight = options.fontMetrics().axisHeight; - - if (center) { - axisHeight *= options.sizeMultiplier; - } // Calculate the depth - - - const depth = realHeightTotal / 2 - axisHeight; // This function differs from the TeX procedure in one way. - // We shift each repeat element downwards by 0.005em, to prevent a gap - // due to browser floating point rounding error. - // Then, at the last element-to element joint, we add one extra repeat - // element to cover the gap created by the shifts. - // Find the shift needed to align the upper end of the extra element at a point - // 0.005em above the lower end of the top element. - - const shiftOfExtraElement = (repeatCount + 1) * 0.005 - repeatHeightTotal; // Now, we start building the pieces that will go into the vlist - // Keep a list of the inner pieces - - const inners = []; // Add the bottom symbol - - inners.push(makeInner(bottom, font, mode)); - - if (middle === null) { - // Add that many symbols - for (let i = 0; i < repeatCount; i++) { - inners.push(lap); // overlap - - inners.push(makeInner(repeat, font, mode)); - } - } else { - // When there is a middle bit, we need the middle part and two repeated - // sections - for (let i = 0; i < repeatCount; i++) { - inners.push(lap); - inners.push(makeInner(repeat, font, mode)); - } // Insert one extra repeat element. - - - inners.push({ - type: "kern", - size: shiftOfExtraElement - }); - inners.push(makeInner(repeat, font, mode)); - inners.push(lap); // Now insert the middle of the brace. - - inners.push(makeInner(middle, font, mode)); - - for (let i = 0; i < repeatCount; i++) { - inners.push(lap); - inners.push(makeInner(repeat, font, mode)); - } - } // To cover the gap create by the overlaps, insert one more repeat element, - // at a position that juts 0.005 above the bottom of the top element. - - - if ((repeat === "\u239c" || repeat === "\u239f") && repeatCount === 0) { - // Parentheses need a short repeat element in order to avoid an overrun. - // We'll make a 0.3em tall element from a SVG. - const overlap = buildCommon.svgData.leftParenInner[2] / 2; - inners.push({ - type: "kern", - size: -overlap - }); - const pathName = repeat === "\u239c" ? "leftParenInner" : "rightParenInner"; - const innerSpan = buildCommon.staticSvg(pathName, options); - inners.push({ - type: "elem", - elem: innerSpan - }); - inners.push({ - type: "kern", - size: -overlap - }); - } else { - inners.push({ - type: "kern", - size: shiftOfExtraElement - }); - inners.push(makeInner(repeat, font, mode)); - inners.push(lap); - } // Add the top symbol - - - inners.push(makeInner(top, font, mode)); // Finally, build the vlist - - const newOptions = options.havingBaseStyle(Style$1.TEXT); - const inner = buildCommon.makeVList({ - positionType: "bottom", - positionData: depth, - children: inners - }, newOptions); - return styleWrap(buildCommon.makeSpan(["delimsizing", "mult"], [inner], newOptions), Style$1.TEXT, options, classes); -}; // All surds have 0.08em padding above the viniculum inside the SVG. -// That keeps browser span height rounding error from pinching the line. - - -const vbPad = 80; // padding above the surd, measured inside the viewBox. - -const emPad = 0.08; // padding, in ems, measured in the document. - -const sqrtSvg = function sqrtSvg(sqrtName, height, viewBoxHeight, extraViniculum, options) { - const path = sqrtPath(sqrtName, extraViniculum, viewBoxHeight); - const pathNode = new PathNode(sqrtName, path); - const svg = new SvgNode([pathNode], { - // Note: 1000:1 ratio of viewBox to document em width. - "width": "400em", - "height": height + "em", - "viewBox": "0 0 400000 " + viewBoxHeight, - "preserveAspectRatio": "xMinYMin slice" - }); - return buildCommon.makeSvgSpan(["hide-tail"], [svg], options); -}; -/** - * Make a sqrt image of the given height, - */ - - -const makeSqrtImage = function makeSqrtImage(height, options) { - // Define a newOptions that removes the effect of size changes such as \Huge. - // We don't pick different a height surd for \Huge. For it, we scale up. - const newOptions = options.havingBaseSizing(); // Pick the desired surd glyph from a sequence of surds. - - const delim = traverseSequence("\\surd", height * newOptions.sizeMultiplier, stackLargeDelimiterSequence, newOptions); - let sizeMultiplier = newOptions.sizeMultiplier; // default - // The standard sqrt SVGs each have a 0.04em thick viniculum. - // If Settings.minRuleThickness is larger than that, we add extraViniculum. - - const extraViniculum = Math.max(0, options.minRuleThickness - options.fontMetrics().sqrtRuleThickness); // Create a span containing an SVG image of a sqrt symbol. - - let span; - let spanHeight = 0; - let texHeight = 0; - let viewBoxHeight = 0; - let advanceWidth; // We create viewBoxes with 80 units of "padding" above each surd. - // Then browser rounding error on the parent span height will not - // encroach on the ink of the viniculum. But that padding is not - // included in the TeX-like `height` used for calculation of - // vertical alignment. So texHeight = span.height < span.style.height. - - if (delim.type === "small") { - // Get an SVG that is derived from glyph U+221A in font KaTeX-Main. - // 1000 unit normal glyph height. - viewBoxHeight = 1000 + 1000 * extraViniculum + vbPad; - - if (height < 1.0) { - sizeMultiplier = 1.0; // mimic a \textfont radical - } else if (height < 1.4) { - sizeMultiplier = 0.7; // mimic a \scriptfont radical - } - - spanHeight = (1.0 + extraViniculum + emPad) / sizeMultiplier; - texHeight = (1.00 + extraViniculum) / sizeMultiplier; - span = sqrtSvg("sqrtMain", spanHeight, viewBoxHeight, extraViniculum, options); - span.style.minWidth = "0.853em"; - advanceWidth = 0.833 / sizeMultiplier; // from the font. - } else if (delim.type === "large") { - // These SVGs come from fonts: KaTeX_Size1, _Size2, etc. - viewBoxHeight = (1000 + vbPad) * sizeToMaxHeight[delim.size]; - texHeight = (sizeToMaxHeight[delim.size] + extraViniculum) / sizeMultiplier; - spanHeight = (sizeToMaxHeight[delim.size] + extraViniculum + emPad) / sizeMultiplier; - span = sqrtSvg("sqrtSize" + delim.size, spanHeight, viewBoxHeight, extraViniculum, options); - span.style.minWidth = "1.02em"; - advanceWidth = 1.0 / sizeMultiplier; // 1.0 from the font. - } else { - // Tall sqrt. In TeX, this would be stacked using multiple glyphs. - // We'll use a single SVG to accomplish the same thing. - spanHeight = height + extraViniculum + emPad; - texHeight = height + extraViniculum; - viewBoxHeight = Math.floor(1000 * height + extraViniculum) + vbPad; - span = sqrtSvg("sqrtTall", spanHeight, viewBoxHeight, extraViniculum, options); - span.style.minWidth = "0.742em"; - advanceWidth = 1.056; - } - - span.height = texHeight; - span.style.height = spanHeight + "em"; - return { - span, - advanceWidth, - // Calculate the actual line width. - // This actually should depend on the chosen font -- e.g. \boldmath - // should use the thicker surd symbols from e.g. KaTeX_Main-Bold, and - // have thicker rules. - ruleWidth: (options.fontMetrics().sqrtRuleThickness + extraViniculum) * sizeMultiplier - }; -}; // There are three kinds of delimiters, delimiters that stack when they become -// too large - - -const stackLargeDelimiters = ["(", "\\lparen", ")", "\\rparen", "[", "\\lbrack", "]", "\\rbrack", "\\{", "\\lbrace", "\\}", "\\rbrace", "\\lfloor", "\\rfloor", "\u230a", "\u230b", "\\lceil", "\\rceil", "\u2308", "\u2309", "\\surd"]; // delimiters that always stack - -const stackAlwaysDelimiters = ["\\uparrow", "\\downarrow", "\\updownarrow", "\\Uparrow", "\\Downarrow", "\\Updownarrow", "|", "\\|", "\\vert", "\\Vert", "\\lvert", "\\rvert", "\\lVert", "\\rVert", "\\lgroup", "\\rgroup", "\u27ee", "\u27ef", "\\lmoustache", "\\rmoustache", "\u23b0", "\u23b1"]; // and delimiters that never stack - -const stackNeverDelimiters = ["<", ">", "\\langle", "\\rangle", "/", "\\backslash", "\\lt", "\\gt"]; // Metrics of the different sizes. Found by looking at TeX's output of -// $\bigl| // \Bigl| \biggl| \Biggl| \showlists$ -// Used to create stacked delimiters of appropriate sizes in makeSizedDelim. - -const sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0]; -/** - * Used to create a delimiter of a specific size, where `size` is 1, 2, 3, or 4. - */ - -const makeSizedDelim = function makeSizedDelim(delim, size, options, mode, classes) { - // < and > turn into \langle and \rangle in delimiters - if (delim === "<" || delim === "\\lt" || delim === "\u27e8") { - delim = "\\langle"; - } else if (delim === ">" || delim === "\\gt" || delim === "\u27e9") { - delim = "\\rangle"; - } // Sized delimiters are never centered. - - - if (utils.contains(stackLargeDelimiters, delim) || utils.contains(stackNeverDelimiters, delim)) { - return makeLargeDelim(delim, size, false, options, mode, classes); - } else if (utils.contains(stackAlwaysDelimiters, delim)) { - return makeStackedDelim(delim, sizeToMaxHeight[size], false, options, mode, classes); - } else { - throw new ParseError("Illegal delimiter: '" + delim + "'"); - } -}; -/** - * There are three different sequences of delimiter sizes that the delimiters - * follow depending on the kind of delimiter. This is used when creating custom - * sized delimiters to decide whether to create a small, large, or stacked - * delimiter. - * - * In real TeX, these sequences aren't explicitly defined, but are instead - * defined inside the font metrics. Since there are only three sequences that - * are possible for the delimiters that TeX defines, it is easier to just encode - * them explicitly here. - */ - - -// Delimiters that never stack try small delimiters and large delimiters only -const stackNeverDelimiterSequence = [{ - type: "small", - style: Style$1.SCRIPTSCRIPT -}, { - type: "small", - style: Style$1.SCRIPT -}, { - type: "small", - style: Style$1.TEXT -}, { - type: "large", - size: 1 -}, { - type: "large", - size: 2 -}, { - type: "large", - size: 3 -}, { - type: "large", - size: 4 -}]; // Delimiters that always stack try the small delimiters first, then stack - -const stackAlwaysDelimiterSequence = [{ - type: "small", - style: Style$1.SCRIPTSCRIPT -}, { - type: "small", - style: Style$1.SCRIPT -}, { - type: "small", - style: Style$1.TEXT -}, { - type: "stack" -}]; // Delimiters that stack when large try the small and then large delimiters, and -// stack afterwards - -const stackLargeDelimiterSequence = [{ - type: "small", - style: Style$1.SCRIPTSCRIPT -}, { - type: "small", - style: Style$1.SCRIPT -}, { - type: "small", - style: Style$1.TEXT -}, { - type: "large", - size: 1 -}, { - type: "large", - size: 2 -}, { - type: "large", - size: 3 -}, { - type: "large", - size: 4 -}, { - type: "stack" -}]; -/** - * Get the font used in a delimiter based on what kind of delimiter it is. - * TODO(#963) Use more specific font family return type once that is introduced. - */ - -const delimTypeToFont = function delimTypeToFont(type) { - if (type.type === "small") { - return "Main-Regular"; - } else if (type.type === "large") { - return "Size" + type.size + "-Regular"; - } else if (type.type === "stack") { - return "Size4-Regular"; - } else { - throw new Error(`Add support for delim type '${type.type}' here.`); - } -}; -/** - * Traverse a sequence of types of delimiters to decide what kind of delimiter - * should be used to create a delimiter of the given height+depth. - */ - - -const traverseSequence = function traverseSequence(delim, height, sequence, options) { - // Here, we choose the index we should start at in the sequences. In smaller - // sizes (which correspond to larger numbers in style.size) we start earlier - // in the sequence. Thus, scriptscript starts at index 3-3=0, script starts - // at index 3-2=1, text starts at 3-1=2, and display starts at min(2,3-0)=2 - const start = Math.min(2, 3 - options.style.size); - - for (let i = start; i < sequence.length; i++) { - if (sequence[i].type === "stack") { - // This is always the last delimiter, so we just break the loop now. - break; - } - - const metrics = getMetrics(delim, delimTypeToFont(sequence[i]), "math"); - let heightDepth = metrics.height + metrics.depth; // Small delimiters are scaled down versions of the same font, so we - // account for the style change size. - - if (sequence[i].type === "small") { - const newOptions = options.havingBaseStyle(sequence[i].style); - heightDepth *= newOptions.sizeMultiplier; - } // Check if the delimiter at this size works for the given height. - - - if (heightDepth > height) { - return sequence[i]; - } - } // If we reached the end of the sequence, return the last sequence element. - - - return sequence[sequence.length - 1]; -}; -/** - * Make a delimiter of a given height+depth, with optional centering. Here, we - * traverse the sequences, and create a delimiter that the sequence tells us to. - */ - - -const makeCustomSizedDelim = function makeCustomSizedDelim(delim, height, center, options, mode, classes) { - if (delim === "<" || delim === "\\lt" || delim === "\u27e8") { - delim = "\\langle"; - } else if (delim === ">" || delim === "\\gt" || delim === "\u27e9") { - delim = "\\rangle"; - } // Decide what sequence to use - - - let sequence; - - if (utils.contains(stackNeverDelimiters, delim)) { - sequence = stackNeverDelimiterSequence; - } else if (utils.contains(stackLargeDelimiters, delim)) { - sequence = stackLargeDelimiterSequence; - } else { - sequence = stackAlwaysDelimiterSequence; - } // Look through the sequence - - - const delimType = traverseSequence(delim, height, sequence, options); // Get the delimiter from font glyphs. - // Depending on the sequence element we decided on, call the - // appropriate function. - - if (delimType.type === "small") { - return makeSmallDelim(delim, delimType.style, center, options, mode, classes); - } else if (delimType.type === "large") { - return makeLargeDelim(delim, delimType.size, center, options, mode, classes); - } else - /* if (delimType.type === "stack") */ - { - return makeStackedDelim(delim, height, center, options, mode, classes); - } -}; -/** - * Make a delimiter for use with `\left` and `\right`, given a height and depth - * of an expression that the delimiters surround. - */ - - -const makeLeftRightDelim = function makeLeftRightDelim(delim, height, depth, options, mode, classes) { - // We always center \left/\right delimiters, so the axis is always shifted - const axisHeight = options.fontMetrics().axisHeight * options.sizeMultiplier; // Taken from TeX source, tex.web, function make_left_right - - const delimiterFactor = 901; - const delimiterExtend = 5.0 / options.fontMetrics().ptPerEm; - const maxDistFromAxis = Math.max(height - axisHeight, depth + axisHeight); - const totalHeight = Math.max( // In real TeX, calculations are done using integral values which are - // 65536 per pt, or 655360 per em. So, the division here truncates in - // TeX but doesn't here, producing different results. If we wanted to - // exactly match TeX's calculation, we could do - // Math.floor(655360 * maxDistFromAxis / 500) * - // delimiterFactor / 655360 - // (To see the difference, compare - // x^{x^{\left(\rule{0.1em}{0.68em}\right)}} - // in TeX and KaTeX) - maxDistFromAxis / 500 * delimiterFactor, 2 * maxDistFromAxis - delimiterExtend); // Finally, we defer to `makeCustomSizedDelim` with our calculated total - // height - - return makeCustomSizedDelim(delim, totalHeight, true, options, mode, classes); -}; - -var delimiter = { - sqrtImage: makeSqrtImage, - sizedDelim: makeSizedDelim, - customSizedDelim: makeCustomSizedDelim, - leftRightDelim: makeLeftRightDelim -}; - -// Extra data needed for the delimiter handler down below -const delimiterSizes = { - "\\bigl": { - mclass: "mopen", - size: 1 - }, - "\\Bigl": { - mclass: "mopen", - size: 2 - }, - "\\biggl": { - mclass: "mopen", - size: 3 - }, - "\\Biggl": { - mclass: "mopen", - size: 4 - }, - "\\bigr": { - mclass: "mclose", - size: 1 - }, - "\\Bigr": { - mclass: "mclose", - size: 2 - }, - "\\biggr": { - mclass: "mclose", - size: 3 - }, - "\\Biggr": { - mclass: "mclose", - size: 4 - }, - "\\bigm": { - mclass: "mrel", - size: 1 - }, - "\\Bigm": { - mclass: "mrel", - size: 2 - }, - "\\biggm": { - mclass: "mrel", - size: 3 - }, - "\\Biggm": { - mclass: "mrel", - size: 4 - }, - "\\big": { - mclass: "mord", - size: 1 - }, - "\\Big": { - mclass: "mord", - size: 2 - }, - "\\bigg": { - mclass: "mord", - size: 3 - }, - "\\Bigg": { - mclass: "mord", - size: 4 - } -}; -const delimiters = ["(", "\\lparen", ")", "\\rparen", "[", "\\lbrack", "]", "\\rbrack", "\\{", "\\lbrace", "\\}", "\\rbrace", "\\lfloor", "\\rfloor", "\u230a", "\u230b", "\\lceil", "\\rceil", "\u2308", "\u2309", "<", ">", "\\langle", "\u27e8", "\\rangle", "\u27e9", "\\lt", "\\gt", "\\lvert", "\\rvert", "\\lVert", "\\rVert", "\\lgroup", "\\rgroup", "\u27ee", "\u27ef", "\\lmoustache", "\\rmoustache", "\u23b0", "\u23b1", "/", "\\backslash", "|", "\\vert", "\\|", "\\Vert", "\\uparrow", "\\Uparrow", "\\downarrow", "\\Downarrow", "\\updownarrow", "\\Updownarrow", "."]; - -// Delimiter functions -function checkDelimiter(delim, context) { - const symDelim = checkSymbolNodeType(delim); - - if (symDelim && utils.contains(delimiters, symDelim.text)) { - return symDelim; - } else if (symDelim) { - throw new ParseError(`Invalid delimiter '${symDelim.text}' after '${context.funcName}'`, delim); - } else { - throw new ParseError(`Invalid delimiter type '${delim.type}'`, delim); - } -} - -defineFunction({ - type: "delimsizing", - names: ["\\bigl", "\\Bigl", "\\biggl", "\\Biggl", "\\bigr", "\\Bigr", "\\biggr", "\\Biggr", "\\bigm", "\\Bigm", "\\biggm", "\\Biggm", "\\big", "\\Big", "\\bigg", "\\Bigg"], - props: { - numArgs: 1 - }, - handler: (context, args) => { - const delim = checkDelimiter(args[0], context); - return { - type: "delimsizing", - mode: context.parser.mode, - size: delimiterSizes[context.funcName].size, - mclass: delimiterSizes[context.funcName].mclass, - delim: delim.text - }; - }, - htmlBuilder: (group, options) => { - if (group.delim === ".") { - // Empty delimiters still count as elements, even though they don't - // show anything. - return buildCommon.makeSpan([group.mclass]); - } // Use delimiter.sizedDelim to generate the delimiter. - - - return delimiter.sizedDelim(group.delim, group.size, options, group.mode, [group.mclass]); - }, - mathmlBuilder: group => { - const children = []; - - if (group.delim !== ".") { - children.push(makeText(group.delim, group.mode)); - } - - const node = new mathMLTree.MathNode("mo", children); - - if (group.mclass === "mopen" || group.mclass === "mclose") { - // Only some of the delimsizing functions act as fences, and they - // return "mopen" or "mclose" mclass. - node.setAttribute("fence", "true"); - } else { - // Explicitly disable fencing if it's not a fence, to override the - // defaults. - node.setAttribute("fence", "false"); - } - - return node; - } -}); - -function assertParsed(group) { - if (!group.body) { - throw new Error("Bug: The leftright ParseNode wasn't fully parsed."); - } -} - -defineFunction({ - type: "leftright-right", - names: ["\\right"], - props: { - numArgs: 1 - }, - handler: (context, args) => { - // \left case below triggers parsing of \right in - // `const right = parser.parseFunction();` - // uses this return value. - const color = context.parser.gullet.macros.get("\\current@color"); - - if (color && typeof color !== "string") { - throw new ParseError("\\current@color set to non-string in \\right"); - } - - return { - type: "leftright-right", - mode: context.parser.mode, - delim: checkDelimiter(args[0], context).text, - color // undefined if not set via \color - - }; - } -}); -defineFunction({ - type: "leftright", - names: ["\\left"], - props: { - numArgs: 1 - }, - handler: (context, args) => { - const delim = checkDelimiter(args[0], context); - const parser = context.parser; // Parse out the implicit body - - ++parser.leftrightDepth; // parseExpression stops before '\\right' - - const body = parser.parseExpression(false); - --parser.leftrightDepth; // Check the next token - - parser.expect("\\right", false); - const right = assertNodeType(parser.parseFunction(), "leftright-right"); - return { - type: "leftright", - mode: parser.mode, - body, - left: delim.text, - right: right.delim, - rightColor: right.color - }; - }, - htmlBuilder: (group, options) => { - assertParsed(group); // Build the inner expression - - const inner = buildExpression(group.body, options, true, ["mopen", "mclose"]); - let innerHeight = 0; - let innerDepth = 0; - let hadMiddle = false; // Calculate its height and depth - - for (let i = 0; i < inner.length; i++) { - // Property `isMiddle` not defined on `span`. See comment in - // "middle"'s htmlBuilder. - // $FlowFixMe - if (inner[i].isMiddle) { - hadMiddle = true; - } else { - innerHeight = Math.max(inner[i].height, innerHeight); - innerDepth = Math.max(inner[i].depth, innerDepth); - } - } // The size of delimiters is the same, regardless of what style we are - // in. Thus, to correctly calculate the size of delimiter we need around - // a group, we scale down the inner size based on the size. - - - innerHeight *= options.sizeMultiplier; - innerDepth *= options.sizeMultiplier; - let leftDelim; - - if (group.left === ".") { - // Empty delimiters in \left and \right make null delimiter spaces. - leftDelim = makeNullDelimiter(options, ["mopen"]); - } else { - // Otherwise, use leftRightDelim to generate the correct sized - // delimiter. - leftDelim = delimiter.leftRightDelim(group.left, innerHeight, innerDepth, options, group.mode, ["mopen"]); - } // Add it to the beginning of the expression - - - inner.unshift(leftDelim); // Handle middle delimiters - - if (hadMiddle) { - for (let i = 1; i < inner.length; i++) { - const middleDelim = inner[i]; // Property `isMiddle` not defined on `span`. See comment in - // "middle"'s htmlBuilder. - // $FlowFixMe - - const isMiddle = middleDelim.isMiddle; - - if (isMiddle) { - // Apply the options that were active when \middle was called - inner[i] = delimiter.leftRightDelim(isMiddle.delim, innerHeight, innerDepth, isMiddle.options, group.mode, []); - } - } - } - - let rightDelim; // Same for the right delimiter, but using color specified by \color - - if (group.right === ".") { - rightDelim = makeNullDelimiter(options, ["mclose"]); - } else { - const colorOptions = group.rightColor ? options.withColor(group.rightColor) : options; - rightDelim = delimiter.leftRightDelim(group.right, innerHeight, innerDepth, colorOptions, group.mode, ["mclose"]); - } // Add it to the end of the expression. - - - inner.push(rightDelim); - return buildCommon.makeSpan(["minner"], inner, options); - }, - mathmlBuilder: (group, options) => { - assertParsed(group); - const inner = buildExpression$1(group.body, options); - - if (group.left !== ".") { - const leftNode = new mathMLTree.MathNode("mo", [makeText(group.left, group.mode)]); - leftNode.setAttribute("fence", "true"); - inner.unshift(leftNode); - } - - if (group.right !== ".") { - const rightNode = new mathMLTree.MathNode("mo", [makeText(group.right, group.mode)]); - rightNode.setAttribute("fence", "true"); - - if (group.rightColor) { - rightNode.setAttribute("mathcolor", group.rightColor); - } - - inner.push(rightNode); - } - - return makeRow(inner); - } -}); -defineFunction({ - type: "middle", - names: ["\\middle"], - props: { - numArgs: 1 - }, - handler: (context, args) => { - const delim = checkDelimiter(args[0], context); - - if (!context.parser.leftrightDepth) { - throw new ParseError("\\middle without preceding \\left", delim); - } - - return { - type: "middle", - mode: context.parser.mode, - delim: delim.text - }; - }, - htmlBuilder: (group, options) => { - let middleDelim; - - if (group.delim === ".") { - middleDelim = makeNullDelimiter(options, []); - } else { - middleDelim = delimiter.sizedDelim(group.delim, 1, options, group.mode, []); - const isMiddle = { - delim: group.delim, - options - }; // Property `isMiddle` not defined on `span`. It is only used in - // this file above. - // TODO: Fix this violation of the `span` type and possibly rename - // things since `isMiddle` sounds like a boolean, but is a struct. - // $FlowFixMe - - middleDelim.isMiddle = isMiddle; - } - - return middleDelim; - }, - mathmlBuilder: (group, options) => { - // A Firefox \middle will stretch a character vertically only if it - // is in the fence part of the operator dictionary at: - // https://www.w3.org/TR/MathML3/appendixc.html. - // So we need to avoid U+2223 and use plain "|" instead. - const textNode = group.delim === "\\vert" || group.delim === "|" ? makeText("|", "text") : makeText(group.delim, group.mode); - const middleNode = new mathMLTree.MathNode("mo", [textNode]); - middleNode.setAttribute("fence", "true"); // MathML gives 5/18em spacing to each element. - // \middle should get delimiter spacing instead. - - middleNode.setAttribute("lspace", "0.05em"); - middleNode.setAttribute("rspace", "0.05em"); - return middleNode; - } -}); - -const htmlBuilder$2 = (group, options) => { - // \cancel, \bcancel, \xcancel, \sout, \fbox, \colorbox, \fcolorbox - // Some groups can return document fragments. Handle those by wrapping - // them in a span. - const inner = buildCommon.wrapFragment(buildGroup(group.body, options), options); - const label = group.label.substr(1); - const scale = options.sizeMultiplier; - let img; - let imgShift = 0; // In the LaTeX cancel package, line geometry is slightly different - // depending on whether the subject is wider than it is tall, or vice versa. - // We don't know the width of a group, so as a proxy, we test if - // the subject is a single character. This captures most of the - // subjects that should get the "tall" treatment. - - const isSingleChar = utils.isCharacterBox(group.body); - - if (label === "sout") { - img = buildCommon.makeSpan(["stretchy", "sout"]); - img.height = options.fontMetrics().defaultRuleThickness / scale; - imgShift = -0.5 * options.fontMetrics().xHeight; - } else { - // Add horizontal padding - if (/cancel/.test(label)) { - if (!isSingleChar) { - inner.classes.push("cancel-pad"); - } - } else { - inner.classes.push("boxpad"); - } // Add vertical padding - - - let vertPad = 0; - let ruleThickness = 0; // ref: cancel package: \advance\totalheight2\p@ % "+2" - - if (/box/.test(label)) { - ruleThickness = Math.max(options.fontMetrics().fboxrule, // default - options.minRuleThickness // User override. - ); - vertPad = options.fontMetrics().fboxsep + (label === "colorbox" ? 0 : ruleThickness); - } else { - vertPad = isSingleChar ? 0.2 : 0; - } - - img = stretchy.encloseSpan(inner, label, vertPad, options); - - if (/fbox|boxed|fcolorbox/.test(label)) { - img.style.borderStyle = "solid"; - img.style.borderWidth = `${ruleThickness}em`; - } - - imgShift = inner.depth + vertPad; - - if (group.backgroundColor) { - img.style.backgroundColor = group.backgroundColor; - - if (group.borderColor) { - img.style.borderColor = group.borderColor; - } - } - } - - let vlist; - - if (group.backgroundColor) { - vlist = buildCommon.makeVList({ - positionType: "individualShift", - children: [// Put the color background behind inner; - { - type: "elem", - elem: img, - shift: imgShift - }, { - type: "elem", - elem: inner, - shift: 0 - }] - }, options); - } else { - vlist = buildCommon.makeVList({ - positionType: "individualShift", - children: [// Write the \cancel stroke on top of inner. - { - type: "elem", - elem: inner, - shift: 0 - }, { - type: "elem", - elem: img, - shift: imgShift, - wrapperClasses: /cancel/.test(label) ? ["svg-align"] : [] - }] - }, options); - } - - if (/cancel/.test(label)) { - // The cancel package documentation says that cancel lines add their height - // to the expression, but tests show that isn't how it actually works. - vlist.height = inner.height; - vlist.depth = inner.depth; - } - - if (/cancel/.test(label) && !isSingleChar) { - // cancel does not create horiz space for its line extension. - return buildCommon.makeSpan(["mord", "cancel-lap"], [vlist], options); - } else { - return buildCommon.makeSpan(["mord"], [vlist], options); - } -}; - -const mathmlBuilder$2 = (group, options) => { - let fboxsep = 0; - const node = new mathMLTree.MathNode(group.label.indexOf("colorbox") > -1 ? "mpadded" : "menclose", [buildGroup$1(group.body, options)]); - - switch (group.label) { - case "\\cancel": - node.setAttribute("notation", "updiagonalstrike"); - break; - - case "\\bcancel": - node.setAttribute("notation", "downdiagonalstrike"); - break; - - case "\\sout": - node.setAttribute("notation", "horizontalstrike"); - break; - - case "\\fbox": - node.setAttribute("notation", "box"); - break; - - case "\\fcolorbox": - case "\\colorbox": - // doesn't have a good notation option. So use - // instead. Set some attributes that come included with . - fboxsep = options.fontMetrics().fboxsep * options.fontMetrics().ptPerEm; - node.setAttribute("width", `+${2 * fboxsep}pt`); - node.setAttribute("height", `+${2 * fboxsep}pt`); - node.setAttribute("lspace", `${fboxsep}pt`); // - - node.setAttribute("voffset", `${fboxsep}pt`); - - if (group.label === "\\fcolorbox") { - const thk = Math.max(options.fontMetrics().fboxrule, // default - options.minRuleThickness // user override - ); - node.setAttribute("style", "border: " + thk + "em solid " + String(group.borderColor)); - } - - break; - - case "\\xcancel": - node.setAttribute("notation", "updiagonalstrike downdiagonalstrike"); - break; - } - - if (group.backgroundColor) { - node.setAttribute("mathbackground", group.backgroundColor); - } - - return node; -}; - -defineFunction({ - type: "enclose", - names: ["\\colorbox"], - props: { - numArgs: 2, - allowedInText: true, - greediness: 3, - argTypes: ["color", "text"] - }, - - handler(_ref, args, optArgs) { - let parser = _ref.parser, - funcName = _ref.funcName; - const color = assertNodeType(args[0], "color-token").color; - const body = args[1]; - return { - type: "enclose", - mode: parser.mode, - label: funcName, - backgroundColor: color, - body - }; - }, - - htmlBuilder: htmlBuilder$2, - mathmlBuilder: mathmlBuilder$2 -}); -defineFunction({ - type: "enclose", - names: ["\\fcolorbox"], - props: { - numArgs: 3, - allowedInText: true, - greediness: 3, - argTypes: ["color", "color", "text"] - }, - - handler(_ref2, args, optArgs) { - let parser = _ref2.parser, - funcName = _ref2.funcName; - const borderColor = assertNodeType(args[0], "color-token").color; - const backgroundColor = assertNodeType(args[1], "color-token").color; - const body = args[2]; - return { - type: "enclose", - mode: parser.mode, - label: funcName, - backgroundColor, - borderColor, - body - }; - }, - - htmlBuilder: htmlBuilder$2, - mathmlBuilder: mathmlBuilder$2 -}); -defineFunction({ - type: "enclose", - names: ["\\fbox"], - props: { - numArgs: 1, - argTypes: ["hbox"], - allowedInText: true - }, - - handler(_ref3, args) { - let parser = _ref3.parser; - return { - type: "enclose", - mode: parser.mode, - label: "\\fbox", - body: args[0] - }; - } - -}); -defineFunction({ - type: "enclose", - names: ["\\cancel", "\\bcancel", "\\xcancel", "\\sout"], - props: { - numArgs: 1 - }, - - handler(_ref4, args, optArgs) { - let parser = _ref4.parser, - funcName = _ref4.funcName; - const body = args[0]; - return { - type: "enclose", - mode: parser.mode, - label: funcName, - body - }; - }, - - htmlBuilder: htmlBuilder$2, - mathmlBuilder: mathmlBuilder$2 -}); - -/** - * All registered environments. - * `environments.js` exports this same dictionary again and makes it public. - * `Parser.js` requires this dictionary via `environments.js`. - */ -const _environments = {}; -function defineEnvironment(_ref) { - let type = _ref.type, - names = _ref.names, - props = _ref.props, - handler = _ref.handler, - htmlBuilder = _ref.htmlBuilder, - mathmlBuilder = _ref.mathmlBuilder; - // Set default values of environments. - const data = { - type, - numArgs: props.numArgs || 0, - greediness: 1, - allowedInText: false, - numOptionalArgs: 0, - handler - }; - - for (let i = 0; i < names.length; ++i) { - // TODO: The value type of _environments should be a type union of all - // possible `EnvSpec<>` possibilities instead of `EnvSpec<*>`, which is - // an existential type. - // $FlowFixMe - _environments[names[i]] = data; - } - - if (htmlBuilder) { - _htmlGroupBuilders[type] = htmlBuilder; - } - - if (mathmlBuilder) { - _mathmlGroupBuilders[type] = mathmlBuilder; - } -} - -function getHLines(parser) { - // Return an array. The array length = number of hlines. - // Each element in the array tells if the line is dashed. - const hlineInfo = []; - parser.consumeSpaces(); - let nxt = parser.fetch().text; - - while (nxt === "\\hline" || nxt === "\\hdashline") { - parser.consume(); - hlineInfo.push(nxt === "\\hdashline"); - parser.consumeSpaces(); - nxt = parser.fetch().text; - } - - return hlineInfo; -} -/** - * Parse the body of the environment, with rows delimited by \\ and - * columns delimited by &, and create a nested list in row-major order - * with one group per cell. If given an optional argument style - * ("text", "display", etc.), then each cell is cast into that style. - */ - - -function parseArray(parser, _ref, style) { - let hskipBeforeAndAfter = _ref.hskipBeforeAndAfter, - addJot = _ref.addJot, - cols = _ref.cols, - arraystretch = _ref.arraystretch, - colSeparationType = _ref.colSeparationType; - // Parse body of array with \\ temporarily mapped to \cr - parser.gullet.beginGroup(); - parser.gullet.macros.set("\\\\", "\\cr"); // Get current arraystretch if it's not set by the environment - - if (!arraystretch) { - const stretch = parser.gullet.expandMacroAsText("\\arraystretch"); - - if (stretch == null) { - // Default \arraystretch from lttab.dtx - arraystretch = 1; - } else { - arraystretch = parseFloat(stretch); - - if (!arraystretch || arraystretch < 0) { - throw new ParseError(`Invalid \\arraystretch: ${stretch}`); - } - } - } // Start group for first cell - - - parser.gullet.beginGroup(); - let row = []; - const body = [row]; - const rowGaps = []; - const hLinesBeforeRow = []; // Test for \hline at the top of the array. - - hLinesBeforeRow.push(getHLines(parser)); - - while (true) { - // eslint-disable-line no-constant-condition - // Parse each cell in its own group (namespace) - let cell = parser.parseExpression(false, "\\cr"); - parser.gullet.endGroup(); - parser.gullet.beginGroup(); - cell = { - type: "ordgroup", - mode: parser.mode, - body: cell - }; - - if (style) { - cell = { - type: "styling", - mode: parser.mode, - style, - body: [cell] - }; - } - - row.push(cell); - const next = parser.fetch().text; - - if (next === "&") { - parser.consume(); - } else if (next === "\\end") { - // Arrays terminate newlines with `\crcr` which consumes a `\cr` if - // the last line is empty. - // NOTE: Currently, `cell` is the last item added into `row`. - if (row.length === 1 && cell.type === "styling" && cell.body[0].body.length === 0) { - body.pop(); - } - - if (hLinesBeforeRow.length < body.length + 1) { - hLinesBeforeRow.push([]); - } - - break; - } else if (next === "\\cr") { - const cr = assertNodeType(parser.parseFunction(), "cr"); - rowGaps.push(cr.size); // check for \hline(s) following the row separator - - hLinesBeforeRow.push(getHLines(parser)); - row = []; - body.push(row); - } else { - throw new ParseError("Expected & or \\\\ or \\cr or \\end", parser.nextToken); - } - } // End cell group - - - parser.gullet.endGroup(); // End array group defining \\ - - parser.gullet.endGroup(); - return { - type: "array", - mode: parser.mode, - addJot, - arraystretch, - body, - cols, - rowGaps, - hskipBeforeAndAfter, - hLinesBeforeRow, - colSeparationType - }; -} // Decides on a style for cells in an array according to whether the given -// environment name starts with the letter 'd'. - - -function dCellStyle(envName) { - if (envName.substr(0, 1) === "d") { - return "display"; - } else { - return "text"; - } -} - -const htmlBuilder$3 = function htmlBuilder(group, options) { - let r; - let c; - const nr = group.body.length; - const hLinesBeforeRow = group.hLinesBeforeRow; - let nc = 0; - let body = new Array(nr); - const hlines = []; - const ruleThickness = Math.max( // From LaTeX \showthe\arrayrulewidth. Equals 0.04 em. - options.fontMetrics().arrayRuleWidth, options.minRuleThickness // User override. - ); // Horizontal spacing - - const pt = 1 / options.fontMetrics().ptPerEm; - let arraycolsep = 5 * pt; // default value, i.e. \arraycolsep in article.cls - - if (group.colSeparationType && group.colSeparationType === "small") { - // We're in a {smallmatrix}. Default column space is \thickspace, - // i.e. 5/18em = 0.2778em, per amsmath.dtx for {smallmatrix}. - // But that needs adjustment because LaTeX applies \scriptstyle to the - // entire array, including the colspace, but this function applies - // \scriptstyle only inside each element. - const localMultiplier = options.havingStyle(Style$1.SCRIPT).sizeMultiplier; - arraycolsep = 0.2778 * (localMultiplier / options.sizeMultiplier); - } // Vertical spacing - - - const baselineskip = 12 * pt; // see size10.clo - // Default \jot from ltmath.dtx - // TODO(edemaine): allow overriding \jot via \setlength (#687) - - const jot = 3 * pt; - const arrayskip = group.arraystretch * baselineskip; - const arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and - - const arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx - - let totalHeight = 0; // Set a position for \hline(s) at the top of the array, if any. - - function setHLinePos(hlinesInGap) { - for (let i = 0; i < hlinesInGap.length; ++i) { - if (i > 0) { - totalHeight += 0.25; - } - - hlines.push({ - pos: totalHeight, - isDashed: hlinesInGap[i] - }); - } - } - - setHLinePos(hLinesBeforeRow[0]); - - for (r = 0; r < group.body.length; ++r) { - const inrow = group.body[r]; - let height = arstrutHeight; // \@array adds an \@arstrut - - let depth = arstrutDepth; // to each tow (via the template) - - if (nc < inrow.length) { - nc = inrow.length; - } - - const outrow = new Array(inrow.length); - - for (c = 0; c < inrow.length; ++c) { - const elt = buildGroup(inrow[c], options); - - if (depth < elt.depth) { - depth = elt.depth; - } - - if (height < elt.height) { - height = elt.height; - } - - outrow[c] = elt; - } - - const rowGap = group.rowGaps[r]; - let gap = 0; - - if (rowGap) { - gap = calculateSize(rowGap, options); - - if (gap > 0) { - // \@argarraycr - gap += arstrutDepth; - - if (depth < gap) { - depth = gap; // \@xargarraycr - } - - gap = 0; - } - } // In AMS multiline environments such as aligned and gathered, rows - // correspond to lines that have additional \jot added to the - // \baselineskip via \openup. - - - if (group.addJot) { - depth += jot; - } - - outrow.height = height; - outrow.depth = depth; - totalHeight += height; - outrow.pos = totalHeight; - totalHeight += depth + gap; // \@yargarraycr - - body[r] = outrow; // Set a position for \hline(s), if any. - - setHLinePos(hLinesBeforeRow[r + 1]); - } - - const offset = totalHeight / 2 + options.fontMetrics().axisHeight; - const colDescriptions = group.cols || []; - const cols = []; - let colSep; - let colDescrNum; - - for (c = 0, colDescrNum = 0; // Continue while either there are more columns or more column - // descriptions, so trailing separators don't get lost. - c < nc || colDescrNum < colDescriptions.length; ++c, ++colDescrNum) { - let colDescr = colDescriptions[colDescrNum] || {}; - let firstSeparator = true; - - while (colDescr.type === "separator") { - // If there is more than one separator in a row, add a space - // between them. - if (!firstSeparator) { - colSep = buildCommon.makeSpan(["arraycolsep"], []); - colSep.style.width = options.fontMetrics().doubleRuleSep + "em"; - cols.push(colSep); - } - - if (colDescr.separator === "|" || colDescr.separator === ":") { - const lineType = colDescr.separator === "|" ? "solid" : "dashed"; - const separator = buildCommon.makeSpan(["vertical-separator"], [], options); - separator.style.height = totalHeight + "em"; - separator.style.borderRightWidth = `${ruleThickness}em`; - separator.style.borderRightStyle = lineType; - separator.style.margin = `0 -${ruleThickness / 2}em`; - separator.style.verticalAlign = -(totalHeight - offset) + "em"; - cols.push(separator); - } else { - throw new ParseError("Invalid separator type: " + colDescr.separator); - } - - colDescrNum++; - colDescr = colDescriptions[colDescrNum] || {}; - firstSeparator = false; - } - - if (c >= nc) { - continue; - } - - let sepwidth; - - if (c > 0 || group.hskipBeforeAndAfter) { - sepwidth = utils.deflt(colDescr.pregap, arraycolsep); - - if (sepwidth !== 0) { - colSep = buildCommon.makeSpan(["arraycolsep"], []); - colSep.style.width = sepwidth + "em"; - cols.push(colSep); - } - } - - let col = []; - - for (r = 0; r < nr; ++r) { - const row = body[r]; - const elem = row[c]; - - if (!elem) { - continue; - } - - const shift = row.pos - offset; - elem.depth = row.depth; - elem.height = row.height; - col.push({ - type: "elem", - elem: elem, - shift: shift - }); - } - - col = buildCommon.makeVList({ - positionType: "individualShift", - children: col - }, options); - col = buildCommon.makeSpan(["col-align-" + (colDescr.align || "c")], [col]); - cols.push(col); - - if (c < nc - 1 || group.hskipBeforeAndAfter) { - sepwidth = utils.deflt(colDescr.postgap, arraycolsep); - - if (sepwidth !== 0) { - colSep = buildCommon.makeSpan(["arraycolsep"], []); - colSep.style.width = sepwidth + "em"; - cols.push(colSep); - } - } - } - - body = buildCommon.makeSpan(["mtable"], cols); // Add \hline(s), if any. - - if (hlines.length > 0) { - const line = buildCommon.makeLineSpan("hline", options, ruleThickness); - const dashes = buildCommon.makeLineSpan("hdashline", options, ruleThickness); - const vListElems = [{ - type: "elem", - elem: body, - shift: 0 - }]; - - while (hlines.length > 0) { - const hline = hlines.pop(); - const lineShift = hline.pos - offset; - - if (hline.isDashed) { - vListElems.push({ - type: "elem", - elem: dashes, - shift: lineShift - }); - } else { - vListElems.push({ - type: "elem", - elem: line, - shift: lineShift - }); - } - } - - body = buildCommon.makeVList({ - positionType: "individualShift", - children: vListElems - }, options); - } - - return buildCommon.makeSpan(["mord"], [body], options); -}; - -const alignMap = { - c: "center ", - l: "left ", - r: "right " -}; - -const mathmlBuilder$3 = function mathmlBuilder(group, options) { - let table = new mathMLTree.MathNode("mtable", group.body.map(function (row) { - return new mathMLTree.MathNode("mtr", row.map(function (cell) { - return new mathMLTree.MathNode("mtd", [buildGroup$1(cell, options)]); - })); - })); // Set column alignment, row spacing, column spacing, and - // array lines by setting attributes on the table element. - // Set the row spacing. In MathML, we specify a gap distance. - // We do not use rowGap[] because MathML automatically increases - // cell height with the height/depth of the element content. - // LaTeX \arraystretch multiplies the row baseline-to-baseline distance. - // We simulate this by adding (arraystretch - 1)em to the gap. This - // does a reasonable job of adjusting arrays containing 1 em tall content. - // The 0.16 and 0.09 values are found empirically. They produce an array - // similar to LaTeX and in which content does not interfere with \hines. - - const gap = group.arraystretch === 0.5 ? 0.1 // {smallmatrix}, {subarray} - : 0.16 + group.arraystretch - 1 + (group.addJot ? 0.09 : 0); - table.setAttribute("rowspacing", gap + "em"); // MathML table lines go only between cells. - // To place a line on an edge we'll use , if necessary. - - let menclose = ""; - let align = ""; - - if (group.cols && group.cols.length > 0) { - // Find column alignment, column spacing, and vertical lines. - const cols = group.cols; - let columnLines = ""; - let prevTypeWasAlign = false; - let iStart = 0; - let iEnd = cols.length; - - if (cols[0].type === "separator") { - menclose += "top "; - iStart = 1; - } - - if (cols[cols.length - 1].type === "separator") { - menclose += "bottom "; - iEnd -= 1; - } - - for (let i = iStart; i < iEnd; i++) { - if (cols[i].type === "align") { - align += alignMap[cols[i].align]; - - if (prevTypeWasAlign) { - columnLines += "none "; - } - - prevTypeWasAlign = true; - } else if (cols[i].type === "separator") { - // MathML accepts only single lines between cells. - // So we read only the first of consecutive separators. - if (prevTypeWasAlign) { - columnLines += cols[i].separator === "|" ? "solid " : "dashed "; - prevTypeWasAlign = false; - } - } - } - - table.setAttribute("columnalign", align.trim()); - - if (/[sd]/.test(columnLines)) { - table.setAttribute("columnlines", columnLines.trim()); - } - } // Set column spacing. - - - if (group.colSeparationType === "align") { - const cols = group.cols || []; - let spacing = ""; - - for (let i = 1; i < cols.length; i++) { - spacing += i % 2 ? "0em " : "1em "; - } - - table.setAttribute("columnspacing", spacing.trim()); - } else if (group.colSeparationType === "alignat") { - table.setAttribute("columnspacing", "0em"); - } else if (group.colSeparationType === "small") { - table.setAttribute("columnspacing", "0.2778em"); - } else { - table.setAttribute("columnspacing", "1em"); - } // Address \hline and \hdashline - - - let rowLines = ""; - const hlines = group.hLinesBeforeRow; - menclose += hlines[0].length > 0 ? "left " : ""; - menclose += hlines[hlines.length - 1].length > 0 ? "right " : ""; - - for (let i = 1; i < hlines.length - 1; i++) { - rowLines += hlines[i].length === 0 ? "none " // MathML accepts only a single line between rows. Read one element. - : hlines[i][0] ? "dashed " : "solid "; - } - - if (/[sd]/.test(rowLines)) { - table.setAttribute("rowlines", rowLines.trim()); - } - - if (menclose !== "") { - table = new mathMLTree.MathNode("menclose", [table]); - table.setAttribute("notation", menclose.trim()); - } - - if (group.arraystretch && group.arraystretch < 1) { - // A small array. Wrap in scriptstyle so row gap is not too large. - table = new mathMLTree.MathNode("mstyle", [table]); - table.setAttribute("scriptlevel", "1"); - } - - return table; -}; // Convenience function for aligned and alignedat environments. - - -const alignedHandler = function alignedHandler(context, args) { - const cols = []; - const res = parseArray(context.parser, { - cols, - addJot: true - }, "display"); // Determining number of columns. - // 1. If the first argument is given, we use it as a number of columns, - // and makes sure that each row doesn't exceed that number. - // 2. Otherwise, just count number of columns = maximum number - // of cells in each row ("aligned" mode -- isAligned will be true). - // - // At the same time, prepend empty group {} at beginning of every second - // cell in each row (starting with second cell) so that operators become - // binary. This behavior is implemented in amsmath's \start@aligned. - - let numMaths; - let numCols = 0; - const emptyGroup = { - type: "ordgroup", - mode: context.mode, - body: [] - }; - - if (args[0] && args[0].type === "ordgroup") { - let arg0 = ""; - - for (let i = 0; i < args[0].body.length; i++) { - const textord = assertNodeType(args[0].body[i], "textord"); - arg0 += textord.text; - } - - numMaths = Number(arg0); - numCols = numMaths * 2; - } - - const isAligned = !numCols; - res.body.forEach(function (row) { - for (let i = 1; i < row.length; i += 2) { - // Modify ordgroup node within styling node - const styling = assertNodeType(row[i], "styling"); - const ordgroup = assertNodeType(styling.body[0], "ordgroup"); - ordgroup.body.unshift(emptyGroup); - } - - if (!isAligned) { - // Case 1 - const curMaths = row.length / 2; - - if (numMaths < curMaths) { - throw new ParseError("Too many math in a row: " + `expected ${numMaths}, but got ${curMaths}`, row[0]); - } - } else if (numCols < row.length) { - // Case 2 - numCols = row.length; - } - }); // Adjusting alignment. - // In aligned mode, we add one \qquad between columns; - // otherwise we add nothing. - - for (let i = 0; i < numCols; ++i) { - let align = "r"; - let pregap = 0; - - if (i % 2 === 1) { - align = "l"; - } else if (i > 0 && isAligned) { - // "aligned" mode. - pregap = 1; // add one \quad - } - - cols[i] = { - type: "align", - align: align, - pregap: pregap, - postgap: 0 - }; - } - - res.colSeparationType = isAligned ? "align" : "alignat"; - return res; -}; // Arrays are part of LaTeX, defined in lttab.dtx so its documentation -// is part of the source2e.pdf file of LaTeX2e source documentation. -// {darray} is an {array} environment where cells are set in \displaystyle, -// as defined in nccmath.sty. - - -defineEnvironment({ - type: "array", - names: ["array", "darray"], - props: { - numArgs: 1 - }, - - handler(context, args) { - // Since no types are specified above, the two possibilities are - // - The argument is wrapped in {} or [], in which case Parser's - // parseGroup() returns an "ordgroup" wrapping some symbol node. - // - The argument is a bare symbol node. - const symNode = checkSymbolNodeType(args[0]); - const colalign = symNode ? [args[0]] : assertNodeType(args[0], "ordgroup").body; - const cols = colalign.map(function (nde) { - const node = assertSymbolNodeType(nde); - const ca = node.text; - - if ("lcr".indexOf(ca) !== -1) { - return { - type: "align", - align: ca - }; - } else if (ca === "|") { - return { - type: "separator", - separator: "|" - }; - } else if (ca === ":") { - return { - type: "separator", - separator: ":" - }; - } - - throw new ParseError("Unknown column alignment: " + ca, nde); - }); - const res = { - cols, - hskipBeforeAndAfter: true // \@preamble in lttab.dtx - - }; - return parseArray(context.parser, res, dCellStyle(context.envName)); - }, - - htmlBuilder: htmlBuilder$3, - mathmlBuilder: mathmlBuilder$3 -}); // The matrix environments of amsmath builds on the array environment -// of LaTeX, which is discussed above. - -defineEnvironment({ - type: "array", - names: ["matrix", "pmatrix", "bmatrix", "Bmatrix", "vmatrix", "Vmatrix"], - props: { - numArgs: 0 - }, - - handler(context) { - const delimiters = { - "matrix": null, - "pmatrix": ["(", ")"], - "bmatrix": ["[", "]"], - "Bmatrix": ["\\{", "\\}"], - "vmatrix": ["|", "|"], - "Vmatrix": ["\\Vert", "\\Vert"] - }[context.envName]; // \hskip -\arraycolsep in amsmath - - const payload = { - hskipBeforeAndAfter: false - }; - const res = parseArray(context.parser, payload, dCellStyle(context.envName)); - return delimiters ? { - type: "leftright", - mode: context.mode, - body: [res], - left: delimiters[0], - right: delimiters[1], - rightColor: undefined // \right uninfluenced by \color in array - - } : res; - }, - - htmlBuilder: htmlBuilder$3, - mathmlBuilder: mathmlBuilder$3 -}); -defineEnvironment({ - type: "array", - names: ["smallmatrix"], - props: { - numArgs: 0 - }, - - handler(context) { - const payload = { - arraystretch: 0.5 - }; - const res = parseArray(context.parser, payload, "script"); - res.colSeparationType = "small"; - return res; - }, - - htmlBuilder: htmlBuilder$3, - mathmlBuilder: mathmlBuilder$3 -}); -defineEnvironment({ - type: "array", - names: ["subarray"], - props: { - numArgs: 1 - }, - - handler(context, args) { - // Parsing of {subarray} is similar to {array} - const symNode = checkSymbolNodeType(args[0]); - const colalign = symNode ? [args[0]] : assertNodeType(args[0], "ordgroup").body; - const cols = colalign.map(function (nde) { - const node = assertSymbolNodeType(nde); - const ca = node.text; // {subarray} only recognizes "l" & "c" - - if ("lc".indexOf(ca) !== -1) { - return { - type: "align", - align: ca - }; - } - - throw new ParseError("Unknown column alignment: " + ca, nde); - }); - - if (cols.length > 1) { - throw new ParseError("{subarray} can contain only one column"); - } - - let res = { - cols, - hskipBeforeAndAfter: false, - arraystretch: 0.5 - }; - res = parseArray(context.parser, res, "script"); - - if (res.body.length > 0 && res.body[0].length > 1) { - throw new ParseError("{subarray} can contain only one column"); - } - - return res; - }, - - htmlBuilder: htmlBuilder$3, - mathmlBuilder: mathmlBuilder$3 -}); // A cases environment (in amsmath.sty) is almost equivalent to -// \def\arraystretch{1.2}% -// \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right. -// {dcases} is a {cases} environment where cells are set in \displaystyle, -// as defined in mathtools.sty. -// {rcases} is another mathtools environment. It's brace is on the right side. - -defineEnvironment({ - type: "array", - names: ["cases", "dcases", "rcases", "drcases"], - props: { - numArgs: 0 - }, - - handler(context) { - const payload = { - arraystretch: 1.2, - cols: [{ - type: "align", - align: "l", - pregap: 0, - // TODO(kevinb) get the current style. - // For now we use the metrics for TEXT style which is what we were - // doing before. Before attempting to get the current style we - // should look at TeX's behavior especially for \over and matrices. - postgap: 1.0 - /* 1em quad */ - - }, { - type: "align", - align: "l", - pregap: 0, - postgap: 0 - }] - }; - const res = parseArray(context.parser, payload, dCellStyle(context.envName)); - return { - type: "leftright", - mode: context.mode, - body: [res], - left: context.envName.indexOf("r") > -1 ? "." : "\\{", - right: context.envName.indexOf("r") > -1 ? "\\}" : ".", - rightColor: undefined - }; - }, - - htmlBuilder: htmlBuilder$3, - mathmlBuilder: mathmlBuilder$3 -}); // An aligned environment is like the align* environment -// except it operates within math mode. -// Note that we assume \nomallineskiplimit to be zero, -// so that \strut@ is the same as \strut. - -defineEnvironment({ - type: "array", - names: ["aligned"], - props: { - numArgs: 0 - }, - handler: alignedHandler, - htmlBuilder: htmlBuilder$3, - mathmlBuilder: mathmlBuilder$3 -}); // A gathered environment is like an array environment with one centered -// column, but where rows are considered lines so get \jot line spacing -// and contents are set in \displaystyle. - -defineEnvironment({ - type: "array", - names: ["gathered"], - props: { - numArgs: 0 - }, - - handler(context) { - const res = { - cols: [{ - type: "align", - align: "c" - }], - addJot: true - }; - return parseArray(context.parser, res, "display"); - }, - - htmlBuilder: htmlBuilder$3, - mathmlBuilder: mathmlBuilder$3 -}); // alignat environment is like an align environment, but one must explicitly -// specify maximum number of columns in each row, and can adjust spacing between -// each columns. - -defineEnvironment({ - type: "array", - names: ["alignedat"], - // One for numbered and for unnumbered; - // but, KaTeX doesn't supports math numbering yet, - // they make no difference for now. - props: { - numArgs: 1 - }, - handler: alignedHandler, - htmlBuilder: htmlBuilder$3, - mathmlBuilder: mathmlBuilder$3 -}); // Catch \hline outside array environment - -defineFunction({ - type: "text", - // Doesn't matter what this is. - names: ["\\hline", "\\hdashline"], - props: { - numArgs: 0, - allowedInText: true, - allowedInMath: true - }, - - handler(context, args) { - throw new ParseError(`${context.funcName} valid only within array environment`); - } - -}); - -const environments = _environments; - -// defineEnvironment definitions. -// $FlowFixMe, "environment" handler returns an environment ParseNode - -defineFunction({ - type: "environment", - names: ["\\begin", "\\end"], - props: { - numArgs: 1, - argTypes: ["text"] - }, - - handler(_ref, args) { - let parser = _ref.parser, - funcName = _ref.funcName; - const nameGroup = args[0]; - - if (nameGroup.type !== "ordgroup") { - throw new ParseError("Invalid environment name", nameGroup); - } - - let envName = ""; - - for (let i = 0; i < nameGroup.body.length; ++i) { - envName += assertNodeType(nameGroup.body[i], "textord").text; - } - - if (funcName === "\\begin") { - // begin...end is similar to left...right - if (!environments.hasOwnProperty(envName)) { - throw new ParseError("No such environment: " + envName, nameGroup); - } // Build the environment object. Arguments and other information will - // be made available to the begin and end methods using properties. - - - const env = environments[envName]; - - const _parser$parseArgument = parser.parseArguments("\\begin{" + envName + "}", env), - args = _parser$parseArgument.args, - optArgs = _parser$parseArgument.optArgs; - - const context = { - mode: parser.mode, - envName, - parser - }; - const result = env.handler(context, args, optArgs); - parser.expect("\\end", false); - const endNameToken = parser.nextToken; - const end = assertNodeType(parser.parseFunction(), "environment"); - - if (end.name !== envName) { - throw new ParseError(`Mismatch: \\begin{${envName}} matched by \\end{${end.name}}`, endNameToken); - } - - return result; - } - - return { - type: "environment", - mode: parser.mode, - name: envName, - nameGroup - }; - } - -}); - -const makeSpan$2 = buildCommon.makeSpan; - -function htmlBuilder$4(group, options) { - const elements = buildExpression(group.body, options, true); - return makeSpan$2([group.mclass], elements, options); -} - -function mathmlBuilder$4(group, options) { - let node; - const inner = buildExpression$1(group.body, options); - - if (group.mclass === "minner") { - return mathMLTree.newDocumentFragment(inner); - } else if (group.mclass === "mord") { - if (group.isCharacterBox) { - node = inner[0]; - node.type = "mi"; - } else { - node = new mathMLTree.MathNode("mi", inner); - } - } else { - if (group.isCharacterBox) { - node = inner[0]; - node.type = "mo"; - } else { - node = new mathMLTree.MathNode("mo", inner); - } // Set spacing based on what is the most likely adjacent atom type. - // See TeXbook p170. - - - if (group.mclass === "mbin") { - node.attributes.lspace = "0.22em"; // medium space - - node.attributes.rspace = "0.22em"; - } else if (group.mclass === "mpunct") { - node.attributes.lspace = "0em"; - node.attributes.rspace = "0.17em"; // thinspace - } else if (group.mclass === "mopen" || group.mclass === "mclose") { - node.attributes.lspace = "0em"; - node.attributes.rspace = "0em"; - } // MathML default space is 5/18 em, so needs no action. - // Ref: https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mo - - } - - return node; -} // Math class commands except \mathop - - -defineFunction({ - type: "mclass", - names: ["\\mathord", "\\mathbin", "\\mathrel", "\\mathopen", "\\mathclose", "\\mathpunct", "\\mathinner"], - props: { - numArgs: 1 - }, - - handler(_ref, args) { - let parser = _ref.parser, - funcName = _ref.funcName; - const body = args[0]; - return { - type: "mclass", - mode: parser.mode, - mclass: "m" + funcName.substr(5), - // TODO(kevinb): don't prefix with 'm' - body: ordargument(body), - isCharacterBox: utils.isCharacterBox(body) - }; - }, - - htmlBuilder: htmlBuilder$4, - mathmlBuilder: mathmlBuilder$4 -}); -const binrelClass = arg => { - // \binrel@ spacing varies with (bin|rel|ord) of the atom in the argument. - // (by rendering separately and with {}s before and after, and measuring - // the change in spacing). We'll do roughly the same by detecting the - // atom type directly. - const atom = arg.type === "ordgroup" && arg.body.length ? arg.body[0] : arg; - - if (atom.type === "atom" && (atom.family === "bin" || atom.family === "rel")) { - return "m" + atom.family; - } else { - return "mord"; - } -}; // \@binrel{x}{y} renders like y but as mbin/mrel/mord if x is mbin/mrel/mord. -// This is equivalent to \binrel@{x}\binrel@@{y} in AMSTeX. - -defineFunction({ - type: "mclass", - names: ["\\@binrel"], - props: { - numArgs: 2 - }, - - handler(_ref2, args) { - let parser = _ref2.parser; - return { - type: "mclass", - mode: parser.mode, - mclass: binrelClass(args[0]), - body: [args[1]], - isCharacterBox: utils.isCharacterBox(args[1]) - }; - } - -}); // Build a relation or stacked op by placing one symbol on top of another - -defineFunction({ - type: "mclass", - names: ["\\stackrel", "\\overset", "\\underset"], - props: { - numArgs: 2 - }, - - handler(_ref3, args) { - let parser = _ref3.parser, - funcName = _ref3.funcName; - const baseArg = args[1]; - const shiftedArg = args[0]; - let mclass; - - if (funcName !== "\\stackrel") { - // LaTeX applies \binrel spacing to \overset and \underset. - mclass = binrelClass(baseArg); - } else { - mclass = "mrel"; // for \stackrel - } - - const baseOp = { - type: "op", - mode: baseArg.mode, - limits: true, - alwaysHandleSupSub: true, - parentIsSupSub: false, - symbol: false, - suppressBaseShift: funcName !== "\\stackrel", - body: ordargument(baseArg) - }; - const supsub = { - type: "supsub", - mode: shiftedArg.mode, - base: baseOp, - sup: funcName === "\\underset" ? null : shiftedArg, - sub: funcName === "\\underset" ? shiftedArg : null - }; - return { - type: "mclass", - mode: parser.mode, - mclass, - body: [supsub], - isCharacterBox: utils.isCharacterBox(supsub) - }; - }, - - htmlBuilder: htmlBuilder$4, - mathmlBuilder: mathmlBuilder$4 -}); - -// TODO(kevinb): implement \\sl and \\sc - -const htmlBuilder$5 = (group, options) => { - const font = group.font; - const newOptions = options.withFont(font); - return buildGroup(group.body, newOptions); -}; - -const mathmlBuilder$5 = (group, options) => { - const font = group.font; - const newOptions = options.withFont(font); - return buildGroup$1(group.body, newOptions); -}; - -const fontAliases = { - "\\Bbb": "\\mathbb", - "\\bold": "\\mathbf", - "\\frak": "\\mathfrak", - "\\bm": "\\boldsymbol" -}; -defineFunction({ - type: "font", - names: [// styles, except \boldsymbol defined below - "\\mathrm", "\\mathit", "\\mathbf", "\\mathnormal", // families - "\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf", "\\mathtt", // aliases, except \bm defined below - "\\Bbb", "\\bold", "\\frak"], - props: { - numArgs: 1, - greediness: 2 - }, - handler: (_ref, args) => { - let parser = _ref.parser, - funcName = _ref.funcName; - const body = args[0]; - let func = funcName; - - if (func in fontAliases) { - func = fontAliases[func]; - } - - return { - type: "font", - mode: parser.mode, - font: func.slice(1), - body - }; - }, - htmlBuilder: htmlBuilder$5, - mathmlBuilder: mathmlBuilder$5 -}); -defineFunction({ - type: "mclass", - names: ["\\boldsymbol", "\\bm"], - props: { - numArgs: 1, - greediness: 2 - }, - handler: (_ref2, args) => { - let parser = _ref2.parser; - const body = args[0]; - const isCharacterBox = utils.isCharacterBox(body); // amsbsy.sty's \boldsymbol uses \binrel spacing to inherit the - // argument's bin|rel|ord status - - return { - type: "mclass", - mode: parser.mode, - mclass: binrelClass(body), - body: [{ - type: "font", - mode: parser.mode, - font: "boldsymbol", - body - }], - isCharacterBox: isCharacterBox - }; - } -}); // Old font changing functions - -defineFunction({ - type: "font", - names: ["\\rm", "\\sf", "\\tt", "\\bf", "\\it", "\\cal"], - props: { - numArgs: 0, - allowedInText: true - }, - handler: (_ref3, args) => { - let parser = _ref3.parser, - funcName = _ref3.funcName, - breakOnTokenText = _ref3.breakOnTokenText; - const mode = parser.mode; - const body = parser.parseExpression(true, breakOnTokenText); - const style = `math${funcName.slice(1)}`; - return { - type: "font", - mode: mode, - font: style, - body: { - type: "ordgroup", - mode: parser.mode, - body - } - }; - }, - htmlBuilder: htmlBuilder$5, - mathmlBuilder: mathmlBuilder$5 -}); - -const adjustStyle = (size, originalStyle) => { - // Figure out what style this fraction should be in based on the - // function used - let style = originalStyle; - - if (size === "display") { - // Get display style as a default. - // If incoming style is sub/sup, use style.text() to get correct size. - style = style.id >= Style$1.SCRIPT.id ? style.text() : Style$1.DISPLAY; - } else if (size === "text" && style.size === Style$1.DISPLAY.size) { - // We're in a \tfrac but incoming style is displaystyle, so: - style = Style$1.TEXT; - } else if (size === "script") { - style = Style$1.SCRIPT; - } else if (size === "scriptscript") { - style = Style$1.SCRIPTSCRIPT; - } - - return style; -}; - -const htmlBuilder$6 = (group, options) => { - // Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e). - const style = adjustStyle(group.size, options.style); - const nstyle = style.fracNum(); - const dstyle = style.fracDen(); - let newOptions; - newOptions = options.havingStyle(nstyle); - const numerm = buildGroup(group.numer, newOptions, options); - - if (group.continued) { - // \cfrac inserts a \strut into the numerator. - // Get \strut dimensions from TeXbook page 353. - const hStrut = 8.5 / options.fontMetrics().ptPerEm; - const dStrut = 3.5 / options.fontMetrics().ptPerEm; - numerm.height = numerm.height < hStrut ? hStrut : numerm.height; - numerm.depth = numerm.depth < dStrut ? dStrut : numerm.depth; - } - - newOptions = options.havingStyle(dstyle); - const denomm = buildGroup(group.denom, newOptions, options); - let rule; - let ruleWidth; - let ruleSpacing; - - if (group.hasBarLine) { - if (group.barSize) { - ruleWidth = calculateSize(group.barSize, options); - rule = buildCommon.makeLineSpan("frac-line", options, ruleWidth); - } else { - rule = buildCommon.makeLineSpan("frac-line", options); - } - - ruleWidth = rule.height; - ruleSpacing = rule.height; - } else { - rule = null; - ruleWidth = 0; - ruleSpacing = options.fontMetrics().defaultRuleThickness; - } // Rule 15b - - - let numShift; - let clearance; - let denomShift; - - if (style.size === Style$1.DISPLAY.size || group.size === "display") { - numShift = options.fontMetrics().num1; - - if (ruleWidth > 0) { - clearance = 3 * ruleSpacing; - } else { - clearance = 7 * ruleSpacing; - } - - denomShift = options.fontMetrics().denom1; - } else { - if (ruleWidth > 0) { - numShift = options.fontMetrics().num2; - clearance = ruleSpacing; - } else { - numShift = options.fontMetrics().num3; - clearance = 3 * ruleSpacing; - } - - denomShift = options.fontMetrics().denom2; - } - - let frac; - - if (!rule) { - // Rule 15c - const candidateClearance = numShift - numerm.depth - (denomm.height - denomShift); - - if (candidateClearance < clearance) { - numShift += 0.5 * (clearance - candidateClearance); - denomShift += 0.5 * (clearance - candidateClearance); - } - - frac = buildCommon.makeVList({ - positionType: "individualShift", - children: [{ - type: "elem", - elem: denomm, - shift: denomShift - }, { - type: "elem", - elem: numerm, - shift: -numShift - }] - }, options); - } else { - // Rule 15d - const axisHeight = options.fontMetrics().axisHeight; - - if (numShift - numerm.depth - (axisHeight + 0.5 * ruleWidth) < clearance) { - numShift += clearance - (numShift - numerm.depth - (axisHeight + 0.5 * ruleWidth)); - } - - if (axisHeight - 0.5 * ruleWidth - (denomm.height - denomShift) < clearance) { - denomShift += clearance - (axisHeight - 0.5 * ruleWidth - (denomm.height - denomShift)); - } - - const midShift = -(axisHeight - 0.5 * ruleWidth); - frac = buildCommon.makeVList({ - positionType: "individualShift", - children: [{ - type: "elem", - elem: denomm, - shift: denomShift - }, { - type: "elem", - elem: rule, - shift: midShift - }, { - type: "elem", - elem: numerm, - shift: -numShift - }] - }, options); - } // Since we manually change the style sometimes (with \dfrac or \tfrac), - // account for the possible size change here. - - - newOptions = options.havingStyle(style); - frac.height *= newOptions.sizeMultiplier / options.sizeMultiplier; - frac.depth *= newOptions.sizeMultiplier / options.sizeMultiplier; // Rule 15e - - let delimSize; - - if (style.size === Style$1.DISPLAY.size) { - delimSize = options.fontMetrics().delim1; - } else { - delimSize = options.fontMetrics().delim2; - } - - let leftDelim; - let rightDelim; - - if (group.leftDelim == null) { - leftDelim = makeNullDelimiter(options, ["mopen"]); - } else { - leftDelim = delimiter.customSizedDelim(group.leftDelim, delimSize, true, options.havingStyle(style), group.mode, ["mopen"]); - } - - if (group.continued) { - rightDelim = buildCommon.makeSpan([]); // zero width for \cfrac - } else if (group.rightDelim == null) { - rightDelim = makeNullDelimiter(options, ["mclose"]); - } else { - rightDelim = delimiter.customSizedDelim(group.rightDelim, delimSize, true, options.havingStyle(style), group.mode, ["mclose"]); - } - - return buildCommon.makeSpan(["mord"].concat(newOptions.sizingClasses(options)), [leftDelim, buildCommon.makeSpan(["mfrac"], [frac]), rightDelim], options); -}; - -const mathmlBuilder$6 = (group, options) => { - let node = new mathMLTree.MathNode("mfrac", [buildGroup$1(group.numer, options), buildGroup$1(group.denom, options)]); - - if (!group.hasBarLine) { - node.setAttribute("linethickness", "0px"); - } else if (group.barSize) { - const ruleWidth = calculateSize(group.barSize, options); - node.setAttribute("linethickness", ruleWidth + "em"); - } - - const style = adjustStyle(group.size, options.style); - - if (style.size !== options.style.size) { - node = new mathMLTree.MathNode("mstyle", [node]); - const isDisplay = style.size === Style$1.DISPLAY.size ? "true" : "false"; - node.setAttribute("displaystyle", isDisplay); - node.setAttribute("scriptlevel", "0"); - } - - if (group.leftDelim != null || group.rightDelim != null) { - const withDelims = []; - - if (group.leftDelim != null) { - const leftOp = new mathMLTree.MathNode("mo", [new mathMLTree.TextNode(group.leftDelim.replace("\\", ""))]); - leftOp.setAttribute("fence", "true"); - withDelims.push(leftOp); - } - - withDelims.push(node); - - if (group.rightDelim != null) { - const rightOp = new mathMLTree.MathNode("mo", [new mathMLTree.TextNode(group.rightDelim.replace("\\", ""))]); - rightOp.setAttribute("fence", "true"); - withDelims.push(rightOp); - } - - return makeRow(withDelims); - } - - return node; -}; - -defineFunction({ - type: "genfrac", - names: ["\\cfrac", "\\dfrac", "\\frac", "\\tfrac", "\\dbinom", "\\binom", "\\tbinom", "\\\\atopfrac", // can’t be entered directly - "\\\\bracefrac", "\\\\brackfrac"], - props: { - numArgs: 2, - greediness: 2 - }, - handler: (_ref, args) => { - let parser = _ref.parser, - funcName = _ref.funcName; - const numer = args[0]; - const denom = args[1]; - let hasBarLine; - let leftDelim = null; - let rightDelim = null; - let size = "auto"; - - switch (funcName) { - case "\\cfrac": - case "\\dfrac": - case "\\frac": - case "\\tfrac": - hasBarLine = true; - break; - - case "\\\\atopfrac": - hasBarLine = false; - break; - - case "\\dbinom": - case "\\binom": - case "\\tbinom": - hasBarLine = false; - leftDelim = "("; - rightDelim = ")"; - break; - - case "\\\\bracefrac": - hasBarLine = false; - leftDelim = "\\{"; - rightDelim = "\\}"; - break; - - case "\\\\brackfrac": - hasBarLine = false; - leftDelim = "["; - rightDelim = "]"; - break; - - default: - throw new Error("Unrecognized genfrac command"); - } - - switch (funcName) { - case "\\cfrac": - case "\\dfrac": - case "\\dbinom": - size = "display"; - break; - - case "\\tfrac": - case "\\tbinom": - size = "text"; - break; - } - - return { - type: "genfrac", - mode: parser.mode, - continued: funcName === "\\cfrac", - numer, - denom, - hasBarLine, - leftDelim, - rightDelim, - size, - barSize: null - }; - }, - htmlBuilder: htmlBuilder$6, - mathmlBuilder: mathmlBuilder$6 -}); // Infix generalized fractions -- these are not rendered directly, but replaced -// immediately by one of the variants above. - -defineFunction({ - type: "infix", - names: ["\\over", "\\choose", "\\atop", "\\brace", "\\brack"], - props: { - numArgs: 0, - infix: true - }, - - handler(_ref2) { - let parser = _ref2.parser, - funcName = _ref2.funcName, - token = _ref2.token; - let replaceWith; - - switch (funcName) { - case "\\over": - replaceWith = "\\frac"; - break; - - case "\\choose": - replaceWith = "\\binom"; - break; - - case "\\atop": - replaceWith = "\\\\atopfrac"; - break; - - case "\\brace": - replaceWith = "\\\\bracefrac"; - break; - - case "\\brack": - replaceWith = "\\\\brackfrac"; - break; - - default: - throw new Error("Unrecognized infix genfrac command"); - } - - return { - type: "infix", - mode: parser.mode, - replaceWith, - token - }; - } - -}); -const stylArray = ["display", "text", "script", "scriptscript"]; - -const delimFromValue = function delimFromValue(delimString) { - let delim = null; - - if (delimString.length > 0) { - delim = delimString; - delim = delim === "." ? null : delim; - } - - return delim; -}; - -defineFunction({ - type: "genfrac", - names: ["\\genfrac"], - props: { - numArgs: 6, - greediness: 6, - argTypes: ["math", "math", "size", "text", "math", "math"] - }, - - handler(_ref3, args) { - let parser = _ref3.parser; - const numer = args[4]; - const denom = args[5]; // Look into the parse nodes to get the desired delimiters. - - const leftDelim = args[0].type === "atom" && args[0].family === "open" ? delimFromValue(args[0].text) : null; - const rightDelim = args[1].type === "atom" && args[1].family === "close" ? delimFromValue(args[1].text) : null; - const barNode = assertNodeType(args[2], "size"); - let hasBarLine; - let barSize = null; - - if (barNode.isBlank) { - // \genfrac acts differently than \above. - // \genfrac treats an empty size group as a signal to use a - // standard bar size. \above would see size = 0 and omit the bar. - hasBarLine = true; - } else { - barSize = barNode.value; - hasBarLine = barSize.number > 0; - } // Find out if we want displaystyle, textstyle, etc. - - - let size = "auto"; - let styl = args[3]; - - if (styl.type === "ordgroup") { - if (styl.body.length > 0) { - const textOrd = assertNodeType(styl.body[0], "textord"); - size = stylArray[Number(textOrd.text)]; - } - } else { - styl = assertNodeType(styl, "textord"); - size = stylArray[Number(styl.text)]; - } - - return { - type: "genfrac", - mode: parser.mode, - numer, - denom, - continued: false, - hasBarLine, - barSize, - leftDelim, - rightDelim, - size - }; - }, - - htmlBuilder: htmlBuilder$6, - mathmlBuilder: mathmlBuilder$6 -}); // \above is an infix fraction that also defines a fraction bar size. - -defineFunction({ - type: "infix", - names: ["\\above"], - props: { - numArgs: 1, - argTypes: ["size"], - infix: true - }, - - handler(_ref4, args) { - let parser = _ref4.parser, - funcName = _ref4.funcName, - token = _ref4.token; - return { - type: "infix", - mode: parser.mode, - replaceWith: "\\\\abovefrac", - size: assertNodeType(args[0], "size").value, - token - }; - } - -}); -defineFunction({ - type: "genfrac", - names: ["\\\\abovefrac"], - props: { - numArgs: 3, - argTypes: ["math", "size", "math"] - }, - handler: (_ref5, args) => { - let parser = _ref5.parser, - funcName = _ref5.funcName; - const numer = args[0]; - const barSize = assert(assertNodeType(args[1], "infix").size); - const denom = args[2]; - const hasBarLine = barSize.number > 0; - return { - type: "genfrac", - mode: parser.mode, - numer, - denom, - continued: false, - hasBarLine, - barSize, - leftDelim: null, - rightDelim: null, - size: "auto" - }; - }, - htmlBuilder: htmlBuilder$6, - mathmlBuilder: mathmlBuilder$6 -}); - -// NOTE: Unlike most `htmlBuilder`s, this one handles not only "horizBrace", but -const htmlBuilder$7 = (grp, options) => { - const style = options.style; // Pull out the `ParseNode<"horizBrace">` if `grp` is a "supsub" node. - - let supSubGroup; - let group; - - if (grp.type === "supsub") { - // Ref: LaTeX source2e: }}}}\limits} - // i.e. LaTeX treats the brace similar to an op and passes it - // with \limits, so we need to assign supsub style. - supSubGroup = grp.sup ? buildGroup(grp.sup, options.havingStyle(style.sup()), options) : buildGroup(grp.sub, options.havingStyle(style.sub()), options); - group = assertNodeType(grp.base, "horizBrace"); - } else { - group = assertNodeType(grp, "horizBrace"); - } // Build the base group - - - const body = buildGroup(group.base, options.havingBaseStyle(Style$1.DISPLAY)); // Create the stretchy element - - const braceBody = stretchy.svgSpan(group, options); // Generate the vlist, with the appropriate kerns ┏━━━━━━━━┓ - // This first vlist contains the content and the brace: equation - - let vlist; - - if (group.isOver) { - vlist = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: body - }, { - type: "kern", - size: 0.1 - }, { - type: "elem", - elem: braceBody - }] - }, options); // $FlowFixMe: Replace this with passing "svg-align" into makeVList. - - vlist.children[0].children[0].children[1].classes.push("svg-align"); - } else { - vlist = buildCommon.makeVList({ - positionType: "bottom", - positionData: body.depth + 0.1 + braceBody.height, - children: [{ - type: "elem", - elem: braceBody - }, { - type: "kern", - size: 0.1 - }, { - type: "elem", - elem: body - }] - }, options); // $FlowFixMe: Replace this with passing "svg-align" into makeVList. - - vlist.children[0].children[0].children[0].classes.push("svg-align"); - } - - if (supSubGroup) { - // To write the supsub, wrap the first vlist in another vlist: - // They can't all go in the same vlist, because the note might be - // wider than the equation. We want the equation to control the - // brace width. - // note long note long note - // ┏━━━━━━━━┓ or ┏━━━┓ not ┏━━━━━━━━━┓ - // equation eqn eqn - const vSpan = buildCommon.makeSpan(["mord", group.isOver ? "mover" : "munder"], [vlist], options); - - if (group.isOver) { - vlist = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: vSpan - }, { - type: "kern", - size: 0.2 - }, { - type: "elem", - elem: supSubGroup - }] - }, options); - } else { - vlist = buildCommon.makeVList({ - positionType: "bottom", - positionData: vSpan.depth + 0.2 + supSubGroup.height + supSubGroup.depth, - children: [{ - type: "elem", - elem: supSubGroup - }, { - type: "kern", - size: 0.2 - }, { - type: "elem", - elem: vSpan - }] - }, options); - } - } - - return buildCommon.makeSpan(["mord", group.isOver ? "mover" : "munder"], [vlist], options); -}; - -const mathmlBuilder$7 = (group, options) => { - const accentNode = stretchy.mathMLnode(group.label); - return new mathMLTree.MathNode(group.isOver ? "mover" : "munder", [buildGroup$1(group.base, options), accentNode]); -}; // Horizontal stretchy braces - - -defineFunction({ - type: "horizBrace", - names: ["\\overbrace", "\\underbrace"], - props: { - numArgs: 1 - }, - - handler(_ref, args) { - let parser = _ref.parser, - funcName = _ref.funcName; - return { - type: "horizBrace", - mode: parser.mode, - label: funcName, - isOver: /^\\over/.test(funcName), - base: args[0] - }; - }, - - htmlBuilder: htmlBuilder$7, - mathmlBuilder: mathmlBuilder$7 -}); - -defineFunction({ - type: "href", - names: ["\\href"], - props: { - numArgs: 2, - argTypes: ["url", "original"], - allowedInText: true - }, - handler: (_ref, args) => { - let parser = _ref.parser; - const body = args[1]; - const href = assertNodeType(args[0], "url").url; - - if (!parser.settings.isTrusted({ - command: "\\href", - url: href - })) { - return parser.formatUnsupportedCmd("\\href"); - } - - return { - type: "href", - mode: parser.mode, - href, - body: ordargument(body) - }; - }, - htmlBuilder: (group, options) => { - const elements = buildExpression(group.body, options, false); - return buildCommon.makeAnchor(group.href, [], elements, options); - }, - mathmlBuilder: (group, options) => { - let math = buildExpressionRow(group.body, options); - - if (!(math instanceof MathNode)) { - math = new MathNode("mrow", [math]); - } - - math.setAttribute("href", group.href); - return math; - } -}); -defineFunction({ - type: "href", - names: ["\\url"], - props: { - numArgs: 1, - argTypes: ["url"], - allowedInText: true - }, - handler: (_ref2, args) => { - let parser = _ref2.parser; - const href = assertNodeType(args[0], "url").url; - - if (!parser.settings.isTrusted({ - command: "\\url", - url: href - })) { - return parser.formatUnsupportedCmd("\\url"); - } - - const chars = []; - - for (let i = 0; i < href.length; i++) { - let c = href[i]; - - if (c === "~") { - c = "\\textasciitilde"; - } - - chars.push({ - type: "textord", - mode: "text", - text: c - }); - } - - const body = { - type: "text", - mode: parser.mode, - font: "\\texttt", - body: chars - }; - return { - type: "href", - mode: parser.mode, - href, - body: ordargument(body) - }; - } -}); - -defineFunction({ - type: "html", - names: ["\\htmlClass", "\\htmlId", "\\htmlStyle", "\\htmlData"], - props: { - numArgs: 2, - argTypes: ["raw", "original"], - allowedInText: true - }, - handler: (_ref, args) => { - let parser = _ref.parser, - funcName = _ref.funcName, - token = _ref.token; - const value = assertNodeType(args[0], "raw").string; - const body = args[1]; - - if (parser.settings.strict) { - parser.settings.reportNonstrict("htmlExtension", "HTML extension is disabled on strict mode"); - } - - let trustContext; - const attributes = {}; - - switch (funcName) { - case "\\htmlClass": - attributes.class = value; - trustContext = { - command: "\\htmlClass", - class: value - }; - break; - - case "\\htmlId": - attributes.id = value; - trustContext = { - command: "\\htmlId", - id: value - }; - break; - - case "\\htmlStyle": - attributes.style = value; - trustContext = { - command: "\\htmlStyle", - style: value - }; - break; - - case "\\htmlData": - { - const data = value.split(","); - - for (let i = 0; i < data.length; i++) { - const keyVal = data[i].split("="); - - if (keyVal.length !== 2) { - throw new ParseError("Error parsing key-value for \\htmlData"); - } - - attributes["data-" + keyVal[0].trim()] = keyVal[1].trim(); - } - - trustContext = { - command: "\\htmlData", - attributes - }; - break; - } - - default: - throw new Error("Unrecognized html command"); - } - - if (!parser.settings.isTrusted(trustContext)) { - return parser.formatUnsupportedCmd(funcName); - } - - return { - type: "html", - mode: parser.mode, - attributes, - body: ordargument(body) - }; - }, - htmlBuilder: (group, options) => { - const elements = buildExpression(group.body, options, false); - const classes = ["enclosing"]; - - if (group.attributes.class) { - classes.push(...group.attributes.class.trim().split(/\s+/)); - } - - const span = buildCommon.makeSpan(classes, elements, options); - - for (const attr in group.attributes) { - if (attr !== "class" && group.attributes.hasOwnProperty(attr)) { - span.setAttribute(attr, group.attributes[attr]); - } - } - - return span; - }, - mathmlBuilder: (group, options) => { - return buildExpressionRow(group.body, options); - } -}); - -defineFunction({ - type: "htmlmathml", - names: ["\\html@mathml"], - props: { - numArgs: 2, - allowedInText: true - }, - handler: (_ref, args) => { - let parser = _ref.parser; - return { - type: "htmlmathml", - mode: parser.mode, - html: ordargument(args[0]), - mathml: ordargument(args[1]) - }; - }, - htmlBuilder: (group, options) => { - const elements = buildExpression(group.html, options, false); - return buildCommon.makeFragment(elements); - }, - mathmlBuilder: (group, options) => { - return buildExpressionRow(group.mathml, options); - } -}); - -const sizeData = function sizeData(str) { - if (/^[-+]? *(\d+(\.\d*)?|\.\d+)$/.test(str)) { - // str is a number with no unit specified. - // default unit is bp, per graphix package. - return { - number: +str, - unit: "bp" - }; - } else { - const match = /([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/.exec(str); - - if (!match) { - throw new ParseError("Invalid size: '" + str + "' in \\includegraphics"); - } - - const data = { - number: +(match[1] + match[2]), - // sign + magnitude, cast to number - unit: match[3] - }; - - if (!validUnit(data)) { - throw new ParseError("Invalid unit: '" + data.unit + "' in \\includegraphics."); - } - - return data; - } -}; - -defineFunction({ - type: "includegraphics", - names: ["\\includegraphics"], - props: { - numArgs: 1, - numOptionalArgs: 1, - argTypes: ["raw", "url"], - allowedInText: false - }, - handler: (_ref, args, optArgs) => { - let parser = _ref.parser; - let width = { - number: 0, - unit: "em" - }; - let height = { - number: 0.9, - unit: "em" - }; // sorta character sized. - - let totalheight = { - number: 0, - unit: "em" - }; - let alt = ""; - - if (optArgs[0]) { - const attributeStr = assertNodeType(optArgs[0], "raw").string; // Parser.js does not parse key/value pairs. We get a string. - - const attributes = attributeStr.split(","); - - for (let i = 0; i < attributes.length; i++) { - const keyVal = attributes[i].split("="); - - if (keyVal.length === 2) { - const str = keyVal[1].trim(); - - switch (keyVal[0].trim()) { - case "alt": - alt = str; - break; - - case "width": - width = sizeData(str); - break; - - case "height": - height = sizeData(str); - break; - - case "totalheight": - totalheight = sizeData(str); - break; - - default: - throw new ParseError("Invalid key: '" + keyVal[0] + "' in \\includegraphics."); - } - } - } - } - - const src = assertNodeType(args[0], "url").url; - - if (alt === "") { - // No alt given. Use the file name. Strip away the path. - alt = src; - alt = alt.replace(/^.*[\\/]/, ''); - alt = alt.substring(0, alt.lastIndexOf('.')); - } - - if (!parser.settings.isTrusted({ - command: "\\includegraphics", - url: src - })) { - return parser.formatUnsupportedCmd("\\includegraphics"); - } - - return { - type: "includegraphics", - mode: parser.mode, - alt: alt, - width: width, - height: height, - totalheight: totalheight, - src: src - }; - }, - htmlBuilder: (group, options) => { - const height = calculateSize(group.height, options); - let depth = 0; - - if (group.totalheight.number > 0) { - depth = calculateSize(group.totalheight, options) - height; - depth = Number(depth.toFixed(2)); - } - - let width = 0; - - if (group.width.number > 0) { - width = calculateSize(group.width, options); - } - - const style = { - height: height + depth + "em" - }; - - if (width > 0) { - style.width = width + "em"; - } - - if (depth > 0) { - style.verticalAlign = -depth + "em"; - } - - const node = new Img(group.src, group.alt, style); - node.height = height; - node.depth = depth; - return node; - }, - mathmlBuilder: (group, options) => { - const node = new mathMLTree.MathNode("mglyph", []); - node.setAttribute("alt", group.alt); - const height = calculateSize(group.height, options); - let depth = 0; - - if (group.totalheight.number > 0) { - depth = calculateSize(group.totalheight, options) - height; - depth = depth.toFixed(2); - node.setAttribute("valign", "-" + depth + "em"); - } - - node.setAttribute("height", height + depth + "em"); - - if (group.width.number > 0) { - const width = calculateSize(group.width, options); - node.setAttribute("width", width + "em"); - } - - node.setAttribute("src", group.src); - return node; - } -}); - -// Horizontal spacing commands - -defineFunction({ - type: "kern", - names: ["\\kern", "\\mkern", "\\hskip", "\\mskip"], - props: { - numArgs: 1, - argTypes: ["size"], - allowedInText: true - }, - - handler(_ref, args) { - let parser = _ref.parser, - funcName = _ref.funcName; - const size = assertNodeType(args[0], "size"); - - if (parser.settings.strict) { - const mathFunction = funcName[1] === 'm'; // \mkern, \mskip - - const muUnit = size.value.unit === 'mu'; - - if (mathFunction) { - if (!muUnit) { - parser.settings.reportNonstrict("mathVsTextUnits", `LaTeX's ${funcName} supports only mu units, ` + `not ${size.value.unit} units`); - } - - if (parser.mode !== "math") { - parser.settings.reportNonstrict("mathVsTextUnits", `LaTeX's ${funcName} works only in math mode`); - } - } else { - // !mathFunction - if (muUnit) { - parser.settings.reportNonstrict("mathVsTextUnits", `LaTeX's ${funcName} doesn't support mu units`); - } - } - } - - return { - type: "kern", - mode: parser.mode, - dimension: size.value - }; - }, - - htmlBuilder(group, options) { - return buildCommon.makeGlue(group.dimension, options); - }, - - mathmlBuilder(group, options) { - const dimension = calculateSize(group.dimension, options); - return new mathMLTree.SpaceNode(dimension); - } - -}); - -// Horizontal overlap functions -defineFunction({ - type: "lap", - names: ["\\mathllap", "\\mathrlap", "\\mathclap"], - props: { - numArgs: 1, - allowedInText: true - }, - handler: (_ref, args) => { - let parser = _ref.parser, - funcName = _ref.funcName; - const body = args[0]; - return { - type: "lap", - mode: parser.mode, - alignment: funcName.slice(5), - body - }; - }, - htmlBuilder: (group, options) => { - // mathllap, mathrlap, mathclap - let inner; - - if (group.alignment === "clap") { - // ref: https://www.math.lsu.edu/~aperlis/publications/mathclap/ - inner = buildCommon.makeSpan([], [buildGroup(group.body, options)]); // wrap, since CSS will center a .clap > .inner > span - - inner = buildCommon.makeSpan(["inner"], [inner], options); - } else { - inner = buildCommon.makeSpan(["inner"], [buildGroup(group.body, options)]); - } - - const fix = buildCommon.makeSpan(["fix"], []); - let node = buildCommon.makeSpan([group.alignment], [inner, fix], options); // At this point, we have correctly set horizontal alignment of the - // two items involved in the lap. - // Next, use a strut to set the height of the HTML bounding box. - // Otherwise, a tall argument may be misplaced. - // This code resolved issue #1153 - - const strut = buildCommon.makeSpan(["strut"]); - strut.style.height = node.height + node.depth + "em"; - strut.style.verticalAlign = -node.depth + "em"; - node.children.unshift(strut); // Next, prevent vertical misplacement when next to something tall. - // This code resolves issue #1234 - - node = buildCommon.makeSpan(["thinbox"], [node], options); - return buildCommon.makeSpan(["mord", "vbox"], [node], options); - }, - mathmlBuilder: (group, options) => { - // mathllap, mathrlap, mathclap - const node = new mathMLTree.MathNode("mpadded", [buildGroup$1(group.body, options)]); - - if (group.alignment !== "rlap") { - const offset = group.alignment === "llap" ? "-1" : "-0.5"; - node.setAttribute("lspace", offset + "width"); - } - - node.setAttribute("width", "0px"); - return node; - } -}); - -defineFunction({ - type: "styling", - names: ["\\(", "$"], - props: { - numArgs: 0, - allowedInText: true, - allowedInMath: false - }, - - handler(_ref, args) { - let funcName = _ref.funcName, - parser = _ref.parser; - const outerMode = parser.mode; - parser.switchMode("math"); - const close = funcName === "\\(" ? "\\)" : "$"; - const body = parser.parseExpression(false, close); - parser.expect(close); - parser.switchMode(outerMode); - return { - type: "styling", - mode: parser.mode, - style: "text", - body - }; - } - -}); // Check for extra closing math delimiters - -defineFunction({ - type: "text", - // Doesn't matter what this is. - names: ["\\)", "\\]"], - props: { - numArgs: 0, - allowedInText: true, - allowedInMath: false - }, - - handler(context, args) { - throw new ParseError(`Mismatched ${context.funcName}`); - } - -}); - -const chooseMathStyle = (group, options) => { - switch (options.style.size) { - case Style$1.DISPLAY.size: - return group.display; - - case Style$1.TEXT.size: - return group.text; - - case Style$1.SCRIPT.size: - return group.script; - - case Style$1.SCRIPTSCRIPT.size: - return group.scriptscript; - - default: - return group.text; - } -}; - -defineFunction({ - type: "mathchoice", - names: ["\\mathchoice"], - props: { - numArgs: 4 - }, - handler: (_ref, args) => { - let parser = _ref.parser; - return { - type: "mathchoice", - mode: parser.mode, - display: ordargument(args[0]), - text: ordargument(args[1]), - script: ordargument(args[2]), - scriptscript: ordargument(args[3]) - }; - }, - htmlBuilder: (group, options) => { - const body = chooseMathStyle(group, options); - const elements = buildExpression(body, options, false); - return buildCommon.makeFragment(elements); - }, - mathmlBuilder: (group, options) => { - const body = chooseMathStyle(group, options); - return buildExpressionRow(body, options); - } -}); - -// For an operator with limits, assemble the base, sup, and sub into a span. -const assembleSupSub = (base, supGroup, subGroup, options, style, slant, baseShift) => { - base = buildCommon.makeSpan([], [base]); - let sub; - let sup; // We manually have to handle the superscripts and subscripts. This, - // aside from the kern calculations, is copied from supsub. - - if (supGroup) { - const elem = buildGroup(supGroup, options.havingStyle(style.sup()), options); - sup = { - elem, - kern: Math.max(options.fontMetrics().bigOpSpacing1, options.fontMetrics().bigOpSpacing3 - elem.depth) - }; - } - - if (subGroup) { - const elem = buildGroup(subGroup, options.havingStyle(style.sub()), options); - sub = { - elem, - kern: Math.max(options.fontMetrics().bigOpSpacing2, options.fontMetrics().bigOpSpacing4 - elem.height) - }; - } // Build the final group as a vlist of the possible subscript, base, - // and possible superscript. - - - let finalGroup; - - if (sup && sub) { - const bottom = options.fontMetrics().bigOpSpacing5 + sub.elem.height + sub.elem.depth + sub.kern + base.depth + baseShift; - finalGroup = buildCommon.makeVList({ - positionType: "bottom", - positionData: bottom, - children: [{ - type: "kern", - size: options.fontMetrics().bigOpSpacing5 - }, { - type: "elem", - elem: sub.elem, - marginLeft: -slant + "em" - }, { - type: "kern", - size: sub.kern - }, { - type: "elem", - elem: base - }, { - type: "kern", - size: sup.kern - }, { - type: "elem", - elem: sup.elem, - marginLeft: slant + "em" - }, { - type: "kern", - size: options.fontMetrics().bigOpSpacing5 - }] - }, options); - } else if (sub) { - const top = base.height - baseShift; // Shift the limits by the slant of the symbol. Note - // that we are supposed to shift the limits by 1/2 of the slant, - // but since we are centering the limits adding a full slant of - // margin will shift by 1/2 that. - - finalGroup = buildCommon.makeVList({ - positionType: "top", - positionData: top, - children: [{ - type: "kern", - size: options.fontMetrics().bigOpSpacing5 - }, { - type: "elem", - elem: sub.elem, - marginLeft: -slant + "em" - }, { - type: "kern", - size: sub.kern - }, { - type: "elem", - elem: base - }] - }, options); - } else if (sup) { - const bottom = base.depth + baseShift; - finalGroup = buildCommon.makeVList({ - positionType: "bottom", - positionData: bottom, - children: [{ - type: "elem", - elem: base - }, { - type: "kern", - size: sup.kern - }, { - type: "elem", - elem: sup.elem, - marginLeft: slant + "em" - }, { - type: "kern", - size: options.fontMetrics().bigOpSpacing5 - }] - }, options); - } else { - // This case probably shouldn't occur (this would mean the - // supsub was sending us a group with no superscript or - // subscript) but be safe. - return base; - } - - return buildCommon.makeSpan(["mop", "op-limits"], [finalGroup], options); -}; - -// Limits, symbols -// Most operators have a large successor symbol, but these don't. -const noSuccessor = ["\\smallint"]; // NOTE: Unlike most `htmlBuilder`s, this one handles not only "op", but also -// "supsub" since some of them (like \int) can affect super/subscripting. - -const htmlBuilder$8 = (grp, options) => { - // Operators are handled in the TeXbook pg. 443-444, rule 13(a). - let supGroup; - let subGroup; - let hasLimits = false; - let group; - - if (grp.type === "supsub") { - // If we have limits, supsub will pass us its group to handle. Pull - // out the superscript and subscript and set the group to the op in - // its base. - supGroup = grp.sup; - subGroup = grp.sub; - group = assertNodeType(grp.base, "op"); - hasLimits = true; - } else { - group = assertNodeType(grp, "op"); - } - - const style = options.style; - let large = false; - - if (style.size === Style$1.DISPLAY.size && group.symbol && !utils.contains(noSuccessor, group.name)) { - // Most symbol operators get larger in displaystyle (rule 13) - large = true; - } - - let base; - - if (group.symbol) { - // If this is a symbol, create the symbol. - const fontName = large ? "Size2-Regular" : "Size1-Regular"; - let stash = ""; - - if (group.name === "\\oiint" || group.name === "\\oiiint") { - // No font glyphs yet, so use a glyph w/o the oval. - // TODO: When font glyphs are available, delete this code. - stash = group.name.substr(1); // $FlowFixMe - - group.name = stash === "oiint" ? "\\iint" : "\\iiint"; - } - - base = buildCommon.makeSymbol(group.name, fontName, "math", options, ["mop", "op-symbol", large ? "large-op" : "small-op"]); - - if (stash.length > 0) { - // We're in \oiint or \oiiint. Overlay the oval. - // TODO: When font glyphs are available, delete this code. - const italic = base.italic; - const oval = buildCommon.staticSvg(stash + "Size" + (large ? "2" : "1"), options); - base = buildCommon.makeVList({ - positionType: "individualShift", - children: [{ - type: "elem", - elem: base, - shift: 0 - }, { - type: "elem", - elem: oval, - shift: large ? 0.08 : 0 - }] - }, options); // $FlowFixMe - - group.name = "\\" + stash; - base.classes.unshift("mop"); // $FlowFixMe - - base.italic = italic; - } - } else if (group.body) { - // If this is a list, compose that list. - const inner = buildExpression(group.body, options, true); - - if (inner.length === 1 && inner[0] instanceof SymbolNode) { - base = inner[0]; - base.classes[0] = "mop"; // replace old mclass - } else { - base = buildCommon.makeSpan(["mop"], buildCommon.tryCombineChars(inner), options); - } - } else { - // Otherwise, this is a text operator. Build the text from the - // operator's name. - // TODO(emily): Add a space in the middle of some of these - // operators, like \limsup - const output = []; - - for (let i = 1; i < group.name.length; i++) { - output.push(buildCommon.mathsym(group.name[i], group.mode, options)); - } - - base = buildCommon.makeSpan(["mop"], output, options); - } // If content of op is a single symbol, shift it vertically. - - - let baseShift = 0; - let slant = 0; - - if ((base instanceof SymbolNode || group.name === "\\oiint" || group.name === "\\oiiint") && !group.suppressBaseShift) { - // We suppress the shift of the base of \overset and \underset. Otherwise, - // shift the symbol so its center lies on the axis (rule 13). It - // appears that our fonts have the centers of the symbols already - // almost on the axis, so these numbers are very small. Note we - // don't actually apply this here, but instead it is used either in - // the vlist creation or separately when there are no limits. - baseShift = (base.height - base.depth) / 2 - options.fontMetrics().axisHeight; // The slant of the symbol is just its italic correction. - // $FlowFixMe - - slant = base.italic; - } - - if (hasLimits) { - return assembleSupSub(base, supGroup, subGroup, options, style, slant, baseShift); - } else { - if (baseShift) { - base.style.position = "relative"; - base.style.top = baseShift + "em"; - } - - return base; - } -}; - -const mathmlBuilder$8 = (group, options) => { - let node; - - if (group.symbol) { - // This is a symbol. Just add the symbol. - node = new MathNode("mo", [makeText(group.name, group.mode)]); - - if (utils.contains(noSuccessor, group.name)) { - node.setAttribute("largeop", "false"); - } - } else if (group.body) { - // This is an operator with children. Add them. - node = new MathNode("mo", buildExpression$1(group.body, options)); - } else { - // This is a text operator. Add all of the characters from the - // operator's name. - node = new MathNode("mi", [new TextNode(group.name.slice(1))]); // Append an . - // ref: https://www.w3.org/TR/REC-MathML/chap3_2.html#sec3.2.4 - - const operator = new MathNode("mo", [makeText("\u2061", "text")]); - - if (group.parentIsSupSub) { - node = new MathNode("mo", [node, operator]); - } else { - node = newDocumentFragment([node, operator]); - } - } - - return node; -}; - -const singleCharBigOps = { - "\u220F": "\\prod", - "\u2210": "\\coprod", - "\u2211": "\\sum", - "\u22c0": "\\bigwedge", - "\u22c1": "\\bigvee", - "\u22c2": "\\bigcap", - "\u22c3": "\\bigcup", - "\u2a00": "\\bigodot", - "\u2a01": "\\bigoplus", - "\u2a02": "\\bigotimes", - "\u2a04": "\\biguplus", - "\u2a06": "\\bigsqcup" -}; -defineFunction({ - type: "op", - names: ["\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap", "\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes", "\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint", "\u220F", "\u2210", "\u2211", "\u22c0", "\u22c1", "\u22c2", "\u22c3", "\u2a00", "\u2a01", "\u2a02", "\u2a04", "\u2a06"], - props: { - numArgs: 0 - }, - handler: (_ref, args) => { - let parser = _ref.parser, - funcName = _ref.funcName; - let fName = funcName; - - if (fName.length === 1) { - fName = singleCharBigOps[fName]; - } - - return { - type: "op", - mode: parser.mode, - limits: true, - parentIsSupSub: false, - symbol: true, - name: fName - }; - }, - htmlBuilder: htmlBuilder$8, - mathmlBuilder: mathmlBuilder$8 -}); // Note: calling defineFunction with a type that's already been defined only -// works because the same htmlBuilder and mathmlBuilder are being used. - -defineFunction({ - type: "op", - names: ["\\mathop"], - props: { - numArgs: 1 - }, - handler: (_ref2, args) => { - let parser = _ref2.parser; - const body = args[0]; - return { - type: "op", - mode: parser.mode, - limits: false, - parentIsSupSub: false, - symbol: false, - body: ordargument(body) - }; - }, - htmlBuilder: htmlBuilder$8, - mathmlBuilder: mathmlBuilder$8 -}); // There are 2 flags for operators; whether they produce limits in -// displaystyle, and whether they are symbols and should grow in -// displaystyle. These four groups cover the four possible choices. - -const singleCharIntegrals = { - "\u222b": "\\int", - "\u222c": "\\iint", - "\u222d": "\\iiint", - "\u222e": "\\oint", - "\u222f": "\\oiint", - "\u2230": "\\oiiint" -}; // No limits, not symbols - -defineFunction({ - type: "op", - names: ["\\arcsin", "\\arccos", "\\arctan", "\\arctg", "\\arcctg", "\\arg", "\\ch", "\\cos", "\\cosec", "\\cosh", "\\cot", "\\cotg", "\\coth", "\\csc", "\\ctg", "\\cth", "\\deg", "\\dim", "\\exp", "\\hom", "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh", "\\sh", "\\tan", "\\tanh", "\\tg", "\\th"], - props: { - numArgs: 0 - }, - - handler(_ref3) { - let parser = _ref3.parser, - funcName = _ref3.funcName; - return { - type: "op", - mode: parser.mode, - limits: false, - parentIsSupSub: false, - symbol: false, - name: funcName - }; - }, - - htmlBuilder: htmlBuilder$8, - mathmlBuilder: mathmlBuilder$8 -}); // Limits, not symbols - -defineFunction({ - type: "op", - names: ["\\det", "\\gcd", "\\inf", "\\lim", "\\max", "\\min", "\\Pr", "\\sup"], - props: { - numArgs: 0 - }, - - handler(_ref4) { - let parser = _ref4.parser, - funcName = _ref4.funcName; - return { - type: "op", - mode: parser.mode, - limits: true, - parentIsSupSub: false, - symbol: false, - name: funcName - }; - }, - - htmlBuilder: htmlBuilder$8, - mathmlBuilder: mathmlBuilder$8 -}); // No limits, symbols - -defineFunction({ - type: "op", - names: ["\\int", "\\iint", "\\iiint", "\\oint", "\\oiint", "\\oiiint", "\u222b", "\u222c", "\u222d", "\u222e", "\u222f", "\u2230"], - props: { - numArgs: 0 - }, - - handler(_ref5) { - let parser = _ref5.parser, - funcName = _ref5.funcName; - let fName = funcName; - - if (fName.length === 1) { - fName = singleCharIntegrals[fName]; - } - - return { - type: "op", - mode: parser.mode, - limits: false, - parentIsSupSub: false, - symbol: true, - name: fName - }; - }, - - htmlBuilder: htmlBuilder$8, - mathmlBuilder: mathmlBuilder$8 -}); - -// NOTE: Unlike most `htmlBuilder`s, this one handles not only -// "operatorname", but also "supsub" since \operatorname* can -const htmlBuilder$9 = (grp, options) => { - // Operators are handled in the TeXbook pg. 443-444, rule 13(a). - let supGroup; - let subGroup; - let hasLimits = false; - let group; - - if (grp.type === "supsub") { - // If we have limits, supsub will pass us its group to handle. Pull - // out the superscript and subscript and set the group to the op in - // its base. - supGroup = grp.sup; - subGroup = grp.sub; - group = assertNodeType(grp.base, "operatorname"); - hasLimits = true; - } else { - group = assertNodeType(grp, "operatorname"); - } - - let base; - - if (group.body.length > 0) { - const body = group.body.map(child => { - // $FlowFixMe: Check if the node has a string `text` property. - const childText = child.text; - - if (typeof childText === "string") { - return { - type: "textord", - mode: child.mode, - text: childText - }; - } else { - return child; - } - }); // Consolidate function names into symbol characters. - - const expression = buildExpression(body, options.withFont("mathrm"), true); - - for (let i = 0; i < expression.length; i++) { - const child = expression[i]; - - if (child instanceof SymbolNode) { - // Per amsopn package, - // change minus to hyphen and \ast to asterisk - child.text = child.text.replace(/\u2212/, "-").replace(/\u2217/, "*"); - } - } - - base = buildCommon.makeSpan(["mop"], expression, options); - } else { - base = buildCommon.makeSpan(["mop"], [], options); - } - - if (hasLimits) { - return assembleSupSub(base, supGroup, subGroup, options, options.style, 0, 0); - } else { - return base; - } -}; - -const mathmlBuilder$9 = (group, options) => { - // The steps taken here are similar to the html version. - let expression = buildExpression$1(group.body, options.withFont("mathrm")); // Is expression a string or has it something like a fraction? - - let isAllString = true; // default - - for (let i = 0; i < expression.length; i++) { - const node = expression[i]; - - if (node instanceof mathMLTree.SpaceNode) ; else if (node instanceof mathMLTree.MathNode) { - switch (node.type) { - case "mi": - case "mn": - case "ms": - case "mspace": - case "mtext": - break; - // Do nothing yet. - - case "mo": - { - const child = node.children[0]; - - if (node.children.length === 1 && child instanceof mathMLTree.TextNode) { - child.text = child.text.replace(/\u2212/, "-").replace(/\u2217/, "*"); - } else { - isAllString = false; - } - - break; - } - - default: - isAllString = false; - } - } else { - isAllString = false; - } - } - - if (isAllString) { - // Write a single TextNode instead of multiple nested tags. - const word = expression.map(node => node.toText()).join(""); - expression = [new mathMLTree.TextNode(word)]; - } - - const identifier = new mathMLTree.MathNode("mi", expression); - identifier.setAttribute("mathvariant", "normal"); // \u2061 is the same as ⁡ - // ref: https://www.w3schools.com/charsets/ref_html_entities_a.asp - - const operator = new mathMLTree.MathNode("mo", [makeText("\u2061", "text")]); - - if (group.parentIsSupSub) { - return new mathMLTree.MathNode("mo", [identifier, operator]); - } else { - return mathMLTree.newDocumentFragment([identifier, operator]); - } -}; // \operatorname -// amsopn.dtx: \mathop{#1\kern\z@\operator@font#3}\newmcodes@ - - -defineFunction({ - type: "operatorname", - names: ["\\operatorname", "\\operatorname*"], - props: { - numArgs: 1 - }, - handler: (_ref, args) => { - let parser = _ref.parser, - funcName = _ref.funcName; - const body = args[0]; - return { - type: "operatorname", - mode: parser.mode, - body: ordargument(body), - alwaysHandleSupSub: funcName === "\\operatorname*", - limits: false, - parentIsSupSub: false - }; - }, - htmlBuilder: htmlBuilder$9, - mathmlBuilder: mathmlBuilder$9 -}); - -defineFunctionBuilders({ - type: "ordgroup", - - htmlBuilder(group, options) { - if (group.semisimple) { - return buildCommon.makeFragment(buildExpression(group.body, options, false)); - } - - return buildCommon.makeSpan(["mord"], buildExpression(group.body, options, true), options); - }, - - mathmlBuilder(group, options) { - return buildExpressionRow(group.body, options, true); - } - -}); - -defineFunction({ - type: "overline", - names: ["\\overline"], - props: { - numArgs: 1 - }, - - handler(_ref, args) { - let parser = _ref.parser; - const body = args[0]; - return { - type: "overline", - mode: parser.mode, - body - }; - }, - - htmlBuilder(group, options) { - // Overlines are handled in the TeXbook pg 443, Rule 9. - // Build the inner group in the cramped style. - const innerGroup = buildGroup(group.body, options.havingCrampedStyle()); // Create the line above the body - - const line = buildCommon.makeLineSpan("overline-line", options); // Generate the vlist, with the appropriate kerns - - const defaultRuleThickness = options.fontMetrics().defaultRuleThickness; - const vlist = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: innerGroup - }, { - type: "kern", - size: 3 * defaultRuleThickness - }, { - type: "elem", - elem: line - }, { - type: "kern", - size: defaultRuleThickness - }] - }, options); - return buildCommon.makeSpan(["mord", "overline"], [vlist], options); - }, - - mathmlBuilder(group, options) { - const operator = new mathMLTree.MathNode("mo", [new mathMLTree.TextNode("\u203e")]); - operator.setAttribute("stretchy", "true"); - const node = new mathMLTree.MathNode("mover", [buildGroup$1(group.body, options), operator]); - node.setAttribute("accent", "true"); - return node; - } - -}); - -defineFunction({ - type: "phantom", - names: ["\\phantom"], - props: { - numArgs: 1, - allowedInText: true - }, - handler: (_ref, args) => { - let parser = _ref.parser; - const body = args[0]; - return { - type: "phantom", - mode: parser.mode, - body: ordargument(body) - }; - }, - htmlBuilder: (group, options) => { - const elements = buildExpression(group.body, options.withPhantom(), false); // \phantom isn't supposed to affect the elements it contains. - // See "color" for more details. - - return buildCommon.makeFragment(elements); - }, - mathmlBuilder: (group, options) => { - const inner = buildExpression$1(group.body, options); - return new mathMLTree.MathNode("mphantom", inner); - } -}); -defineFunction({ - type: "hphantom", - names: ["\\hphantom"], - props: { - numArgs: 1, - allowedInText: true - }, - handler: (_ref2, args) => { - let parser = _ref2.parser; - const body = args[0]; - return { - type: "hphantom", - mode: parser.mode, - body - }; - }, - htmlBuilder: (group, options) => { - let node = buildCommon.makeSpan([], [buildGroup(group.body, options.withPhantom())]); - node.height = 0; - node.depth = 0; - - if (node.children) { - for (let i = 0; i < node.children.length; i++) { - node.children[i].height = 0; - node.children[i].depth = 0; - } - } // See smash for comment re: use of makeVList - - - node = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: node - }] - }, options); // For spacing, TeX treats \smash as a math group (same spacing as ord). - - return buildCommon.makeSpan(["mord"], [node], options); - }, - mathmlBuilder: (group, options) => { - const inner = buildExpression$1(ordargument(group.body), options); - const phantom = new mathMLTree.MathNode("mphantom", inner); - const node = new mathMLTree.MathNode("mpadded", [phantom]); - node.setAttribute("height", "0px"); - node.setAttribute("depth", "0px"); - return node; - } -}); -defineFunction({ - type: "vphantom", - names: ["\\vphantom"], - props: { - numArgs: 1, - allowedInText: true - }, - handler: (_ref3, args) => { - let parser = _ref3.parser; - const body = args[0]; - return { - type: "vphantom", - mode: parser.mode, - body - }; - }, - htmlBuilder: (group, options) => { - const inner = buildCommon.makeSpan(["inner"], [buildGroup(group.body, options.withPhantom())]); - const fix = buildCommon.makeSpan(["fix"], []); - return buildCommon.makeSpan(["mord", "rlap"], [inner, fix], options); - }, - mathmlBuilder: (group, options) => { - const inner = buildExpression$1(ordargument(group.body), options); - const phantom = new mathMLTree.MathNode("mphantom", inner); - const node = new mathMLTree.MathNode("mpadded", [phantom]); - node.setAttribute("width", "0px"); - return node; - } -}); - -defineFunction({ - type: "raisebox", - names: ["\\raisebox"], - props: { - numArgs: 2, - argTypes: ["size", "hbox"], - allowedInText: true - }, - - handler(_ref, args) { - let parser = _ref.parser; - const amount = assertNodeType(args[0], "size").value; - const body = args[1]; - return { - type: "raisebox", - mode: parser.mode, - dy: amount, - body - }; - }, - - htmlBuilder(group, options) { - const body = buildGroup(group.body, options); - const dy = calculateSize(group.dy, options); - return buildCommon.makeVList({ - positionType: "shift", - positionData: -dy, - children: [{ - type: "elem", - elem: body - }] - }, options); - }, - - mathmlBuilder(group, options) { - const node = new mathMLTree.MathNode("mpadded", [buildGroup$1(group.body, options)]); - const dy = group.dy.number + group.dy.unit; - node.setAttribute("voffset", dy); - return node; - } - -}); - -defineFunction({ - type: "rule", - names: ["\\rule"], - props: { - numArgs: 2, - numOptionalArgs: 1, - argTypes: ["size", "size", "size"] - }, - - handler(_ref, args, optArgs) { - let parser = _ref.parser; - const shift = optArgs[0]; - const width = assertNodeType(args[0], "size"); - const height = assertNodeType(args[1], "size"); - return { - type: "rule", - mode: parser.mode, - shift: shift && assertNodeType(shift, "size").value, - width: width.value, - height: height.value - }; - }, - - htmlBuilder(group, options) { - // Make an empty span for the rule - const rule = buildCommon.makeSpan(["mord", "rule"], [], options); // Calculate the shift, width, and height of the rule, and account for units - - const width = calculateSize(group.width, options); - const height = calculateSize(group.height, options); - const shift = group.shift ? calculateSize(group.shift, options) : 0; // Style the rule to the right size - - rule.style.borderRightWidth = width + "em"; - rule.style.borderTopWidth = height + "em"; - rule.style.bottom = shift + "em"; // Record the height and width - - rule.width = width; - rule.height = height + shift; - rule.depth = -shift; // Font size is the number large enough that the browser will - // reserve at least `absHeight` space above the baseline. - // The 1.125 factor was empirically determined - - rule.maxFontSize = height * 1.125 * options.sizeMultiplier; - return rule; - }, - - mathmlBuilder(group, options) { - const width = calculateSize(group.width, options); - const height = calculateSize(group.height, options); - const shift = group.shift ? calculateSize(group.shift, options) : 0; - const color = options.color && options.getColor() || "black"; - const rule = new mathMLTree.MathNode("mspace"); - rule.setAttribute("mathbackground", color); - rule.setAttribute("width", width + "em"); - rule.setAttribute("height", height + "em"); - const wrapper = new mathMLTree.MathNode("mpadded", [rule]); - - if (shift >= 0) { - wrapper.setAttribute("height", "+" + shift + "em"); - } else { - wrapper.setAttribute("height", shift + "em"); - wrapper.setAttribute("depth", "+" + -shift + "em"); - } - - wrapper.setAttribute("voffset", shift + "em"); - return wrapper; - } - -}); - -function sizingGroup(value, options, baseOptions) { - const inner = buildExpression(value, options, false); - const multiplier = options.sizeMultiplier / baseOptions.sizeMultiplier; // Add size-resetting classes to the inner list and set maxFontSize - // manually. Handle nested size changes. - - for (let i = 0; i < inner.length; i++) { - const pos = inner[i].classes.indexOf("sizing"); - - if (pos < 0) { - Array.prototype.push.apply(inner[i].classes, options.sizingClasses(baseOptions)); - } else if (inner[i].classes[pos + 1] === "reset-size" + options.size) { - // This is a nested size change: e.g., inner[i] is the "b" in - // `\Huge a \small b`. Override the old size (the `reset-` class) - // but not the new size. - inner[i].classes[pos + 1] = "reset-size" + baseOptions.size; - } - - inner[i].height *= multiplier; - inner[i].depth *= multiplier; - } - - return buildCommon.makeFragment(inner); -} -const sizeFuncs = ["\\tiny", "\\sixptsize", "\\scriptsize", "\\footnotesize", "\\small", "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge"]; -const htmlBuilder$a = (group, options) => { - // Handle sizing operators like \Huge. Real TeX doesn't actually allow - // these functions inside of math expressions, so we do some special - // handling. - const newOptions = options.havingSize(group.size); - return sizingGroup(group.body, newOptions, options); -}; -defineFunction({ - type: "sizing", - names: sizeFuncs, - props: { - numArgs: 0, - allowedInText: true - }, - handler: (_ref, args) => { - let breakOnTokenText = _ref.breakOnTokenText, - funcName = _ref.funcName, - parser = _ref.parser; - const body = parser.parseExpression(false, breakOnTokenText); - return { - type: "sizing", - mode: parser.mode, - // Figure out what size to use based on the list of functions above - size: sizeFuncs.indexOf(funcName) + 1, - body - }; - }, - htmlBuilder: htmlBuilder$a, - mathmlBuilder: (group, options) => { - const newOptions = options.havingSize(group.size); - const inner = buildExpression$1(group.body, newOptions); - const node = new mathMLTree.MathNode("mstyle", inner); // TODO(emily): This doesn't produce the correct size for nested size - // changes, because we don't keep state of what style we're currently - // in, so we can't reset the size to normal before changing it. Now - // that we're passing an options parameter we should be able to fix - // this. - - node.setAttribute("mathsize", newOptions.sizeMultiplier + "em"); - return node; - } -}); - -// smash, with optional [tb], as in AMS -defineFunction({ - type: "smash", - names: ["\\smash"], - props: { - numArgs: 1, - numOptionalArgs: 1, - allowedInText: true - }, - handler: (_ref, args, optArgs) => { - let parser = _ref.parser; - let smashHeight = false; - let smashDepth = false; - const tbArg = optArgs[0] && assertNodeType(optArgs[0], "ordgroup"); - - if (tbArg) { - // Optional [tb] argument is engaged. - // ref: amsmath: \renewcommand{\smash}[1][tb]{% - // def\mb@t{\ht}\def\mb@b{\dp}\def\mb@tb{\ht\z@\z@\dp}% - let letter = ""; - - for (let i = 0; i < tbArg.body.length; ++i) { - const node = tbArg.body[i]; // $FlowFixMe: Not every node type has a `text` property. - - letter = node.text; - - if (letter === "t") { - smashHeight = true; - } else if (letter === "b") { - smashDepth = true; - } else { - smashHeight = false; - smashDepth = false; - break; - } - } - } else { - smashHeight = true; - smashDepth = true; - } - - const body = args[0]; - return { - type: "smash", - mode: parser.mode, - body, - smashHeight, - smashDepth - }; - }, - htmlBuilder: (group, options) => { - const node = buildCommon.makeSpan([], [buildGroup(group.body, options)]); - - if (!group.smashHeight && !group.smashDepth) { - return node; - } - - if (group.smashHeight) { - node.height = 0; // In order to influence makeVList, we have to reset the children. - - if (node.children) { - for (let i = 0; i < node.children.length; i++) { - node.children[i].height = 0; - } - } - } - - if (group.smashDepth) { - node.depth = 0; - - if (node.children) { - for (let i = 0; i < node.children.length; i++) { - node.children[i].depth = 0; - } - } - } // At this point, we've reset the TeX-like height and depth values. - // But the span still has an HTML line height. - // makeVList applies "display: table-cell", which prevents the browser - // from acting on that line height. So we'll call makeVList now. - - - const smashedNode = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: node - }] - }, options); // For spacing, TeX treats \hphantom as a math group (same spacing as ord). - - return buildCommon.makeSpan(["mord"], [smashedNode], options); - }, - mathmlBuilder: (group, options) => { - const node = new mathMLTree.MathNode("mpadded", [buildGroup$1(group.body, options)]); - - if (group.smashHeight) { - node.setAttribute("height", "0px"); - } - - if (group.smashDepth) { - node.setAttribute("depth", "0px"); - } - - return node; - } -}); - -defineFunction({ - type: "sqrt", - names: ["\\sqrt"], - props: { - numArgs: 1, - numOptionalArgs: 1 - }, - - handler(_ref, args, optArgs) { - let parser = _ref.parser; - const index = optArgs[0]; - const body = args[0]; - return { - type: "sqrt", - mode: parser.mode, - body, - index - }; - }, - - htmlBuilder(group, options) { - // Square roots are handled in the TeXbook pg. 443, Rule 11. - // First, we do the same steps as in overline to build the inner group - // and line - let inner = buildGroup(group.body, options.havingCrampedStyle()); - - if (inner.height === 0) { - // Render a small surd. - inner.height = options.fontMetrics().xHeight; - } // Some groups can return document fragments. Handle those by wrapping - // them in a span. - - - inner = buildCommon.wrapFragment(inner, options); // Calculate the minimum size for the \surd delimiter - - const metrics = options.fontMetrics(); - const theta = metrics.defaultRuleThickness; - let phi = theta; - - if (options.style.id < Style$1.TEXT.id) { - phi = options.fontMetrics().xHeight; - } // Calculate the clearance between the body and line - - - let lineClearance = theta + phi / 4; - const minDelimiterHeight = inner.height + inner.depth + lineClearance + theta; // Create a sqrt SVG of the required minimum size - - const _delimiter$sqrtImage = delimiter.sqrtImage(minDelimiterHeight, options), - img = _delimiter$sqrtImage.span, - ruleWidth = _delimiter$sqrtImage.ruleWidth, - advanceWidth = _delimiter$sqrtImage.advanceWidth; - - const delimDepth = img.height - ruleWidth; // Adjust the clearance based on the delimiter size - - if (delimDepth > inner.height + inner.depth + lineClearance) { - lineClearance = (lineClearance + delimDepth - inner.height - inner.depth) / 2; - } // Shift the sqrt image - - - const imgShift = img.height - inner.height - lineClearance - ruleWidth; - inner.style.paddingLeft = advanceWidth + "em"; // Overlay the image and the argument. - - const body = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [{ - type: "elem", - elem: inner, - wrapperClasses: ["svg-align"] - }, { - type: "kern", - size: -(inner.height + imgShift) - }, { - type: "elem", - elem: img - }, { - type: "kern", - size: ruleWidth - }] - }, options); - - if (!group.index) { - return buildCommon.makeSpan(["mord", "sqrt"], [body], options); - } else { - // Handle the optional root index - // The index is always in scriptscript style - const newOptions = options.havingStyle(Style$1.SCRIPTSCRIPT); - const rootm = buildGroup(group.index, newOptions, options); // The amount the index is shifted by. This is taken from the TeX - // source, in the definition of `\r@@t`. - - const toShift = 0.6 * (body.height - body.depth); // Build a VList with the superscript shifted up correctly - - const rootVList = buildCommon.makeVList({ - positionType: "shift", - positionData: -toShift, - children: [{ - type: "elem", - elem: rootm - }] - }, options); // Add a class surrounding it so we can add on the appropriate - // kerning - - const rootVListWrap = buildCommon.makeSpan(["root"], [rootVList]); - return buildCommon.makeSpan(["mord", "sqrt"], [rootVListWrap, body], options); - } - }, - - mathmlBuilder(group, options) { - const body = group.body, - index = group.index; - return index ? new mathMLTree.MathNode("mroot", [buildGroup$1(body, options), buildGroup$1(index, options)]) : new mathMLTree.MathNode("msqrt", [buildGroup$1(body, options)]); - } - -}); - -const styleMap$1 = { - "display": Style$1.DISPLAY, - "text": Style$1.TEXT, - "script": Style$1.SCRIPT, - "scriptscript": Style$1.SCRIPTSCRIPT -}; -defineFunction({ - type: "styling", - names: ["\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle"], - props: { - numArgs: 0, - allowedInText: true - }, - - handler(_ref, args) { - let breakOnTokenText = _ref.breakOnTokenText, - funcName = _ref.funcName, - parser = _ref.parser; - // parse out the implicit body - const body = parser.parseExpression(true, breakOnTokenText); // TODO: Refactor to avoid duplicating styleMap in multiple places (e.g. - // here and in buildHTML and de-dupe the enumeration of all the styles). - // $FlowFixMe: The names above exactly match the styles. - - const style = funcName.slice(1, funcName.length - 5); - return { - type: "styling", - mode: parser.mode, - // Figure out what style to use by pulling out the style from - // the function name - style, - body - }; - }, - - htmlBuilder(group, options) { - // Style changes are handled in the TeXbook on pg. 442, Rule 3. - const newStyle = styleMap$1[group.style]; - const newOptions = options.havingStyle(newStyle).withFont(''); - return sizingGroup(group.body, newOptions, options); - }, - - mathmlBuilder(group, options) { - // Figure out what style we're changing to. - const newStyle = styleMap$1[group.style]; - const newOptions = options.havingStyle(newStyle); - const inner = buildExpression$1(group.body, newOptions); - const node = new mathMLTree.MathNode("mstyle", inner); - const styleAttributes = { - "display": ["0", "true"], - "text": ["0", "false"], - "script": ["1", "false"], - "scriptscript": ["2", "false"] - }; - const attr = styleAttributes[group.style]; - node.setAttribute("scriptlevel", attr[0]); - node.setAttribute("displaystyle", attr[1]); - return node; - } - -}); - -/** - * Sometimes, groups perform special rules when they have superscripts or - * subscripts attached to them. This function lets the `supsub` group know that - * Sometimes, groups perform special rules when they have superscripts or - * its inner element should handle the superscripts and subscripts instead of - * handling them itself. - */ -const htmlBuilderDelegate = function htmlBuilderDelegate(group, options) { - const base = group.base; - - if (!base) { - return null; - } else if (base.type === "op") { - // Operators handle supsubs differently when they have limits - // (e.g. `\displaystyle\sum_2^3`) - const delegate = base.limits && (options.style.size === Style$1.DISPLAY.size || base.alwaysHandleSupSub); - return delegate ? htmlBuilder$8 : null; - } else if (base.type === "operatorname") { - const delegate = base.alwaysHandleSupSub && (options.style.size === Style$1.DISPLAY.size || base.limits); - return delegate ? htmlBuilder$9 : null; - } else if (base.type === "accent") { - return utils.isCharacterBox(base.base) ? htmlBuilder : null; - } else if (base.type === "horizBrace") { - const isSup = !group.sub; - return isSup === base.isOver ? htmlBuilder$7 : null; - } else { - return null; - } -}; // Super scripts and subscripts, whose precise placement can depend on other -// functions that precede them. - - -defineFunctionBuilders({ - type: "supsub", - - htmlBuilder(group, options) { - // Superscript and subscripts are handled in the TeXbook on page - // 445-446, rules 18(a-f). - // Here is where we defer to the inner group if it should handle - // superscripts and subscripts itself. - const builderDelegate = htmlBuilderDelegate(group, options); - - if (builderDelegate) { - return builderDelegate(group, options); - } - - const valueBase = group.base, - valueSup = group.sup, - valueSub = group.sub; - const base = buildGroup(valueBase, options); - let supm; - let subm; - const metrics = options.fontMetrics(); // Rule 18a - - let supShift = 0; - let subShift = 0; - const isCharacterBox = valueBase && utils.isCharacterBox(valueBase); - - if (valueSup) { - const newOptions = options.havingStyle(options.style.sup()); - supm = buildGroup(valueSup, newOptions, options); - - if (!isCharacterBox) { - supShift = base.height - newOptions.fontMetrics().supDrop * newOptions.sizeMultiplier / options.sizeMultiplier; - } - } - - if (valueSub) { - const newOptions = options.havingStyle(options.style.sub()); - subm = buildGroup(valueSub, newOptions, options); - - if (!isCharacterBox) { - subShift = base.depth + newOptions.fontMetrics().subDrop * newOptions.sizeMultiplier / options.sizeMultiplier; - } - } // Rule 18c - - - let minSupShift; - - if (options.style === Style$1.DISPLAY) { - minSupShift = metrics.sup1; - } else if (options.style.cramped) { - minSupShift = metrics.sup3; - } else { - minSupShift = metrics.sup2; - } // scriptspace is a font-size-independent size, so scale it - // appropriately for use as the marginRight. - - - const multiplier = options.sizeMultiplier; - const marginRight = 0.5 / metrics.ptPerEm / multiplier + "em"; - let marginLeft = null; - - if (subm) { - // Subscripts shouldn't be shifted by the base's italic correction. - // Account for that by shifting the subscript back the appropriate - // amount. Note we only do this when the base is a single symbol. - const isOiint = group.base && group.base.type === "op" && group.base.name && (group.base.name === "\\oiint" || group.base.name === "\\oiiint"); - - if (base instanceof SymbolNode || isOiint) { - // $FlowFixMe - marginLeft = -base.italic + "em"; - } - } - - let supsub; - - if (supm && subm) { - supShift = Math.max(supShift, minSupShift, supm.depth + 0.25 * metrics.xHeight); - subShift = Math.max(subShift, metrics.sub2); - const ruleWidth = metrics.defaultRuleThickness; // Rule 18e - - const maxWidth = 4 * ruleWidth; - - if (supShift - supm.depth - (subm.height - subShift) < maxWidth) { - subShift = maxWidth - (supShift - supm.depth) + subm.height; - const psi = 0.8 * metrics.xHeight - (supShift - supm.depth); - - if (psi > 0) { - supShift += psi; - subShift -= psi; - } - } - - const vlistElem = [{ - type: "elem", - elem: subm, - shift: subShift, - marginRight, - marginLeft - }, { - type: "elem", - elem: supm, - shift: -supShift, - marginRight - }]; - supsub = buildCommon.makeVList({ - positionType: "individualShift", - children: vlistElem - }, options); - } else if (subm) { - // Rule 18b - subShift = Math.max(subShift, metrics.sub1, subm.height - 0.8 * metrics.xHeight); - const vlistElem = [{ - type: "elem", - elem: subm, - marginLeft, - marginRight - }]; - supsub = buildCommon.makeVList({ - positionType: "shift", - positionData: subShift, - children: vlistElem - }, options); - } else if (supm) { - // Rule 18c, d - supShift = Math.max(supShift, minSupShift, supm.depth + 0.25 * metrics.xHeight); - supsub = buildCommon.makeVList({ - positionType: "shift", - positionData: -supShift, - children: [{ - type: "elem", - elem: supm, - marginRight - }] - }, options); - } else { - throw new Error("supsub must have either sup or sub."); - } // Wrap the supsub vlist in a span.msupsub to reset text-align. - - - const mclass = getTypeOfDomTree(base, "right") || "mord"; - return buildCommon.makeSpan([mclass], [base, buildCommon.makeSpan(["msupsub"], [supsub])], options); - }, - - mathmlBuilder(group, options) { - // Is the inner group a relevant horizonal brace? - let isBrace = false; - let isOver; - let isSup; - - if (group.base && group.base.type === "horizBrace") { - isSup = !!group.sup; - - if (isSup === group.base.isOver) { - isBrace = true; - isOver = group.base.isOver; - } - } - - if (group.base && (group.base.type === "op" || group.base.type === "operatorname")) { - group.base.parentIsSupSub = true; - } - - const children = [buildGroup$1(group.base, options)]; - - if (group.sub) { - children.push(buildGroup$1(group.sub, options)); - } - - if (group.sup) { - children.push(buildGroup$1(group.sup, options)); - } - - let nodeType; - - if (isBrace) { - nodeType = isOver ? "mover" : "munder"; - } else if (!group.sub) { - const base = group.base; - - if (base && base.type === "op" && base.limits && (options.style === Style$1.DISPLAY || base.alwaysHandleSupSub)) { - nodeType = "mover"; - } else if (base && base.type === "operatorname" && base.alwaysHandleSupSub && (base.limits || options.style === Style$1.DISPLAY)) { - nodeType = "mover"; - } else { - nodeType = "msup"; - } - } else if (!group.sup) { - const base = group.base; - - if (base && base.type === "op" && base.limits && (options.style === Style$1.DISPLAY || base.alwaysHandleSupSub)) { - nodeType = "munder"; - } else if (base && base.type === "operatorname" && base.alwaysHandleSupSub && (base.limits || options.style === Style$1.DISPLAY)) { - nodeType = "munder"; - } else { - nodeType = "msub"; - } - } else { - const base = group.base; - - if (base && base.type === "op" && base.limits && options.style === Style$1.DISPLAY) { - nodeType = "munderover"; - } else if (base && base.type === "operatorname" && base.alwaysHandleSupSub && (options.style === Style$1.DISPLAY || base.limits)) { - nodeType = "munderover"; - } else { - nodeType = "msubsup"; - } - } - - const node = new mathMLTree.MathNode(nodeType, children); - return node; - } - -}); - -defineFunctionBuilders({ - type: "atom", - - htmlBuilder(group, options) { - return buildCommon.mathsym(group.text, group.mode, options, ["m" + group.family]); - }, - - mathmlBuilder(group, options) { - const node = new mathMLTree.MathNode("mo", [makeText(group.text, group.mode)]); - - if (group.family === "bin") { - const variant = getVariant(group, options); - - if (variant === "bold-italic") { - node.setAttribute("mathvariant", variant); - } - } else if (group.family === "punct") { - node.setAttribute("separator", "true"); - } else if (group.family === "open" || group.family === "close") { - // Delims built here should not stretch vertically. - // See delimsizing.js for stretchy delims. - node.setAttribute("stretchy", "false"); - } - - return node; - } - -}); - -// "mathord" and "textord" ParseNodes created in Parser.js from symbol Groups in -const defaultVariant = { - "mi": "italic", - "mn": "normal", - "mtext": "normal" -}; -defineFunctionBuilders({ - type: "mathord", - - htmlBuilder(group, options) { - return buildCommon.makeOrd(group, options, "mathord"); - }, - - mathmlBuilder(group, options) { - const node = new mathMLTree.MathNode("mi", [makeText(group.text, group.mode, options)]); - const variant = getVariant(group, options) || "italic"; - - if (variant !== defaultVariant[node.type]) { - node.setAttribute("mathvariant", variant); - } - - return node; - } - -}); -defineFunctionBuilders({ - type: "textord", - - htmlBuilder(group, options) { - return buildCommon.makeOrd(group, options, "textord"); - }, - - mathmlBuilder(group, options) { - const text = makeText(group.text, group.mode, options); - const variant = getVariant(group, options) || "normal"; - let node; - - if (group.mode === 'text') { - node = new mathMLTree.MathNode("mtext", [text]); - } else if (/[0-9]/.test(group.text)) { - // TODO(kevinb) merge adjacent nodes - // do it as a post processing step - node = new mathMLTree.MathNode("mn", [text]); - } else if (group.text === "\\prime") { - node = new mathMLTree.MathNode("mo", [text]); - } else { - node = new mathMLTree.MathNode("mi", [text]); - } - - if (variant !== defaultVariant[node.type]) { - node.setAttribute("mathvariant", variant); - } - - return node; - } - -}); - -const cssSpace = { - "\\nobreak": "nobreak", - "\\allowbreak": "allowbreak" -}; // A lookup table to determine whether a spacing function/symbol should be -// treated like a regular space character. If a symbol or command is a key -// in this table, then it should be a regular space character. Furthermore, -// the associated value may have a `className` specifying an extra CSS class -// to add to the created `span`. - -const regularSpace = { - " ": {}, - "\\ ": {}, - "~": { - className: "nobreak" - }, - "\\space": {}, - "\\nobreakspace": { - className: "nobreak" - } -}; // ParseNode<"spacing"> created in Parser.js from the "spacing" symbol Groups in -// src/symbols.js. - -defineFunctionBuilders({ - type: "spacing", - - htmlBuilder(group, options) { - if (regularSpace.hasOwnProperty(group.text)) { - const className = regularSpace[group.text].className || ""; // Spaces are generated by adding an actual space. Each of these - // things has an entry in the symbols table, so these will be turned - // into appropriate outputs. - - if (group.mode === "text") { - const ord = buildCommon.makeOrd(group, options, "textord"); - ord.classes.push(className); - return ord; - } else { - return buildCommon.makeSpan(["mspace", className], [buildCommon.mathsym(group.text, group.mode, options)], options); - } - } else if (cssSpace.hasOwnProperty(group.text)) { - // Spaces based on just a CSS class. - return buildCommon.makeSpan(["mspace", cssSpace[group.text]], [], options); - } else { - throw new ParseError(`Unknown type of space "${group.text}"`); - } - }, - - mathmlBuilder(group, options) { - let node; - - if (regularSpace.hasOwnProperty(group.text)) { - node = new mathMLTree.MathNode("mtext", [new mathMLTree.TextNode("\u00a0")]); - } else if (cssSpace.hasOwnProperty(group.text)) { - // CSS-based MathML spaces (\nobreak, \allowbreak) are ignored - return new mathMLTree.MathNode("mspace"); - } else { - throw new ParseError(`Unknown type of space "${group.text}"`); - } - - return node; - } - -}); - -const pad = () => { - const padNode = new mathMLTree.MathNode("mtd", []); - padNode.setAttribute("width", "50%"); - return padNode; -}; - -defineFunctionBuilders({ - type: "tag", - - mathmlBuilder(group, options) { - const table = new mathMLTree.MathNode("mtable", [new mathMLTree.MathNode("mtr", [pad(), new mathMLTree.MathNode("mtd", [buildExpressionRow(group.body, options)]), pad(), new mathMLTree.MathNode("mtd", [buildExpressionRow(group.tag, options)])])]); - table.setAttribute("width", "100%"); - return table; // TODO: Left-aligned tags. - // Currently, the group and options passed here do not contain - // enough info to set tag alignment. `leqno` is in Settings but it is - // not passed to Options. On the HTML side, leqno is - // set by a CSS class applied in buildTree.js. That would have worked - // in MathML if browsers supported . Since they don't, we - // need to rewrite the way this function is called. - } - -}); - -const textFontFamilies = { - "\\text": undefined, - "\\textrm": "textrm", - "\\textsf": "textsf", - "\\texttt": "texttt", - "\\textnormal": "textrm" -}; -const textFontWeights = { - "\\textbf": "textbf", - "\\textmd": "textmd" -}; -const textFontShapes = { - "\\textit": "textit", - "\\textup": "textup" -}; - -const optionsWithFont = (group, options) => { - const font = group.font; // Checks if the argument is a font family or a font style. - - if (!font) { - return options; - } else if (textFontFamilies[font]) { - return options.withTextFontFamily(textFontFamilies[font]); - } else if (textFontWeights[font]) { - return options.withTextFontWeight(textFontWeights[font]); - } else { - return options.withTextFontShape(textFontShapes[font]); - } -}; - -defineFunction({ - type: "text", - names: [// Font families - "\\text", "\\textrm", "\\textsf", "\\texttt", "\\textnormal", // Font weights - "\\textbf", "\\textmd", // Font Shapes - "\\textit", "\\textup"], - props: { - numArgs: 1, - argTypes: ["text"], - greediness: 2, - allowedInText: true - }, - - handler(_ref, args) { - let parser = _ref.parser, - funcName = _ref.funcName; - const body = args[0]; - return { - type: "text", - mode: parser.mode, - body: ordargument(body), - font: funcName - }; - }, - - htmlBuilder(group, options) { - const newOptions = optionsWithFont(group, options); - const inner = buildExpression(group.body, newOptions, true); - return buildCommon.makeSpan(["mord", "text"], buildCommon.tryCombineChars(inner), newOptions); - }, - - mathmlBuilder(group, options) { - const newOptions = optionsWithFont(group, options); - return buildExpressionRow(group.body, newOptions); - } - -}); - -defineFunction({ - type: "underline", - names: ["\\underline"], - props: { - numArgs: 1, - allowedInText: true - }, - - handler(_ref, args) { - let parser = _ref.parser; - return { - type: "underline", - mode: parser.mode, - body: args[0] - }; - }, - - htmlBuilder(group, options) { - // Underlines are handled in the TeXbook pg 443, Rule 10. - // Build the inner group. - const innerGroup = buildGroup(group.body, options); // Create the line to go below the body - - const line = buildCommon.makeLineSpan("underline-line", options); // Generate the vlist, with the appropriate kerns - - const defaultRuleThickness = options.fontMetrics().defaultRuleThickness; - const vlist = buildCommon.makeVList({ - positionType: "top", - positionData: innerGroup.height, - children: [{ - type: "kern", - size: defaultRuleThickness - }, { - type: "elem", - elem: line - }, { - type: "kern", - size: 3 * defaultRuleThickness - }, { - type: "elem", - elem: innerGroup - }] - }, options); - return buildCommon.makeSpan(["mord", "underline"], [vlist], options); - }, - - mathmlBuilder(group, options) { - const operator = new mathMLTree.MathNode("mo", [new mathMLTree.TextNode("\u203e")]); - operator.setAttribute("stretchy", "true"); - const node = new mathMLTree.MathNode("munder", [buildGroup$1(group.body, options), operator]); - node.setAttribute("accentunder", "true"); - return node; - } - -}); - -defineFunction({ - type: "verb", - names: ["\\verb"], - props: { - numArgs: 0, - allowedInText: true - }, - - handler(context, args, optArgs) { - // \verb and \verb* are dealt with directly in Parser.js. - // If we end up here, it's because of a failure to match the two delimiters - // in the regex in Lexer.js. LaTeX raises the following error when \verb is - // terminated by end of line (or file). - throw new ParseError("\\verb ended by end of line instead of matching delimiter"); - }, - - htmlBuilder(group, options) { - const text = makeVerb(group); - const body = []; // \verb enters text mode and therefore is sized like \textstyle - - const newOptions = options.havingStyle(options.style.text()); - - for (let i = 0; i < text.length; i++) { - let c = text[i]; - - if (c === '~') { - c = '\\textasciitilde'; - } - - body.push(buildCommon.makeSymbol(c, "Typewriter-Regular", group.mode, newOptions, ["mord", "texttt"])); - } - - return buildCommon.makeSpan(["mord", "text"].concat(newOptions.sizingClasses(options)), buildCommon.tryCombineChars(body), newOptions); - }, - - mathmlBuilder(group, options) { - const text = new mathMLTree.TextNode(makeVerb(group)); - const node = new mathMLTree.MathNode("mtext", [text]); - node.setAttribute("mathvariant", "monospace"); - return node; - } - -}); -/** - * Converts verb group into body string. - * - * \verb* replaces each space with an open box \u2423 - * \verb replaces each space with a no-break space \xA0 - */ - -const makeVerb = group => group.body.replace(/ /g, group.star ? '\u2423' : '\xA0'); - -/** Include this to ensure that all functions are defined. */ -const functions = _functions; - -/** - * The Lexer class handles tokenizing the input in various ways. Since our - * parser expects us to be able to backtrack, the lexer allows lexing from any - * given starting point. - * - * Its main exposed function is the `lex` function, which takes a position to - * lex from and a type of token to lex. It defers to the appropriate `_innerLex` - * function. - * - * The various `_innerLex` functions perform the actual lexing of different - * kinds. - */ - -/* The following tokenRegex - * - matches typical whitespace (but not NBSP etc.) using its first group - * - does not match any control character \x00-\x1f except whitespace - * - does not match a bare backslash - * - matches any ASCII character except those just mentioned - * - does not match the BMP private use area \uE000-\uF8FF - * - does not match bare surrogate code units - * - matches any BMP character except for those just described - * - matches any valid Unicode surrogate pair - * - matches a backslash followed by one or more letters - * - matches a backslash followed by any BMP character, including newline - * Just because the Lexer matches something doesn't mean it's valid input: - * If there is no matching function or symbol definition, the Parser will - * still reject the input. - */ -const spaceRegexString = "[ \r\n\t]"; -const controlWordRegexString = "\\\\[a-zA-Z@]+"; -const controlSymbolRegexString = "\\\\[^\uD800-\uDFFF]"; -const controlWordWhitespaceRegexString = `${controlWordRegexString}${spaceRegexString}*`; -const controlWordWhitespaceRegex = new RegExp(`^(${controlWordRegexString})${spaceRegexString}*$`); -const combiningDiacriticalMarkString = "[\u0300-\u036f]"; -const combiningDiacriticalMarksEndRegex = new RegExp(`${combiningDiacriticalMarkString}+$`); -const tokenRegexString = `(${spaceRegexString}+)|` + // whitespace -"([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + // single codepoint -`${combiningDiacriticalMarkString}*` + // ...plus accents -"|[\uD800-\uDBFF][\uDC00-\uDFFF]" + // surrogate pair -`${combiningDiacriticalMarkString}*` + // ...plus accents -"|\\\\verb\\*([^]).*?\\3" + // \verb* -"|\\\\verb([^*a-zA-Z]).*?\\4" + // \verb unstarred -"|\\\\operatorname\\*" + // \operatorname* -`|${controlWordWhitespaceRegexString}` + // \macroName + spaces -`|${controlSymbolRegexString})`; // \\, \', etc. - -/** Main Lexer class */ - -class Lexer { - // category codes, only supports comment characters (14) for now - constructor(input, settings) { - this.input = void 0; - this.settings = void 0; - this.tokenRegex = void 0; - this.catcodes = void 0; - // Separate accents from characters - this.input = input; - this.settings = settings; - this.tokenRegex = new RegExp(tokenRegexString, 'g'); - this.catcodes = { - "%": 14 // comment character - - }; - } - - setCatcode(char, code) { - this.catcodes[char] = code; - } - /** - * This function lexes a single token. - */ - - - lex() { - const input = this.input; - const pos = this.tokenRegex.lastIndex; - - if (pos === input.length) { - return new Token("EOF", new SourceLocation(this, pos, pos)); - } - - const match = this.tokenRegex.exec(input); - - if (match === null || match.index !== pos) { - throw new ParseError(`Unexpected character: '${input[pos]}'`, new Token(input[pos], new SourceLocation(this, pos, pos + 1))); - } - - let text = match[2] || " "; - - if (this.catcodes[text] === 14) { - // comment character - const nlIndex = input.indexOf('\n', this.tokenRegex.lastIndex); - - if (nlIndex === -1) { - this.tokenRegex.lastIndex = input.length; // EOF - - this.settings.reportNonstrict("commentAtEnd", "% comment has no terminating newline; LaTeX would " + "fail because of commenting the end of math mode (e.g. $)"); - } else { - this.tokenRegex.lastIndex = nlIndex + 1; - } - - return this.lex(); - } // Trim any trailing whitespace from control word match - - - const controlMatch = text.match(controlWordWhitespaceRegex); - - if (controlMatch) { - text = controlMatch[1]; - } - - return new Token(text, new SourceLocation(this, pos, this.tokenRegex.lastIndex)); - } - -} - -/** - * A `Namespace` refers to a space of nameable things like macros or lengths, - * which can be `set` either globally or local to a nested group, using an - * undo stack similar to how TeX implements this functionality. - * Performance-wise, `get` and local `set` take constant time, while global - * `set` takes time proportional to the depth of group nesting. - */ -class Namespace { - /** - * Both arguments are optional. The first argument is an object of - * built-in mappings which never change. The second argument is an object - * of initial (global-level) mappings, which will constantly change - * according to any global/top-level `set`s done. - */ - constructor(builtins, globalMacros) { - if (builtins === void 0) { - builtins = {}; - } - - if (globalMacros === void 0) { - globalMacros = {}; - } - - this.current = void 0; - this.builtins = void 0; - this.undefStack = void 0; - this.current = globalMacros; - this.builtins = builtins; - this.undefStack = []; - } - /** - * Start a new nested group, affecting future local `set`s. - */ - - - beginGroup() { - this.undefStack.push({}); - } - /** - * End current nested group, restoring values before the group began. - */ - - - endGroup() { - if (this.undefStack.length === 0) { - throw new ParseError("Unbalanced namespace destruction: attempt " + "to pop global namespace; please report this as a bug"); - } - - const undefs = this.undefStack.pop(); - - for (const undef in undefs) { - if (undefs.hasOwnProperty(undef)) { - if (undefs[undef] === undefined) { - delete this.current[undef]; - } else { - this.current[undef] = undefs[undef]; - } - } - } - } - /** - * Detect whether `name` has a definition. Equivalent to - * `get(name) != null`. - */ - - - has(name) { - return this.current.hasOwnProperty(name) || this.builtins.hasOwnProperty(name); - } - /** - * Get the current value of a name, or `undefined` if there is no value. - * - * Note: Do not use `if (namespace.get(...))` to detect whether a macro - * is defined, as the definition may be the empty string which evaluates - * to `false` in JavaScript. Use `if (namespace.get(...) != null)` or - * `if (namespace.has(...))`. - */ - - - get(name) { - if (this.current.hasOwnProperty(name)) { - return this.current[name]; - } else { - return this.builtins[name]; - } - } - /** - * Set the current value of a name, and optionally set it globally too. - * Local set() sets the current value and (when appropriate) adds an undo - * operation to the undo stack. Global set() may change the undo - * operation at every level, so takes time linear in their number. - */ - - - set(name, value, global) { - if (global === void 0) { - global = false; - } - - if (global) { - // Global set is equivalent to setting in all groups. Simulate this - // by destroying any undos currently scheduled for this name, - // and adding an undo with the *new* value (in case it later gets - // locally reset within this environment). - for (let i = 0; i < this.undefStack.length; i++) { - delete this.undefStack[i][name]; - } - - if (this.undefStack.length > 0) { - this.undefStack[this.undefStack.length - 1][name] = value; - } - } else { - // Undo this set at end of this group (possibly to `undefined`), - // unless an undo is already in place, in which case that older - // value is the correct one. - const top = this.undefStack[this.undefStack.length - 1]; - - if (top && !top.hasOwnProperty(name)) { - top[name] = this.current[name]; - } - } - - this.current[name] = value; - } - -} - -/** - * Predefined macros for KaTeX. - * This can be used to define some commands in terms of others. - */ -const builtinMacros = {}; - -function defineMacro(name, body) { - builtinMacros[name] = body; -} ////////////////////////////////////////////////////////////////////// -// macro tools - -defineMacro("\\noexpand", function (context) { - // The expansion is the token itself; but that token is interpreted - // as if its meaning were ‘\relax’ if it is a control sequence that - // would ordinarily be expanded by TeX’s expansion rules. - const t = context.popToken(); - - if (context.isExpandable(t.text)) { - t.noexpand = true; - t.treatAsRelax = true; - } - - return { - tokens: [t], - numArgs: 0 - }; -}); -defineMacro("\\expandafter", function (context) { - // TeX first reads the token that comes immediately after \expandafter, - // without expanding it; let’s call this token t. Then TeX reads the - // token that comes after t (and possibly more tokens, if that token - // has an argument), replacing it by its expansion. Finally TeX puts - // t back in front of that expansion. - const t = context.popToken(); - context.expandOnce(true); // expand only an expandable token - - return { - tokens: [t], - numArgs: 0 - }; -}); // LaTeX's \@firstoftwo{#1}{#2} expands to #1, skipping #2 -// TeX source: \long\def\@firstoftwo#1#2{#1} - -defineMacro("\\@firstoftwo", function (context) { - const args = context.consumeArgs(2); - return { - tokens: args[0], - numArgs: 0 - }; -}); // LaTeX's \@secondoftwo{#1}{#2} expands to #2, skipping #1 -// TeX source: \long\def\@secondoftwo#1#2{#2} - -defineMacro("\\@secondoftwo", function (context) { - const args = context.consumeArgs(2); - return { - tokens: args[1], - numArgs: 0 - }; -}); // LaTeX's \@ifnextchar{#1}{#2}{#3} looks ahead to the next (unexpanded) -// symbol that isn't a space, consuming any spaces but not consuming the -// first nonspace character. If that nonspace character matches #1, then -// the macro expands to #2; otherwise, it expands to #3. - -defineMacro("\\@ifnextchar", function (context) { - const args = context.consumeArgs(3); // symbol, if, else - - context.consumeSpaces(); - const nextToken = context.future(); - - if (args[0].length === 1 && args[0][0].text === nextToken.text) { - return { - tokens: args[1], - numArgs: 0 - }; - } else { - return { - tokens: args[2], - numArgs: 0 - }; - } -}); // LaTeX's \@ifstar{#1}{#2} looks ahead to the next (unexpanded) symbol. -// If it is `*`, then it consumes the symbol, and the macro expands to #1; -// otherwise, the macro expands to #2 (without consuming the symbol). -// TeX source: \def\@ifstar#1{\@ifnextchar *{\@firstoftwo{#1}}} - -defineMacro("\\@ifstar", "\\@ifnextchar *{\\@firstoftwo{#1}}"); // LaTeX's \TextOrMath{#1}{#2} expands to #1 in text mode, #2 in math mode - -defineMacro("\\TextOrMath", function (context) { - const args = context.consumeArgs(2); - - if (context.mode === 'text') { - return { - tokens: args[0], - numArgs: 0 - }; - } else { - return { - tokens: args[1], - numArgs: 0 - }; - } -}); // Lookup table for parsing numbers in base 8 through 16 - -const digitToNumber = { - "0": 0, - "1": 1, - "2": 2, - "3": 3, - "4": 4, - "5": 5, - "6": 6, - "7": 7, - "8": 8, - "9": 9, - "a": 10, - "A": 10, - "b": 11, - "B": 11, - "c": 12, - "C": 12, - "d": 13, - "D": 13, - "e": 14, - "E": 14, - "f": 15, - "F": 15 -}; // TeX \char makes a literal character (catcode 12) using the following forms: -// (see The TeXBook, p. 43) -// \char123 -- decimal -// \char'123 -- octal -// \char"123 -- hex -// \char`x -- character that can be written (i.e. isn't active) -// \char`\x -- character that cannot be written (e.g. %) -// These all refer to characters from the font, so we turn them into special -// calls to a function \@char dealt with in the Parser. - -defineMacro("\\char", function (context) { - let token = context.popToken(); - let base; - let number = ''; - - if (token.text === "'") { - base = 8; - token = context.popToken(); - } else if (token.text === '"') { - base = 16; - token = context.popToken(); - } else if (token.text === "`") { - token = context.popToken(); - - if (token.text[0] === "\\") { - number = token.text.charCodeAt(1); - } else if (token.text === "EOF") { - throw new ParseError("\\char` missing argument"); - } else { - number = token.text.charCodeAt(0); - } - } else { - base = 10; - } - - if (base) { - // Parse a number in the given base, starting with first `token`. - number = digitToNumber[token.text]; - - if (number == null || number >= base) { - throw new ParseError(`Invalid base-${base} digit ${token.text}`); - } - - let digit; - - while ((digit = digitToNumber[context.future().text]) != null && digit < base) { - number *= base; - number += digit; - context.popToken(); - } - } - - return `\\@char{${number}}`; -}); // \newcommand{\macro}[args]{definition} -// \renewcommand{\macro}[args]{definition} -// TODO: Optional arguments: \newcommand{\macro}[args][default]{definition} - -const newcommand = (context, existsOK, nonexistsOK) => { - let arg = context.consumeArgs(1)[0]; - - if (arg.length !== 1) { - throw new ParseError("\\newcommand's first argument must be a macro name"); - } - - const name = arg[0].text; - const exists = context.isDefined(name); - - if (exists && !existsOK) { - throw new ParseError(`\\newcommand{${name}} attempting to redefine ` + `${name}; use \\renewcommand`); - } - - if (!exists && !nonexistsOK) { - throw new ParseError(`\\renewcommand{${name}} when command ${name} ` + `does not yet exist; use \\newcommand`); - } - - let numArgs = 0; - arg = context.consumeArgs(1)[0]; - - if (arg.length === 1 && arg[0].text === "[") { - let argText = ''; - let token = context.expandNextToken(); - - while (token.text !== "]" && token.text !== "EOF") { - // TODO: Should properly expand arg, e.g., ignore {}s - argText += token.text; - token = context.expandNextToken(); - } - - if (!argText.match(/^\s*[0-9]+\s*$/)) { - throw new ParseError(`Invalid number of arguments: ${argText}`); - } - - numArgs = parseInt(argText); - arg = context.consumeArgs(1)[0]; - } // Final arg is the expansion of the macro - - - context.macros.set(name, { - tokens: arg, - numArgs - }); - return ''; -}; - -defineMacro("\\newcommand", context => newcommand(context, false, true)); -defineMacro("\\renewcommand", context => newcommand(context, true, false)); -defineMacro("\\providecommand", context => newcommand(context, true, true)); // terminal (console) tools - -defineMacro("\\message", context => { - const arg = context.consumeArgs(1)[0]; // eslint-disable-next-line no-console - - console.log(arg.reverse().map(token => token.text).join("")); - return ''; -}); -defineMacro("\\errmessage", context => { - const arg = context.consumeArgs(1)[0]; // eslint-disable-next-line no-console - - console.error(arg.reverse().map(token => token.text).join("")); - return ''; -}); -defineMacro("\\show", context => { - const tok = context.popToken(); - const name = tok.text; // eslint-disable-next-line no-console - - console.log(tok, context.macros.get(name), functions[name], symbols.math[name], symbols.text[name]); - return ''; -}); ////////////////////////////////////////////////////////////////////// -// Grouping -// \let\bgroup={ \let\egroup=} - -defineMacro("\\bgroup", "{"); -defineMacro("\\egroup", "}"); // Symbols from latex.ltx: -// \def\lq{`} -// \def\rq{'} -// \def \aa {\r a} -// \def \AA {\r A} - -defineMacro("\\lq", "`"); -defineMacro("\\rq", "'"); -defineMacro("\\aa", "\\r a"); -defineMacro("\\AA", "\\r A"); // Copyright (C) and registered (R) symbols. Use raw symbol in MathML. -// \DeclareTextCommandDefault{\textcopyright}{\textcircled{c}} -// \DeclareTextCommandDefault{\textregistered}{\textcircled{% -// \check@mathfonts\fontsize\sf@size\z@\math@fontsfalse\selectfont R}} -// \DeclareRobustCommand{\copyright}{% -// \ifmmode{\nfss@text{\textcopyright}}\else\textcopyright\fi} - -defineMacro("\\textcopyright", "\\html@mathml{\\textcircled{c}}{\\char`©}"); -defineMacro("\\copyright", "\\TextOrMath{\\textcopyright}{\\text{\\textcopyright}}"); -defineMacro("\\textregistered", "\\html@mathml{\\textcircled{\\scriptsize R}}{\\char`®}"); // Characters omitted from Unicode range 1D400–1D7FF - -defineMacro("\u212C", "\\mathscr{B}"); // script - -defineMacro("\u2130", "\\mathscr{E}"); -defineMacro("\u2131", "\\mathscr{F}"); -defineMacro("\u210B", "\\mathscr{H}"); -defineMacro("\u2110", "\\mathscr{I}"); -defineMacro("\u2112", "\\mathscr{L}"); -defineMacro("\u2133", "\\mathscr{M}"); -defineMacro("\u211B", "\\mathscr{R}"); -defineMacro("\u212D", "\\mathfrak{C}"); // Fraktur - -defineMacro("\u210C", "\\mathfrak{H}"); -defineMacro("\u2128", "\\mathfrak{Z}"); // Define \Bbbk with a macro that works in both HTML and MathML. - -defineMacro("\\Bbbk", "\\Bbb{k}"); // Unicode middle dot -// The KaTeX fonts do not contain U+00B7. Instead, \cdotp displays -// the dot at U+22C5 and gives it punct spacing. - -defineMacro("\u00b7", "\\cdotp"); // \llap and \rlap render their contents in text mode - -defineMacro("\\llap", "\\mathllap{\\textrm{#1}}"); -defineMacro("\\rlap", "\\mathrlap{\\textrm{#1}}"); -defineMacro("\\clap", "\\mathclap{\\textrm{#1}}"); // \not is defined by base/fontmath.ltx via -// \DeclareMathSymbol{\not}{\mathrel}{symbols}{"36} -// It's thus treated like a \mathrel, but defined by a symbol that has zero -// width but extends to the right. We use \rlap to get that spacing. -// For MathML we write U+0338 here. buildMathML.js will then do the overlay. - -defineMacro("\\not", '\\html@mathml{\\mathrel{\\mathrlap\\@not}}{\\char"338}'); // Negated symbols from base/fontmath.ltx: -// \def\neq{\not=} \let\ne=\neq -// \DeclareRobustCommand -// \notin{\mathrel{\m@th\mathpalette\c@ncel\in}} -// \def\c@ncel#1#2{\m@th\ooalign{$\hfil#1\mkern1mu/\hfil$\crcr$#1#2$}} - -defineMacro("\\neq", "\\html@mathml{\\mathrel{\\not=}}{\\mathrel{\\char`≠}}"); -defineMacro("\\ne", "\\neq"); -defineMacro("\u2260", "\\neq"); -defineMacro("\\notin", "\\html@mathml{\\mathrel{{\\in}\\mathllap{/\\mskip1mu}}}" + "{\\mathrel{\\char`∉}}"); -defineMacro("\u2209", "\\notin"); // Unicode stacked relations - -defineMacro("\u2258", "\\html@mathml{" + "\\mathrel{=\\kern{-1em}\\raisebox{0.4em}{$\\scriptsize\\frown$}}" + "}{\\mathrel{\\char`\u2258}}"); -defineMacro("\u2259", "\\html@mathml{\\stackrel{\\tiny\\wedge}{=}}{\\mathrel{\\char`\u2258}}"); -defineMacro("\u225A", "\\html@mathml{\\stackrel{\\tiny\\vee}{=}}{\\mathrel{\\char`\u225A}}"); -defineMacro("\u225B", "\\html@mathml{\\stackrel{\\scriptsize\\star}{=}}" + "{\\mathrel{\\char`\u225B}}"); -defineMacro("\u225D", "\\html@mathml{\\stackrel{\\tiny\\mathrm{def}}{=}}" + "{\\mathrel{\\char`\u225D}}"); -defineMacro("\u225E", "\\html@mathml{\\stackrel{\\tiny\\mathrm{m}}{=}}" + "{\\mathrel{\\char`\u225E}}"); -defineMacro("\u225F", "\\html@mathml{\\stackrel{\\tiny?}{=}}{\\mathrel{\\char`\u225F}}"); // Misc Unicode - -defineMacro("\u27C2", "\\perp"); -defineMacro("\u203C", "\\mathclose{!\\mkern-0.8mu!}"); -defineMacro("\u220C", "\\notni"); -defineMacro("\u231C", "\\ulcorner"); -defineMacro("\u231D", "\\urcorner"); -defineMacro("\u231E", "\\llcorner"); -defineMacro("\u231F", "\\lrcorner"); -defineMacro("\u00A9", "\\copyright"); -defineMacro("\u00AE", "\\textregistered"); -defineMacro("\uFE0F", "\\textregistered"); // The KaTeX fonts have corners at codepoints that don't match Unicode. -// For MathML purposes, use the Unicode code point. - -defineMacro("\\ulcorner", "\\html@mathml{\\@ulcorner}{\\mathop{\\char\"231c}}"); -defineMacro("\\urcorner", "\\html@mathml{\\@urcorner}{\\mathop{\\char\"231d}}"); -defineMacro("\\llcorner", "\\html@mathml{\\@llcorner}{\\mathop{\\char\"231e}}"); -defineMacro("\\lrcorner", "\\html@mathml{\\@lrcorner}{\\mathop{\\char\"231f}}"); ////////////////////////////////////////////////////////////////////// -// LaTeX_2ε -// \vdots{\vbox{\baselineskip4\p@ \lineskiplimit\z@ -// \kern6\p@\hbox{.}\hbox{.}\hbox{.}}} -// We'll call \varvdots, which gets a glyph from symbols.js. -// The zero-width rule gets us an equivalent to the vertical 6pt kern. - -defineMacro("\\vdots", "\\mathord{\\varvdots\\rule{0pt}{15pt}}"); -defineMacro("\u22ee", "\\vdots"); ////////////////////////////////////////////////////////////////////// -// amsmath.sty -// http://mirrors.concertpass.com/tex-archive/macros/latex/required/amsmath/amsmath.pdf -// Italic Greek capital letters. AMS defines these with \DeclareMathSymbol, -// but they are equivalent to \mathit{\Letter}. - -defineMacro("\\varGamma", "\\mathit{\\Gamma}"); -defineMacro("\\varDelta", "\\mathit{\\Delta}"); -defineMacro("\\varTheta", "\\mathit{\\Theta}"); -defineMacro("\\varLambda", "\\mathit{\\Lambda}"); -defineMacro("\\varXi", "\\mathit{\\Xi}"); -defineMacro("\\varPi", "\\mathit{\\Pi}"); -defineMacro("\\varSigma", "\\mathit{\\Sigma}"); -defineMacro("\\varUpsilon", "\\mathit{\\Upsilon}"); -defineMacro("\\varPhi", "\\mathit{\\Phi}"); -defineMacro("\\varPsi", "\\mathit{\\Psi}"); -defineMacro("\\varOmega", "\\mathit{\\Omega}"); //\newcommand{\substack}[1]{\subarray{c}#1\endsubarray} - -defineMacro("\\substack", "\\begin{subarray}{c}#1\\end{subarray}"); // \renewcommand{\colon}{\nobreak\mskip2mu\mathpunct{}\nonscript -// \mkern-\thinmuskip{:}\mskip6muplus1mu\relax} - -defineMacro("\\colon", "\\nobreak\\mskip2mu\\mathpunct{}" + "\\mathchoice{\\mkern-3mu}{\\mkern-3mu}{}{}{:}\\mskip6mu"); // \newcommand{\boxed}[1]{\fbox{\m@th$\displaystyle#1$}} - -defineMacro("\\boxed", "\\fbox{$\\displaystyle{#1}$}"); // \def\iff{\DOTSB\;\Longleftrightarrow\;} -// \def\implies{\DOTSB\;\Longrightarrow\;} -// \def\impliedby{\DOTSB\;\Longleftarrow\;} - -defineMacro("\\iff", "\\DOTSB\\;\\Longleftrightarrow\\;"); -defineMacro("\\implies", "\\DOTSB\\;\\Longrightarrow\\;"); -defineMacro("\\impliedby", "\\DOTSB\\;\\Longleftarrow\\;"); // AMSMath's automatic \dots, based on \mdots@@ macro. - -const dotsByToken = { - ',': '\\dotsc', - '\\not': '\\dotsb', - // \keybin@ checks for the following: - '+': '\\dotsb', - '=': '\\dotsb', - '<': '\\dotsb', - '>': '\\dotsb', - '-': '\\dotsb', - '*': '\\dotsb', - ':': '\\dotsb', - // Symbols whose definition starts with \DOTSB: - '\\DOTSB': '\\dotsb', - '\\coprod': '\\dotsb', - '\\bigvee': '\\dotsb', - '\\bigwedge': '\\dotsb', - '\\biguplus': '\\dotsb', - '\\bigcap': '\\dotsb', - '\\bigcup': '\\dotsb', - '\\prod': '\\dotsb', - '\\sum': '\\dotsb', - '\\bigotimes': '\\dotsb', - '\\bigoplus': '\\dotsb', - '\\bigodot': '\\dotsb', - '\\bigsqcup': '\\dotsb', - '\\And': '\\dotsb', - '\\longrightarrow': '\\dotsb', - '\\Longrightarrow': '\\dotsb', - '\\longleftarrow': '\\dotsb', - '\\Longleftarrow': '\\dotsb', - '\\longleftrightarrow': '\\dotsb', - '\\Longleftrightarrow': '\\dotsb', - '\\mapsto': '\\dotsb', - '\\longmapsto': '\\dotsb', - '\\hookrightarrow': '\\dotsb', - '\\doteq': '\\dotsb', - // Symbols whose definition starts with \mathbin: - '\\mathbin': '\\dotsb', - // Symbols whose definition starts with \mathrel: - '\\mathrel': '\\dotsb', - '\\relbar': '\\dotsb', - '\\Relbar': '\\dotsb', - '\\xrightarrow': '\\dotsb', - '\\xleftarrow': '\\dotsb', - // Symbols whose definition starts with \DOTSI: - '\\DOTSI': '\\dotsi', - '\\int': '\\dotsi', - '\\oint': '\\dotsi', - '\\iint': '\\dotsi', - '\\iiint': '\\dotsi', - '\\iiiint': '\\dotsi', - '\\idotsint': '\\dotsi', - // Symbols whose definition starts with \DOTSX: - '\\DOTSX': '\\dotsx' -}; -defineMacro("\\dots", function (context) { - // TODO: If used in text mode, should expand to \textellipsis. - // However, in KaTeX, \textellipsis and \ldots behave the same - // (in text mode), and it's unlikely we'd see any of the math commands - // that affect the behavior of \dots when in text mode. So fine for now - // (until we support \ifmmode ... \else ... \fi). - let thedots = '\\dotso'; - const next = context.expandAfterFuture().text; - - if (next in dotsByToken) { - thedots = dotsByToken[next]; - } else if (next.substr(0, 4) === '\\not') { - thedots = '\\dotsb'; - } else if (next in symbols.math) { - if (utils.contains(['bin', 'rel'], symbols.math[next].group)) { - thedots = '\\dotsb'; - } - } - - return thedots; -}); -const spaceAfterDots = { - // \rightdelim@ checks for the following: - ')': true, - ']': true, - '\\rbrack': true, - '\\}': true, - '\\rbrace': true, - '\\rangle': true, - '\\rceil': true, - '\\rfloor': true, - '\\rgroup': true, - '\\rmoustache': true, - '\\right': true, - '\\bigr': true, - '\\biggr': true, - '\\Bigr': true, - '\\Biggr': true, - // \extra@ also tests for the following: - '$': true, - // \extrap@ checks for the following: - ';': true, - '.': true, - ',': true -}; -defineMacro("\\dotso", function (context) { - const next = context.future().text; - - if (next in spaceAfterDots) { - return "\\ldots\\,"; - } else { - return "\\ldots"; - } -}); -defineMacro("\\dotsc", function (context) { - const next = context.future().text; // \dotsc uses \extra@ but not \extrap@, instead specially checking for - // ';' and '.', but doesn't check for ','. - - if (next in spaceAfterDots && next !== ',') { - return "\\ldots\\,"; - } else { - return "\\ldots"; - } -}); -defineMacro("\\cdots", function (context) { - const next = context.future().text; - - if (next in spaceAfterDots) { - return "\\@cdots\\,"; - } else { - return "\\@cdots"; - } -}); -defineMacro("\\dotsb", "\\cdots"); -defineMacro("\\dotsm", "\\cdots"); -defineMacro("\\dotsi", "\\!\\cdots"); // amsmath doesn't actually define \dotsx, but \dots followed by a macro -// starting with \DOTSX implies \dotso, and then \extra@ detects this case -// and forces the added `\,`. - -defineMacro("\\dotsx", "\\ldots\\,"); // \let\DOTSI\relax -// \let\DOTSB\relax -// \let\DOTSX\relax - -defineMacro("\\DOTSI", "\\relax"); -defineMacro("\\DOTSB", "\\relax"); -defineMacro("\\DOTSX", "\\relax"); // Spacing, based on amsmath.sty's override of LaTeX defaults -// \DeclareRobustCommand{\tmspace}[3]{% -// \ifmmode\mskip#1#2\else\kern#1#3\fi\relax} - -defineMacro("\\tmspace", "\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax"); // \renewcommand{\,}{\tmspace+\thinmuskip{.1667em}} -// TODO: math mode should use \thinmuskip - -defineMacro("\\,", "\\tmspace+{3mu}{.1667em}"); // \let\thinspace\, - -defineMacro("\\thinspace", "\\,"); // \def\>{\mskip\medmuskip} -// \renewcommand{\:}{\tmspace+\medmuskip{.2222em}} -// TODO: \> and math mode of \: should use \medmuskip = 4mu plus 2mu minus 4mu - -defineMacro("\\>", "\\mskip{4mu}"); -defineMacro("\\:", "\\tmspace+{4mu}{.2222em}"); // \let\medspace\: - -defineMacro("\\medspace", "\\:"); // \renewcommand{\;}{\tmspace+\thickmuskip{.2777em}} -// TODO: math mode should use \thickmuskip = 5mu plus 5mu - -defineMacro("\\;", "\\tmspace+{5mu}{.2777em}"); // \let\thickspace\; - -defineMacro("\\thickspace", "\\;"); // \renewcommand{\!}{\tmspace-\thinmuskip{.1667em}} -// TODO: math mode should use \thinmuskip - -defineMacro("\\!", "\\tmspace-{3mu}{.1667em}"); // \let\negthinspace\! - -defineMacro("\\negthinspace", "\\!"); // \newcommand{\negmedspace}{\tmspace-\medmuskip{.2222em}} -// TODO: math mode should use \medmuskip - -defineMacro("\\negmedspace", "\\tmspace-{4mu}{.2222em}"); // \newcommand{\negthickspace}{\tmspace-\thickmuskip{.2777em}} -// TODO: math mode should use \thickmuskip - -defineMacro("\\negthickspace", "\\tmspace-{5mu}{.277em}"); // \def\enspace{\kern.5em } - -defineMacro("\\enspace", "\\kern.5em "); // \def\enskip{\hskip.5em\relax} - -defineMacro("\\enskip", "\\hskip.5em\\relax"); // \def\quad{\hskip1em\relax} - -defineMacro("\\quad", "\\hskip1em\\relax"); // \def\qquad{\hskip2em\relax} - -defineMacro("\\qquad", "\\hskip2em\\relax"); // \tag@in@display form of \tag - -defineMacro("\\tag", "\\@ifstar\\tag@literal\\tag@paren"); -defineMacro("\\tag@paren", "\\tag@literal{({#1})}"); -defineMacro("\\tag@literal", context => { - if (context.macros.get("\\df@tag")) { - throw new ParseError("Multiple \\tag"); - } - - return "\\gdef\\df@tag{\\text{#1}}"; -}); // \renewcommand{\bmod}{\nonscript\mskip-\medmuskip\mkern5mu\mathbin -// {\operator@font mod}\penalty900 -// \mkern5mu\nonscript\mskip-\medmuskip} -// \newcommand{\pod}[1]{\allowbreak -// \if@display\mkern18mu\else\mkern8mu\fi(#1)} -// \renewcommand{\pmod}[1]{\pod{{\operator@font mod}\mkern6mu#1}} -// \newcommand{\mod}[1]{\allowbreak\if@display\mkern18mu -// \else\mkern12mu\fi{\operator@font mod}\,\,#1} -// TODO: math mode should use \medmuskip = 4mu plus 2mu minus 4mu - -defineMacro("\\bmod", "\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}" + "\\mathbin{\\rm mod}" + "\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}"); -defineMacro("\\pod", "\\allowbreak" + "\\mathchoice{\\mkern18mu}{\\mkern8mu}{\\mkern8mu}{\\mkern8mu}(#1)"); -defineMacro("\\pmod", "\\pod{{\\rm mod}\\mkern6mu#1}"); -defineMacro("\\mod", "\\allowbreak" + "\\mathchoice{\\mkern18mu}{\\mkern12mu}{\\mkern12mu}{\\mkern12mu}" + "{\\rm mod}\\,\\,#1"); // \pmb -- A simulation of bold. -// The version in ambsy.sty works by typesetting three copies of the argument -// with small offsets. We use two copies. We omit the vertical offset because -// of rendering problems that makeVList encounters in Safari. - -defineMacro("\\pmb", "\\html@mathml{" + "\\@binrel{#1}{\\mathrlap{#1}\\kern0.5px#1}}" + "{\\mathbf{#1}}"); ////////////////////////////////////////////////////////////////////// -// LaTeX source2e -// \\ defaults to \newline, but changes to \cr within array environment - -defineMacro("\\\\", "\\newline"); // \def\TeX{T\kern-.1667em\lower.5ex\hbox{E}\kern-.125emX\@} -// TODO: Doesn't normally work in math mode because \@ fails. KaTeX doesn't -// support \@ yet, so that's omitted, and we add \text so that the result -// doesn't look funny in math mode. - -defineMacro("\\TeX", "\\textrm{\\html@mathml{" + "T\\kern-.1667em\\raisebox{-.5ex}{E}\\kern-.125emX" + "}{TeX}}"); // \DeclareRobustCommand{\LaTeX}{L\kern-.36em% -// {\sbox\z@ T% -// \vbox to\ht\z@{\hbox{\check@mathfonts -// \fontsize\sf@size\z@ -// \math@fontsfalse\selectfont -// A}% -// \vss}% -// }% -// \kern-.15em% -// \TeX} -// This code aligns the top of the A with the T (from the perspective of TeX's -// boxes, though visually the A appears to extend above slightly). -// We compute the corresponding \raisebox when A is rendered in \normalsize -// \scriptstyle, which has a scale factor of 0.7 (see Options.js). - -const latexRaiseA = metricMap['Main-Regular']["T".charCodeAt(0)][1] - 0.7 * metricMap['Main-Regular']["A".charCodeAt(0)][1] + "em"; -defineMacro("\\LaTeX", "\\textrm{\\html@mathml{" + `L\\kern-.36em\\raisebox{${latexRaiseA}}{\\scriptstyle A}` + "\\kern-.15em\\TeX}{LaTeX}}"); // New KaTeX logo based on tweaking LaTeX logo - -defineMacro("\\KaTeX", "\\textrm{\\html@mathml{" + `K\\kern-.17em\\raisebox{${latexRaiseA}}{\\scriptstyle A}` + "\\kern-.15em\\TeX}{KaTeX}}"); // \DeclareRobustCommand\hspace{\@ifstar\@hspacer\@hspace} -// \def\@hspace#1{\hskip #1\relax} -// \def\@hspacer#1{\vrule \@width\z@\nobreak -// \hskip #1\hskip \z@skip} - -defineMacro("\\hspace", "\\@ifstar\\@hspacer\\@hspace"); -defineMacro("\\@hspace", "\\hskip #1\\relax"); -defineMacro("\\@hspacer", "\\rule{0pt}{0pt}\\hskip #1\\relax"); ////////////////////////////////////////////////////////////////////// -// mathtools.sty -//\providecommand\ordinarycolon{:} - -defineMacro("\\ordinarycolon", ":"); //\def\vcentcolon{\mathrel{\mathop\ordinarycolon}} -//TODO(edemaine): Not yet centered. Fix via \raisebox or #726 - -defineMacro("\\vcentcolon", "\\mathrel{\\mathop\\ordinarycolon}"); // \providecommand*\dblcolon{\vcentcolon\mathrel{\mkern-.9mu}\vcentcolon} - -defineMacro("\\dblcolon", "\\html@mathml{" + "\\mathrel{\\vcentcolon\\mathrel{\\mkern-.9mu}\\vcentcolon}}" + "{\\mathop{\\char\"2237}}"); // \providecommand*\coloneqq{\vcentcolon\mathrel{\mkern-1.2mu}=} - -defineMacro("\\coloneqq", "\\html@mathml{" + "\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}=}}" + "{\\mathop{\\char\"2254}}"); // ≔ -// \providecommand*\Coloneqq{\dblcolon\mathrel{\mkern-1.2mu}=} - -defineMacro("\\Coloneqq", "\\html@mathml{" + "\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}=}}" + "{\\mathop{\\char\"2237\\char\"3d}}"); // \providecommand*\coloneq{\vcentcolon\mathrel{\mkern-1.2mu}\mathrel{-}} - -defineMacro("\\coloneq", "\\html@mathml{" + "\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}" + "{\\mathop{\\char\"3a\\char\"2212}}"); // \providecommand*\Coloneq{\dblcolon\mathrel{\mkern-1.2mu}\mathrel{-}} - -defineMacro("\\Coloneq", "\\html@mathml{" + "\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}" + "{\\mathop{\\char\"2237\\char\"2212}}"); // \providecommand*\eqqcolon{=\mathrel{\mkern-1.2mu}\vcentcolon} - -defineMacro("\\eqqcolon", "\\html@mathml{" + "\\mathrel{=\\mathrel{\\mkern-1.2mu}\\vcentcolon}}" + "{\\mathop{\\char\"2255}}"); // ≕ -// \providecommand*\Eqqcolon{=\mathrel{\mkern-1.2mu}\dblcolon} - -defineMacro("\\Eqqcolon", "\\html@mathml{" + "\\mathrel{=\\mathrel{\\mkern-1.2mu}\\dblcolon}}" + "{\\mathop{\\char\"3d\\char\"2237}}"); // \providecommand*\eqcolon{\mathrel{-}\mathrel{\mkern-1.2mu}\vcentcolon} - -defineMacro("\\eqcolon", "\\html@mathml{" + "\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\vcentcolon}}" + "{\\mathop{\\char\"2239}}"); // \providecommand*\Eqcolon{\mathrel{-}\mathrel{\mkern-1.2mu}\dblcolon} - -defineMacro("\\Eqcolon", "\\html@mathml{" + "\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\dblcolon}}" + "{\\mathop{\\char\"2212\\char\"2237}}"); // \providecommand*\colonapprox{\vcentcolon\mathrel{\mkern-1.2mu}\approx} - -defineMacro("\\colonapprox", "\\html@mathml{" + "\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\approx}}" + "{\\mathop{\\char\"3a\\char\"2248}}"); // \providecommand*\Colonapprox{\dblcolon\mathrel{\mkern-1.2mu}\approx} - -defineMacro("\\Colonapprox", "\\html@mathml{" + "\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\approx}}" + "{\\mathop{\\char\"2237\\char\"2248}}"); // \providecommand*\colonsim{\vcentcolon\mathrel{\mkern-1.2mu}\sim} - -defineMacro("\\colonsim", "\\html@mathml{" + "\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\sim}}" + "{\\mathop{\\char\"3a\\char\"223c}}"); // \providecommand*\Colonsim{\dblcolon\mathrel{\mkern-1.2mu}\sim} - -defineMacro("\\Colonsim", "\\html@mathml{" + "\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\sim}}" + "{\\mathop{\\char\"2237\\char\"223c}}"); // Some Unicode characters are implemented with macros to mathtools functions. - -defineMacro("\u2237", "\\dblcolon"); // :: - -defineMacro("\u2239", "\\eqcolon"); // -: - -defineMacro("\u2254", "\\coloneqq"); // := - -defineMacro("\u2255", "\\eqqcolon"); // =: - -defineMacro("\u2A74", "\\Coloneqq"); // ::= -////////////////////////////////////////////////////////////////////// -// colonequals.sty -// Alternate names for mathtools's macros: - -defineMacro("\\ratio", "\\vcentcolon"); -defineMacro("\\coloncolon", "\\dblcolon"); -defineMacro("\\colonequals", "\\coloneqq"); -defineMacro("\\coloncolonequals", "\\Coloneqq"); -defineMacro("\\equalscolon", "\\eqqcolon"); -defineMacro("\\equalscoloncolon", "\\Eqqcolon"); -defineMacro("\\colonminus", "\\coloneq"); -defineMacro("\\coloncolonminus", "\\Coloneq"); -defineMacro("\\minuscolon", "\\eqcolon"); -defineMacro("\\minuscoloncolon", "\\Eqcolon"); // \colonapprox name is same in mathtools and colonequals. - -defineMacro("\\coloncolonapprox", "\\Colonapprox"); // \colonsim name is same in mathtools and colonequals. - -defineMacro("\\coloncolonsim", "\\Colonsim"); // Additional macros, implemented by analogy with mathtools definitions: - -defineMacro("\\simcolon", "\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\vcentcolon}"); -defineMacro("\\simcoloncolon", "\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\dblcolon}"); -defineMacro("\\approxcolon", "\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\vcentcolon}"); -defineMacro("\\approxcoloncolon", "\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\dblcolon}"); // Present in newtxmath, pxfonts and txfonts - -defineMacro("\\notni", "\\html@mathml{\\not\\ni}{\\mathrel{\\char`\u220C}}"); -defineMacro("\\limsup", "\\DOTSB\\operatorname*{lim\\,sup}"); -defineMacro("\\liminf", "\\DOTSB\\operatorname*{lim\\,inf}"); ////////////////////////////////////////////////////////////////////// -// MathML alternates for KaTeX glyphs in the Unicode private area - -defineMacro("\\gvertneqq", "\\html@mathml{\\@gvertneqq}{\u2269}"); -defineMacro("\\lvertneqq", "\\html@mathml{\\@lvertneqq}{\u2268}"); -defineMacro("\\ngeqq", "\\html@mathml{\\@ngeqq}{\u2271}"); -defineMacro("\\ngeqslant", "\\html@mathml{\\@ngeqslant}{\u2271}"); -defineMacro("\\nleqq", "\\html@mathml{\\@nleqq}{\u2270}"); -defineMacro("\\nleqslant", "\\html@mathml{\\@nleqslant}{\u2270}"); -defineMacro("\\nshortmid", "\\html@mathml{\\@nshortmid}{∤}"); -defineMacro("\\nshortparallel", "\\html@mathml{\\@nshortparallel}{∦}"); -defineMacro("\\nsubseteqq", "\\html@mathml{\\@nsubseteqq}{\u2288}"); -defineMacro("\\nsupseteqq", "\\html@mathml{\\@nsupseteqq}{\u2289}"); -defineMacro("\\varsubsetneq", "\\html@mathml{\\@varsubsetneq}{⊊}"); -defineMacro("\\varsubsetneqq", "\\html@mathml{\\@varsubsetneqq}{⫋}"); -defineMacro("\\varsupsetneq", "\\html@mathml{\\@varsupsetneq}{⊋}"); -defineMacro("\\varsupsetneqq", "\\html@mathml{\\@varsupsetneqq}{⫌}"); -defineMacro("\\imath", "\\html@mathml{\\@imath}{\u0131}"); -defineMacro("\\jmath", "\\html@mathml{\\@jmath}{\u0237}"); ////////////////////////////////////////////////////////////////////// -// stmaryrd and semantic -// The stmaryrd and semantic packages render the next four items by calling a -// glyph. Those glyphs do not exist in the KaTeX fonts. Hence the macros. - -defineMacro("\\llbracket", "\\html@mathml{" + "\\mathopen{[\\mkern-3.2mu[}}" + "{\\mathopen{\\char`\u27e6}}"); -defineMacro("\\rrbracket", "\\html@mathml{" + "\\mathclose{]\\mkern-3.2mu]}}" + "{\\mathclose{\\char`\u27e7}}"); -defineMacro("\u27e6", "\\llbracket"); // blackboard bold [ - -defineMacro("\u27e7", "\\rrbracket"); // blackboard bold ] - -defineMacro("\\lBrace", "\\html@mathml{" + "\\mathopen{\\{\\mkern-3.2mu[}}" + "{\\mathopen{\\char`\u2983}}"); -defineMacro("\\rBrace", "\\html@mathml{" + "\\mathclose{]\\mkern-3.2mu\\}}}" + "{\\mathclose{\\char`\u2984}}"); -defineMacro("\u2983", "\\lBrace"); // blackboard bold { - -defineMacro("\u2984", "\\rBrace"); // blackboard bold } -// TODO: Create variable sized versions of the last two items. I believe that -// will require new font glyphs. -// The stmaryrd function `\minuso` provides a "Plimsoll" symbol that -// superimposes the characters \circ and \mathminus. Used in chemistry. - -defineMacro("\\minuso", "\\mathbin{\\html@mathml{" + "{\\mathrlap{\\mathchoice{\\kern{0.145em}}{\\kern{0.145em}}" + "{\\kern{0.1015em}}{\\kern{0.0725em}}\\circ}{-}}}" + "{\\char`⦵}}"); -defineMacro("⦵", "\\minuso"); ////////////////////////////////////////////////////////////////////// -// texvc.sty -// The texvc package contains macros available in mediawiki pages. -// We omit the functions deprecated at -// https://en.wikipedia.org/wiki/Help:Displaying_a_formula#Deprecated_syntax -// We also omit texvc's \O, which conflicts with \text{\O} - -defineMacro("\\darr", "\\downarrow"); -defineMacro("\\dArr", "\\Downarrow"); -defineMacro("\\Darr", "\\Downarrow"); -defineMacro("\\lang", "\\langle"); -defineMacro("\\rang", "\\rangle"); -defineMacro("\\uarr", "\\uparrow"); -defineMacro("\\uArr", "\\Uparrow"); -defineMacro("\\Uarr", "\\Uparrow"); -defineMacro("\\N", "\\mathbb{N}"); -defineMacro("\\R", "\\mathbb{R}"); -defineMacro("\\Z", "\\mathbb{Z}"); -defineMacro("\\alef", "\\aleph"); -defineMacro("\\alefsym", "\\aleph"); -defineMacro("\\Alpha", "\\mathrm{A}"); -defineMacro("\\Beta", "\\mathrm{B}"); -defineMacro("\\bull", "\\bullet"); -defineMacro("\\Chi", "\\mathrm{X}"); -defineMacro("\\clubs", "\\clubsuit"); -defineMacro("\\cnums", "\\mathbb{C}"); -defineMacro("\\Complex", "\\mathbb{C}"); -defineMacro("\\Dagger", "\\ddagger"); -defineMacro("\\diamonds", "\\diamondsuit"); -defineMacro("\\empty", "\\emptyset"); -defineMacro("\\Epsilon", "\\mathrm{E}"); -defineMacro("\\Eta", "\\mathrm{H}"); -defineMacro("\\exist", "\\exists"); -defineMacro("\\harr", "\\leftrightarrow"); -defineMacro("\\hArr", "\\Leftrightarrow"); -defineMacro("\\Harr", "\\Leftrightarrow"); -defineMacro("\\hearts", "\\heartsuit"); -defineMacro("\\image", "\\Im"); -defineMacro("\\infin", "\\infty"); -defineMacro("\\Iota", "\\mathrm{I}"); -defineMacro("\\isin", "\\in"); -defineMacro("\\Kappa", "\\mathrm{K}"); -defineMacro("\\larr", "\\leftarrow"); -defineMacro("\\lArr", "\\Leftarrow"); -defineMacro("\\Larr", "\\Leftarrow"); -defineMacro("\\lrarr", "\\leftrightarrow"); -defineMacro("\\lrArr", "\\Leftrightarrow"); -defineMacro("\\Lrarr", "\\Leftrightarrow"); -defineMacro("\\Mu", "\\mathrm{M}"); -defineMacro("\\natnums", "\\mathbb{N}"); -defineMacro("\\Nu", "\\mathrm{N}"); -defineMacro("\\Omicron", "\\mathrm{O}"); -defineMacro("\\plusmn", "\\pm"); -defineMacro("\\rarr", "\\rightarrow"); -defineMacro("\\rArr", "\\Rightarrow"); -defineMacro("\\Rarr", "\\Rightarrow"); -defineMacro("\\real", "\\Re"); -defineMacro("\\reals", "\\mathbb{R}"); -defineMacro("\\Reals", "\\mathbb{R}"); -defineMacro("\\Rho", "\\mathrm{P}"); -defineMacro("\\sdot", "\\cdot"); -defineMacro("\\sect", "\\S"); -defineMacro("\\spades", "\\spadesuit"); -defineMacro("\\sub", "\\subset"); -defineMacro("\\sube", "\\subseteq"); -defineMacro("\\supe", "\\supseteq"); -defineMacro("\\Tau", "\\mathrm{T}"); -defineMacro("\\thetasym", "\\vartheta"); // TODO: defineMacro("\\varcoppa", "\\\mbox{\\coppa}"); - -defineMacro("\\weierp", "\\wp"); -defineMacro("\\Zeta", "\\mathrm{Z}"); ////////////////////////////////////////////////////////////////////// -// statmath.sty -// https://ctan.math.illinois.edu/macros/latex/contrib/statmath/statmath.pdf - -defineMacro("\\argmin", "\\DOTSB\\operatorname*{arg\\,min}"); -defineMacro("\\argmax", "\\DOTSB\\operatorname*{arg\\,max}"); -defineMacro("\\plim", "\\DOTSB\\mathop{\\operatorname{plim}}\\limits"); ////////////////////////////////////////////////////////////////////// -// braket.sty -// http://ctan.math.washington.edu/tex-archive/macros/latex/contrib/braket/braket.pdf - -defineMacro("\\bra", "\\mathinner{\\langle{#1}|}"); -defineMacro("\\ket", "\\mathinner{|{#1}\\rangle}"); -defineMacro("\\braket", "\\mathinner{\\langle{#1}\\rangle}"); -defineMacro("\\Bra", "\\left\\langle#1\\right|"); -defineMacro("\\Ket", "\\left|#1\\right\\rangle"); // Custom Khan Academy colors, should be moved to an optional package - -defineMacro("\\blue", "\\textcolor{##6495ed}{#1}"); -defineMacro("\\orange", "\\textcolor{##ffa500}{#1}"); -defineMacro("\\pink", "\\textcolor{##ff00af}{#1}"); -defineMacro("\\red", "\\textcolor{##df0030}{#1}"); -defineMacro("\\green", "\\textcolor{##28ae7b}{#1}"); -defineMacro("\\gray", "\\textcolor{gray}{#1}"); -defineMacro("\\purple", "\\textcolor{##9d38bd}{#1}"); -defineMacro("\\blueA", "\\textcolor{##ccfaff}{#1}"); -defineMacro("\\blueB", "\\textcolor{##80f6ff}{#1}"); -defineMacro("\\blueC", "\\textcolor{##63d9ea}{#1}"); -defineMacro("\\blueD", "\\textcolor{##11accd}{#1}"); -defineMacro("\\blueE", "\\textcolor{##0c7f99}{#1}"); -defineMacro("\\tealA", "\\textcolor{##94fff5}{#1}"); -defineMacro("\\tealB", "\\textcolor{##26edd5}{#1}"); -defineMacro("\\tealC", "\\textcolor{##01d1c1}{#1}"); -defineMacro("\\tealD", "\\textcolor{##01a995}{#1}"); -defineMacro("\\tealE", "\\textcolor{##208170}{#1}"); -defineMacro("\\greenA", "\\textcolor{##b6ffb0}{#1}"); -defineMacro("\\greenB", "\\textcolor{##8af281}{#1}"); -defineMacro("\\greenC", "\\textcolor{##74cf70}{#1}"); -defineMacro("\\greenD", "\\textcolor{##1fab54}{#1}"); -defineMacro("\\greenE", "\\textcolor{##0d923f}{#1}"); -defineMacro("\\goldA", "\\textcolor{##ffd0a9}{#1}"); -defineMacro("\\goldB", "\\textcolor{##ffbb71}{#1}"); -defineMacro("\\goldC", "\\textcolor{##ff9c39}{#1}"); -defineMacro("\\goldD", "\\textcolor{##e07d10}{#1}"); -defineMacro("\\goldE", "\\textcolor{##a75a05}{#1}"); -defineMacro("\\redA", "\\textcolor{##fca9a9}{#1}"); -defineMacro("\\redB", "\\textcolor{##ff8482}{#1}"); -defineMacro("\\redC", "\\textcolor{##f9685d}{#1}"); -defineMacro("\\redD", "\\textcolor{##e84d39}{#1}"); -defineMacro("\\redE", "\\textcolor{##bc2612}{#1}"); -defineMacro("\\maroonA", "\\textcolor{##ffbde0}{#1}"); -defineMacro("\\maroonB", "\\textcolor{##ff92c6}{#1}"); -defineMacro("\\maroonC", "\\textcolor{##ed5fa6}{#1}"); -defineMacro("\\maroonD", "\\textcolor{##ca337c}{#1}"); -defineMacro("\\maroonE", "\\textcolor{##9e034e}{#1}"); -defineMacro("\\purpleA", "\\textcolor{##ddd7ff}{#1}"); -defineMacro("\\purpleB", "\\textcolor{##c6b9fc}{#1}"); -defineMacro("\\purpleC", "\\textcolor{##aa87ff}{#1}"); -defineMacro("\\purpleD", "\\textcolor{##7854ab}{#1}"); -defineMacro("\\purpleE", "\\textcolor{##543b78}{#1}"); -defineMacro("\\mintA", "\\textcolor{##f5f9e8}{#1}"); -defineMacro("\\mintB", "\\textcolor{##edf2df}{#1}"); -defineMacro("\\mintC", "\\textcolor{##e0e5cc}{#1}"); -defineMacro("\\grayA", "\\textcolor{##f6f7f7}{#1}"); -defineMacro("\\grayB", "\\textcolor{##f0f1f2}{#1}"); -defineMacro("\\grayC", "\\textcolor{##e3e5e6}{#1}"); -defineMacro("\\grayD", "\\textcolor{##d6d8da}{#1}"); -defineMacro("\\grayE", "\\textcolor{##babec2}{#1}"); -defineMacro("\\grayF", "\\textcolor{##888d93}{#1}"); -defineMacro("\\grayG", "\\textcolor{##626569}{#1}"); -defineMacro("\\grayH", "\\textcolor{##3b3e40}{#1}"); -defineMacro("\\grayI", "\\textcolor{##21242c}{#1}"); -defineMacro("\\kaBlue", "\\textcolor{##314453}{#1}"); -defineMacro("\\kaGreen", "\\textcolor{##71B307}{#1}"); - -/** - * This file contains the “gullet” where macros are expanded - * until only non-macro tokens remain. - */ -// List of commands that act like macros but aren't defined as a macro, -// function, or symbol. Used in `isDefined`. -const implicitCommands = { - "\\relax": true, - // MacroExpander.js - "^": true, - // Parser.js - "_": true, - // Parser.js - "\\limits": true, - // Parser.js - "\\nolimits": true // Parser.js - -}; -class MacroExpander { - constructor(input, settings, mode) { - this.settings = void 0; - this.expansionCount = void 0; - this.lexer = void 0; - this.macros = void 0; - this.stack = void 0; - this.mode = void 0; - this.settings = settings; - this.expansionCount = 0; - this.feed(input); // Make new global namespace - - this.macros = new Namespace(builtinMacros, settings.macros); - this.mode = mode; - this.stack = []; // contains tokens in REVERSE order - } - /** - * Feed a new input string to the same MacroExpander - * (with existing macros etc.). - */ - - - feed(input) { - this.lexer = new Lexer(input, this.settings); - } - /** - * Switches between "text" and "math" modes. - */ - - - switchMode(newMode) { - this.mode = newMode; - } - /** - * Start a new group nesting within all namespaces. - */ - - - beginGroup() { - this.macros.beginGroup(); - } - /** - * End current group nesting within all namespaces. - */ - - - endGroup() { - this.macros.endGroup(); - } - /** - * Returns the topmost token on the stack, without expanding it. - * Similar in behavior to TeX's `\futurelet`. - */ - - - future() { - if (this.stack.length === 0) { - this.pushToken(this.lexer.lex()); - } - - return this.stack[this.stack.length - 1]; - } - /** - * Remove and return the next unexpanded token. - */ - - - popToken() { - this.future(); // ensure non-empty stack - - return this.stack.pop(); - } - /** - * Add a given token to the token stack. In particular, this get be used - * to put back a token returned from one of the other methods. - */ - - - pushToken(token) { - this.stack.push(token); - } - /** - * Append an array of tokens to the token stack. - */ - - - pushTokens(tokens) { - this.stack.push(...tokens); - } - /** - * Consume all following space tokens, without expansion. - */ - - - consumeSpaces() { - for (;;) { - const token = this.future(); - - if (token.text === " ") { - this.stack.pop(); - } else { - break; - } - } - } - /** - * Consume the specified number of arguments from the token stream, - * and return the resulting array of arguments. - */ - - - consumeArgs(numArgs) { - const args = []; // obtain arguments, either single token or balanced {…} group - - for (let i = 0; i < numArgs; ++i) { - this.consumeSpaces(); // ignore spaces before each argument - - const startOfArg = this.popToken(); - - if (startOfArg.text === "{") { - const arg = []; - let depth = 1; - - while (depth !== 0) { - const tok = this.popToken(); - arg.push(tok); - - if (tok.text === "{") { - ++depth; - } else if (tok.text === "}") { - --depth; - } else if (tok.text === "EOF") { - throw new ParseError("End of input in macro argument", startOfArg); - } - } - - arg.pop(); // remove last } - - arg.reverse(); // like above, to fit in with stack order - - args[i] = arg; - } else if (startOfArg.text === "EOF") { - throw new ParseError("End of input expecting macro argument"); - } else { - args[i] = [startOfArg]; - } - } - - return args; - } - /** - * Expand the next token only once if possible. - * - * If the token is expanded, the resulting tokens will be pushed onto - * the stack in reverse order and will be returned as an array, - * also in reverse order. - * - * If not, the next token will be returned without removing it - * from the stack. This case can be detected by a `Token` return value - * instead of an `Array` return value. - * - * In either case, the next token will be on the top of the stack, - * or the stack will be empty. - * - * Used to implement `expandAfterFuture` and `expandNextToken`. - * - * At the moment, macro expansion doesn't handle delimited macros, - * i.e. things like those defined by \def\foo#1\end{…}. - * See the TeX book page 202ff. for details on how those should behave. - * - * If expandableOnly, only expandable tokens are expanded and - * an undefined control sequence results in an error. - */ - - - expandOnce(expandableOnly) { - const topToken = this.popToken(); - const name = topToken.text; - const expansion = !topToken.noexpand ? this._getExpansion(name) : null; - - if (expansion == null || expandableOnly && expansion.unexpandable) { - if (expandableOnly && expansion == null && name[0] === "\\" && !this.isDefined(name)) { - throw new ParseError("Undefined control sequence: " + name); - } - - this.pushToken(topToken); - return topToken; - } - - this.expansionCount++; - - if (this.expansionCount > this.settings.maxExpand) { - throw new ParseError("Too many expansions: infinite loop or " + "need to increase maxExpand setting"); - } - - let tokens = expansion.tokens; - - if (expansion.numArgs) { - const args = this.consumeArgs(expansion.numArgs); // paste arguments in place of the placeholders - - tokens = tokens.slice(); // make a shallow copy - - for (let i = tokens.length - 1; i >= 0; --i) { - let tok = tokens[i]; - - if (tok.text === "#") { - if (i === 0) { - throw new ParseError("Incomplete placeholder at end of macro body", tok); - } - - tok = tokens[--i]; // next token on stack - - if (tok.text === "#") { - // ## → # - tokens.splice(i + 1, 1); // drop first # - } else if (/^[1-9]$/.test(tok.text)) { - // replace the placeholder with the indicated argument - tokens.splice(i, 2, ...args[+tok.text - 1]); - } else { - throw new ParseError("Not a valid argument number", tok); - } - } - } - } // Concatenate expansion onto top of stack. - - - this.pushTokens(tokens); - return tokens; - } - /** - * Expand the next token only once (if possible), and return the resulting - * top token on the stack (without removing anything from the stack). - * Similar in behavior to TeX's `\expandafter\futurelet`. - * Equivalent to expandOnce() followed by future(). - */ - - - expandAfterFuture() { - this.expandOnce(); - return this.future(); - } - /** - * Recursively expand first token, then return first non-expandable token. - */ - - - expandNextToken() { - for (;;) { - const expanded = this.expandOnce(); // expandOnce returns Token if and only if it's fully expanded. - - if (expanded instanceof Token) { - // \relax stops the expansion, but shouldn't get returned (a - // null return value couldn't get implemented as a function). - // the token after \noexpand is interpreted as if its meaning - // were ‘\relax’ - if (expanded.text === "\\relax" || expanded.treatAsRelax) { - this.stack.pop(); - } else { - return this.stack.pop(); // === expanded - } - } - } // Flow unable to figure out that this pathway is impossible. - // https://github.com/facebook/flow/issues/4808 - - - throw new Error(); // eslint-disable-line no-unreachable - } - /** - * Fully expand the given macro name and return the resulting list of - * tokens, or return `undefined` if no such macro is defined. - */ - - - expandMacro(name) { - return this.macros.has(name) ? this.expandTokens([new Token(name)]) : undefined; - } - /** - * Fully expand the given token stream and return the resulting list of tokens - */ - - - expandTokens(tokens) { - const output = []; - const oldStackLength = this.stack.length; - this.pushTokens(tokens); - - while (this.stack.length > oldStackLength) { - const expanded = this.expandOnce(true); // expand only expandable tokens - // expandOnce returns Token if and only if it's fully expanded. - - if (expanded instanceof Token) { - if (expanded.treatAsRelax) { - // the expansion of \noexpand is the token itself - expanded.noexpand = false; - expanded.treatAsRelax = false; - } - - output.push(this.stack.pop()); - } - } - - return output; - } - /** - * Fully expand the given macro name and return the result as a string, - * or return `undefined` if no such macro is defined. - */ - - - expandMacroAsText(name) { - const tokens = this.expandMacro(name); - - if (tokens) { - return tokens.map(token => token.text).join(""); - } else { - return tokens; - } - } - /** - * Returns the expanded macro as a reversed array of tokens and a macro - * argument count. Or returns `null` if no such macro. - */ - - - _getExpansion(name) { - const definition = this.macros.get(name); - - if (definition == null) { - // mainly checking for undefined here - return definition; - } - - const expansion = typeof definition === "function" ? definition(this) : definition; - - if (typeof expansion === "string") { - let numArgs = 0; - - if (expansion.indexOf("#") !== -1) { - const stripped = expansion.replace(/##/g, ""); - - while (stripped.indexOf("#" + (numArgs + 1)) !== -1) { - ++numArgs; - } - } - - const bodyLexer = new Lexer(expansion, this.settings); - const tokens = []; - let tok = bodyLexer.lex(); - - while (tok.text !== "EOF") { - tokens.push(tok); - tok = bodyLexer.lex(); - } - - tokens.reverse(); // to fit in with stack using push and pop - - const expanded = { - tokens, - numArgs - }; - return expanded; - } - - return expansion; - } - /** - * Determine whether a command is currently "defined" (has some - * functionality), meaning that it's a macro (in the current group), - * a function, a symbol, or one of the special commands listed in - * `implicitCommands`. - */ - - - isDefined(name) { - return this.macros.has(name) || functions.hasOwnProperty(name) || symbols.math.hasOwnProperty(name) || symbols.text.hasOwnProperty(name) || implicitCommands.hasOwnProperty(name); - } - /** - * Determine whether a command is expandable. - */ - - - isExpandable(name) { - const macro = this.macros.get(name); - return macro != null ? typeof macro === "string" || typeof macro === "function" || !macro.unexpandable // TODO(ylem): #2085 - : functions.hasOwnProperty(name) - /* && !functions[name].primitive*/ - ; - } - -} - -/* eslint no-constant-condition:0 */ - -const unicodeAccents = { - "́": { - "text": "\\'", - "math": "\\acute" - }, - "̀": { - "text": "\\`", - "math": "\\grave" - }, - "̈": { - "text": "\\\"", - "math": "\\ddot" - }, - "̃": { - "text": "\\~", - "math": "\\tilde" - }, - "̄": { - "text": "\\=", - "math": "\\bar" - }, - "̆": { - "text": "\\u", - "math": "\\breve" - }, - "̌": { - "text": "\\v", - "math": "\\check" - }, - "̂": { - "text": "\\^", - "math": "\\hat" - }, - "̇": { - "text": "\\.", - "math": "\\dot" - }, - "̊": { - "text": "\\r", - "math": "\\mathring" - }, - "̋": { - "text": "\\H" - } -}; -const unicodeSymbols = { - "á": "á", - "à": "à", - "ä": "ä", - "ǟ": "ǟ", - "ã": "ã", - "ā": "ā", - "ă": "ă", - "ắ": "ắ", - "ằ": "ằ", - "ẵ": "ẵ", - "ǎ": "ǎ", - "â": "â", - "ấ": "ấ", - "ầ": "ầ", - "ẫ": "ẫ", - "ȧ": "ȧ", - "ǡ": "ǡ", - "å": "å", - "ǻ": "ǻ", - "ḃ": "ḃ", - "ć": "ć", - "č": "č", - "ĉ": "ĉ", - "ċ": "ċ", - "ď": "ď", - "ḋ": "ḋ", - "é": "é", - "è": "è", - "ë": "ë", - "ẽ": "ẽ", - "ē": "ē", - "ḗ": "ḗ", - "ḕ": "ḕ", - "ĕ": "ĕ", - "ě": "ě", - "ê": "ê", - "ế": "ế", - "ề": "ề", - "ễ": "ễ", - "ė": "ė", - "ḟ": "ḟ", - "ǵ": "ǵ", - "ḡ": "ḡ", - "ğ": "ğ", - "ǧ": "ǧ", - "ĝ": "ĝ", - "ġ": "ġ", - "ḧ": "ḧ", - "ȟ": "ȟ", - "ĥ": "ĥ", - "ḣ": "ḣ", - "í": "í", - "ì": "ì", - "ï": "ï", - "ḯ": "ḯ", - "ĩ": "ĩ", - "ī": "ī", - "ĭ": "ĭ", - "ǐ": "ǐ", - "î": "î", - "ǰ": "ǰ", - "ĵ": "ĵ", - "ḱ": "ḱ", - "ǩ": "ǩ", - "ĺ": "ĺ", - "ľ": "ľ", - "ḿ": "ḿ", - "ṁ": "ṁ", - "ń": "ń", - "ǹ": "ǹ", - "ñ": "ñ", - "ň": "ň", - "ṅ": "ṅ", - "ó": "ó", - "ò": "ò", - "ö": "ö", - "ȫ": "ȫ", - "õ": "õ", - "ṍ": "ṍ", - "ṏ": "ṏ", - "ȭ": "ȭ", - "ō": "ō", - "ṓ": "ṓ", - "ṑ": "ṑ", - "ŏ": "ŏ", - "ǒ": "ǒ", - "ô": "ô", - "ố": "ố", - "ồ": "ồ", - "ỗ": "ỗ", - "ȯ": "ȯ", - "ȱ": "ȱ", - "ő": "ő", - "ṕ": "ṕ", - "ṗ": "ṗ", - "ŕ": "ŕ", - "ř": "ř", - "ṙ": "ṙ", - "ś": "ś", - "ṥ": "ṥ", - "š": "š", - "ṧ": "ṧ", - "ŝ": "ŝ", - "ṡ": "ṡ", - "ẗ": "ẗ", - "ť": "ť", - "ṫ": "ṫ", - "ú": "ú", - "ù": "ù", - "ü": "ü", - "ǘ": "ǘ", - "ǜ": "ǜ", - "ǖ": "ǖ", - "ǚ": "ǚ", - "ũ": "ũ", - "ṹ": "ṹ", - "ū": "ū", - "ṻ": "ṻ", - "ŭ": "ŭ", - "ǔ": "ǔ", - "û": "û", - "ů": "ů", - "ű": "ű", - "ṽ": "ṽ", - "ẃ": "ẃ", - "ẁ": "ẁ", - "ẅ": "ẅ", - "ŵ": "ŵ", - "ẇ": "ẇ", - "ẘ": "ẘ", - "ẍ": "ẍ", - "ẋ": "ẋ", - "ý": "ý", - "ỳ": "ỳ", - "ÿ": "ÿ", - "ỹ": "ỹ", - "ȳ": "ȳ", - "ŷ": "ŷ", - "ẏ": "ẏ", - "ẙ": "ẙ", - "ź": "ź", - "ž": "ž", - "ẑ": "ẑ", - "ż": "ż", - "Á": "Á", - "À": "À", - "Ä": "Ä", - "Ǟ": "Ǟ", - "Ã": "Ã", - "Ā": "Ā", - "Ă": "Ă", - "Ắ": "Ắ", - "Ằ": "Ằ", - "Ẵ": "Ẵ", - "Ǎ": "Ǎ", - "Â": "Â", - "Ấ": "Ấ", - "Ầ": "Ầ", - "Ẫ": "Ẫ", - "Ȧ": "Ȧ", - "Ǡ": "Ǡ", - "Å": "Å", - "Ǻ": "Ǻ", - "Ḃ": "Ḃ", - "Ć": "Ć", - "Č": "Č", - "Ĉ": "Ĉ", - "Ċ": "Ċ", - "Ď": "Ď", - "Ḋ": "Ḋ", - "É": "É", - "È": "È", - "Ë": "Ë", - "Ẽ": "Ẽ", - "Ē": "Ē", - "Ḗ": "Ḗ", - "Ḕ": "Ḕ", - "Ĕ": "Ĕ", - "Ě": "Ě", - "Ê": "Ê", - "Ế": "Ế", - "Ề": "Ề", - "Ễ": "Ễ", - "Ė": "Ė", - "Ḟ": "Ḟ", - "Ǵ": "Ǵ", - "Ḡ": "Ḡ", - "Ğ": "Ğ", - "Ǧ": "Ǧ", - "Ĝ": "Ĝ", - "Ġ": "Ġ", - "Ḧ": "Ḧ", - "Ȟ": "Ȟ", - "Ĥ": "Ĥ", - "Ḣ": "Ḣ", - "Í": "Í", - "Ì": "Ì", - "Ï": "Ï", - "Ḯ": "Ḯ", - "Ĩ": "Ĩ", - "Ī": "Ī", - "Ĭ": "Ĭ", - "Ǐ": "Ǐ", - "Î": "Î", - "İ": "İ", - "Ĵ": "Ĵ", - "Ḱ": "Ḱ", - "Ǩ": "Ǩ", - "Ĺ": "Ĺ", - "Ľ": "Ľ", - "Ḿ": "Ḿ", - "Ṁ": "Ṁ", - "Ń": "Ń", - "Ǹ": "Ǹ", - "Ñ": "Ñ", - "Ň": "Ň", - "Ṅ": "Ṅ", - "Ó": "Ó", - "Ò": "Ò", - "Ö": "Ö", - "Ȫ": "Ȫ", - "Õ": "Õ", - "Ṍ": "Ṍ", - "Ṏ": "Ṏ", - "Ȭ": "Ȭ", - "Ō": "Ō", - "Ṓ": "Ṓ", - "Ṑ": "Ṑ", - "Ŏ": "Ŏ", - "Ǒ": "Ǒ", - "Ô": "Ô", - "Ố": "Ố", - "Ồ": "Ồ", - "Ỗ": "Ỗ", - "Ȯ": "Ȯ", - "Ȱ": "Ȱ", - "Ő": "Ő", - "Ṕ": "Ṕ", - "Ṗ": "Ṗ", - "Ŕ": "Ŕ", - "Ř": "Ř", - "Ṙ": "Ṙ", - "Ś": "Ś", - "Ṥ": "Ṥ", - "Š": "Š", - "Ṧ": "Ṧ", - "Ŝ": "Ŝ", - "Ṡ": "Ṡ", - "Ť": "Ť", - "Ṫ": "Ṫ", - "Ú": "Ú", - "Ù": "Ù", - "Ü": "Ü", - "Ǘ": "Ǘ", - "Ǜ": "Ǜ", - "Ǖ": "Ǖ", - "Ǚ": "Ǚ", - "Ũ": "Ũ", - "Ṹ": "Ṹ", - "Ū": "Ū", - "Ṻ": "Ṻ", - "Ŭ": "Ŭ", - "Ǔ": "Ǔ", - "Û": "Û", - "Ů": "Ů", - "Ű": "Ű", - "Ṽ": "Ṽ", - "Ẃ": "Ẃ", - "Ẁ": "Ẁ", - "Ẅ": "Ẅ", - "Ŵ": "Ŵ", - "Ẇ": "Ẇ", - "Ẍ": "Ẍ", - "Ẋ": "Ẋ", - "Ý": "Ý", - "Ỳ": "Ỳ", - "Ÿ": "Ÿ", - "Ỹ": "Ỹ", - "Ȳ": "Ȳ", - "Ŷ": "Ŷ", - "Ẏ": "Ẏ", - "Ź": "Ź", - "Ž": "Ž", - "Ẑ": "Ẑ", - "Ż": "Ż", - "ά": "ά", - "ὰ": "ὰ", - "ᾱ": "ᾱ", - "ᾰ": "ᾰ", - "έ": "έ", - "ὲ": "ὲ", - "ή": "ή", - "ὴ": "ὴ", - "ί": "ί", - "ὶ": "ὶ", - "ϊ": "ϊ", - "ΐ": "ΐ", - "ῒ": "ῒ", - "ῑ": "ῑ", - "ῐ": "ῐ", - "ό": "ό", - "ὸ": "ὸ", - "ύ": "ύ", - "ὺ": "ὺ", - "ϋ": "ϋ", - "ΰ": "ΰ", - "ῢ": "ῢ", - "ῡ": "ῡ", - "ῠ": "ῠ", - "ώ": "ώ", - "ὼ": "ὼ", - "Ύ": "Ύ", - "Ὺ": "Ὺ", - "Ϋ": "Ϋ", - "Ῡ": "Ῡ", - "Ῠ": "Ῠ", - "Ώ": "Ώ", - "Ὼ": "Ὼ" -}; - -/** - * This file contains the parser used to parse out a TeX expression from the - * input. Since TeX isn't context-free, standard parsers don't work particularly - * well. - * - * The strategy of this parser is as such: - * - * The main functions (the `.parse...` ones) take a position in the current - * parse string to parse tokens from. The lexer (found in Lexer.js, stored at - * this.gullet.lexer) also supports pulling out tokens at arbitrary places. When - * individual tokens are needed at a position, the lexer is called to pull out a - * token, which is then used. - * - * The parser has a property called "mode" indicating the mode that - * the parser is currently in. Currently it has to be one of "math" or - * "text", which denotes whether the current environment is a math-y - * one or a text-y one (e.g. inside \text). Currently, this serves to - * limit the functions which can be used in text mode. - * - * The main functions then return an object which contains the useful data that - * was parsed at its given point, and a new position at the end of the parsed - * data. The main functions can call each other and continue the parsing by - * using the returned position as a new starting point. - * - * There are also extra `.handle...` functions, which pull out some reused - * functionality into self-contained functions. - * - * The functions return ParseNodes. - */ -class Parser { - constructor(input, settings) { - this.mode = void 0; - this.gullet = void 0; - this.settings = void 0; - this.leftrightDepth = void 0; - this.nextToken = void 0; - // Start in math mode - this.mode = "math"; // Create a new macro expander (gullet) and (indirectly via that) also a - // new lexer (mouth) for this parser (stomach, in the language of TeX) - - this.gullet = new MacroExpander(input, settings, this.mode); // Store the settings for use in parsing - - this.settings = settings; // Count leftright depth (for \middle errors) - - this.leftrightDepth = 0; - } - /** - * Checks a result to make sure it has the right type, and throws an - * appropriate error otherwise. - */ - - - expect(text, consume) { - if (consume === void 0) { - consume = true; - } - - if (this.fetch().text !== text) { - throw new ParseError(`Expected '${text}', got '${this.fetch().text}'`, this.fetch()); - } - - if (consume) { - this.consume(); - } - } - /** - * Discards the current lookahead token, considering it consumed. - */ - - - consume() { - this.nextToken = null; - } - /** - * Return the current lookahead token, or if there isn't one (at the - * beginning, or if the previous lookahead token was consume()d), - * fetch the next token as the new lookahead token and return it. - */ - - - fetch() { - if (this.nextToken == null) { - this.nextToken = this.gullet.expandNextToken(); - } - - return this.nextToken; - } - /** - * Switches between "text" and "math" modes. - */ - - - switchMode(newMode) { - this.mode = newMode; - this.gullet.switchMode(newMode); - } - /** - * Main parsing function, which parses an entire input. - */ - - - parse() { - if (!this.settings.globalGroup) { - // Create a group namespace for the math expression. - // (LaTeX creates a new group for every $...$, $$...$$, \[...\].) - this.gullet.beginGroup(); - } // Use old \color behavior (same as LaTeX's \textcolor) if requested. - // We do this within the group for the math expression, so it doesn't - // pollute settings.macros. - - - if (this.settings.colorIsTextColor) { - this.gullet.macros.set("\\color", "\\textcolor"); - } // Try to parse the input - - - const parse = this.parseExpression(false); // If we succeeded, make sure there's an EOF at the end - - this.expect("EOF"); // End the group namespace for the expression - - if (!this.settings.globalGroup) { - this.gullet.endGroup(); - } - - return parse; - } - - parseExpression(breakOnInfix, breakOnTokenText) { - const body = []; // Keep adding atoms to the body until we can't parse any more atoms (either - // we reached the end, a }, or a \right) - - while (true) { - // Ignore spaces in math mode - if (this.mode === "math") { - this.consumeSpaces(); - } - - const lex = this.fetch(); - - if (Parser.endOfExpression.indexOf(lex.text) !== -1) { - break; - } - - if (breakOnTokenText && lex.text === breakOnTokenText) { - break; - } - - if (breakOnInfix && functions[lex.text] && functions[lex.text].infix) { - break; - } - - const atom = this.parseAtom(breakOnTokenText); - - if (!atom) { - break; - } else if (atom.type === "internal") { - continue; - } - - body.push(atom); - } - - if (this.mode === "text") { - this.formLigatures(body); - } - - return this.handleInfixNodes(body); - } - /** - * Rewrites infix operators such as \over with corresponding commands such - * as \frac. - * - * There can only be one infix operator per group. If there's more than one - * then the expression is ambiguous. This can be resolved by adding {}. - */ - - - handleInfixNodes(body) { - let overIndex = -1; - let funcName; - - for (let i = 0; i < body.length; i++) { - if (body[i].type === "infix") { - if (overIndex !== -1) { - throw new ParseError("only one infix operator per group", body[i].token); - } - - overIndex = i; - funcName = body[i].replaceWith; - } - } - - if (overIndex !== -1 && funcName) { - let numerNode; - let denomNode; - const numerBody = body.slice(0, overIndex); - const denomBody = body.slice(overIndex + 1); - - if (numerBody.length === 1 && numerBody[0].type === "ordgroup") { - numerNode = numerBody[0]; - } else { - numerNode = { - type: "ordgroup", - mode: this.mode, - body: numerBody - }; - } - - if (denomBody.length === 1 && denomBody[0].type === "ordgroup") { - denomNode = denomBody[0]; - } else { - denomNode = { - type: "ordgroup", - mode: this.mode, - body: denomBody - }; - } - - let node; - - if (funcName === "\\\\abovefrac") { - node = this.callFunction(funcName, [numerNode, body[overIndex], denomNode], []); - } else { - node = this.callFunction(funcName, [numerNode, denomNode], []); - } - - return [node]; - } else { - return body; - } - } // The greediness of a superscript or subscript - - - /** - * Handle a subscript or superscript with nice errors. - */ - handleSupSubscript(name) { - const symbolToken = this.fetch(); - const symbol = symbolToken.text; - this.consume(); - const group = this.parseGroup(name, false, Parser.SUPSUB_GREEDINESS, undefined, undefined, true); // ignore spaces before sup/subscript argument - - if (!group) { - throw new ParseError("Expected group after '" + symbol + "'", symbolToken); - } - - return group; - } - /** - * Converts the textual input of an unsupported command into a text node - * contained within a color node whose color is determined by errorColor - */ - - - formatUnsupportedCmd(text) { - const textordArray = []; - - for (let i = 0; i < text.length; i++) { - textordArray.push({ - type: "textord", - mode: "text", - text: text[i] - }); - } - - const textNode = { - type: "text", - mode: this.mode, - body: textordArray - }; - const colorNode = { - type: "color", - mode: this.mode, - color: this.settings.errorColor, - body: [textNode] - }; - return colorNode; - } - /** - * Parses a group with optional super/subscripts. - */ - - - parseAtom(breakOnTokenText) { - // The body of an atom is an implicit group, so that things like - // \left(x\right)^2 work correctly. - const base = this.parseGroup("atom", false, null, breakOnTokenText); // In text mode, we don't have superscripts or subscripts - - if (this.mode === "text") { - return base; - } // Note that base may be empty (i.e. null) at this point. - - - let superscript; - let subscript; - - while (true) { - // Guaranteed in math mode, so eat any spaces first. - this.consumeSpaces(); // Lex the first token - - const lex = this.fetch(); - - if (lex.text === "\\limits" || lex.text === "\\nolimits") { - // We got a limit control - if (base && base.type === "op") { - const limits = lex.text === "\\limits"; - base.limits = limits; - base.alwaysHandleSupSub = true; - } else if (base && base.type === "operatorname" && base.alwaysHandleSupSub) { - const limits = lex.text === "\\limits"; - base.limits = limits; - } else { - throw new ParseError("Limit controls must follow a math operator", lex); - } - - this.consume(); - } else if (lex.text === "^") { - // We got a superscript start - if (superscript) { - throw new ParseError("Double superscript", lex); - } - - superscript = this.handleSupSubscript("superscript"); - } else if (lex.text === "_") { - // We got a subscript start - if (subscript) { - throw new ParseError("Double subscript", lex); - } - - subscript = this.handleSupSubscript("subscript"); - } else if (lex.text === "'") { - // We got a prime - if (superscript) { - throw new ParseError("Double superscript", lex); - } - - const prime = { - type: "textord", - mode: this.mode, - text: "\\prime" - }; // Many primes can be grouped together, so we handle this here - - const primes = [prime]; - this.consume(); // Keep lexing tokens until we get something that's not a prime - - while (this.fetch().text === "'") { - // For each one, add another prime to the list - primes.push(prime); - this.consume(); - } // If there's a superscript following the primes, combine that - // superscript in with the primes. - - - if (this.fetch().text === "^") { - primes.push(this.handleSupSubscript("superscript")); - } // Put everything into an ordgroup as the superscript - - - superscript = { - type: "ordgroup", - mode: this.mode, - body: primes - }; - } else { - // If it wasn't ^, _, or ', stop parsing super/subscripts - break; - } - } // Base must be set if superscript or subscript are set per logic above, - // but need to check here for type check to pass. - - - if (superscript || subscript) { - // If we got either a superscript or subscript, create a supsub - return { - type: "supsub", - mode: this.mode, - base: base, - sup: superscript, - sub: subscript - }; - } else { - // Otherwise return the original body - return base; - } - } - /** - * Parses an entire function, including its base and all of its arguments. - */ - - - parseFunction(breakOnTokenText, name, // For error reporting. - greediness) { - const token = this.fetch(); - const func = token.text; - const funcData = functions[func]; - - if (!funcData) { - return null; - } - - this.consume(); // consume command token - - if (greediness != null && funcData.greediness <= greediness) { - throw new ParseError("Got function '" + func + "' with no arguments" + (name ? " as " + name : ""), token); - } else if (this.mode === "text" && !funcData.allowedInText) { - throw new ParseError("Can't use function '" + func + "' in text mode", token); - } else if (this.mode === "math" && funcData.allowedInMath === false) { - throw new ParseError("Can't use function '" + func + "' in math mode", token); - } - - const _this$parseArguments = this.parseArguments(func, funcData), - args = _this$parseArguments.args, - optArgs = _this$parseArguments.optArgs; - - return this.callFunction(func, args, optArgs, token, breakOnTokenText); - } - /** - * Call a function handler with a suitable context and arguments. - */ - - - callFunction(name, args, optArgs, token, breakOnTokenText) { - const context = { - funcName: name, - parser: this, - token, - breakOnTokenText - }; - const func = functions[name]; - - if (func && func.handler) { - return func.handler(context, args, optArgs); - } else { - throw new ParseError(`No function handler for ${name}`); - } - } - /** - * Parses the arguments of a function or environment - */ - - - parseArguments(func, // Should look like "\name" or "\begin{name}". - funcData) { - const totalArgs = funcData.numArgs + funcData.numOptionalArgs; - - if (totalArgs === 0) { - return { - args: [], - optArgs: [] - }; - } - - const baseGreediness = funcData.greediness; - const args = []; - const optArgs = []; - - for (let i = 0; i < totalArgs; i++) { - const argType = funcData.argTypes && funcData.argTypes[i]; - const isOptional = i < funcData.numOptionalArgs; // Ignore spaces between arguments. As the TeXbook says: - // "After you have said ‘\def\row#1#2{...}’, you are allowed to - // put spaces between the arguments (e.g., ‘\row x n’), because - // TeX doesn’t use single spaces as undelimited arguments." - - const consumeSpaces = i > 0 && !isOptional || // Also consume leading spaces in math mode, as parseSymbol - // won't know what to do with them. This can only happen with - // macros, e.g. \frac\foo\foo where \foo expands to a space symbol. - // In LaTeX, the \foo's get treated as (blank) arguments. - // In KaTeX, for now, both spaces will get consumed. - // TODO(edemaine) - i === 0 && !isOptional && this.mode === "math"; - const arg = this.parseGroupOfType(`argument to '${func}'`, argType, isOptional, baseGreediness, consumeSpaces); - - if (!arg) { - if (isOptional) { - optArgs.push(null); - continue; - } - - throw new ParseError(`Expected group after '${func}'`, this.fetch()); - } - - (isOptional ? optArgs : args).push(arg); - } - - return { - args, - optArgs - }; - } - /** - * Parses a group when the mode is changing. - */ - - - parseGroupOfType(name, type, optional, greediness, consumeSpaces) { - switch (type) { - case "color": - if (consumeSpaces) { - this.consumeSpaces(); - } - - return this.parseColorGroup(optional); - - case "size": - if (consumeSpaces) { - this.consumeSpaces(); - } - - return this.parseSizeGroup(optional); - - case "url": - return this.parseUrlGroup(optional, consumeSpaces); - - case "math": - case "text": - return this.parseGroup(name, optional, greediness, undefined, type, consumeSpaces); - - case "hbox": - { - // hbox argument type wraps the argument in the equivalent of - // \hbox, which is like \text but switching to \textstyle size. - const group = this.parseGroup(name, optional, greediness, undefined, "text", consumeSpaces); - - if (!group) { - return group; - } - - const styledGroup = { - type: "styling", - mode: group.mode, - body: [group], - style: "text" // simulate \textstyle - - }; - return styledGroup; - } - - case "raw": - { - if (consumeSpaces) { - this.consumeSpaces(); - } - - if (optional && this.fetch().text === "{") { - return null; - } - - const token = this.parseStringGroup("raw", optional, true); - - if (token) { - return { - type: "raw", - mode: "text", - string: token.text - }; - } else { - throw new ParseError("Expected raw group", this.fetch()); - } - } - - case "original": - case null: - case undefined: - return this.parseGroup(name, optional, greediness, undefined, undefined, consumeSpaces); - - default: - throw new ParseError("Unknown group type as " + name, this.fetch()); - } - } - /** - * Discard any space tokens, fetching the next non-space token. - */ - - - consumeSpaces() { - while (this.fetch().text === " ") { - this.consume(); - } - } - /** - * Parses a group, essentially returning the string formed by the - * brace-enclosed tokens plus some position information. - */ - - - parseStringGroup(modeName, // Used to describe the mode in error messages. - optional, raw) { - const groupBegin = optional ? "[" : "{"; - const groupEnd = optional ? "]" : "}"; - const beginToken = this.fetch(); - - if (beginToken.text !== groupBegin) { - if (optional) { - return null; - } else if (raw && beginToken.text !== "EOF" && /[^{}[\]]/.test(beginToken.text)) { - this.consume(); - return beginToken; - } - } - - const outerMode = this.mode; - this.mode = "text"; - this.expect(groupBegin); - let str = ""; - const firstToken = this.fetch(); - let nested = 0; // allow nested braces in raw string group - - let lastToken = firstToken; - let nextToken; - - while ((nextToken = this.fetch()).text !== groupEnd || raw && nested > 0) { - switch (nextToken.text) { - case "EOF": - throw new ParseError("Unexpected end of input in " + modeName, firstToken.range(lastToken, str)); - - case groupBegin: - nested++; - break; - - case groupEnd: - nested--; - break; - } - - lastToken = nextToken; - str += lastToken.text; - this.consume(); - } - - this.expect(groupEnd); - this.mode = outerMode; - return firstToken.range(lastToken, str); - } - /** - * Parses a regex-delimited group: the largest sequence of tokens - * whose concatenated strings match `regex`. Returns the string - * formed by the tokens plus some position information. - */ - - - parseRegexGroup(regex, modeName) { - const outerMode = this.mode; - this.mode = "text"; - const firstToken = this.fetch(); - let lastToken = firstToken; - let str = ""; - let nextToken; - - while ((nextToken = this.fetch()).text !== "EOF" && regex.test(str + nextToken.text)) { - lastToken = nextToken; - str += lastToken.text; - this.consume(); - } - - if (str === "") { - throw new ParseError("Invalid " + modeName + ": '" + firstToken.text + "'", firstToken); - } - - this.mode = outerMode; - return firstToken.range(lastToken, str); - } - /** - * Parses a color description. - */ - - - parseColorGroup(optional) { - const res = this.parseStringGroup("color", optional); - - if (!res) { - return null; - } - - const match = /^(#[a-f0-9]{3}|#?[a-f0-9]{6}|[a-z]+)$/i.exec(res.text); - - if (!match) { - throw new ParseError("Invalid color: '" + res.text + "'", res); - } - - let color = match[0]; - - if (/^[0-9a-f]{6}$/i.test(color)) { - // We allow a 6-digit HTML color spec without a leading "#". - // This follows the xcolor package's HTML color model. - // Predefined color names are all missed by this RegEx pattern. - color = "#" + color; - } - - return { - type: "color-token", - mode: this.mode, - color - }; - } - /** - * Parses a size specification, consisting of magnitude and unit. - */ - - - parseSizeGroup(optional) { - let res; - let isBlank = false; - - if (!optional && this.fetch().text !== "{") { - res = this.parseRegexGroup(/^[-+]? *(?:$|\d+|\d+\.\d*|\.\d*) *[a-z]{0,2} *$/, "size"); - } else { - res = this.parseStringGroup("size", optional); - } - - if (!res) { - return null; - } - - if (!optional && res.text.length === 0) { - // Because we've tested for what is !optional, this block won't - // affect \kern, \hspace, etc. It will capture the mandatory arguments - // to \genfrac and \above. - res.text = "0pt"; // Enable \above{} - - isBlank = true; // This is here specifically for \genfrac - } - - const match = /([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/.exec(res.text); - - if (!match) { - throw new ParseError("Invalid size: '" + res.text + "'", res); - } - - const data = { - number: +(match[1] + match[2]), - // sign + magnitude, cast to number - unit: match[3] - }; - - if (!validUnit(data)) { - throw new ParseError("Invalid unit: '" + data.unit + "'", res); - } - - return { - type: "size", - mode: this.mode, - value: data, - isBlank - }; - } - /** - * Parses an URL, checking escaped letters and allowed protocols, - * and setting the catcode of % as an active character (as in \hyperref). - */ - - - parseUrlGroup(optional, consumeSpaces) { - this.gullet.lexer.setCatcode("%", 13); // active character - - const res = this.parseStringGroup("url", optional, true); // get raw string - - this.gullet.lexer.setCatcode("%", 14); // comment character - - if (!res) { - return null; - } // hyperref package allows backslashes alone in href, but doesn't - // generate valid links in such cases; we interpret this as - // "undefined" behaviour, and keep them as-is. Some browser will - // replace backslashes with forward slashes. - - - const url = res.text.replace(/\\([#$%&~_^{}])/g, '$1'); - return { - type: "url", - mode: this.mode, - url - }; - } - /** - * If `optional` is false or absent, this parses an ordinary group, - * which is either a single nucleus (like "x") or an expression - * in braces (like "{x+y}") or an implicit group, a group that starts - * at the current position, and ends right before a higher explicit - * group ends, or at EOF. - * If `optional` is true, it parses either a bracket-delimited expression - * (like "[x+y]") or returns null to indicate the absence of a - * bracket-enclosed group. - * If `mode` is present, switches to that mode while parsing the group, - * and switches back after. - */ - - - parseGroup(name, // For error reporting. - optional, greediness, breakOnTokenText, mode, consumeSpaces) { - // Switch to specified mode - const outerMode = this.mode; - - if (mode) { - this.switchMode(mode); - } // Consume spaces if requested, crucially *after* we switch modes, - // so that the next non-space token is parsed in the correct mode. - - - if (consumeSpaces) { - this.consumeSpaces(); - } // Get first token - - - const firstToken = this.fetch(); - const text = firstToken.text; - let result; // Try to parse an open brace or \begingroup - - if (optional ? text === "[" : text === "{" || text === "\\begingroup") { - this.consume(); - const groupEnd = Parser.endOfGroup[text]; // Start a new group namespace - - this.gullet.beginGroup(); // If we get a brace, parse an expression - - const expression = this.parseExpression(false, groupEnd); - const lastToken = this.fetch(); // Check that we got a matching closing brace - - this.expect(groupEnd); // End group namespace - - this.gullet.endGroup(); - result = { - type: "ordgroup", - mode: this.mode, - loc: SourceLocation.range(firstToken, lastToken), - body: expression, - // A group formed by \begingroup...\endgroup is a semi-simple group - // which doesn't affect spacing in math mode, i.e., is transparent. - // https://tex.stackexchange.com/questions/1930/when-should-one- - // use-begingroup-instead-of-bgroup - semisimple: text === "\\begingroup" || undefined - }; - } else if (optional) { - // Return nothing for an optional group - result = null; - } else { - // If there exists a function with this name, parse the function. - // Otherwise, just return a nucleus - result = this.parseFunction(breakOnTokenText, name, greediness) || this.parseSymbol(); - - if (result == null && text[0] === "\\" && !implicitCommands.hasOwnProperty(text)) { - if (this.settings.throwOnError) { - throw new ParseError("Undefined control sequence: " + text, firstToken); - } - - result = this.formatUnsupportedCmd(text); - this.consume(); - } - } // Switch mode back - - - if (mode) { - this.switchMode(outerMode); - } - - return result; - } - /** - * Form ligature-like combinations of characters for text mode. - * This includes inputs like "--", "---", "``" and "''". - * The result will simply replace multiple textord nodes with a single - * character in each value by a single textord node having multiple - * characters in its value. The representation is still ASCII source. - * The group will be modified in place. - */ - - - formLigatures(group) { - let n = group.length - 1; - - for (let i = 0; i < n; ++i) { - const a = group[i]; // $FlowFixMe: Not every node type has a `text` property. - - const v = a.text; - - if (v === "-" && group[i + 1].text === "-") { - if (i + 1 < n && group[i + 2].text === "-") { - group.splice(i, 3, { - type: "textord", - mode: "text", - loc: SourceLocation.range(a, group[i + 2]), - text: "---" - }); - n -= 2; - } else { - group.splice(i, 2, { - type: "textord", - mode: "text", - loc: SourceLocation.range(a, group[i + 1]), - text: "--" - }); - n -= 1; - } - } - - if ((v === "'" || v === "`") && group[i + 1].text === v) { - group.splice(i, 2, { - type: "textord", - mode: "text", - loc: SourceLocation.range(a, group[i + 1]), - text: v + v - }); - n -= 1; - } - } - } - /** - * Parse a single symbol out of the string. Here, we handle single character - * symbols and special functions like \verb. - */ - - - parseSymbol() { - const nucleus = this.fetch(); - let text = nucleus.text; - - if (/^\\verb[^a-zA-Z]/.test(text)) { - this.consume(); - let arg = text.slice(5); - const star = arg.charAt(0) === "*"; - - if (star) { - arg = arg.slice(1); - } // Lexer's tokenRegex is constructed to always have matching - // first/last characters. - - - if (arg.length < 2 || arg.charAt(0) !== arg.slice(-1)) { - throw new ParseError(`\\verb assertion failed -- - please report what input caused this bug`); - } - - arg = arg.slice(1, -1); // remove first and last char - - return { - type: "verb", - mode: "text", - body: arg, - star - }; - } // At this point, we should have a symbol, possibly with accents. - // First expand any accented base symbol according to unicodeSymbols. - - - if (unicodeSymbols.hasOwnProperty(text[0]) && !symbols[this.mode][text[0]]) { - // This behavior is not strict (XeTeX-compatible) in math mode. - if (this.settings.strict && this.mode === "math") { - this.settings.reportNonstrict("unicodeTextInMathMode", `Accented Unicode text character "${text[0]}" used in ` + `math mode`, nucleus); - } - - text = unicodeSymbols[text[0]] + text.substr(1); - } // Strip off any combining characters - - - const match = combiningDiacriticalMarksEndRegex.exec(text); - - if (match) { - text = text.substring(0, match.index); - - if (text === 'i') { - text = '\u0131'; // dotless i, in math and text mode - } else if (text === 'j') { - text = '\u0237'; // dotless j, in math and text mode - } - } // Recognize base symbol - - - let symbol; - - if (symbols[this.mode][text]) { - if (this.settings.strict && this.mode === 'math' && extraLatin.indexOf(text) >= 0) { - this.settings.reportNonstrict("unicodeTextInMathMode", `Latin-1/Unicode text character "${text[0]}" used in ` + `math mode`, nucleus); - } - - const group = symbols[this.mode][text].group; - const loc = SourceLocation.range(nucleus); - let s; - - if (ATOMS.hasOwnProperty(group)) { - // $FlowFixMe - const family = group; - s = { - type: "atom", - mode: this.mode, - family, - loc, - text - }; - } else { - // $FlowFixMe - s = { - type: group, - mode: this.mode, - loc, - text - }; - } - - symbol = s; - } else if (text.charCodeAt(0) >= 0x80) { - // no symbol for e.g. ^ - if (this.settings.strict) { - if (!supportedCodepoint(text.charCodeAt(0))) { - this.settings.reportNonstrict("unknownSymbol", `Unrecognized Unicode character "${text[0]}"` + ` (${text.charCodeAt(0)})`, nucleus); - } else if (this.mode === "math") { - this.settings.reportNonstrict("unicodeTextInMathMode", `Unicode text character "${text[0]}" used in math mode`, nucleus); - } - } // All nonmathematical Unicode characters are rendered as if they - // are in text mode (wrapped in \text) because that's what it - // takes to render them in LaTeX. Setting `mode: this.mode` is - // another natural choice (the user requested math mode), but - // this makes it more difficult for getCharacterMetrics() to - // distinguish Unicode characters without metrics and those for - // which we want to simulate the letter M. - - - symbol = { - type: "textord", - mode: "text", - loc: SourceLocation.range(nucleus), - text - }; - } else { - return null; // EOF, ^, _, {, }, etc. - } - - this.consume(); // Transform combining characters into accents - - if (match) { - for (let i = 0; i < match[0].length; i++) { - const accent = match[0][i]; - - if (!unicodeAccents[accent]) { - throw new ParseError(`Unknown accent ' ${accent}'`, nucleus); - } - - const command = unicodeAccents[accent][this.mode]; - - if (!command) { - throw new ParseError(`Accent ${accent} unsupported in ${this.mode} mode`, nucleus); - } - - symbol = { - type: "accent", - mode: this.mode, - loc: SourceLocation.range(nucleus), - label: command, - isStretchy: false, - isShifty: true, - base: symbol - }; - } - } - - return symbol; - } - -} -Parser.endOfExpression = ["}", "\\endgroup", "\\end", "\\right", "&"]; -Parser.endOfGroup = { - "[": "]", - "{": "}", - "\\begingroup": "\\endgroup" - /** - * Parses an "expression", which is a list of atoms. - * - * `breakOnInfix`: Should the parsing stop when we hit infix nodes? This - * happens when functions have higher precendence han infix - * nodes in implicit parses. - * - * `breakOnTokenText`: The text of the token that the expression should end - * with, or `null` if something else should end the - * expression. - */ - -}; -Parser.SUPSUB_GREEDINESS = 1; - -/** - * Provides a single function for parsing an expression using a Parser - * TODO(emily): Remove this - */ - -/** - * Parses an expression using a Parser, then returns the parsed result. - */ -const parseTree = function parseTree(toParse, settings) { - if (!(typeof toParse === 'string' || toParse instanceof String)) { - throw new TypeError('KaTeX can only parse string typed expression'); - } - - const parser = new Parser(toParse, settings); // Blank out any \df@tag to avoid spurious "Duplicate \tag" errors - - delete parser.gullet.macros.current["\\df@tag"]; - let tree = parser.parse(); // If the input used \tag, it will set the \df@tag macro to the tag. - // In this case, we separately parse the tag and wrap the tree. - - if (parser.gullet.macros.get("\\df@tag")) { - if (!settings.displayMode) { - throw new ParseError("\\tag works only in display equations"); - } - - parser.gullet.feed("\\df@tag"); - tree = [{ - type: "tag", - mode: "text", - body: tree, - tag: parser.parse() - }]; - } - - return tree; -}; - -/* eslint no-console:0 */ - -/** - * Parse and build an expression, and place that expression in the DOM node - * given. - */ -let render = function render(expression, baseNode, options) { - baseNode.textContent = ""; - const node = renderToDomTree(expression, options).toNode(); - baseNode.appendChild(node); -}; // KaTeX's styles don't work properly in quirks mode. Print out an error, and -// disable rendering. - - -if (typeof document !== "undefined") { - if (document.compatMode !== "CSS1Compat") { - typeof console !== "undefined" && console.warn("Warning: KaTeX doesn't work in quirks mode. Make sure your " + "website has a suitable doctype."); - - render = function render() { - throw new ParseError("KaTeX doesn't work in quirks mode."); - }; - } -} -/** - * Parse and build an expression, and return the markup for that. - */ - - -const renderToString = function renderToString(expression, options) { - const markup = renderToDomTree(expression, options).toMarkup(); - return markup; -}; -/** - * Parse an expression and return the parse tree. - */ - - -const generateParseTree = function generateParseTree(expression, options) { - const settings = new Settings(options); - return parseTree(expression, settings); -}; -/** - * If the given error is a KaTeX ParseError and options.throwOnError is false, - * renders the invalid LaTeX as a span with hover title giving the KaTeX - * error message. Otherwise, simply throws the error. - */ - - -const renderError = function renderError(error, expression, options) { - if (options.throwOnError || !(error instanceof ParseError)) { - throw error; - } - - const node = buildCommon.makeSpan(["katex-error"], [new SymbolNode(expression)]); - node.setAttribute("title", error.toString()); - node.setAttribute("style", `color:${options.errorColor}`); - return node; -}; -/** - * Generates and returns the katex build tree. This is used for advanced - * use cases (like rendering to custom output). - */ - - -const renderToDomTree = function renderToDomTree(expression, options) { - const settings = new Settings(options); - - try { - const tree = parseTree(expression, settings); - return buildTree(tree, expression, settings); - } catch (error) { - return renderError(error, expression, settings); - } -}; -/** - * Generates and returns the katex build tree, with just HTML (no MathML). - * This is used for advanced use cases (like rendering to custom output). - */ - - -const renderToHTMLTree = function renderToHTMLTree(expression, options) { - const settings = new Settings(options); - - try { - const tree = parseTree(expression, settings); - return buildHTMLTree(tree, expression, settings); - } catch (error) { - return renderError(error, expression, settings); - } -}; - -var katex = { - /** - * Current KaTeX version - */ - version: "0.12.0", - - /** - * Renders the given LaTeX into an HTML+MathML combination, and adds - * it as a child to the specified DOM node. - */ - render, - - /** - * Renders the given LaTeX into an HTML+MathML combination string, - * for sending to the client. - */ - renderToString, - - /** - * KaTeX error, usually during parsing. - */ - ParseError, - - /** - * Parses the given LaTeX into KaTeX's internal parse tree structure, - * without rendering to HTML or MathML. - * - * NOTE: This method is not currently recommended for public use. - * The internal tree representation is unstable and is very likely - * to change. Use at your own risk. - */ - __parse: generateParseTree, - - /** - * Renders the given LaTeX into an HTML+MathML internal DOM tree - * representation, without flattening that representation to a string. - * - * NOTE: This method is not currently recommended for public use. - * The internal tree representation is unstable and is very likely - * to change. Use at your own risk. - */ - __renderToDomTree: renderToDomTree, - - /** - * Renders the given LaTeX into an HTML internal DOM tree representation, - * without MathML and without flattening that representation to a string. - * - * NOTE: This method is not currently recommended for public use. - * The internal tree representation is unstable and is very likely - * to change. Use at your own risk. - */ - __renderToHTMLTree: renderToHTMLTree, - - /** - * extends internal font metrics object with a new object - * each key in the new object represents a font name - */ - __setFontMetrics: setFontMetrics, - - /** - * adds a new symbol to builtin symbols table - */ - __defineSymbol: defineSymbol, - - /** - * adds a new macro to builtin macro list - */ - __defineMacro: defineMacro, - - /** - * Expose the dom tree node types, which can be useful for type checking nodes. - * - * NOTE: This method is not currently recommended for public use. - * The internal tree representation is unstable and is very likely - * to change. Use at your own risk. - */ - __domTree: { - Span, - Anchor, - SymbolNode, - SvgNode, - PathNode, - LineNode - } -}; - -export default katex; diff --git a/download-utils/Cargo.toml b/download-utils/Cargo.toml index cc9523832e1..95791558249 100644 --- a/download-utils/Cargo.toml +++ b/download-utils/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -29,3 +31,6 @@ solana-runtime = { workspace = true } [dev-dependencies] solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/entry/Cargo.toml b/entry/Cargo.toml index ab6625d63ca..b7d0b8ccd53 100644 --- a/entry/Cargo.toml +++ b/entry/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -23,7 +25,6 @@ dev-context-only-utils = [] [dependencies] agave-votor-messages = { workspace = true } crossbeam-channel = { workspace = true } -dlopen2 = { workspace = true } log = { workspace = true } num_cpus = { workspace = true } rayon = { workspace = true } @@ -66,3 +67,6 @@ solana-transaction = { workspace = true, features = ["verify"] } [[bench]] name = "verify_signatures" harness = false + +[lints] +workspace = true diff --git a/entry/src/block_component.rs b/entry/src/block_component.rs index 1bfcadf5472..fe44e4d4d1b 100644 --- a/entry/src/block_component.rs +++ b/entry/src/block_component.rs @@ -66,30 +66,30 @@ /// /// ### BlockFooterV1 Layout /// ```text -/// ┌─────────────────────────────────────────┐ -/// │ Bank Hash (32 bytes) │ -/// ├─────────────────────────────────────────┤ -/// │ Producer Time Nanos (8 bytes) │ -/// ├─────────────────────────────────────────┤ -/// │ User Agent Length (1 byte) │ -/// ├─────────────────────────────────────────┤ -/// │ User Agent Bytes (0-255 bytes) │ -/// ├─────────────────────────────────────────┤ -/// │ Final Cert Present (1 byte) │ -/// ├─────────────────────────────────────────┤ -/// │ FinalCertificate (if present, variable) │ -/// ├─────────────────────────────────────────┤ -/// │ Skip reward cert Present (1 byte) │ -/// ├─────────────────────────────────────────┤ -/// │ SkipRewardCert (if present, variable) │ -/// ├─────────────────────────────────────────┤ -/// │ Notar reward cert Present (1 byte) │ -/// ├─────────────────────────────────────────┤ -/// │ NotarRewardCert (if present, variable) │ -/// └─────────────────────────────────────────┘ +/// ┌──────────────────────────────────────────────┐ +/// │ Bank Hash (32 bytes) │ +/// ├──────────────────────────────────────────────┤ +/// │ Producer Time Nanos (8 bytes) │ +/// ├──────────────────────────────────────────────┤ +/// │ User Agent Length (1 byte) │ +/// ├──────────────────────────────────────────────┤ +/// │ User Agent Bytes (0-255 bytes) │ +/// ├──────────────────────────────────────────────┤ +/// │ Block Final Cert Present (1 byte) │ +/// ├──────────────────────────────────────────────┤ +/// │ BlockFinalizationCert (if present, variable) │ +/// ├──────────────────────────────────────────────┤ +/// │ Skip reward cert Present (1 byte) │ +/// ├──────────────────────────────────────────────┤ +/// │ SkipRewardCert (if present, variable) │ +/// ├──────────────────────────────────────────────┤ +/// │ Notar reward cert Present (1 byte) │ +/// ├──────────────────────────────────────────────┤ +/// │ NotarRewardCert (if present, variable) │ +/// └──────────────────────────────────────────────┘ /// ``` /// -/// ### FinalCertificate Layout +/// ### BlockFinalizationCert Layout /// ```text /// ┌─────────────────────────────────────────┐ /// │ Slot (8 bytes) │ @@ -132,7 +132,7 @@ use { crate::entry::{Entry, MaxDataShredsLen}, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType}, + certificate::{CertSignature, GenesisCert}, reward_certificate::{NotarRewardCertificate, SkipRewardCertificate}, }, solana_bls_signatures::{ @@ -210,7 +210,7 @@ pub struct BlockFooterV1 { pub block_producer_time_nanos: u64, #[wincode(with = "WincodeVec>")] pub block_user_agent: Vec, - pub final_cert: Option, + pub block_final_cert: Option, pub skip_reward_cert: Option, pub notar_reward_cert: Option, } @@ -229,7 +229,7 @@ pub struct UpdateParentV1 { /// Attests to genesis block finalization with a BLS aggregate signature. #[derive(Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] -pub struct GenesisCertificate { +pub struct GenesisCertBlockMarker { pub slot: Slot, pub block_id: Hash, #[wincode(with = "PodBLSSignature")] @@ -238,56 +238,43 @@ pub struct GenesisCertificate { pub bitmap: Vec, } -impl GenesisCertificate { +impl GenesisCertBlockMarker { /// Max bitmap size in bytes (supports up to 4096 validators). pub const MAX_BITMAP_SIZE: usize = 512; } -impl TryFrom for GenesisCertificate { +impl TryFrom for GenesisCertBlockMarker { type Error = String; - fn try_from(cert: Certificate) -> Result { - let CertificateType::Genesis(slot, block_id) = cert.cert_type else { - return Err("expected genesis certificate".into()); - }; - if cert.bitmap.len() > Self::MAX_BITMAP_SIZE { + fn try_from(cert: GenesisCert) -> Result { + if cert.signature.bitmap.len() > Self::MAX_BITMAP_SIZE { return Err(format!( "bitmap size {} exceeds max {}", - cert.bitmap.len(), + cert.signature.bitmap.len(), Self::MAX_BITMAP_SIZE )); } Ok(Self { - slot, - block_id, - bls_signature: cert.signature, - bitmap: cert.bitmap, + slot: cert.block.slot, + block_id: cert.block.block_id, + bls_signature: cert.signature.signature, + bitmap: cert.signature.bitmap, }) } } -impl From for Certificate { - fn from(cert: GenesisCertificate) -> Self { - Self { - cert_type: CertificateType::Genesis(cert.slot, cert.block_id), - signature: cert.bls_signature, - bitmap: cert.bitmap, - } - } -} - #[derive(Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] -pub struct FinalCertificate { +pub struct BlockFinalizationCert { pub slot: Slot, pub block_id: Hash, pub final_aggregate: VotesAggregate, pub notar_aggregate: Option, } -impl FinalCertificate { +impl BlockFinalizationCert { #[cfg(feature = "dev-context-only-utils")] - pub fn new_for_tests() -> FinalCertificate { - FinalCertificate { + pub fn new_for_tests() -> BlockFinalizationCert { + BlockFinalizationCert { slot: 1234567890, block_id: Hash::new_from_array([1u8; 32]), final_aggregate: VotesAggregate { @@ -315,11 +302,11 @@ impl VotesAggregate { /// # Panics /// Panics if the signature cannot be converted to compressed format. /// This should never happen for valid certificates from the consensus pool. - pub fn from_certificate(cert: &Certificate) -> Self { + pub fn from_cert_signature(signature: CertSignature) -> Self { Self { - signature: BLSSignatureCompressed::try_from(&cert.signature) + signature: BLSSignatureCompressed::try_from(&signature.signature) .expect("valid certificate signature should convert to compressed format"), - bitmap: cert.bitmap.clone(), + bitmap: signature.bitmap, } } @@ -363,7 +350,7 @@ pub enum BlockMarkerV1 { BlockFooter(LengthPrefixed), BlockHeader(LengthPrefixed), UpdateParent(LengthPrefixed), - GenesisCertificate(LengthPrefixed), + GenesisCertificate(LengthPrefixed), } impl BlockMarkerV1 { @@ -379,7 +366,7 @@ impl BlockMarkerV1 { Self::UpdateParent(LengthPrefixed::new(u)) } - pub fn new_genesis_certificate(c: GenesisCertificate) -> Self { + pub fn new_genesis_certificate(c: GenesisCertBlockMarker) -> Self { Self::GenesisCertificate(LengthPrefixed::new(c)) } @@ -404,7 +391,7 @@ impl BlockMarkerV1 { } } - pub fn as_genesis_certificate(&self) -> Option<&GenesisCertificate> { + pub fn as_genesis_certificate(&self) -> Option<&GenesisCertBlockMarker> { match self { Self::GenesisCertificate(lp) => Some(lp.inner()), _ => None, @@ -424,27 +411,41 @@ impl VersionedBlockMarker { Self::V1(marker) } - pub fn new_block_footer(f: BlockFooterV1) -> Self { + pub fn from_block_footer(f: BlockFooterV1) -> Self { let f = VersionedBlockFooter::V1(f); let f = BlockMarkerV1::BlockFooter(LengthPrefixed::new(f)); - VersionedBlockMarker::V1(f) + Self::new(f) } - pub fn new_block_header(h: BlockHeaderV1) -> Self { + pub fn from_block_header(h: BlockHeaderV1) -> Self { let h = VersionedBlockHeader::V1(h); let h = BlockMarkerV1::BlockHeader(LengthPrefixed::new(h)); - VersionedBlockMarker::V1(h) + Self::new(h) } - pub fn new_update_parent(u: UpdateParentV1) -> Self { + pub fn from_update_parent(u: UpdateParentV1) -> Self { let u = VersionedUpdateParent::V1(u); let u = BlockMarkerV1::UpdateParent(LengthPrefixed::new(u)); - VersionedBlockMarker::V1(u) + Self::new(u) } - pub fn new_genesis_certificate(g: GenesisCertificate) -> Self { + pub fn from_genesis_cert_block_marker(g: GenesisCertBlockMarker) -> Self { let g = BlockMarkerV1::GenesisCertificate(LengthPrefixed::new(g)); - VersionedBlockMarker::V1(g) + Self::new(g) + } + + pub fn is_update_parent(&self) -> bool { + match self { + Self::V1(BlockMarkerV1::UpdateParent(_)) => true, + Self::V1(_) => false, + } + } + + pub fn is_footer(&self) -> bool { + match self { + Self::V1(BlockMarkerV1::BlockFooter(_)) => true, + Self::V1(_) => false, + } } } @@ -458,6 +459,7 @@ pub enum BlockComponent { impl BlockComponent { const MAX_ENTRIES: usize = u32::MAX as usize; const ENTRY_COUNT_SIZE: usize = 8; + const EMPTY_ENTRY_BATCH: [u8; Self::ENTRY_COUNT_SIZE] = 0u64.to_le_bytes(); pub fn new_entry_batch(entries: Vec) -> Result { if entries.is_empty() { @@ -483,7 +485,7 @@ impl BlockComponent { parent_slot, parent_block_id, }; - Self::new_block_marker(VersionedBlockMarker::new_block_header(header)) + Self::new_block_marker(VersionedBlockMarker::from_block_header(header)) } pub const fn as_marker(&self) -> Option<&VersionedBlockMarker> { @@ -503,6 +505,13 @@ impl BlockComponent { pub fn infer_is_block_marker(data: &[u8]) -> Option { Self::infer_is_entry_batch(data).map(|is_entry_batch| !is_entry_batch) } + + /// In Alpenglow an empty entry batch will fail to deserialize + /// Leader's should only use an empty entry batch to indicate that they + /// are aborting the block + pub fn infer_is_empty_entry_batch(data: &[u8]) -> bool { + *data == Self::EMPTY_ENTRY_BATCH + } } unsafe impl SchemaWrite for BlockComponent { @@ -570,7 +579,7 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: 1234567890, block_user_agent: b"test-agent".to_vec(), - final_cert: Some(FinalCertificate::new_for_tests()), + block_final_cert: Some(BlockFinalizationCert::new_for_tests()), skip_reward_cert: None, notar_reward_cert: None, } @@ -595,19 +604,19 @@ mod tests { wincode::deserialize::(&bytes).unwrap() ); - let cert = GenesisCertificate { + let marker = GenesisCertBlockMarker { slot: 999, block_id: Hash::new_unique(), bls_signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: vec![1, 2, 3], }; - let bytes = wincode::serialize(&cert).unwrap(); + let bytes = wincode::serialize(&marker).unwrap(); assert_eq!( - cert, - wincode::deserialize::(&bytes).unwrap() + marker, + wincode::deserialize::(&bytes).unwrap() ); - let marker = VersionedBlockMarker::new_block_footer(footer.clone()); + let marker = VersionedBlockMarker::from_block_footer(footer.clone()); let bytes = wincode::serialize(&marker).unwrap(); assert_eq!( marker, diff --git a/entry/src/entry.rs b/entry/src/entry.rs index 60ffbad5b00..94fe5870209 100644 --- a/entry/src/entry.rs +++ b/entry/src/entry.rs @@ -5,7 +5,6 @@ use { crate::poh::Poh, crossbeam_channel::{Receiver, Sender}, - dlopen2::symbor::{Container, SymBorApi, Symbol}, log::*, rayon::{ThreadPool, prelude::*}, smallvec::SmallVec, @@ -17,68 +16,13 @@ use { solana_signature::Signature, solana_transaction::{Transaction, versioned::VersionedTransaction}, solana_transaction_error::{TransactionError, TransactionResult as Result}, - std::{ - ffi::OsStr, - iter::repeat_with, - sync::{Once, OnceLock}, - time::Instant, - }, + std::{iter::repeat_with, time::Instant}, wincode::{SchemaRead, SchemaWrite, containers::Vec as WincodeVec, len::BincodeLen}, }; pub type EntrySender = Sender>; pub type EntryReceiver = Receiver>; -static API: OnceLock> = OnceLock::new(); - -pub fn init_poh() { - init(OsStr::new("libpoh-simd.so")); -} - -fn init(name: &OsStr) { - static INIT_HOOK: Once = Once::new(); - - info!("Loading {name:?}"); - INIT_HOOK.call_once(|| { - let path; - let lib_name = if let Some(perf_libs_path) = solana_perf::perf_libs::locate_perf_libs() { - solana_perf::perf_libs::append_to_ld_library_path( - perf_libs_path.to_str().unwrap_or("").to_string(), - ); - path = perf_libs_path.join(name); - path.as_os_str() - } else { - name - }; - - match unsafe { Container::load(lib_name) } { - Ok(api) => _ = API.set(api), - Err(err) => error!("Unable to load {lib_name:?}: {err}"), - } - }) -} - -pub fn api() -> Option<&'static Container>> { - { - static INIT_HOOK: Once = Once::new(); - INIT_HOOK.call_once(|| { - if std::env::var("TEST_PERF_LIBS").is_ok() { - init_poh() - } - }); - } - - API.get() -} - -#[derive(SymBorApi)] -pub struct Api<'a> { - pub poh_verify_many_simd_avx512skx: - Symbol<'a, unsafe extern "C" fn(hashes: *mut u8, num_hashes: *const u64)>, - pub poh_verify_many_simd_avx2: - Symbol<'a, unsafe extern "C" fn(hashes: *mut u8, num_hashes: *const u64)>, -} - pub const MAX_DATA_SHREDS_SIZE: usize = DEFAULT_MAX_DATA_SHREDS_PER_SLOT as usize * solana_packet::PACKET_DATA_SIZE; pub type MaxDataShredsLen = BincodeLen; @@ -480,20 +424,6 @@ where }) } -fn compare_hashes(computed_hash: Hash, ref_entry: &EntryVerificationData) -> bool { - let actual = if ref_entry.num_transactions != 0 { - let tx_hash = hash_signatures(&ref_entry.signatures); - let mut poh = Poh::new(computed_hash, None); - poh.record(tx_hash).unwrap().hash - } else if ref_entry.num_hashes > 0 { - let mut poh = Poh::new(computed_hash, None); - poh.tick().unwrap().hash - } else { - computed_hash - }; - actual == ref_entry.hash -} - pub fn verify_entries_cpu_in_pool( entries: &[EntryVerificationData], start_hash: &Hash, @@ -531,110 +461,11 @@ fn verify_entries_cpu_generic( } } -fn verify_entries_cpu_x86_simd( - entries: &[EntryVerificationData], - start_hash: &Hash, - simd_len: usize, -) -> EntryVerificationState { - use solana_hash::HASH_BYTES; - let now = Instant::now(); - let genesis = [EntryVerificationData { - num_hashes: 0, - hash: *start_hash, - num_transactions: 0, - signatures: Vec::new(), - }]; - - let aligned_len = entries.len().div_ceil(simd_len) * simd_len; - let mut hashes_bytes = vec![0u8; HASH_BYTES * aligned_len]; - genesis - .iter() - .chain(entries) - .enumerate() - .for_each(|(i, entry)| { - if i < entries.len() { - let start = i * HASH_BYTES; - let end = start + HASH_BYTES; - hashes_bytes[start..end].copy_from_slice(&entry.hash.to_bytes()); - } - }); - let mut hashes_chunked: Vec<_> = hashes_bytes.chunks_mut(simd_len * HASH_BYTES).collect(); - - let mut num_hashes: Vec = entries - .iter() - .map(|entry| entry.num_hashes.saturating_sub(1)) - .collect(); - num_hashes.resize(aligned_len, 0); - let num_hashes: Vec<_> = num_hashes.chunks(simd_len).collect(); - - let res = hashes_chunked - .par_iter_mut() - .zip(num_hashes) - .enumerate() - .all(|(i, (chunk, num_hashes))| { - match simd_len { - 8 => unsafe { - (api().unwrap().poh_verify_many_simd_avx2)( - chunk.as_mut_ptr(), - num_hashes.as_ptr(), - ); - }, - 16 => unsafe { - (api().unwrap().poh_verify_many_simd_avx512skx)( - chunk.as_mut_ptr(), - num_hashes.as_ptr(), - ); - }, - _ => { - panic!("unsupported simd len: {simd_len}"); - } - } - let entry_start = i * simd_len; - // The last chunk may produce indexes larger than what we have in the reference entries - // because it is aligned to simd_len. - let entry_end = std::cmp::min(entry_start + simd_len, entries.len()); - entries[entry_start..entry_end] - .iter() - .enumerate() - .all(|(j, ref_entry)| { - let start = j * HASH_BYTES; - let end = start + HASH_BYTES; - let hash = <[u8; HASH_BYTES]>::try_from(&chunk[start..end]) - .map(Hash::new_from_array) - .unwrap(); - compare_hashes(hash, ref_entry) - }) - }); - let poh_duration_us = now.elapsed().as_micros() as u64; - EntryVerificationState { - verification_status: res, - poh_duration_us, - } -} - pub fn verify_entries_cpu( entries: &[EntryVerificationData], start_hash: &Hash, ) -> EntryVerificationState { - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - let (has_avx2, has_avx512) = ( - is_x86_feature_detected!("avx2"), - is_x86_feature_detected!("avx512f"), - ); - #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] - let (has_avx2, has_avx512) = (false, false); - - if api().is_some() { - if has_avx512 && entries.len() >= 128 { - verify_entries_cpu_x86_simd(entries, start_hash, 16) - } else if has_avx2 && entries.len() >= 48 { - verify_entries_cpu_x86_simd(entries, start_hash, 8) - } else { - verify_entries_cpu_generic(entries, start_hash) - } - } else { - verify_entries_cpu_generic(entries, start_hash) - } + verify_entries_cpu_generic(entries, start_hash) } // an EntrySlice is a slice of Entries @@ -642,7 +473,6 @@ pub trait EntrySlice { /// Verifies the hashes and counts of a slice of transactions are all consistent. fn verify_cpu(&self, start_hash: &Hash) -> EntryVerificationState; fn verify_cpu_generic(&self, start_hash: &Hash) -> EntryVerificationState; - fn verify_cpu_x86_simd(&self, start_hash: &Hash, simd_len: usize) -> EntryVerificationState; fn verify(&self, start_hash: &Hash, thread_pool: &ThreadPool) -> EntryVerificationState; /// Checks that each entry tick has the correct number of hashes. Entry slices do not /// necessarily end in a tick, so `tick_hash_count` is used to carry over the hash count @@ -663,11 +493,6 @@ impl EntrySlice for [Entry] { verify_entries_cpu_generic(&verification_entries, start_hash) } - fn verify_cpu_x86_simd(&self, start_hash: &Hash, simd_len: usize) -> EntryVerificationState { - let verification_entries = entries_to_verification_data(self); - verify_entries_cpu_x86_simd(&verification_entries, start_hash, simd_len) - } - fn verify_cpu(&self, start_hash: &Hash) -> EntryVerificationState { let verification_entries = entries_to_verification_data(self); verify_entries_cpu(&verification_entries, start_hash) @@ -682,6 +507,9 @@ impl EntrySlice for [Entry] { for entry in self { *tick_hash_count = tick_hash_count.saturating_add(entry.num_hashes); if entry.is_tick() { + if entry.num_hashes == 0 { + return false; + } if *tick_hash_count != hashes_per_tick { warn!( "invalid tick hash count!: entry: {entry:#?}, tick_hash_count: \ @@ -763,11 +591,15 @@ mod tests { solana_hash::Hash, solana_keypair::Keypair, solana_measure::measure::Measure, - solana_message::SimpleAddressLoader, + solana_message::{ + MessageHeader, SimpleAddressLoader, VersionedMessage, + compiled_instruction::CompiledInstruction, v1, + }, solana_perf::test_tx::test_tx, solana_pubkey::Pubkey, solana_runtime_transaction::runtime_transaction::RuntimeTransaction, solana_sha256_hasher::hash, + solana_signature::Signature, solana_signer::Signer, solana_system_transaction as system_transaction, solana_transaction::{ @@ -777,6 +609,27 @@ mod tests { solana_transaction_error::TransactionResult as Result, }; + fn simple_v1_transaction_for_deserialization_tests() -> VersionedTransaction { + VersionedTransaction { + signatures: vec![Signature::default()], + message: VersionedMessage::V1(v1::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + config: v1::TransactionConfig::empty(), + lifetime_specifier: Hash::new_unique(), + account_keys: vec![Pubkey::new_unique(), Pubkey::new_unique()], + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + }), + } + } + fn create_random_ticks(num_ticks: u64, max_hashes_per_tick: u64, mut hash: Hash) -> Vec { repeat_with(|| { let hashes_per_tick = rng().random_range(1..max_hashes_per_tick); @@ -1164,11 +1017,17 @@ mod tests { assert_eq!(tick_hash_count, hashes_per_tick - 1); tick_hash_count = 0; - // full tx entry with tick entry should succeed - entries = vec![full_tx_entry.clone(), no_hash_tick_entry]; - assert!(entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick)); + // no hash tick entry should fail + entries = vec![no_hash_tick_entry.clone()]; + assert!(!entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick)); assert_eq!(tick_hash_count, 0); + // full tx entry with no hash tick entry should still fail + entries = vec![full_tx_entry.clone(), no_hash_tick_entry]; + assert!(!entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick)); + assert_eq!(tick_hash_count, hashes_per_tick); + tick_hash_count = 0; + // full tx entry with oversized tick entry should fail entries = vec![full_tx_entry.clone(), single_hash_tick_entry.clone()]; assert!(!entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick)); @@ -1248,4 +1107,43 @@ mod tests { let hash2 = hash_transactions(&transactions); assert_ne!(hash1, hash2); } + + #[test] + fn test_deserialize_entries_rejects_txv1_unknown_config_mask_bit() { + let tx = simple_v1_transaction_for_deserialization_tests(); + let entries = vec![next_versioned_entry(&Hash::default(), 1, vec![tx.clone()])]; + let mut serialized_entries = wincode::serialize(&entries).unwrap(); + + assert!(wincode::deserialize::>(&serialized_entries).is_ok()); + + let serialized_tx = wincode::serialize(&tx).unwrap(); + let tx_offset = serialized_entries + .windows(serialized_tx.len()) + .position(|window| window == serialized_tx) + .expect("serialized transaction should be embedded in serialized entry"); + + // txv1 begins with the version byte and the 3-byte legacy header. + const TXV1_CONFIG_MASK_OFFSET: usize = 1 + 3; + let mask_offset = tx_offset + TXV1_CONFIG_MASK_OFFSET; + let mask_range = mask_offset..mask_offset + core::mem::size_of::(); + let mask = u32::from_le_bytes(serialized_entries[mask_range.clone()].try_into().unwrap()); + assert_eq!(mask, 0); + + let unknown_config_mask_bit = 1u32 + .checked_shl(v1::TransactionConfigMask::KNOWN_BITS.trailing_ones()) + .expect("txv1 config mask should have at least one unknown bit"); + assert_ne!( + unknown_config_mask_bit & v1::TransactionConfigMask::KNOWN_BITS, + unknown_config_mask_bit + ); + serialized_entries[mask_range] + .copy_from_slice(&(mask | unknown_config_mask_bit).to_le_bytes()); + + assert!(matches!( + wincode::deserialize::>(&serialized_entries), + Err(wincode::ReadError::InvalidValue( + "invalid transaction config mask" + )) + )); + } } diff --git a/entry/src/poh.rs b/entry/src/poh.rs index 7008a215bbe..bdf6db6b130 100644 --- a/entry/src/poh.rs +++ b/entry/src/poh.rs @@ -52,6 +52,10 @@ impl Poh { self.hashes_per_tick } + pub fn hashes_per_tick_config(&self) -> Option { + (self.hashes_per_tick != LOW_POWER_MODE).then_some(self.hashes_per_tick) + } + pub fn target_poh_time(&self, target_ns_per_tick: u64) -> Instant { assert!(self.hashes_per_tick > 0); let offset_tick_ns = target_ns_per_tick * self.tick_number; diff --git a/faucet-cli/Cargo.toml b/faucet-cli/Cargo.toml index a6512ac6184..efe827d8b07 100644 --- a/faucet-cli/Cargo.toml +++ b/faucet-cli/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [[bin]] name = "solana-faucet" @@ -22,8 +24,11 @@ clap = { workspace = true } log = { workspace = true } solana-clap-utils = { workspace = true } solana-cli-config = { workspace = true } -solana-faucet = { workspace = true, features = ["agave-unstable-api"] } +solana-faucet = { workspace = true } solana-keypair = { workspace = true } solana-metrics = { workspace = true } solana-version = { workspace = true } tokio = { workspace = true, features = ["full"] } + +[lints] +workspace = true diff --git a/faucet/Cargo.toml b/faucet/Cargo.toml index 43f8f0b8ae7..919186d1e64 100644 --- a/faucet/Cargo.toml +++ b/faucet/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -43,3 +45,6 @@ wincode = { workspace = true } [dev-dependencies] solana-faucet = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/faucet/src/faucet.rs b/faucet/src/faucet.rs index 5a03a58e2c4..42d9d9bf922 100644 --- a/faucet/src/faucet.rs +++ b/faucet/src/faucet.rs @@ -33,7 +33,7 @@ use { wincode::{SchemaRead, SchemaWrite, deserialize, serialize, serialized_size}, }; #[cfg(feature = "dev-context-only-utils")] -use {crossbeam_channel::unbounded, std::net::Ipv4Addr, std::thread}; +use {crossbeam_channel::bounded, std::net::Ipv4Addr, std::thread}; #[macro_export] macro_rules! socketaddr { @@ -123,15 +123,14 @@ impl Faucet { allowed_ips: HashSet, ) -> Self { let time_slice = Duration::new(time_input.unwrap_or(TIME_SLICE), 0); - if let Some((per_request_cap, per_time_cap)) = per_request_cap.zip(per_time_cap) { - if per_time_cap < per_request_cap { - warn!( - "per_time_cap {} SOL < per_request_cap {} SOL; maximum single requests will \ - fail", - build_balance_message(per_time_cap, false, false), - build_balance_message(per_request_cap, false, false), - ); - } + if let Some((per_request_cap, per_time_cap)) = per_request_cap.zip(per_time_cap) + && per_time_cap < per_request_cap + { + warn!( + "per_time_cap {} SOL < per_request_cap {} SOL; maximum single requests will fail", + build_balance_message(per_time_cap, false, false), + build_balance_message(per_request_cap, false, false), + ); } Self { faucet_keypair, @@ -151,15 +150,15 @@ impl Faucet { ) -> Result<(), FaucetError> { let new_total = to.check_cache(self, request_amount); to.datapoint_info(request_amount, new_total); - if let Some(cap) = self.per_time_cap { - if new_total > cap { - return Err(FaucetError::PerTimeCapExceeded( - build_balance_message(request_amount, false, false), - to.to_string(), - build_balance_message(new_total, false, false), - build_balance_message(cap, false, false), - )); - } + if let Some(cap) = self.per_time_cap + && new_total > cap + { + return Err(FaucetError::PerTimeCapExceeded( + build_balance_message(request_amount, false, false), + to.to_string(), + build_balance_message(new_total, false, false), + build_balance_message(cap, false, false), + )); } Ok(()) } @@ -192,26 +191,26 @@ impl Faucet { to ); - if let Some(cap) = self.per_request_cap { - if lamports > cap { - let memo = format!( - "{}", - FaucetError::PerRequestCapExceeded( - build_balance_message(lamports, false, false), - build_balance_message(cap, false, false), - ) - ); - let memo_instruction = Instruction { - program_id: spl_memo_interface::v3::id(), - accounts: vec![], - data: memo.as_bytes().to_vec(), - }; - let message = Message::new(&[memo_instruction], Some(&mint_pubkey)); - return Ok(FaucetTransaction::Memo(( - Transaction::new(&[&self.faucet_keypair], message, blockhash), - memo, - ))); - } + if let Some(cap) = self.per_request_cap + && lamports > cap + { + let memo = format!( + "{}", + FaucetError::PerRequestCapExceeded( + build_balance_message(lamports, false, false), + build_balance_message(cap, false, false), + ) + ); + let memo_instruction = Instruction { + program_id: spl_memo_interface::v4::id(), + accounts: vec![], + data: memo.as_bytes().to_vec(), + }; + let message = Message::new(&[memo_instruction], Some(&mint_pubkey)); + return Ok(FaucetTransaction::Memo(( + Transaction::new(&[&self.faucet_keypair], message, blockhash), + memo, + ))); } if !ip.is_loopback() && !self.allowed_ips.contains(&ip) { self.check_time_request_limit(lamports, ip)?; @@ -364,7 +363,7 @@ pub fn run_local_faucet_for_tests( per_time_cap: Option, port: u16, ) -> SocketAddr { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); run_local_faucet_with_config( sender, LocalFaucetConfig { @@ -675,7 +674,7 @@ mod tests { assert_eq!(tx.signatures.len(), 1); assert_eq!( message.account_keys, - vec![mint_pubkey, spl_memo_interface::v3::id()] + vec![mint_pubkey, spl_memo_interface::v4::id()] ); assert_eq!(message.recent_blockhash, blockhash); diff --git a/feature-set/Cargo.toml b/feature-set/Cargo.toml index 5e0de57dfd2..ef2c8917ccd 100644 --- a/feature-set/Cargo.toml +++ b/feature-set/Cargo.toml @@ -9,6 +9,11 @@ license = { workspace = true } edition = { workspace = true } readme = false +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] frozen-abi = ["dep:solana-frozen-abi", "dep:solana-frozen-abi-macro"] diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index 56b55d0d8bc..ccb33426327 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -52,6 +52,7 @@ pub struct FeatureSnapshot { pub fix_alt_bn128_multiplication_input_length: bool, pub formalize_loaded_transaction_data_size: bool, pub alpenglow: bool, + pub alpenglow_fast_leader_handover: bool, pub disable_zk_elgamal_proof_program: bool, pub reenable_zk_elgamal_proof_program: bool, pub raise_block_limits_to_100m: bool, @@ -74,7 +75,6 @@ pub struct FeatureSnapshot { pub set_lamports_per_byte_to_5080: bool, pub set_lamports_per_byte_to_2575: bool, pub set_lamports_per_byte_to_1322: bool, - pub remove_simple_vote_from_cost_model: bool, pub limit_instruction_accounts: bool, pub block_revenue_sharing: bool, pub vote_account_initialize_v2: bool, @@ -87,6 +87,8 @@ pub struct FeatureSnapshot { pub relax_post_exec_min_balance_check: bool, pub enable_tx_v1: bool, pub define_ltds_fee_only_semantics: bool, + pub validate_chained_block_id_2: bool, + pub upgrade_bpf_stake_program_to_v5_1: bool, } impl From<&AHashMap> for FeatureSnapshot { @@ -155,6 +157,7 @@ impl From<&AHashMap> for FeatureSnapshot { &formalize_loaded_transaction_data_size::ID, ), alpenglow: is_active(&alpenglow::ID), + alpenglow_fast_leader_handover: is_active(&alpenglow_fast_leader_handover::ID), disable_zk_elgamal_proof_program: is_active(&disable_zk_elgamal_proof_program::ID), reenable_zk_elgamal_proof_program: is_active(&reenable_zk_elgamal_proof_program::ID), raise_block_limits_to_100m: is_active(&raise_block_limits_to_100m::ID), @@ -183,7 +186,6 @@ impl From<&AHashMap> for FeatureSnapshot { set_lamports_per_byte_to_5080: is_active(&set_lamports_per_byte_to_5080::ID), set_lamports_per_byte_to_2575: is_active(&set_lamports_per_byte_to_2575::ID), set_lamports_per_byte_to_1322: is_active(&set_lamports_per_byte_to_1322::ID), - remove_simple_vote_from_cost_model: is_active(&remove_simple_vote_from_cost_model::ID), limit_instruction_accounts: is_active(&limit_instruction_accounts::ID), block_revenue_sharing: is_active(&block_revenue_sharing::ID), vote_account_initialize_v2: is_active(&vote_account_initialize_v2::ID), @@ -200,6 +202,8 @@ impl From<&AHashMap> for FeatureSnapshot { relax_post_exec_min_balance_check: is_active(&relax_post_exec_min_balance_check::ID), enable_tx_v1: is_active(&enable_tx_v1::ID), define_ltds_fee_only_semantics: is_active(&define_ltds_fee_only_semantics::ID), + validate_chained_block_id_2: is_active(&validate_chained_block_id_2::ID), + upgrade_bpf_stake_program_to_v5_1: is_active(&upgrade_bpf_stake_program_to_v5_1::ID), } } } @@ -1280,10 +1284,6 @@ pub mod relax_intrabatch_account_locks { solana_pubkey::declare_id!("4WeHX6QoXCCwqbSFgi6dxnB6QsPo6YApaNTH7P4MLQ99"); } -pub mod create_slashing_program { - solana_pubkey::declare_id!("sProgVaNWkYdP2eTRAy1CPrgb3b9p8yXCASrPEqo6VJ"); -} - pub mod disable_partitioned_rent_collection { solana_pubkey::declare_id!("2B2SBNbUcr438LtGXNcJNBP2GBSxjx81F945SdSkUSfC"); } @@ -1304,10 +1304,6 @@ pub mod mask_out_rent_epoch_in_vm_serialization { solana_pubkey::declare_id!("RENtePQcDLrAbxAsP3k8dwVcnNYQ466hi2uKvALjnXx"); } -pub mod enshrine_slashing_program { - solana_pubkey::declare_id!("sProgVaNWkYdP2eTRAy1CPrgb3b9p8yXCASrPEqo6VJ"); -} - pub mod enable_extend_program_checked { solana_pubkey::declare_id!("ExtendProgCheckedWi11BeDe1eted11111111111111"); } @@ -1317,27 +1313,7 @@ pub mod formalize_loaded_transaction_data_size { } pub mod alpenglow { - #[cfg(feature = "dev-context-only-utils")] - use { - solana_keypair::{Keypair, Signer}, - std::sync::LazyLock, - }; - - // Used to activate alpenglow in local-cluster tests without exposing the actual feature's private key - #[cfg(feature = "dev-context-only-utils")] - pub static TEST_KEYPAIR: LazyLock = LazyLock::new(|| { - let keypair = Keypair::from_base58_string( - "2Vzd6oTWU4RtM5UmsSyBH3tAhPSi1sKqMeMC8bF1jzHHLBMRhEWtrfmBV4EmwQbGSwkunk5Wy67kXNAL1ZL1xQhR", - ); - assert_eq!(keypair.pubkey(), super::alpenglow::id()); - keypair - }); - - #[cfg(not(feature = "dev-context-only-utils"))] - solana_pubkey::declare_id!("mustRekeyVm2QHYB3JPefBiU4BY3Z6JkW2k3Scw5GWP"); - - #[cfg(feature = "dev-context-only-utils")] - solana_pubkey::declare_id!("8KpruRFrT59jQ9NfFX9DU6j8a1hW7y6xchvZNQ5rxD4P"); + solana_pubkey::declare_id!("a1p3RiCfMmzm5jgCva97UUNwUiVLq5EJhtusRWHDBsp"); } pub mod disable_zk_elgamal_proof_program { @@ -1445,7 +1421,7 @@ pub mod commission_rate_in_basis_points { } pub mod custom_commission_collector { - solana_pubkey::declare_id!("CustomCommissionCo11ector111111111111111111"); + solana_pubkey::declare_id!("3HcSrCTGXTUnrTueHi4DAwNuMxZSsm5xui2Ax3mgxHqf"); } pub mod enable_bls12_381_syscall { @@ -1503,6 +1479,10 @@ pub mod validate_chained_block_id { solana_pubkey::declare_id!("vcmrbYbiMVKaq1snKP6eCacNDcr6qZvpCNUjmk6gxvZ"); } +pub mod validate_chained_block_id_2 { + solana_pubkey::declare_id!("vcmrw431aNM8ngQ46derkZXipoTGQdbHkEygBDh12dA"); +} + pub mod validator_admission_ticket { solana_pubkey::declare_id!("VAT9huvhPjRN9cyrPytq9rwvEJ3J4ADtjdncgZRyANJ"); } @@ -1532,7 +1512,7 @@ pub mod relax_post_exec_min_balance_check { } pub mod enable_tx_v1 { - solana_pubkey::declare_id!("txv1hPU76QFBVeq3942jJ65e9Em2xbdbCJrzX8sM4U4"); + solana_pubkey::declare_id!("txv1aq4pp281K9um3tnPgkfX8UqtFT6wcVW3hNezGLL"); } pub mod define_ltds_fee_only_semantics { @@ -1545,6 +1525,34 @@ pub mod set_lamports_per_byte_to_6960 { pub const LAMPORTS_PER_BYTE: u64 = 6960; } +pub mod reduce_slot_time_to_350ms { + solana_pubkey::declare_id!("iBRL5RuWhw4yqaAZu96RUULHckHTZAoe2b77qaV38JZ"); +} + +pub mod reduce_slot_time_to_300ms { + solana_pubkey::declare_id!("iBRLL3k18HST852F1Mf3Lv83waTNQmmqvKDxvYGwQFL"); +} + +pub mod reduce_slot_time_to_250ms { + solana_pubkey::declare_id!("iBRLMc81UjRa8fn8A6eE8bJTnRbgQoPTynM51akENCV"); +} + +pub mod reduce_slot_time_to_200ms { + solana_pubkey::declare_id!("iBRLjhJnkmDZgNoZRDMW11d8ZV7HvsL3vAyRjZB5npW"); +} + +pub mod upgrade_bpf_stake_program_to_v5_1 { + solana_pubkey::declare_id!("s51VGwCAgebo2745DSUris72RavoLkXGUmVJosESCXr"); + + pub mod buffer { + solana_pubkey::declare_id!("p51x11QCYMHwuVS1MBcLHKb3MezWyqGS5BEB41CA1dk"); + } +} + +pub mod alpenglow_fast_leader_handover { + solana_pubkey::declare_id!("FLHoAWBDjNh6zwmJ5i1NKK4KyD8otAiv7XxvmnFnVnKH"); +} + pub static FEATURE_NAMES: LazyLock> = LazyLock::new(|| { [ (secp256k1_program_enabled::id(), "secp256k1 program"), @@ -2395,10 +2403,6 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n relax_intrabatch_account_locks::id(), "SIMD-0083: Allow batched transactions to read/write and write/write the same accounts", ), - ( - create_slashing_program::id(), - "SIMD-0204: creates an enshrined slashing program", - ), ( disable_partitioned_rent_collection::id(), "SIMD-0175: Disable partitioned rent collection #4562", @@ -2419,10 +2423,6 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n mask_out_rent_epoch_in_vm_serialization::id(), "SIMD-0267: Sets rent_epoch to a constant in the VM", ), - ( - enshrine_slashing_program::id(), - "SIMD-0204: Slashable event verification", - ), ( enable_extend_program_checked::id(), "Enable ExtendProgramChecked instruction", @@ -2435,6 +2435,22 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n alpenglow::id(), "SIMD-0326: Alpenglow: new consensus algorithm", ), + ( + reduce_slot_time_to_350ms::id(), + "SIMD-0525: Reduce slot time to 350ms", + ), + ( + reduce_slot_time_to_300ms::id(), + "SIMD-0525: Reduce slot time to 300ms", + ), + ( + reduce_slot_time_to_250ms::id(), + "SIMD-0525: Reduce slot time to 250ms", + ), + ( + reduce_slot_time_to_200ms::id(), + "SIMD-0525: Reduce slot time to 200ms", + ), ( disable_zk_elgamal_proof_program::id(), "Disables zk-elgamal-proof program", @@ -2603,6 +2619,18 @@ pub static FEATURE_NAMES: LazyLock> = LazyLock::n set_lamports_per_byte_to_6960::id(), "SIMD-0438: Reset lamports per byte to legacy value of 6960", ), + ( + validate_chained_block_id_2::id(), + "SIMD-340: Encompassing check for validate chained block ID", + ), + ( + upgrade_bpf_stake_program_to_v5_1::id(), + "SIMD-0391: Upgrade BPF Stake Program to v5.1.0 (fixed-point warmup/cooldown)", + ), + ( + alpenglow_fast_leader_handover::id(), + "SIMD-0326: Alpenglow fast leader handover", + ), /*************** ADD NEW FEATURES HERE ***************/ /***** ADD NEW FEATURE BOOL TO `FeatureSnapshot` *****/ ] diff --git a/fee/Cargo.toml b/fee/Cargo.toml index 6884bacc230..518446af098 100644 --- a/fee/Cargo.toml +++ b/fee/Cargo.toml @@ -7,7 +7,12 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -16,3 +21,6 @@ agave-unstable-api = [] agave-feature-set = { workspace = true } solana-fee-structure = { workspace = true } solana-svm-transaction = { workspace = true } + +[lints] +workspace = true diff --git a/fetch-core-bpf.sh b/fetch-core-bpf.sh index 02a294af278..a4300b25872 100755 --- a/fetch-core-bpf.sh +++ b/fetch-core-bpf.sh @@ -29,6 +29,6 @@ add_core_bpf_program_to_fetch() { add_core_bpf_program_to_fetch address-lookup-table 3.0.0 AddressLookupTab1e1111111111111111111111111 BPFLoaderUpgradeab1e11111111111111111111111 add_core_bpf_program_to_fetch config 3.0.0 Config1111111111111111111111111111111111111 BPFLoaderUpgradeab1e11111111111111111111111 add_core_bpf_program_to_fetch feature-gate 0.0.1 Feature111111111111111111111111111111111111 BPFLoaderUpgradeab1e11111111111111111111111 -add_core_bpf_program_to_fetch stake 4.0.0 Stake11111111111111111111111111111111111111 BPFLoaderUpgradeab1e11111111111111111111111 +add_core_bpf_program_to_fetch stake 5.1.0 Stake11111111111111111111111111111111111111 BPFLoaderUpgradeab1e11111111111111111111111 fetch_programs "$PREFIX" "${programs[@]}" diff --git a/fetch-perf-libs.sh b/fetch-perf-libs.sh deleted file mode 100755 index 759d0c72f21..00000000000 --- a/fetch-perf-libs.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash - -function installPerfLibSymlinks() { - for dir in target/{debug,release}/{,deps/}; do - mkdir -p "$dir" - ln -sfT "$1" "${dir}perf-libs" - done -} - -if [[ -n "$SOLANA_PERF_LIBS_PATH" ]]; then - mkdir -p target - - ln -sfT "$SOLANA_PERF_LIBS_PATH" target/perf-libs - installPerfLibSymlinks "$SOLANA_PERF_LIBS_PATH" - - exit 0 -fi - -PERF_LIBS_VERSION=v0.19.3 -VERSION=$PERF_LIBS_VERSION-1 - -set -e -cd "$(dirname "$0")" - -if [[ $VERSION != "$(cat target/perf-libs/.version 2> /dev/null)" ]]; then - if [[ $(uname) != Linux ]]; then - echo Note: Performance libraries are only available for Linux - exit 0 - fi - - if [[ $(uname -m) != x86_64 ]]; then - echo Note: Performance libraries are only available for x86_64 architecture - exit 0 - fi - - rm -rf target/perf-libs - mkdir -p target/perf-libs - ( - set -x - cd target/perf-libs - - if [[ -r ~/.cache/solana-perf-$PERF_LIBS_VERSION.tgz ]]; then - cp ~/.cache/solana-perf-$PERF_LIBS_VERSION.tgz solana-perf.tgz - else - curl -L --retry 5 --retry-delay 2 --retry-connrefused -o solana-perf.tgz \ - https://github.com/solana-labs/solana-perf-libs/releases/download/$PERF_LIBS_VERSION/solana-perf.tgz - fi - tar zxvf solana-perf.tgz - - if [[ ! -r ~/.cache/solana-perf-$PERF_LIBS_VERSION.tgz ]]; then - # Save it for next time - mkdir -p ~/.cache - mv solana-perf.tgz ~/.cache/solana-perf-$PERF_LIBS_VERSION.tgz - fi - echo "$VERSION" > .version - ) - - # Setup symlinks so the perf-libs/ can be found from all binaries run out of - # target/ - installPerfLibSymlinks ../perf-libs -fi - -exit 0 diff --git a/fetch-spl.sh b/fetch-spl.sh index da2bf2c2f2d..722bbc0f756 100755 --- a/fetch-spl.sh +++ b/fetch-spl.sh @@ -39,10 +39,11 @@ add_spl_program_to_fetch() { programs+=("$name $version $address $loader $download_url") } -add_spl_program_to_fetch token 1.0.0-rc.1 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA BPFLoaderUpgradeab1e11111111111111111111111 p-token +add_spl_program_to_fetch token 1.0.0 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA BPFLoaderUpgradeab1e11111111111111111111111 p-token add_spl_program_to_fetch token-2022 10.0.0 TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb BPFLoaderUpgradeab1e11111111111111111111111 add_spl_program_to_fetch memo 1.0.0 Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo BPFLoader1111111111111111111111111111111111 add_spl_program_to_fetch memo 3.0.0 MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr BPFLoader2111111111111111111111111111111111 +add_spl_program_to_fetch memo 4.0.0 Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH BPFLoaderUpgradeab1e11111111111111111111111 add_spl_program_to_fetch associated-token-account 1.1.2 ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL BPFLoader2111111111111111111111111111111111 add_spl_program_to_fetch feature-proposal 1.0.0 Feat1YXHhH6t1juaWF74WLcfv4XoNocjXA6sPWHNgAse BPFLoader2111111111111111111111111111111111 diff --git a/fs/Cargo.toml b/fs/Cargo.toml index 968ec48cfc2..6ef2f418d44 100644 --- a/fs/Cargo.toml +++ b/fs/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -35,3 +37,6 @@ tempfile = { workspace = true } rand = { workspace = true } tempfile = { workspace = true } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/fs/src/buffered_reader.rs b/fs/src/buffered_reader.rs index a48104755ad..f6ea8765952 100644 --- a/fs/src/buffered_reader.rs +++ b/fs/src/buffered_reader.rs @@ -14,7 +14,9 @@ //! When reading full accounts data whose sizes exceed the small stack buffer, the `BufReaderWithOverflow` //! should be used, which supports dynamically allocated buffer for preparing contiguous data slices. #[cfg(target_os = "linux")] -pub use crate::io_uring::sequential_file_reader::SequentialFileReaderBuilder; +pub use crate::io_uring::sequential_file_reader::{ + SequentialFileReader, SequentialFileReaderBuilder, +}; use { crate::{ FileSize, @@ -71,8 +73,21 @@ pub trait FileBufRead<'a>: BufRead { /// /// `read_limit` provides a pre-defined limit on the number of bytes that can be read /// from the file (unless EOF is reached). + /// + /// If the file was previously queued via `add_file_to_prefetch`, the reader + /// advances to that file (validating identity / `read_limit`) rather than + /// re-queueing it. fn set_file(&mut self, file: &'a File, read_limit: FileSize) -> io::Result<()>; + /// Queue `file` for read-ahead (prefetch). Files are processed in FIFO order + /// by subsequent `set_file` calls. + /// + /// This is a hint to keep the read pipeline saturated for readers that + /// support concurrent read-ahead. Implementations without read-ahead may + /// ignore the call. Callers must still invoke `set_file` to switch the + /// active file. + fn add_file_to_prefetch(&mut self, file: &'a File, read_limit: FileSize) -> io::Result<()>; + /// Returns the current file offset corresponding to the start of the buffer /// that will be returned by the next call to `fill_buf`. /// @@ -131,7 +146,6 @@ pub struct BufferedReader<'a, const N: usize> { } impl<'a, const N: usize> BufferedReader<'a, N> { - #[allow(clippy::new_without_default)] pub const fn new() -> Self { Self { file_offset_of_next_read: 0, @@ -155,6 +169,18 @@ impl<'a, const N: usize> BufferedReader<'a, N> { self.file_offset_of_next_read = 0; self.buf_valid_bytes = 0..0; } + + /// Reset to idle state and re-type with a fresh lifetime `'b`. + pub fn rebind<'b>(self) -> io::Result> { + Ok(BufferedReader { + file_offset_of_next_read: 0, + buf: self.buf, + buf_valid_bytes: 0..0, + file_last_offset: 0, + file_len_valid: 0, + file: None, + }) + } } impl<'a, const N: usize> FileBufRead<'a> for BufferedReader<'a, N> { @@ -163,6 +189,12 @@ impl<'a, const N: usize> FileBufRead<'a> for BufferedReader<'a, N> { Ok(()) } + /// `BufferedReader` does not perform read-ahead — the call is a no-op and + /// the file becomes active only when `set_file` is later invoked. + fn add_file_to_prefetch(&mut self, _file: &'a File, _read_limit: FileSize) -> io::Result<()> { + Ok(()) + } + #[inline(always)] fn get_file_offset(&self) -> FileSize { if self.buf_valid_bytes.is_empty() { @@ -360,6 +392,10 @@ impl<'a, R: FileBufRead<'a>> FileBufRead<'a> for BufReaderWithOverflow { self.reader.set_file(file, read_limit) } + fn add_file_to_prefetch(&mut self, file: &'a File, read_limit: FileSize) -> io::Result<()> { + self.reader.add_file_to_prefetch(file, read_limit) + } + fn get_file_offset(&self) -> FileSize { self.reader.get_file_offset() - self.overflow_buf.len() as FileSize } diff --git a/fs/src/io_uring/file_writer.rs b/fs/src/io_uring/file_writer.rs index 46ae5efa1a2..50baa5c6b4c 100644 --- a/fs/src/io_uring/file_writer.rs +++ b/fs/src/io_uring/file_writer.rs @@ -99,16 +99,13 @@ impl Write for IoUringFileWriter<'_> { } } - /// Flush can only be called once. - /// Flush will close the underlying file. + /// Flush will close the underlying file. Writes are not allowed after flush, + /// and subsequent flushes do nothing. fn flush(&mut self) -> Result<()> { - // @TODO -- once file creator supports multiple partial writes we - // can support multiple flushes + // Since `write` is disallowed after finalization, no new data can arrive + // once we've flushed and this call can be safely ignored. if self.finalized { - return Err(io::Error::new( - io::ErrorKind::Unsupported, - "file writer has already been finalized by flush", - )); + return Ok(()); } // Handle the case where we are flushing after the buffer has already been dispatched during a write. @@ -217,7 +214,7 @@ mod tests { } #[test] - fn test_allow_only_one_flush() { + fn test_no_writes_after_flush_but_flush_is_idempotent() { let temp_dir = tempfile::tempdir().unwrap(); let dir = Arc::new(File::open(temp_dir.path()).unwrap()); let file_path = temp_dir.path().join("test.txt"); @@ -228,7 +225,8 @@ mod tests { let mut io_uring_file_writer = IoUringFileWriter::new(io_uring_file_creator, file_path.clone(), 0o666, dir).unwrap(); io_uring_file_writer.flush().unwrap(); + // Writes are rejected after flush, but a subsequent flush is a no-op and succeeds. assert!(io_uring_file_writer.write(&[0]).is_err()); - assert!(io_uring_file_writer.flush().is_err()); + io_uring_file_writer.flush().unwrap(); } } diff --git a/fs/src/io_uring/sequential_file_reader.rs b/fs/src/io_uring/sequential_file_reader.rs index 29042e1191b..3daf6082bbd 100644 --- a/fs/src/io_uring/sequential_file_reader.rs +++ b/fs/src/io_uring/sequential_file_reader.rs @@ -48,7 +48,6 @@ pub struct SequentialFileReaderBuilder<'sp> { } impl<'sp> SequentialFileReaderBuilder<'sp> { - #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { read_capacity: DEFAULT_READ_SIZE, @@ -139,7 +138,7 @@ impl<'sp> SequentialFileReaderBuilder<'sp> { if self.register_buffer { // Safety: kernel holds unsafe pointers to `buffer`, struct field declaration order - // guarantees that the ring is destroyed before `_backing_buffer` is dropped. + // guarantees that the ring is destroyed before `backing_buffer` is dropped. unsafe { IoBufferChunk::register(buf_slice_mut, &ring)? }; } @@ -166,7 +165,7 @@ impl<'sp> SequentialFileReaderBuilder<'sp> { ring, state: SequentialFileReaderState::default(), open_file_flags, - _backing_buffer: buffer, + backing_buffer: buffer, _phantom: PhantomData, }) } @@ -200,14 +199,14 @@ impl<'sp> SequentialFileReaderBuilder<'sp> { /// /// Implements read-ahead using io_uring. pub struct SequentialFileReader<'a> { - // Note: ring's state is tied to `_backing_buffer` - contains unsafe pointer references - // to the buffer. Ring should be drained and dropped before `_backing_buffer`. + // Note: ring's state is tied to `backing_buffer` - contains unsafe pointer references + // to the buffer. Ring should be drained and dropped before `backing_buffer`. ring: Ring, open_file_flags: i32, state: SequentialFileReaderState, /// Owned buffer used (chunked into `FixedIoBuffer` items) across lifespan of `inner` /// (should get dropped last) - _backing_buffer: PageAlignedMemory, + backing_buffer: PageAlignedMemory, _phantom: PhantomData<&'a ()>, } @@ -251,8 +250,20 @@ impl<'a> SequentialFileReader<'a> { Ok(()) } - fn add_file_to_prefetch(&mut self, file: &'a File, read_limit: FileSize) -> io::Result<()> { - self.add_file_by_fd(file.as_raw_fd(), read_limit) + /// Reset to idle state and re-type with a fresh lifetime `'b`. + /// + /// Drains the prefetch queue (cancels in-flight reads) before returning. + pub fn rebind<'b>(mut self) -> io::Result> { + while !self.state.files.is_empty() { + self.move_to_next_file()?; + } + Ok(SequentialFileReader { + ring: self.ring, + open_file_flags: self.open_file_flags, + state: self.state, + backing_buffer: self.backing_buffer, + _phantom: PhantomData, + }) } /// Caller must ensure that the file is not closed while the reader is using it. @@ -490,6 +501,10 @@ impl<'a> FileBufRead<'a> for SequentialFileReader<'a> { Ok(()) } + fn add_file_to_prefetch(&mut self, file: &'a File, read_limit: FileSize) -> io::Result<()> { + self.add_file_by_fd(file.as_raw_fd(), read_limit) + } + fn get_file_offset(&self) -> FileSize { self.state.current_offset } diff --git a/genesis-utils/Cargo.toml b/genesis-utils/Cargo.toml index 9f2c9915c88..94fd2c241a9 100644 --- a/genesis-utils/Cargo.toml +++ b/genesis-utils/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -27,3 +29,6 @@ solana-genesis-config = { workspace = true } solana-hash = { workspace = true } solana-rpc-client = { workspace = true } thiserror = { workspace = true } + +[lints] +workspace = true diff --git a/genesis-utils/src/lib.rs b/genesis-utils/src/lib.rs index 6b94a933ed6..d566afc17ae 100644 --- a/genesis-utils/src/lib.rs +++ b/genesis-utils/src/lib.rs @@ -17,13 +17,13 @@ fn check_genesis_hash( ) -> Result<(), String> { let genesis_hash = genesis_config.hash(); - if let Some(expected_genesis_hash) = expected_genesis_hash { - if expected_genesis_hash != genesis_hash { - return Err(format!( - "Genesis hash mismatch: expected {expected_genesis_hash} but downloaded genesis \ - hash is {genesis_hash}", - )); - } + if let Some(expected_genesis_hash) = expected_genesis_hash + && expected_genesis_hash != genesis_hash + { + return Err(format!( + "Genesis hash mismatch: expected {expected_genesis_hash} but downloaded genesis hash \ + is {genesis_hash}", + )); } Ok(()) diff --git a/genesis/Cargo.toml b/genesis/Cargo.toml index 443c182e570..6db997f9160 100644 --- a/genesis/Cargo.toml +++ b/genesis/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] name = "solana_genesis" @@ -68,9 +70,13 @@ solana-vote-program = { workspace = true } tempfile = { workspace = true } [dev-dependencies] +bs58 = { workspace = true, features = ["std"] } solana-borsh = { workspace = true } solana-genesis = { path = ".", features = ["agave-unstable-api"] } solana-pubkey = { workspace = true, features = ["rand"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-vote = { path = "../vote", features = ["agave-unstable-api"] } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/genesis/src/main.rs b/genesis/src/main.rs index 9804be3e5f5..d92fd925476 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -7,10 +7,10 @@ use { clap::{App, Arg, ArgMatches, crate_description, crate_name, value_t, value_t_or_exit}, itertools::Itertools, solana_account::{Account, AccountSharedData, ReadableAccount, WritableAccount}, - solana_bls_signatures::{Pubkey as BLSPubkey, PubkeyCompressed as BLSPubkeyCompressed}, + solana_bls_signatures::PubkeyCompressed as BLSPubkeyCompressed, solana_clap_utils::{ input_parsers::{ - bls_pubkeys_of, cluster_type_of, pubkey_of, pubkeys_of, + bls_pubkey_compressed_from_str, bls_pubkeys_of, cluster_type_of, pubkey_of, pubkeys_of, unix_timestamp_from_rfc3339_datetime, }, input_validators::{ @@ -42,7 +42,7 @@ use { solana_rpc_client::rpc_client::RpcClient, solana_rpc_client_api::request::MAX_MULTIPLE_ACCOUNTS, solana_runtime::{ - bank::VAT_TO_BURN_PER_EPOCH, + bank::DEFAULT_VAT_TO_BURN_PER_EPOCH, genesis_utils::{add_genesis_epoch_rewards_account, add_genesis_stake_config_account}, stake_utils, }, @@ -66,7 +66,7 @@ use { /// In order to satisfy the VAT we need to fund all vote accounts /// This corresponds to 100 epochs worth of VAT -const VAT_MINIMUM_LAMPORTS: u64 = VAT_TO_BURN_PER_EPOCH * 100; +const DEFAULT_VAT_MINIMUM_LAMPORTS: u64 = DEFAULT_VAT_TO_BURN_PER_EPOCH * 100; pub enum AccountFileFormat { Pubkey, @@ -83,13 +83,7 @@ fn pubkey_from_str(key_str: &str) -> Result> { } fn bls_pubkey_from_str(key_str: &str) -> Result> { - match BLSPubkeyCompressed::from_str(key_str) { - Ok(bls_pubkey) => Ok(bls_pubkey), - Err(_) => { - let bls_pubkey = BLSPubkey::from_str(key_str)?; - Ok(bls_pubkey.try_into()?) - } - } + bls_pubkey_compressed_from_str(key_str).map_err(Into::into) } pub fn load_genesis_accounts(file: &str, genesis_config: &mut GenesisConfig) -> io::Result { @@ -285,7 +279,7 @@ fn add_validator_accounts( .unwrap_or([0u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE]); // Vote account needs enough lamports for rent exemption plus VAT let vote_account_lamports = - rent.minimum_balance(VoteStateV4::size_of()) + VAT_MINIMUM_LAMPORTS; + rent.minimum_balance(VoteStateV4::size_of()) + DEFAULT_VAT_MINIMUM_LAMPORTS; let vote_account = vote_state::create_v4_account_with_authorized( identity_pubkey, identity_pubkey, @@ -1001,7 +995,7 @@ fn main() -> Result<(), Box> { mod tests { use { super::*, - solana_bls_signatures::keypair::Keypair as BLSKeypair, + solana_bls_signatures::{Pubkey as BLSPubkey, keypair::Keypair as BLSKeypair}, solana_borsh::v1 as borsh1, solana_genesis_config::GenesisConfig, solana_stake_interface as stake, @@ -1357,12 +1351,14 @@ mod tests { assert_eq!(genesis_config.accounts.len(), 3); } - #[test_case(true, true; "add bls compressed pubkey")] - #[test_case(true, false; "add bls pubkey")] - #[test_case(false, false; "no bls pubkey")] + #[test_case(true, true, false; "add bls compressed pubkey")] + #[test_case(true, true, true; "add bls base58 compressed pubkey")] + #[test_case(true, false, false; "add bls pubkey")] + #[test_case(false, false, false; "no bls pubkey")] fn test_append_validator_accounts_to_genesis( add_bls_pubkey: bool, use_compressed_pubkey: bool, + use_base58_pubkey: bool, ) { // Test invalid file returns error assert!( @@ -1382,7 +1378,11 @@ mod tests { let bls_pubkey: BLSPubkey = BLSKeypair::new().public.into_inner().into(); if use_compressed_pubkey { let bls_pubkey_compressed: BLSPubkeyCompressed = bls_pubkey.try_into().unwrap(); - Some(bls_pubkey_compressed.to_string()) + if use_base58_pubkey { + Some(bs58::encode(bls_pubkey_compressed.0).into_string()) + } else { + Some(bls_pubkey_compressed.to_string()) + } } else { Some(bls_pubkey.to_string()) } @@ -1423,7 +1423,9 @@ mod tests { let filename = format!( "test_append_validator_accounts_to_genesis_{}_{}_bls.yml", if add_bls_pubkey { "with" } else { "without" }, - if use_compressed_pubkey { + if use_base58_pubkey { + "base58_compressed" + } else if use_compressed_pubkey { "compressed" } else { "uncompressed" @@ -1466,15 +1468,9 @@ mod tests { let authorized_voters = &vote_state.authorized_voters; assert_eq!(authorized_voters.first().unwrap().1, &identity_pk); if add_bls_pubkey { - let bls_pubkey_compressed_from_input = if use_compressed_pubkey { - BLSPubkeyCompressed::from_str(b64_account.bls_pubkey.as_ref().unwrap()) - .expect("failed to parse BLS pubkey from input") - } else { - BLSPubkey::from_str(b64_account.bls_pubkey.as_ref().unwrap()) - .expect("failed to parse BLS pubkey from input") - .try_into() - .expect("failed to convert BLS pubkey to compressed form") - }; + let bls_pubkey_compressed_from_input = + bls_pubkey_from_str(b64_account.bls_pubkey.as_ref().unwrap()) + .expect("failed to parse BLS pubkey from input"); let bls_pubkey_compressed_from_account = BLSPubkeyCompressed( vote_state .bls_pubkey_compressed diff --git a/geyser-plugin-interface/Cargo.toml b/geyser-plugin-interface/Cargo.toml index 92476f3c575..4fc2376bdeb 100644 --- a/geyser-plugin-interface/Cargo.toml +++ b/geyser-plugin-interface/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -24,3 +26,6 @@ solana-signature = { workspace = true } solana-transaction = { workspace = true } solana-transaction-status = { workspace = true } thiserror = { workspace = true } + +[lints] +workspace = true diff --git a/geyser-plugin-manager/Cargo.toml b/geyser-plugin-manager/Cargo.toml index 5f7b10f0b5c..8a0ea97af83 100644 --- a/geyser-plugin-manager/Cargo.toml +++ b/geyser-plugin-manager/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -35,7 +37,6 @@ solana-hash = { workspace = true } solana-ledger = { workspace = true } solana-measure = { workspace = true } solana-message = { workspace = true } -solana-metrics = { workspace = true } solana-pubkey = { workspace = true } solana-rpc = { workspace = true } solana-runtime = { workspace = true } @@ -44,3 +45,6 @@ solana-transaction = { workspace = true } solana-transaction-status = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } + +[lints] +workspace = true diff --git a/geyser-plugin-manager/src/deshred_transaction_notifier.rs b/geyser-plugin-manager/src/deshred_transaction_notifier.rs index e9538905b54..301ed28e1b7 100644 --- a/geyser-plugin-manager/src/deshred_transaction_notifier.rs +++ b/geyser-plugin-manager/src/deshred_transaction_notifier.rs @@ -10,7 +10,6 @@ use { solana_ledger::deshred_transaction_notifier_interface::DeshredTransactionNotifier, solana_measure::measure::Measure, solana_message::v0::LoadedAddresses, - solana_metrics::*, solana_signature::Signature, solana_transaction::versioned::VersionedTransaction, std::sync::Arc, @@ -77,12 +76,6 @@ impl DeshredTransactionNotifier for DeshredTransactionNotifierImpl { } } measure.stop(); - inc_new_counter_debug!( - "geyser-plugin-notify_plugins_of_deshred_transaction_info-us", - measure.as_us() as usize, - 10000, - 10000 - ); } fn alt_resolution_enabled(&self) -> bool { diff --git a/gossip-cli/Cargo.toml b/gossip-cli/Cargo.toml index 8d4b224faf3..6aa93f32ca1 100644 --- a/gossip-cli/Cargo.toml +++ b/gossip-cli/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [[bin]] name = "solana-gossip" path = "src/main.rs" @@ -22,7 +27,10 @@ agave-logger = { workspace = true } clap = { workspace = true } log = { workspace = true } solana-clap-utils = { workspace = true } -solana-gossip = { workspace = true, features = ["agave-unstable-api"] } -solana-net-utils = { workspace = true, features = ["agave-unstable-api"] } +solana-gossip = { workspace = true } +solana-net-utils = { workspace = true } solana-pubkey = { version = "=4.2.0", features = ["rand"] } solana-version = { workspace = true } + +[lints] +workspace = true diff --git a/gossip-cli/src/main.rs b/gossip-cli/src/main.rs index 53238e3975d..29b5322cfaf 100644 --- a/gossip-cli/src/main.rs +++ b/gossip-cli/src/main.rs @@ -215,16 +215,16 @@ fn process_spy_results( pubkeys: Option<&[Pubkey]>, ) { if timeout.is_some() { - if let Some(num) = num_nodes { - if validators.len() < num { - let add = if num_nodes_exactly.is_some() { - "" - } else { - " or more" - }; - eprintln!("Error: Insufficient validators discovered. Expecting {num}{add}",); - exit(1); - } + if let Some(num) = num_nodes + && validators.len() < num + { + let add = if num_nodes_exactly.is_some() { + "" + } else { + " or more" + }; + eprintln!("Error: Insufficient validators discovered. Expecting {num}{add}",); + exit(1); } if let Some(nodes) = pubkeys { for node in nodes { @@ -239,11 +239,11 @@ fn process_spy_results( } } } - if let Some(num_nodes_exactly) = num_nodes_exactly { - if validators.len() > num_nodes_exactly { - eprintln!("Error: Extra nodes discovered. Expecting exactly {num_nodes_exactly}"); - exit(1); - } + if let Some(num_nodes_exactly) = num_nodes_exactly + && validators.len() > num_nodes_exactly + { + eprintln!("Error: Extra nodes discovered. Expecting exactly {num_nodes_exactly}"); + exit(1); } } diff --git a/gossip/Cargo.toml b/gossip/Cargo.toml index 3410a709662..969d0c8c7a6 100644 --- a/gossip/Cargo.toml +++ b/gossip/Cargo.toml @@ -7,10 +7,12 @@ description = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] bench = false @@ -20,25 +22,31 @@ frozen-abi = [ "dep:solana-frozen-abi", "dep:solana-frozen-abi-macro", "solana-bloom/frozen-abi", + "solana-hash/frozen-abi", "solana-ledger/frozen-abi", "solana-perf/frozen-abi", + "solana-pubkey/frozen-abi", "solana-short-vec/frozen-abi", + "solana-signature/frozen-abi", + "solana-transaction/frozen-abi", "solana-version/frozen-abi", "solana-vote/frozen-abi", "solana-vote-program/frozen-abi", ] agave-unstable-api = [] -dev-context-only-utils = ["dep:prost", "dep:protosol", "solana-bloom/dev-context-only-utils"] +dev-context-only-utils = ["solana-bloom/dev-context-only-utils"] +conformance = ["dep:prost", "dep:protosol", "dev-context-only-utils"] +dummy-for-ci-check = ["conformance"] [dependencies] agave-logger = { workspace = true } agave-random = { workspace = true } arc-swap = { workspace = true } -arrayvec = { workspace = true } assert_matches = { workspace = true } -bincode = { workspace = true } bv = { workspace = true, features = ["serde"] } +bytes = { workspace = true } crossbeam-channel = { workspace = true } +ed25519-dalek = { workspace = true } flate2 = { workspace = true } indexmap = { workspace = true, features = ["rayon"] } itertools = { workspace = true } @@ -53,7 +61,6 @@ rayon = { workspace = true } serde = { workspace = true } serde-big-array = { workspace = true } serde_bytes = { workspace = true } -siphasher = { workspace = true } solana-bloom = { workspace = true } solana-client = { workspace = true } solana-clock = { workspace = true } @@ -84,21 +91,23 @@ solana-signature = { workspace = true, default-features = false } solana-signer = { workspace = true } solana-streamer = { workspace = true } solana-time-utils = { workspace = true } -solana-transaction = { workspace = true } +solana-transaction = { workspace = true, features = ["wincode"] } solana-version = { workspace = true } solana-vote = { workspace = true } solana-vote-program = { workspace = true } +solana-wincode-varint = { workspace = true } static_assertions = { workspace = true } thiserror = { workspace = true } wincode = { workspace = true } [dev-dependencies] anyhow = { workspace = true } +bincode = { workspace = true } bs58 = { workspace = true } criterion = { workspace = true } num_cpus = { workspace = true } serial_test = { workspace = true } -solana-gossip = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } +solana-gossip = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils", "conformance"] } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-perf = { path = "../perf", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } @@ -106,6 +115,9 @@ solana-signature = { workspace = true, features = ["rand"] } solana-system-transaction = { workspace = true } solana-vote-interface = { workspace = true } static_assertions = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } +tempfile = { workspace = true } test-case = { workspace = true } [[bench]] diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs index 25bd5e7e9b8..66f470dc666 100644 --- a/gossip/src/cluster_info.rs +++ b/gossip/src/cluster_info.rs @@ -17,13 +17,13 @@ use { crate::{ cluster_info_metrics::{Counter, GossipStats, ScopedTimer, TimedGuard}, contact_info::{self, ContactInfo, ContactInfoQuery, Error as ContactInfoError}, - crds::{Crds, Cursor, GossipRoute}, + crds::{CRDS_SHARDS_BITS, Crds, Cursor, GossipRoute}, crds_data::{self, CrdsData, EpochSlotsIndex, LowestSlot, MAX_VOTES, SnapshotHashes, Vote}, crds_filter::{GossipFilterDirection, should_retain_crds_value}, crds_gossip::CrdsGossip, crds_gossip_error::CrdsGossipError, crds_gossip_pull::{ - CRDS_GOSSIP_PULL_CRDS_TIMEOUT_MS, CrdsFilter, CrdsTimeouts, ProcessPullStats, + self, CRDS_GOSSIP_PULL_CRDS_TIMEOUT_MS, CrdsFilter, CrdsTimeouts, ProcessPullStats, PullRequest, get_max_bloom_filter_bytes, }, crds_value::{CrdsValue, CrdsValueLabel}, @@ -36,8 +36,9 @@ use { DUPLICATE_SHRED_MAX_PAYLOAD_SIZE, MAX_INCREMENTAL_SNAPSHOT_HASHES, MAX_PRUNE_DATA_NODES, PULL_RESPONSE_MAX_PAYLOAD_SIZE, PULL_RESPONSE_MIN_SERIALIZED_SIZE, PUSH_MESSAGE_MAX_PAYLOAD_SIZE, Ping, PingCache, - Protocol, PruneData, split_gossip_messages, + Protocol, PruneData, deserialize_protocol, split_gossip_messages, }, + sigverify_cache::SigVerifyCache, weighted_shuffle::WeightedShuffle, }, arc_swap::ArcSwap, @@ -53,6 +54,7 @@ use { PortRange, SocketAddrSpace, VALIDATOR_PORT_RANGE, bind_in_range, multihomed_sockets::BindIpAddrs, sockets::{bind_gossip_port_in_range, bind_to_localhost_unique}, + token_bucket::{KeyedRateLimiter, TokenBucket}, }, solana_perf::{ data_budget::DataBudget, @@ -121,6 +123,27 @@ pub(crate) const GOSSIP_CHANNEL_CAPACITY: usize = 4096; // 2^12 const GOSSIP_PING_CACHE_CAPACITY: usize = 126976; const GOSSIP_PING_CACHE_TTL: Duration = Duration::from_secs(1280); const GOSSIP_PING_CACHE_RATE_LIMIT_DELAY: Duration = Duration::from_secs(1280 / 64); +// Per-IP scan budget for incoming pull requests; validators are assumed not +// to share IPs. Mirrors the ping-pong cache capacity. +const GOSSIP_PULL_SCAN_BUDGET_CACHE_CAPACITY: usize = GOSSIP_PING_CACHE_CAPACITY; +const GOSSIP_PULL_SCAN_BUDGET_CAPACITY: u64 = 16 * crds_gossip_pull::MIN_NUM_BLOOM_ITEMS as u64; +const GOSSIP_PULL_SCAN_BUDGET_REFILL_PER_SEC: u64 = + 4 * crds_gossip_pull::MIN_NUM_BLOOM_ITEMS as u64; +const GOSSIP_PULL_SCAN_BUDGET_SHARD_COUNT: usize = 64; + +/// Estimated CRDS shard scan work for a pull request filter. +#[inline] +fn pull_request_scan_cost(crds_len: usize, mask_bits: u32, bloom_hash_count: usize) -> u64 { + // Extra mask bits still require one full shard scan. + let shift = mask_bits.min(CRDS_SHARDS_BITS); + let scan_entries = (crds_len >> shift).max(1) as u64; + + let bloom_hash_count = u64::try_from(bloom_hash_count) + .unwrap_or(u64::MAX) + .max(crds_gossip_pull::KEYS as u64); + scan_entries.saturating_mul(bloom_hash_count) +} + pub const DEFAULT_CONTACT_DEBUG_INTERVAL_MILLIS: u64 = 10_000; pub const DEFAULT_CONTACT_SAVE_INTERVAL_MILLIS: u64 = 60_000; // Limit number of unique pubkeys in the crds table. @@ -132,6 +155,11 @@ pub const DEFAULT_NUM_TVU_RECEIVE_SOCKETS: NonZeroUsize = MINIMUM_NUM_TVU_RECEIV pub const MINIMUM_NUM_TVU_RETRANSMIT_SOCKETS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); pub const DEFAULT_NUM_TVU_RETRANSMIT_SOCKETS: NonZeroUsize = NonZeroUsize::new(12).unwrap(); +// Contact-info save/restore handles trusted local data, so disable wincode's +// default 4 MiB preallocation limit (which defends untrusted wire input). +type ContactInfoFileConfig = + wincode::config::Configuration; + #[derive(Debug, PartialEq, Eq, Error)] pub enum ClusterInfoError { #[error("NoPeers")] @@ -158,6 +186,7 @@ pub struct ClusterInfo { outbound_budget: DataBudget, my_contact_info: RwLock, ping_cache: Mutex, + pull_request_budget: KeyedRateLimiter, pub(crate) stats: GossipStats, local_message_pending_push_queue: Mutex>, contact_debug_interval: u64, // milliseconds, 0 = disabled @@ -165,6 +194,7 @@ pub struct ClusterInfo { contact_info_path: PathBuf, socket_addr_space: SocketAddrSpace, bind_ip_addrs: Arc, + sigverify_cache: SigVerifyCache, } impl ClusterInfo { @@ -182,12 +212,19 @@ impl ClusterInfo { outbound_budget: DataBudget::default(), my_contact_info: RwLock::new(contact_info), ping_cache: Mutex::new(PingCache::new( - &mut rand::rng(), - Instant::now(), GOSSIP_PING_CACHE_TTL, GOSSIP_PING_CACHE_RATE_LIMIT_DELAY, GOSSIP_PING_CACHE_CAPACITY, )), + pull_request_budget: KeyedRateLimiter::new( + GOSSIP_PULL_SCAN_BUDGET_CACHE_CAPACITY, + TokenBucket::new( + GOSSIP_PULL_SCAN_BUDGET_CAPACITY, + GOSSIP_PULL_SCAN_BUDGET_CAPACITY, + GOSSIP_PULL_SCAN_BUDGET_REFILL_PER_SEC as f64, + ), + GOSSIP_PULL_SCAN_BUDGET_SHARD_COUNT, + ), stats: GossipStats::default(), local_message_pending_push_queue: Mutex::default(), contact_debug_interval: DEFAULT_CONTACT_DEBUG_INTERVAL_MILLIS, @@ -195,6 +232,7 @@ impl ClusterInfo { contact_save_interval: 0, // disabled socket_addr_space, bind_ip_addrs: Arc::new(BindIpAddrs::default()), + sigverify_cache: SigVerifyCache::new(), }; me.refresh_my_gossip_contact_info(); me @@ -328,7 +366,11 @@ impl ClusterInfo { match File::create(tmp_filename) { Ok(file) => { let mut writer = BufWriter::new(file); - if let Err(err) = bincode::serialize_into(&mut writer, &nodes) { + if let Err(err) = wincode::config::serialize_into( + &mut writer, + &nodes, + ContactInfoFileConfig::new(), + ) { warn!( "Failed to serialize contact info info {}: {}", tmp_filename.display(), @@ -375,12 +417,14 @@ impl ClusterInfo { } let nodes: Vec = match File::open(&filename) { - Ok(file) => { - bincode::deserialize_from(&mut BufReader::new(file)).unwrap_or_else(|err| { - warn!("Failed to deserialize {}: {}", filename.display(), err); - vec![] - }) - } + Ok(file) => wincode::config::deserialize_from( + &mut BufReader::new(file), + ContactInfoFileConfig::new(), + ) + .unwrap_or_else(|err| { + warn!("Failed to deserialize {}: {}", filename.display(), err); + vec![] + }), Err(err) => { warn!("Failed to open {}: {}", filename.display(), err); vec![] @@ -1629,6 +1673,24 @@ impl ClusterInfo { } } + fn try_consume_pull_request_scan_budget(&self, request: &PullRequest, crds_len: usize) -> bool { + let cost = pull_request_scan_cost( + crds_len, + request.filter.get_mask_bits(), + request.filter.bloom_hash_count(), + ); + if self + .pull_request_budget + .consume_tokens(request.addr.ip(), cost) + .is_ok() + { + true + } else { + self.stats.pull_request_scan_budget_exhausted.add_relaxed(1); + false + } + } + // Pull requests take an incoming bloom filter of contained entries from a node // and tries to send back to them the values it detects are missing. fn handle_pull_requests( @@ -1664,6 +1726,7 @@ impl ClusterInfo { GossipFilterDirection::EgressPullResponse, ) }, + |request, crds_len| self.try_consume_pull_request_scan_budget(request, crds_len), &self.stats, ) }; @@ -2089,9 +2152,13 @@ impl ClusterInfo { packet: PacketRef, stakes: &HashMap, stats: &GossipStats, + sigverify_cache: &SigVerifyCache, ) -> Option<(SocketAddr, Protocol)> { - let mut protocol: Protocol = - stats.record_received_packet(packet.deserialize_slice::(..))?; + let result: wincode::ReadResult = packet + .data(..) + .ok_or(wincode::ReadError::Custom("packet discarded")) + .and_then(deserialize_protocol); + let mut protocol: Protocol = stats.record_received_packet(result)?; protocol.sanitize().ok()?; if let Protocol::PullResponse(_, values) | Protocol::PushMessage(_, values) = &mut protocol @@ -2103,7 +2170,7 @@ impl ClusterInfo { return None; } } - protocol.verify().then(|| { + protocol.verify(sigverify_cache).then(|| { stats.packets_received_verified_count.add_relaxed(1); (packet.meta().socket_addr(), protocol) }) @@ -2117,13 +2184,17 @@ impl ClusterInfo { if packet_buf.len() == 1 { packet_buf[0] .par_iter() - .filter_map(|packet| verify_packet(packet, &stakes, &self.stats)) + .filter_map(|packet| { + verify_packet(packet, &stakes, &self.stats, &self.sigverify_cache) + }) .collect() } else { packet_buf .par_iter() .flatten() - .filter_map(|packet| verify_packet(packet, &stakes, &self.stats)) + .filter_map(|packet| { + verify_packet(packet, &stakes, &self.stats, &self.sigverify_cache) + }) .collect() } }) @@ -2338,7 +2409,7 @@ pub struct Sockets { /// Client-side socket for ForwardingStage non-vote transactions pub tpu_transaction_forwarding_clients: Box<[UdpSocket]>, // quic write only /// Socket for alpenglow consensus logic - pub alpenglow: Option, // udp read/write + pub alpenglow: UdpSocket, // quic read/write /// Connection cache endpoint for QUIC-based Vote pub quic_vote_client: UdpSocket, // quic write only /// Connection cache endpoint for QUIC-based Alpenglow messages @@ -2495,9 +2566,20 @@ fn make_gossip_packet_batch>( recycler: &PacketBatchRecycler, stats: &GossipStats, ) -> RecycledPacketBatch { - let record_gossip_packet = |(_, pkt): &(_, Protocol)| stats.record_gossip_packet(pkt); - let pkts = pkts.into_iter().inspect(record_gossip_packet); - RecycledPacketBatch::new_with_recycler_data_and_dests(recycler, "gossip_packet_batch", pkts) + let pkts = pkts.into_iter(); + let mut batch = + RecycledPacketBatch::new_with_recycler(recycler, pkts.len(), "gossip_packet_batch"); + for (addr, pkt) in pkts { + let addr = addr.borrow(); + if !addr.ip().is_unspecified() && addr.port() != 0 { + if let Some(packet) = make_gossip_packet(addr, &pkt, stats) { + batch.push(packet); + } + } else { + trace!("Dropping packet, as destination is unknown"); + } + } + batch } #[inline] @@ -2506,16 +2588,22 @@ fn make_gossip_packet( pkt: &Protocol, stats: &GossipStats, ) -> Option { - match Packet::from_data(Some(addr.borrow()), pkt) { - Err(err) => { + let mut packet = Packet::default(); + let size = { + let buffer = packet.buffer_mut(); + let initial_len = buffer.len(); + let mut writer: &mut [u8] = buffer; + if let Err(err) = wincode::serialize_into(&mut writer, pkt) { error!("failed to write gossip packet: {err:?}"); - None - } - Ok(out) => { - stats.record_gossip_packet(pkt); - Some(out) + return None; } - } + initial_len - writer.len() + }; + let meta = packet.meta_mut(); + meta.size = size; + meta.set_socket_addr(addr.borrow()); + stats.record_gossip_packet(pkt); + Some(packet) } #[cfg(test)] @@ -2530,7 +2618,6 @@ mod tests { protocol::tests::new_rand_remote_node, socketaddr, }, - bincode::serialize, itertools::izip, solana_keypair::Keypair, solana_ledger::shred::Shredder, @@ -2712,7 +2799,7 @@ mod tests { .collect(); let recycler = PacketBatchRecycler::default(); let packets = { - let (sender, receiver) = crossbeam_channel::unbounded(); + let (sender, receiver) = crossbeam_channel::bounded(1024); cluster_info.handle_batch_ping_messages( remote_nodes.iter().map(|(_, socket)| socket).zip(pings), &recycler, @@ -2727,9 +2814,14 @@ mod tests { pongs.into_iter() ) { assert_eq!(packet.meta().socket_addr(), socket); - let bytes = serialize(&pong).unwrap(); - match packet.deserialize_slice(..).unwrap() { - Protocol::PongMessage(pong) => assert_eq!(serialize(&pong).unwrap(), bytes), + let bytes = wincode::serialize(&pong).unwrap(); + assert_eq!(bytes, bincode::serialize(&pong).unwrap()); + match deserialize_protocol(packet.data(..).unwrap_or_default()).unwrap() { + Protocol::PongMessage(pong) => { + let pong_bytes = wincode::serialize(&pong).unwrap(); + assert_eq!(pong_bytes, bincode::serialize(&pong).unwrap()); + assert_eq!(pong_bytes, bytes); + } _ => panic!("invalid packet!"), } } @@ -2793,6 +2885,46 @@ mod tests { assert!(gossip_crds.get::<&CrdsValue>(&label).is_some()); } + #[test] + fn test_save_restore_contact_info_round_trip() { + let tmpdir = tempfile::tempdir().unwrap(); + + let self_keypair = Arc::new(Keypair::new()); + let self_ci = ContactInfo::new_localhost(&self_keypair.pubkey(), timestamp()); + let mut cluster_info_a = + ClusterInfo::new(self_ci, self_keypair, SocketAddrSpace::Unspecified); + // Sets contact_info_path; no file to load yet. + cluster_info_a.restore_contact_info(tmpdir.path(), 0); + + let peer1 = ContactInfo::new_localhost(&solana_pubkey::new_rand(), timestamp()); + let peer2 = ContactInfo::new_localhost(&solana_pubkey::new_rand(), timestamp()); + let peer1_pubkey = *peer1.pubkey(); + let peer2_pubkey = *peer2.pubkey(); + cluster_info_a.insert_info(peer1); + cluster_info_a.insert_info(peer2); + cluster_info_a.save_contact_info(); + + let other_keypair = Arc::new(Keypair::new()); + let other_ci = ContactInfo::new_localhost(&other_keypair.pubkey(), timestamp()); + let mut cluster_info_b = + ClusterInfo::new(other_ci, other_keypair, SocketAddrSpace::Unspecified); + cluster_info_b.restore_contact_info(tmpdir.path(), 0); + + let gossip_crds = cluster_info_b.gossip.crds.read().unwrap(); + assert!( + gossip_crds + .get::<&CrdsValue>(&CrdsValueLabel::ContactInfo(peer1_pubkey)) + .is_some(), + "peer1 missing after restore" + ); + assert!( + gossip_crds + .get::<&CrdsValue>(&CrdsValueLabel::ContactInfo(peer2_pubkey)) + .is_some(), + "peer2 missing after restore" + ); + } + fn assert_in_range(x: u16, range: (u16, u16)) { assert!(x >= range.0); assert!(x < range.1); @@ -2822,9 +2954,7 @@ mod tests { fn check_node_sockets(node: &Node, ip: IpAddr, range: (u16, u16)) { check_socket(&node.sockets.repair, ip, range); - if let Some(alpenglow_port) = &node.sockets.alpenglow { - check_socket(alpenglow_port, ip, range); - } + check_socket(&node.sockets.alpenglow, ip, range); check_sockets(&node.sockets.gossip, ip, range); check_sockets(&node.sockets.tvu, ip, range); check_sockets(&node.sockets.tpu_quic, ip, range); @@ -2926,9 +3056,11 @@ mod tests { .collect::>>(); // there should be some pushes ready assert!(!push_messages.is_empty()); - push_messages - .values() - .for_each(|v| v.par_iter().for_each(|v| assert!(v.verify()))); + let sigverify_cache = SigVerifyCache::new(); + push_messages.values().for_each(|v| { + v.par_iter() + .for_each(|v| assert!(v.verify_with_cache(&sigverify_cache))) + }); let mut pings = Vec::new(); let _ = cluster_info @@ -3206,13 +3338,16 @@ mod tests { tower.clear(); tower.extend(0..=slot); let vote = new_vote_transaction(vec![slot]); + // `KeyedRateLimiter` is not unwind-safe; this test only checks the panic. assert!( - panic::catch_unwind(|| cluster_info.push_vote(&tower, vote)) - .err() - .and_then(|a| a - .downcast_ref::() - .map(|s| { s.starts_with("Submitting old vote") })) - .unwrap_or_default() + panic::catch_unwind(panic::AssertUnwindSafe(|| { + cluster_info.push_vote(&tower, vote) + })) + .err() + .and_then(|a| a + .downcast_ref::() + .map(|s| { s.starts_with("Submitting old vote") })) + .unwrap_or_default() ); } @@ -3252,7 +3387,7 @@ mod tests { assert_eq!(addr, entrypoint.gossip().unwrap()); match msg { Protocol::PullRequest(_, value) => { - assert!(value.verify()); + assert!(value.verify_with_cache(&SigVerifyCache::new())); assert_eq!(value.pubkey(), cluster_info.id()) } _ => panic!("wrong protocol"), diff --git a/gossip/src/cluster_info_metrics.rs b/gossip/src/cluster_info_metrics.rs index 752dc6336c9..afa3b7f5805 100644 --- a/gossip/src/cluster_info_metrics.rs +++ b/gossip/src/cluster_info_metrics.rs @@ -148,6 +148,7 @@ pub struct GossipStats { pub(crate) prune_received_cache: Counter, pub(crate) pull_from_entrypoint_count: Counter, pub(crate) pull_request_ping_pong_check_failed_count: Counter, + pub(crate) pull_request_scan_budget_exhausted: Counter, pub(crate) purge: Counter, pub(crate) purge_count: Counter, pub(crate) push_fanout_num_entries: Counter, @@ -356,6 +357,11 @@ pub(crate) fn submit_gossip_stats( stats.pull_request_ping_pong_check_failed_count.clear(), i64 ), + ( + "pull_request_scan_budget_exhausted", + stats.pull_request_scan_budget_exhausted.clear(), + i64 + ), ( "generate_pull_responses", stats.generate_pull_responses.clear(), diff --git a/gossip/src/contact_info.rs b/gossip/src/contact_info.rs index 0307a298091..c311c482e95 100644 --- a/gossip/src/contact_info.rs +++ b/gossip/src/contact_info.rs @@ -15,10 +15,12 @@ use { std::{ cmp::Ordering, collections::HashSet, + mem::MaybeUninit, net::{IpAddr, Ipv4Addr, SocketAddr}, time::{SystemTime, UNIX_EPOCH}, }, thiserror::Error, + wincode::{ReadError, ReadResult, SchemaRead, SchemaWrite, config::Config, io::Reader}, }; const DEFAULT_RPC_PORT: u16 = 8899; @@ -65,6 +67,8 @@ pub enum Error { InvalidQuicSocket(Option, Option), #[error("IP addresses saturated")] IpAddrsSaturated, + #[error("IPv6 IP address: {0}")] + Ipv6IpAddr(IpAddr), #[error("Multicast IP address: {0}")] MulticastIpAddr(IpAddr), #[error("Port offsets overflow")] @@ -77,10 +81,20 @@ pub enum Error { UnusedIpAddr(IpAddr), } -#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[cfg_attr( + feature = "frozen-abi", + derive(StableAbi), + frozen_abi( + abi_digest = "8uEsgsqpoykiQgP3Majk8kurCVNB3TESe9dejgDowub4", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, SchemaWrite)] pub struct ContactInfo { pubkey: Pubkey, #[serde(with = "serde_varint")] + #[wincode(with = "solana_wincode_varint::Leb128Int")] wallclock: u64, // When the node instance was first created. // Identifies duplicate running instances. @@ -89,23 +103,28 @@ pub struct ContactInfo { version: solana_version::Version, // All IP addresses are unique and referenced at least once in sockets. #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] addrs: Vec, // All sockets have a unique key and a valid IP address index. #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] sockets: Vec, #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] extensions: Vec, // Only sanitized socket-addrs can be cached! #[serde(skip_serializing)] + #[wincode(skip(default_val = EMPTY_SOCKET_ADDR_CACHE))] cache: [SocketAddr; SOCKET_CACHE_SIZE], } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize, SchemaWrite, SchemaRead)] pub(crate) struct SocketEntry { pub(crate) key: u8, // Protocol identifier, e.g. tvu, tpu, etc pub(crate) index: u8, // IpAddr index in the accompanying addrs vector. #[serde(with = "serde_varint")] + #[wincode(with = "solana_wincode_varint::Leb128Int")] pub(crate) offset: u16, // Port offset with respect to the previous entry. } @@ -127,20 +146,24 @@ define_tlv_enum!( // verified and self.cache needs to be populated. This type serves as a // workaround since serde does not have an initializer. // https://github.com/serde-rs/serde/issues/642 -#[derive(Deserialize)] +#[derive(Deserialize, SchemaRead)] struct ContactInfoLite { pubkey: Pubkey, #[serde(with = "serde_varint")] + #[wincode(with = "solana_wincode_varint::Leb128Int")] wallclock: u64, outset: u64, shred_version: u16, version: solana_version::Version, #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] addrs: Vec, #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] sockets: Vec, #[allow(dead_code)] #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] extensions: Vec, } @@ -248,12 +271,13 @@ impl ContactInfo { &self.version } - #[cfg(any(test, feature = "dev-context-only-utils"))] + // Conformance-only accessors; unused under DCOU. + #[cfg(any(test, feature = "conformance"))] pub(crate) fn addrs(&self) -> &[IpAddr] { &self.addrs } - #[cfg(any(test, feature = "dev-context-only-utils"))] + #[cfg(any(test, feature = "conformance"))] pub(crate) fn sockets(&self) -> &[SocketEntry] { &self.sockets } @@ -419,13 +443,13 @@ impl ContactInfo { } /// port must not be 0 - /// ip must be specified and not multicast + /// ip must be IPv4, specified, and not multicast pub fn is_valid_address(addr: &SocketAddr, socket_addr_space: &SocketAddrSpace) -> bool { addr.port() != 0u16 && Self::is_valid_ip(addr.ip()) && socket_addr_space.check(addr) } fn is_valid_ip(addr: IpAddr) -> bool { - !(addr.is_unspecified() || addr.is_multicast()) + addr.is_ipv4() && !addr.is_unspecified() && !addr.is_multicast() } /// New random ContactInfo for tests and simulations. @@ -598,6 +622,18 @@ impl TryFrom for ContactInfo { } } +unsafe impl<'de, C: Config> SchemaRead<'de, C> for ContactInfo { + type Dst = ContactInfo; + + fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + let lite = >::get(reader)?; + let node = ContactInfo::try_from(lite) + .map_err(|_| ReadError::Custom("ContactInfo: invalid entries"))?; + dst.write(node); + Ok(()) + } +} + impl Sanitize for ContactInfo { fn sanitize(&self) -> Result<(), SanitizeError> { if self.wallclock >= MAX_WALLCLOCK { @@ -612,6 +648,9 @@ pub(crate) fn sanitize_socket(socket: &SocketAddr) -> Result<(), Error> { return Err(Error::InvalidPort(socket.port())); } let addr = socket.ip(); + if matches!(addr, IpAddr::V6(_)) { + return Err(Error::Ipv6IpAddr(addr)); + } if addr.is_unspecified() { return Err(Error::UnspecifiedIpAddr(addr)); } @@ -627,6 +666,9 @@ fn sanitize_entries(addrs: &[IpAddr], sockets: &[SocketEntry]) -> Result<(), Err { let mut seen = HashSet::with_capacity(addrs.len()); for addr in addrs { + if matches!(addr, IpAddr::V6(_)) { + return Err(Error::Ipv6IpAddr(*addr)); + } if !seen.insert(addr) { return Err(Error::DuplicateIpAddr(*addr)); } @@ -686,6 +728,36 @@ impl solana_frozen_abi::abi_example::AbiExample for ContactInfo { } } +// `ContactInfo` derives only `StableAbi` (not `StableAbiSample`): `cache` is a +// derived field recomputed on deserialize, and `addrs`/`sockets` must satisfy +// cross-field invariants (`sanitize_entries`), so independent per-field sampling +// can't produce round-trippable values. We build a valid node via `set_socket`, +// which maintains every invariant (including `cache`), so the digest test can use +// `test_roundtrip = "eq_and_wire"`. +#[cfg(feature = "frozen-abi")] +impl solana_frozen_abi::rand::distr::Distribution + for solana_frozen_abi::rand::distr::StandardUniform +{ + fn sample(&self, rng: &mut R) -> ContactInfo { + use solana_frozen_abi::stable_abi::StableAbi; + let mut node = ContactInfo::new( + ::random(rng), + /*wallclock:*/ rng.random(), + /*shred_version:*/ rng.random(), + ); + node.outset = rng.random(); + node.version = ::random(rng); + for key in 0..SOCKET_CACHE_SIZE as u8 { + if rng.random::() { + // Ignore sockets that fail sanitization (zero port, unspecified or + // multicast IP); `set_socket` leaves the node unchanged on error. + let _ = node.set_socket(key, ::random(rng)); + } + } + node + } +} + #[macro_export] macro_rules! socketaddr { ($ip:expr, $port:expr) => { @@ -705,51 +777,17 @@ macro_rules! socketaddr_any { mod tests { use { super::*, - rand::{ - Rng, - prelude::{IndexedRandom as _, SliceRandom as _}, - }, + rand::{Rng, prelude::SliceRandom as _}, solana_keypair::Keypair, solana_signer::Signer, std::{ collections::{HashMap, HashSet}, - iter::repeat_with, net::{Ipv4Addr, Ipv6Addr}, ops::Range, time::Duration, }, }; - fn new_rand_addr(rng: &mut R) -> IpAddr { - if rng.random() { - let addr = Ipv4Addr::new(rng.random(), rng.random(), rng.random(), rng.random()); - IpAddr::V4(addr) - } else { - let addr = Ipv6Addr::new( - rng.random(), - rng.random(), - rng.random(), - rng.random(), - rng.random(), - rng.random(), - rng.random(), - rng.random(), - ); - IpAddr::V6(addr) - } - } - - fn new_rand_port(rng: &mut R) -> u16 { - let port = rng.random::(); - let bits = u16::BITS - port.leading_zeros(); - let shift = rng.random_range(0u32..bits + 1u32); - port.checked_shr(shift).unwrap_or_default() - } - - fn new_rand_socket(rng: &mut R) -> SocketAddr { - SocketAddr::new(new_rand_addr(rng), new_rand_port(rng)) - } - #[test] fn test_new_gossip_entry_point() { let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, 10)); @@ -773,12 +811,16 @@ mod tests { #[test] fn test_sanitize_entries() { let mut rng = rand::rng(); - let addrs: Vec = repeat_with(|| new_rand_addr(&mut rng)).take(5).collect(); + let addrs = [ + IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), + IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), + IpAddr::V4(Ipv4Addr::new(10, 0, 0, 3)), + ]; let mut keys: Vec = (0u8..=u8::MAX).collect(); keys.shuffle(&mut rng); // Duplicate IP addresses. { - let addrs = [addrs[0], addrs[1], addrs[2], addrs[0], addrs[3]]; + let addrs = [addrs[0], addrs[1], addrs[2], addrs[0]]; assert_matches!( sanitize_entries(&addrs, /*sockets:*/ &[]), Err(Error::DuplicateIpAddr(_)) @@ -812,7 +854,7 @@ mod tests { } // Unused IP address. { - let sockets: Vec<_> = (0..4u8) + let sockets: Vec<_> = (0..2u8) .map(|key| SocketEntry { key, index: key, @@ -852,11 +894,57 @@ mod tests { } } + #[test] + fn test_reject_ipv6_addresses() { + let addr = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 1, 0, 0, 0, 0, 1)); + let socket = SocketAddr::new(addr, 8000); + + assert_matches!(sanitize_socket(&socket), Err(Error::Ipv6IpAddr(a)) if a == addr); + assert!(!ContactInfo::is_valid_address( + &socket, + &SocketAddrSpace::Unspecified + )); + + let mut node = ContactInfo::default(); + assert_matches!(node.set_gossip(socket), Err(Error::Ipv6IpAddr(a)) if a == addr); + + let addrs = [addr]; + let sockets = [SocketEntry { + key: SOCKET_TAG_GOSSIP, + index: 0, + offset: 8000, + }]; + assert_matches!( + sanitize_entries(&addrs, &sockets), + Err(Error::Ipv6IpAddr(a)) if a == addr + ); + + let node = ContactInfo { + pubkey: Pubkey::new_unique(), + wallclock: 0, + outset: 0, + shred_version: 0, + version: solana_version::Version::default(), + addrs: addrs.to_vec(), + sockets: sockets.to_vec(), + extensions: Vec::default(), + cache: EMPTY_SOCKET_ADDR_CACHE, + }; + let bytes = bincode::serialize(&node).unwrap(); + assert!(bincode::deserialize::(&bytes).is_err()); + assert!(wincode::deserialize::(&bytes).is_err()); + } + #[test] fn test_round_trip() { const KEYS: Range = 0u8..16u8; let mut rng = rand::rng(); - let addrs: Vec = repeat_with(|| new_rand_addr(&mut rng)).take(8).collect(); + let addrs = [ + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 2)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 3)), + IpAddr::V4(Ipv4Addr::new(10, 0, 1, 4)), + ]; let mut node = ContactInfo { pubkey: Pubkey::new_unique(), wallclock: rng.random(), @@ -869,17 +957,13 @@ mod tests { cache: EMPTY_SOCKET_ADDR_CACHE, }; let mut sockets = HashMap::::new(); - for _ in 0..1 << 14 { - let addr = addrs.choose(&mut rng).unwrap(); - let socket = SocketAddr::new(*addr, new_rand_port(&mut rng)); + for i in 0..1 << 10 { + let addr = addrs[i % addrs.len()]; + let socket = SocketAddr::new(addr, 10_000u16 + i as u16); let key = rng.random_range(KEYS.start..KEYS.end); - if sanitize_socket(&socket).is_ok() { - sockets.insert(key, socket); - assert_matches!(node.set_socket(key, socket), Ok(())); - assert_matches!(sanitize_entries(&node.addrs, &node.sockets), Ok(())); - } else { - assert_matches!(node.set_socket(key, socket), Err(_)); - } + sockets.insert(key, socket); + assert_matches!(node.set_socket(key, socket), Ok(())); + assert_matches!(sanitize_entries(&node.addrs, &node.sockets), Ok(())); for key in KEYS.clone() { let socket = sockets.get(&key); assert_eq!(node.get_socket(key).ok().as_ref(), socket); @@ -976,8 +1060,10 @@ mod tests { .is_ok() ); // Assert that serde round trips. - let bytes = bincode::serialize(&node).unwrap(); - let other: ContactInfo = bincode::deserialize(&bytes).unwrap(); + let bytes = wincode::serialize(&node).unwrap(); + assert_eq!(bytes, bincode::serialize(&node).unwrap()); + let other: ContactInfo = wincode::deserialize(&bytes).unwrap(); + assert_eq!(other, bincode::deserialize::(&bytes).unwrap()); assert_eq!(node, other); } } @@ -990,9 +1076,7 @@ mod tests { rng.random(), // wallclock rng.random(), // shred_version ); - let socket = repeat_with(|| new_rand_socket(&mut rng)) - .find(|socket| matches!(sanitize_socket(socket), Ok(()))) - .unwrap(); + let socket = SocketAddr::from((Ipv4Addr::new(10, 1, 0, 1), 12_345)); node.set_alpenglow(socket).unwrap(); assert_eq!(node.alpenglow().unwrap(), socket); node.remove_alpenglow(); @@ -1018,11 +1102,15 @@ mod tests { // Updated socket address is not a duplicate instance. { let mut other = node.clone(); - while other.set_gossip(new_rand_socket(&mut rng)).is_err() {} - while other - .set_serve_repair(Protocol::UDP, new_rand_socket(&mut rng)) - .is_err() - {} + other + .set_gossip(SocketAddr::from((Ipv4Addr::new(10, 1, 0, 2), 12_346))) + .unwrap(); + other + .set_serve_repair( + Protocol::UDP, + SocketAddr::from((Ipv4Addr::new(10, 1, 0, 3), 12_347)), + ) + .unwrap(); assert!(!node.check_duplicate(&other)); assert!(!other.check_duplicate(&node)); assert_eq!(node.overrides(&other), None); @@ -1092,4 +1180,41 @@ mod tests { assert_eq!(other.overrides(&node), Some(true)); } } + + #[test] + fn test_wincode_compatibility_contact_info() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let node = ContactInfo::new_rand(&mut rng, None); + + let bincode_bytes = bincode::serialize(&node).unwrap(); + let wincode_decoded: ContactInfo = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(node, wincode_decoded); + + let wincode_bytes = wincode::serialize(&node).unwrap(); + assert_eq!(wincode_bytes, bincode_bytes); + let bincode_decoded: ContactInfo = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(node, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_socket_entry() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let entry = SocketEntry { + key: rng.random(), + index: rng.random(), + offset: rng.random(), + }; + + let bincode_bytes = bincode::serialize(&entry).unwrap(); + let wincode_decoded: SocketEntry = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(entry, wincode_decoded); + + let wincode_bytes = wincode::serialize(&entry).unwrap(); + let bincode_decoded: SocketEntry = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(entry, bincode_decoded); + } + } } diff --git a/gossip/src/crds.rs b/gossip/src/crds.rs index a4f0cc9a9fd..32b062661d6 100644 --- a/gossip/src/crds.rs +++ b/gossip/src/crds.rs @@ -55,7 +55,7 @@ use { }, }; -const CRDS_SHARDS_BITS: u32 = 12; +pub(crate) const CRDS_SHARDS_BITS: u32 = 12; // Number of vote slots to track in an lru-cache for metrics. const VOTE_SLOTS_METRICS_CAP: usize = 100; // Required number of leading zero bits for crds signature to get reported to influx diff --git a/gossip/src/crds_data.rs b/gossip/src/crds_data.rs index 56117a8ee3c..4d324c394fe 100644 --- a/gossip/src/crds_data.rs +++ b/gossip/src/crds_data.rs @@ -14,7 +14,12 @@ use { solana_time_utils::timestamp, solana_transaction::Transaction, solana_vote::vote_parser, - std::collections::BTreeSet, + std::{collections::BTreeSet, mem::MaybeUninit}, + wincode::{ + ReadError, ReadResult, SchemaRead, SchemaWrite, TypeMeta, WriteResult, + config::Config, + io::{Reader, Writer}, + }, }; pub(crate) const MAX_WALLCLOCK: u64 = 1_000_000_000_000_000; @@ -30,22 +35,62 @@ pub(crate) type EpochSlotsIndex = u8; pub(crate) const MAX_EPOCH_SLOTS: EpochSlotsIndex = 255; // Helper for deprecated types -#[derive(Serialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Serialize, Clone, Debug, PartialEq, Eq, SchemaWrite)] pub(crate) struct Deprecated {} reject_deserialize!(Deprecated, "Trying to deserialize deprecated type"); +// Wincode schema that reads a u8 and rejects non-zero values, mirroring the +// serde `reject_nonzero_u8` deserializer used on the LowestSlot index field. +struct RejectNonzeroU8; +unsafe impl SchemaWrite for RejectNonzeroU8 { + type Src = u8; + const TYPE_META: TypeMeta = >::TYPE_META; + + fn size_of(src: &u8) -> WriteResult { + >::size_of(src) + } + fn write(writer: impl Writer, src: &u8) -> WriteResult<()> { + >::write(writer, src) + } +} +unsafe impl<'de, C: Config> SchemaRead<'de, C> for RejectNonzeroU8 { + type Dst = u8; + const TYPE_META: TypeMeta = >::TYPE_META.keep_zero_copy(false); + + fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + let index = >::get(reader)?; + if index != 0 { + return Err(ReadError::Custom("LowestSlot index must be 0")); + } + dst.write(index); + Ok(()) + } +} + /// CrdsData that defines the different types of items CrdsValues can hold /// * Merge Strategy - Latest wallclock is picked /// * LowestSlot index is deprecated #[allow(clippy::large_enum_variant)] -#[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, AbiEnumVisitor, StableAbi), + frozen_abi( + abi_digest = "Dqsrw64E4ALKusk5cjbckL6vNHw1ESwTzKUqKrMSQCic", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[cfg_attr(test, derive(strum_macros::EnumCount))] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] pub enum CrdsData { #[allow(private_interfaces)] LegacyContactInfo(Deprecated), // Deprecated Vote(VoteIndex, Vote), LowestSlot( - #[serde(deserialize_with = "reject_nonzero_u8")] u8, // u8 is deprecated + #[serde(deserialize_with = "reject_nonzero_u8")] + #[wincode(with = "RejectNonzeroU8")] + u8, // u8 is deprecated LowestSlot, ), #[allow(private_interfaces)] @@ -215,8 +260,16 @@ impl From<&ContactInfo> for CrdsData { } } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "EMCUxeoxyA9ATkmZpACEx6a6mPd5nV16g9vpeuFzpyG5", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] pub struct SnapshotHashes { pub from: Pubkey, pub full: (Slot, Hash), @@ -242,8 +295,16 @@ impl Sanitize for SnapshotHashes { } } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "BjFXktfmwd6TT8kdWVEeMJquARTUe8cUvo2oDwzMfhPi", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] pub struct LowestSlot { pub(crate) from: Pubkey, root: Slot, //deprecated @@ -254,12 +315,13 @@ pub struct LowestSlot { } impl LowestSlot { - #[cfg(any(test, feature = "dev-context-only-utils"))] + // Conformance-only accessors; unused under DCOU. + #[cfg(any(test, feature = "conformance"))] pub(crate) fn wallclock(&self) -> u64 { self.wallclock } - #[cfg(any(test, feature = "dev-context-only-utils"))] + #[cfg(any(test, feature = "conformance"))] pub(crate) fn from(&self) -> &Pubkey { &self.from } @@ -322,13 +384,22 @@ where } } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi), + frozen_abi( + abi_digest = "BuDZ4shsJhBqELG47eMS3cDM8EcqHBGMSVu99JeW6VdM", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, SchemaWrite)] pub struct Vote { pub(crate) from: Pubkey, transaction: Transaction, pub(crate) wallclock: u64, #[serde(skip_serializing)] + #[wincode(skip)] slot: Option, } @@ -365,12 +436,13 @@ impl Vote { &self.transaction } - #[cfg(any(test, feature = "dev-context-only-utils"))] + // Conformance-only accessors; unused under DCOU. + #[cfg(any(test, feature = "conformance"))] pub(crate) fn from(&self) -> &Pubkey { &self.from } - #[cfg(any(test, feature = "dev-context-only-utils"))] + #[cfg(any(test, feature = "conformance"))] pub(crate) fn wallclock(&self) -> u64 { self.wallclock } @@ -400,6 +472,27 @@ impl<'de> Deserialize<'de> for Vote { } } +unsafe impl<'de, C: Config> SchemaRead<'de, C> for Vote { + type Dst = Self; + + fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + #[derive(SchemaRead)] + struct VoteLite { + from: Pubkey, + transaction: Transaction, + wallclock: u64, + } + let lite = >::get(reader)?; + lite.transaction + .sanitize() + .map_err(|_| ReadError::Custom("Vote: invalid transaction"))?; + let vote = Vote::new(lite.from, lite.transaction, lite.wallclock) + .ok_or(ReadError::Custom("Vote: invalid vote tx"))?; + dst.write(vote); + Ok(()) + } +} + pub(crate) fn sanitize_wallclock(wallclock: u64) -> Result<(), SanitizeError> { if wallclock >= MAX_WALLCLOCK { Err(SanitizeError::ValueOutOfBounds) @@ -419,10 +512,80 @@ macro_rules! reject_deserialize { Err(serde::de::Error::custom($msg)) } } + + // Mirror the serde rejection for wincode so deprecated types fail to deserialize + // by either path, whether read directly or as a CrdsData variant. + unsafe impl<'de, C: wincode::config::Config> wincode::SchemaRead<'de, C> for $ty { + type Dst = Self; + fn read( + _reader: impl wincode::io::Reader<'de>, + _dst: &mut std::mem::MaybeUninit, + ) -> wincode::ReadResult<()> { + Err(wincode::ReadError::Custom($msg)) + } + } }; } pub(crate) use reject_deserialize; +// `Vote` derives only `StableAbi` (not `StableAbiSample`): its `slot` field is +// recomputed from the transaction on deserialize, and the transaction must parse +// as a vote, so independent per-field sampling can't produce round-trippable +// values. Building via `Vote::new` with a valid vote transaction does, which lets +// the digest test use `test_roundtrip = "eq_and_wire"`. +#[cfg(feature = "frozen-abi")] +impl solana_frozen_abi::rand::distr::Distribution + for solana_frozen_abi::rand::distr::StandardUniform +{ + fn sample(&self, rng: &mut R) -> Vote { + use {solana_frozen_abi::stable_abi::StableAbi, solana_keypair::Keypair}; + let node = Keypair::new_from_array(rng.random()); + let vote = Keypair::new_from_array(rng.random()); + let authorized_voter = Keypair::new_from_array(rng.random()); + let transaction = solana_vote::vote_transaction::new_vote_transaction( + /*slots:*/ vec![rng.random()], + /*bank_hash:*/ ::random(rng), + /*blockhash:*/ ::random(rng), + &node, + &vote, + &authorized_voter, + /*switch_proof_hash:*/ None, + ); + Vote::new( + ::random(rng), + transaction, + rng.random(), + ) + .expect("vote transaction must parse as a vote") + } +} + +// `CrdsData` derives only `StableAbi` (not `StableAbiSample`): the auto-derived +// enum sampler picks a uniform variant including the deprecated ones, which wrap +// `Deprecated` and reject deserialization. This sampler emits only the +// non-deprecated, round-trippable variants (with `LowestSlot`'s index pinned to +// 0), so the digest test can use `test_roundtrip = "eq_and_wire"`. +#[cfg(feature = "frozen-abi")] +impl solana_frozen_abi::rand::distr::Distribution + for solana_frozen_abi::rand::distr::StandardUniform +{ + fn sample(&self, rng: &mut R) -> CrdsData { + use solana_frozen_abi::stable_abi::StableAbi; + match rng.random_range(0..8u8) { + 0 => CrdsData::Vote(rng.random(), ::random(rng)), + 1 => CrdsData::LowestSlot(0, ::random(rng)), + 2 => CrdsData::EpochSlots(rng.random(), ::random(rng)), + 3 => CrdsData::DuplicateShred(rng.random(), ::random(rng)), + 4 => CrdsData::SnapshotHashes(::random(rng)), + 5 => CrdsData::ContactInfo(::random(rng)), + 6 => CrdsData::RestartLastVotedForkSlots( + ::random(rng), + ), + _ => CrdsData::RestartHeaviestFork(::random(rng)), + } + } +} + #[cfg(test)] mod test { use { @@ -494,8 +657,10 @@ mod test { ) .unwrap(); assert_eq!(vote.slot, Some(7)); - let bytes = bincode::serialize(&vote).unwrap(); - let other = bincode::deserialize(&bytes[..]).unwrap(); + let bytes = wincode::serialize(&vote).unwrap(); + assert_eq!(bytes, bincode::serialize(&vote).unwrap()); + let other = wincode::deserialize(&bytes[..]).unwrap(); + assert_eq!(other, bincode::deserialize::(&bytes[..]).unwrap()); assert_eq!(vote, other); assert_eq!(other.slot, Some(7)); let bytes = bincode::options().serialize(&vote).unwrap(); @@ -504,6 +669,66 @@ mod test { assert_eq!(other.slot, Some(7)); } + #[test] + fn test_wincode_compatibility_lowest_slot() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let lowest_slot = LowestSlot::new_rand(&mut rng, None); + + let bincode_bytes = bincode::serialize(&lowest_slot).unwrap(); + let wincode_decoded: LowestSlot = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(lowest_slot, wincode_decoded); + + let wincode_bytes = wincode::serialize(&lowest_slot).unwrap(); + let bincode_decoded: LowestSlot = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(lowest_slot, bincode_decoded); + + assert_eq!(bincode_bytes, wincode_bytes); + } + } + + #[test] + fn test_wincode_compatibility_snapshot_hashes() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let num_incremental = rng.random_range(0usize..5); + let snapshot_hashes = SnapshotHashes { + from: solana_pubkey::new_rand(), + full: (rng.random(), Hash::new_unique()), + incremental: (0..num_incremental) + .map(|_| (rng.random(), Hash::new_unique())) + .collect(), + wallclock: new_rand_timestamp(&mut rng), + }; + + let bincode_bytes = bincode::serialize(&snapshot_hashes).unwrap(); + let wincode_decoded: SnapshotHashes = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(snapshot_hashes, wincode_decoded); + + let wincode_bytes = wincode::serialize(&snapshot_hashes).unwrap(); + let bincode_decoded: SnapshotHashes = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(snapshot_hashes, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_vote() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let vote = + Vote::new(keypair.pubkey(), new_test_vote_tx(&mut rng), timestamp()).unwrap(); + + let bincode_bytes = bincode::serialize(&vote).unwrap(); + let wincode_decoded: Vote = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(vote, wincode_decoded); + + let wincode_bytes = wincode::serialize(&vote).unwrap(); + let bincode_decoded: Vote = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(vote, bincode_decoded); + } + } + #[test] fn test_max_epoch_slots_index() { let keypair = Keypair::new(); @@ -528,8 +753,10 @@ mod test { CrdsData::LegacySnapshotHashes(Deprecated {}), ]; - for value in deprecated_values { - let bytes = bincode::serialize(&value).unwrap(); + for value in &deprecated_values { + let bytes = wincode::serialize(value).unwrap(); + assert_eq!(bytes, bincode::serialize(value).unwrap()); + assert!(wincode::deserialize::(&bytes[..]).is_err()); assert!(bincode::deserialize::(&bytes[..]).is_err()); } @@ -544,7 +771,9 @@ mod test { // LowestSlot(0, ...) -> should be deserialized successfully let lowest_slot = CrdsData::LowestSlot(0, LowestSlot::new(keypair.pubkey(), 0, timestamp())); - let bytes = bincode::serialize(&lowest_slot).unwrap(); + let bytes = wincode::serialize(&lowest_slot).unwrap(); + assert_eq!(bytes, bincode::serialize(&lowest_slot).unwrap()); + assert!(wincode::deserialize::(&bytes[..]).is_ok()); assert!(bincode::deserialize::(&bytes[..]).is_ok()); } } diff --git a/gossip/src/crds_gossip.rs b/gossip/src/crds_gossip.rs index e3eb3d06cdc..f023ba694bf 100644 --- a/gossip/src/crds_gossip.rs +++ b/gossip/src/crds_gossip.rs @@ -236,6 +236,7 @@ impl CrdsGossip { output_size_limit: usize, // Limit number of crds values returned. now: u64, should_retain_crds_value: impl Fn(&CrdsValue) -> bool + Sync, + try_consume_scan_budget: impl Fn(&PullRequest, usize) -> bool + Sync, stats: &GossipStats, ) -> Vec> { CrdsGossipPull::generate_pull_responses( @@ -245,6 +246,7 @@ impl CrdsGossip { output_size_limit, now, should_retain_crds_value, + try_consume_scan_budget, stats, ) } @@ -414,8 +416,6 @@ mod test { ) .unwrap(); let ping_cache = PingCache::new( - &mut rand::rng(), - Instant::now(), Duration::from_secs(20 * 60), // ttl Duration::from_secs(20 * 60) / 64, // rate_limit_delay 128, // capacity diff --git a/gossip/src/crds_gossip_pull.rs b/gossip/src/crds_gossip_pull.rs index e5132fe9c54..eef29150e36 100644 --- a/gossip/src/crds_gossip_pull.rs +++ b/gossip/src/crds_gossip_pull.rs @@ -48,6 +48,7 @@ use { }, time::Duration, }, + wincode::{SchemaRead, SchemaWrite}, }; pub const CRDS_GOSSIP_PULL_CRDS_TIMEOUT_MS: u64 = 15000; @@ -56,14 +57,27 @@ const FAILED_INSERTS_RETENTION_MS: u64 = 20_000; pub const FALSE_RATE: f64 = 0.1f64; pub const KEYS: f64 = 8f64; -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] pub struct CrdsFilter { pub filter: Bloom, mask: u64, mask_bits: u32, } +#[cfg(debug_assertions)] +pub(crate) const MIN_NUM_BLOOM_ITEMS: usize = 512; +#[cfg(not(debug_assertions))] +pub(crate) const MIN_NUM_BLOOM_ITEMS: usize = 65_536; + +// Loosest mask_bits floor accepted for incoming pull requests. +// `PACKET_DATA_SIZE` avoids rejecting honest smaller bloom filters. +static MIN_PULL_REQUEST_MASK_BITS: LazyLock = LazyLock::new(|| { + let max_bits = (PACKET_DATA_SIZE * 8) as f64; + let max_items = CrdsFilter::max_items(max_bits, FALSE_RATE, KEYS); + CrdsFilter::mask_bits(MIN_NUM_BLOOM_ITEMS as f64, max_items) +}); + // Incoming gossip pull request from a remote node. pub struct PullRequest { pub pubkey: Pubkey, // remote node's pubkey @@ -84,22 +98,28 @@ impl Default for CrdsFilter { impl solana_sanitize::Sanitize for CrdsFilter { fn sanitize(&self) -> std::result::Result<(), solana_sanitize::SanitizeError> { - self.filter.sanitize()?; + if self.mask_bits < *MIN_PULL_REQUEST_MASK_BITS { + return Err(solana_sanitize::SanitizeError::InvalidValue); + } Ok(()) } } impl CrdsFilter { - #[cfg(any(test, feature = "dev-context-only-utils"))] + // Conformance-only accessors; unused under DCOU. + #[cfg(any(test, feature = "conformance"))] pub(crate) fn mask(&self) -> u64 { self.mask } - #[cfg(any(test, feature = "dev-context-only-utils"))] - pub(crate) fn get_mask_bits(&self) -> u32 { + pub fn get_mask_bits(&self) -> u32 { self.mask_bits } + pub fn bloom_hash_count(&self) -> usize { + self.filter.keys.len() + } + #[cfg(any(test, feature = "dev-context-only-utils"))] pub fn new_rand(num_items: usize, max_bytes: usize) -> Self { let max_bits = (max_bytes * 8) as f64; @@ -328,6 +348,7 @@ impl CrdsGossipPull { output_size_limit: usize, // Limit number of crds values returned. now: u64, should_retain_crds_value: impl Fn(&CrdsValue) -> bool + Sync, + try_consume_scan_budget: impl Fn(&PullRequest, usize) -> bool + Sync, stats: &GossipStats, ) -> Vec> { Self::filter_crds_values( @@ -337,6 +358,7 @@ impl CrdsGossipPull { output_size_limit, now, should_retain_crds_value, + try_consume_scan_budget, stats, ) } @@ -448,10 +470,6 @@ impl CrdsGossipPull { bloom_size: usize, ) -> Vec { const PAR_MIN_LENGTH: usize = 512; - #[cfg(debug_assertions)] - const MIN_NUM_BLOOM_ITEMS: usize = 512; - #[cfg(not(debug_assertions))] - const MIN_NUM_BLOOM_ITEMS: usize = 65_536; let failed_inserts = self.failed_inserts.read().unwrap(); // crds should be locked last after self.failed_inserts. let crds = crds.read().unwrap(); @@ -485,6 +503,9 @@ impl CrdsGossipPull { now: u64, // Predicate returning false if the CRDS value should be discarded. should_retain_crds_value: impl Fn(&CrdsValue) -> bool + Sync, + // False drops the request before scanning CRDS. + // Implementations may consume caller-owned scan budget. + try_consume_scan_budget: impl Fn(&PullRequest, usize) -> bool + Sync, stats: &GossipStats, ) -> Vec> { let msg_timeout = CRDS_GOSSIP_PULL_CRDS_TIMEOUT_MS; @@ -497,6 +518,7 @@ impl CrdsGossipPull { let output_size_limit = output_size_limit.try_into().unwrap_or(i64::MAX); let output_size_limit = AtomicI64::new(output_size_limit); let crds = crds.read().unwrap(); + let crds_len = crds.len(); let apply_filter = |request: &PullRequest| { if output_size_limit.load(Ordering::Relaxed) <= 0 { return Vec::default(); @@ -507,6 +529,10 @@ impl CrdsGossipPull { dropped_requests.fetch_add(1, Ordering::Relaxed); return Vec::default(); } + // Charge only requests that passed cheaper pre-scan checks. + if !try_consume_scan_budget(request, crds_len) { + return Vec::default(); + } let caller_wallclock = caller_wallclock.checked_add(jitter).unwrap_or(0); let pred = |entry: &&VersionedCrdsValue| { debug_assert!(filter.test_mask(entry.value.hash())); @@ -606,7 +632,7 @@ impl Index<&Pubkey> for CrdsTimeouts<'_> { } } -// Returns max_bytes for the bloom filter such that bincode serialized +// Returns max_bytes for the bloom filter such that the serialized // Protocol::PullRequest(CrdsFilter, CrdsValue) fits in a packet. pub(crate) fn get_max_bloom_filter_bytes(caller: &CrdsValue) -> usize { // Maps serialized size of CrdsFilter to max_bytes of bloom filter. @@ -618,7 +644,7 @@ pub(crate) fn get_max_bloom_filter_bytes(caller: &CrdsValue) -> usize { let mut iter = Vec::::from(filters) .into_iter() .map(|filter| { - bincode::serialized_size(&filter) + wincode::serialized_size(&filter) .map(usize::try_from) .unwrap() .unwrap() @@ -638,12 +664,12 @@ pub(crate) fn get_max_bloom_filter_bytes(caller: &CrdsValue) -> usize { }); out }); - // Maximum bincode serialized size of CrdsFilter in + // Maximum serialized size of CrdsFilter in // Protocol::PullRequest(CrdsFilter, CrdsValue) let size_of_filter = PACKET_DATA_SIZE .checked_sub( // 4 bytes for u32 enum variant identifier of Protocol. - 4 + caller.bincode_serialized_size(), + 4 + caller.serialized_size(), ) .unwrap(); MAX_BYTES_CACHE @@ -667,10 +693,7 @@ pub(crate) mod tests { solana_packet::PACKET_DATA_SIZE, solana_sha256_hasher::hash, solana_time_utils::timestamp, - std::{ - net::{IpAddr, Ipv6Addr}, - time::Instant, - }, + std::{net::Ipv4Addr, time::Instant}, test_case::test_case, }; @@ -728,8 +751,6 @@ pub(crate) mod tests { fn new_ping_cache() -> PingCache { PingCache::new( - &mut rand::rng(), - Instant::now(), Duration::from_secs(20 * 60), // ttl Duration::from_secs(20 * 60) / 64, // rate_limit_delay 128, // capacity @@ -1119,7 +1140,8 @@ pub(crate) mod tests { &requests, usize::MAX, new_wallclock, - |_| true, + |_| true, // should_retain_crds_value + |_, _| true, // try_consume_scan_budget &GossipStats::default(), ); assert_eq!(rsp.len(), 2); @@ -1327,11 +1349,15 @@ pub(crate) mod tests { let packet_data_size_range = (PACKET_DATA_SIZE - 7)..=PACKET_DATA_SIZE; let max_bytes = get_max_bloom_filter_bytes(caller); let filters = CrdsFilterSet::new(rng, num_items, max_bytes); - let request_bytes = caller.bincode_serialized_size() as u64; + let request_bytes = caller.serialized_size() as u64; for filter in Vec::::from(filters) { - let request_bytes = 4 + request_bytes + bincode::serialized_size(&filter).unwrap(); + let filter_size = wincode::serialized_size(&filter).unwrap(); + assert_eq!(filter_size, bincode::serialized_size(&filter).unwrap()); + let request_bytes = 4 + request_bytes + filter_size; let request = Protocol::PullRequest(filter, caller.clone()); - let request = bincode::serialize(&request).unwrap(); + let request_wincode = wincode::serialize(&request).unwrap(); + assert_eq!(request_wincode, bincode::serialize(&request).unwrap()); + let request = request_wincode; assert!(packet_data_size_range.contains(&request.len())); assert_eq!(request.len() as u64, request_bytes); } @@ -1360,15 +1386,14 @@ pub(crate) mod tests { verify_get_max_bloom_filter_bytes(&mut rng, &caller, num_items); } let node = { - let addr = Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888); - let socket = SocketAddr::new(IpAddr::from(addr), 8053); + let socket = SocketAddr::from((Ipv4Addr::new(192, 0, 2, 1), 8053)); let mut node = ContactInfo::new_with_socketaddr(&keypair.pubkey(), &socket); node.set_shred_version(rng.random()); node }; { let caller = CrdsValue::new(CrdsData::from(&node), &keypair); - assert_eq!(get_max_bloom_filter_bytes(&caller), 1165); + assert_eq!(get_max_bloom_filter_bytes(&caller), 1175); verify_get_max_bloom_filter_bytes(&mut rng, &caller, num_items); } } @@ -1481,4 +1506,22 @@ pub(crate) mod tests { assert!(filter.test_mask(&hash)); assert!(!filter.test_mask(&bad_hash)); } + + #[test] + fn test_wincode_compatibility_crds_filter() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let num_items = rng.random_range(0..1000); + let max_bytes = rng.random_range(32..512); + let filter = CrdsFilter::new_rand(num_items, max_bytes); + + let bincode_bytes = bincode::serialize(&filter).unwrap(); + let wincode_decoded: CrdsFilter = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(filter, wincode_decoded); + + let wincode_bytes = wincode::serialize(&filter).unwrap(); + let bincode_decoded: CrdsFilter = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(filter, bincode_decoded); + } + } } diff --git a/gossip/src/crds_gossip_push.rs b/gossip/src/crds_gossip_push.rs index c8f2346efd9..5f9353dd247 100644 --- a/gossip/src/crds_gossip_push.rs +++ b/gossip/src/crds_gossip_push.rs @@ -294,8 +294,6 @@ mod tests { fn new_ping_cache() -> PingCache { PingCache::new( - &mut rand::rng(), - Instant::now(), Duration::from_secs(20 * 60), // ttl Duration::from_secs(20 * 60) / 64, // rate_limit_delay 128, // capacity diff --git a/gossip/src/crds_value.rs b/gossip/src/crds_value.rs index f8310c1e5a3..fee06a2cea1 100644 --- a/gossip/src/crds_value.rs +++ b/gossip/src/crds_value.rs @@ -4,9 +4,8 @@ use { crds_data::{CrdsData, EpochSlotsIndex, VoteIndex}, duplicate_shred::DuplicateShredIndex, epoch_slots::EpochSlots, + sigverify_cache::SigVerifyCache, }, - arrayvec::ArrayVec, - bincode::serialize, rand::Rng, serde::{Deserialize, Serialize, de::Deserializer}, solana_hash::Hash, @@ -16,16 +15,33 @@ use { solana_sanitize::{Sanitize, SanitizeError}, solana_signature::Signature, solana_signer::Signer, - std::borrow::{Borrow, Cow}, + std::{ + borrow::{Borrow, Cow}, + mem::MaybeUninit, + }, + wincode::{ReadError, ReadResult, SchemaRead, SchemaWrite, config::Config, io::Reader}, }; /// CrdsValue that is replicated across the cluster -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "4ABukH5bS69APB3bu1hbMiGyeKPw21nzXAVzCRMtKPih", + abi_serializer = ["bincode", "wincode"], + // `hash` is recomputed from [signature, data] on deserialize, so it can't + // match an independently-sampled value; verify the wire round-trip only. + test_roundtrip = "wire_only", + ) +)] +#[derive(Serialize, Clone, Debug, PartialEq, Eq, SchemaWrite)] pub struct CrdsValue { signature: Signature, data: CrdsData, #[serde(skip_serializing)] + #[wincode(skip)] + // Not on the wire (recomputed on deserialize); keep it out of the sample. + #[cfg_attr(feature = "frozen-abi", stable_abi_sample(with = "Hash::default()"))] hash: Hash, // Sha256 hash of [signature, data]. } @@ -42,7 +58,7 @@ impl Signable for CrdsValue { } fn signable_data(&self) -> Cow<'static, [u8]> { - Cow::Owned(serialize(&self.data).expect("failed to serialize CrdsData")) + Cow::Owned(wincode::serialize(&self.data).expect("failed to serialize CrdsData")) } fn get_signature(&self) -> Signature { @@ -50,7 +66,12 @@ impl Signable for CrdsValue { } fn set_signature(&mut self, signature: Signature) { - self.signature = signature + self.signature = signature; + // Keep self.hash consistent with the new signature: callers (CRDS + // shards, pull filters, the verified-CRDS cache) treat hash as the + // value's identity. + self.hash = + compute_crds_value_hash(&signature, &self.data).expect("failed to serialize CrdsData"); } fn verify(&self) -> bool { @@ -89,10 +110,42 @@ impl CrdsValueLabel { } impl CrdsValue { + /// Verify the signature, short-circuiting on a previously-verified value + /// hash and otherwise reusing a cached decompressed verifying key. Both + /// caches are populated only after `verify_strict` succeeds, so neither can + /// be seeded with arbitrary entries to evict useful ones. + pub(crate) fn verify_with_cache(&self, cache: &SigVerifyCache) -> bool { + if cache.verified_values.contains(&self.hash) { + return true; + } + let pubkey = self.pubkey(); + let signable_data = self.signable_data(); + let message = signable_data.borrow(); + let sig_bytes: [u8; 64] = self.signature.into(); + let signature = ed25519_dalek::Signature::from_bytes(&sig_bytes); + let verified = match cache.verifying_keys.get(&pubkey) { + Some(vk) => vk.verify_strict(message, &signature).is_ok(), + None => { + let Ok(vk) = ed25519_dalek::VerifyingKey::try_from(pubkey.as_ref()) else { + return false; + }; + if vk.verify_strict(message, &signature).is_err() { + return false; + } + cache.verifying_keys.insert(pubkey, vk); + true + } + }; + if verified { + cache.verified_values.insert(self.hash); + } + verified + } + pub fn new(data: CrdsData, keypair: &Keypair) -> Self { - let bincode_serialized_data = bincode::serialize(&data).unwrap(); - let signature = keypair.sign_message(&bincode_serialized_data); - let hash = solana_sha256_hasher::hashv(&[signature.as_ref(), &bincode_serialized_data]); + let serialized_data = wincode::serialize(&data).unwrap(); + let signature = keypair.sign_message(&serialized_data); + let hash = hash_signed_data(&signature, &serialized_data); Self { signature, data, @@ -102,9 +155,8 @@ impl CrdsValue { #[cfg(test)] pub(crate) fn new_unsigned(data: CrdsData) -> Self { - let bincode_serialized_data = bincode::serialize(&data).unwrap(); let signature = Signature::default(); - let hash = solana_sha256_hasher::hashv(&[signature.as_ref(), &bincode_serialized_data]); + let hash = compute_crds_value_hash(&signature, &data).unwrap(); Self { signature, data, @@ -190,15 +242,55 @@ impl CrdsValue { Some(epoch_slots) } - /// Returns the bincode serialized size (in bytes) of the CrdsValue. - pub fn bincode_serialized_size(&self) -> usize { - bincode::serialized_size(&self) + /// Returns the serialized size (in bytes) of the CrdsValue. + pub fn serialized_size(&self) -> usize { + wincode::serialized_size(&self) .map(usize::try_from) .unwrap() .unwrap() } } +// sha256(signature || serialized_data), for callers that already serialized. +fn hash_signed_data(signature: &Signature, serialized_data: &[u8]) -> Hash { + solana_sha256_hasher::hashv(&[signature.as_ref(), serialized_data]) +} + +// Computes sha256(signature || serialize(data)) using a stack buffer. +// PACKET_DATA_SIZE is always enough since the value originated in a packet. +fn compute_crds_value_hash(signature: &Signature, data: &CrdsData) -> wincode::WriteResult { + let mut buffer = [MaybeUninit::::uninit(); PACKET_DATA_SIZE]; + let mut writer: &mut [MaybeUninit] = &mut buffer; + wincode::serialize_into(&mut writer, data)?; + let written = PACKET_DATA_SIZE - writer.len(); + // SAFETY: wincode's "Writer for &mut [MaybeUninit]" initializes every + // consumed slot before advancing the cursor, so the first "written" bytes are init. + let bytes = unsafe { std::slice::from_raw_parts(buffer.as_ptr().cast::(), written) }; + Ok(hash_signed_data(signature, bytes)) +} + +// Manual implementation of SchemaRead for CrdsValue in order to populate +// CrdsValue.hash which is skipped in serialization. +unsafe impl<'de, C: Config> SchemaRead<'de, C> for CrdsValue { + type Dst = Self; + fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + #[derive(SchemaRead)] + struct CrdsValueLite { + signature: Signature, + data: CrdsData, + } + let CrdsValueLite { signature, data } = >::get(reader)?; + let hash = compute_crds_value_hash(&signature, &data) + .map_err(|_| ReadError::Custom("failed to serialize CrdsData"))?; + dst.write(Self { + signature, + data, + hash, + }); + Ok(()) + } +} + // Manual implementation of Deserialize for CrdsValue in order to populate // CrdsValue.hash which is skipped in serialization. impl<'de> Deserialize<'de> for CrdsValue { @@ -212,12 +304,7 @@ impl<'de> Deserialize<'de> for CrdsValue { data: CrdsData, } let CrdsValue { signature, data } = CrdsValue::deserialize(deserializer)?; - // To compute the hash of the received CrdsData we need to re-serialize it - // PACKET_DATA_SIZE is always enough since we have just received the value in a packet - // ArrayVec allows us to write serialized data into stack memory without initializing it - let mut buffer = ArrayVec::::new(); - bincode::serialize_into(&mut buffer, &data).map_err(serde::de::Error::custom)?; - let hash = solana_sha256_hasher::hashv(&[signature.as_ref(), &buffer]); + let hash = compute_crds_value_hash(&signature, &data).map_err(serde::de::Error::custom)?; Ok(Self { signature, data, @@ -231,7 +318,6 @@ mod test { use { super::*, crate::crds_data::{LowestSlot, Vote}, - bincode::deserialize, rand::SeedableRng as _, rand_chacha::ChaChaRng, solana_keypair::Keypair, @@ -244,6 +330,63 @@ mod test { std::str::FromStr, }; + #[test] + fn test_verify_with_cache() { + let keypair = Keypair::new(); + let wrong_keypair = Keypair::new(); + let mut value = CrdsValue::new_unsigned(CrdsData::from(ContactInfo::new_localhost( + &keypair.pubkey(), + 0, + ))); + let cache = SigVerifyCache::new(); + + assert!( + !value.verify_with_cache(&cache), + "unsigned value must not verify" + ); + assert!( + cache.verifying_keys.get(&value.pubkey()).is_none(), + "failed verification must not populate the cache" + ); + + // Wrong signature: rejected, and nothing cached. Checked before any valid + // insert so we know the rejection itself didn't seed the cache. + value.sign(&wrong_keypair); + assert!( + !value.verify_with_cache(&cache), + "value signed by the wrong key must not verify" + ); + assert!( + cache.verifying_keys.get(&value.pubkey()).is_none(), + "failed verification must not populate the cache" + ); + assert!( + !cache.verified_values.contains(&value.hash), + "failed verification must not populate the verified-value cache" + ); + + // Cold miss: a valid signature verifies and populates both caches. + value.sign(&keypair); + assert!( + value.verify_with_cache(&cache), + "validly signed value must verify" + ); + assert!( + cache.verifying_keys.get(&value.pubkey()).is_some(), + "successful verification must populate the verifying-key cache" + ); + assert!( + cache.verified_values.contains(&value.hash), + "successful verification must populate the verified-value cache" + ); + + // Warm hit: verifies again, now short-circuited by the verified-value cache. + assert!( + value.verify_with_cache(&cache), + "validly signed value must verify on a cache hit" + ); + } + #[test] fn test_keys_and_values() { let mut rng = rand::rng(); @@ -293,20 +436,29 @@ mod test { verify_signatures(&mut v, &keypair, &wrong_keypair); } - fn serialize_deserialize_value(value: &mut CrdsValue, keypair: &Keypair) { + fn serialize_deserialize_value( + value: &mut CrdsValue, + keypair: &Keypair, + cache: &SigVerifyCache, + ) { let num_tries = 10; value.sign(keypair); let original_signature = value.get_signature(); for _ in 0..num_tries { - let serialized_value = serialize(value).unwrap(); - let deserialized_value: CrdsValue = deserialize(&serialized_value).unwrap(); + let serialized_value = wincode::serialize(value).unwrap(); + assert_eq!(serialized_value, bincode::serialize(value).unwrap()); + let deserialized_value: CrdsValue = wincode::deserialize(&serialized_value).unwrap(); + assert_eq!( + deserialized_value, + bincode::deserialize::(&serialized_value).unwrap() + ); // Signatures shouldn't change let deserialized_signature = deserialized_value.get_signature(); assert_eq!(original_signature, deserialized_signature); // After deserializing, check that the signature is still the same - assert!(deserialized_value.verify()); + assert!(deserialized_value.verify_with_cache(cache)); } } @@ -315,12 +467,13 @@ mod test { correct_keypair: &Keypair, wrong_keypair: &Keypair, ) { - assert!(!value.verify()); + let cache = SigVerifyCache::new(); + assert!(!value.verify_with_cache(&cache)); value.sign(correct_keypair); - assert!(value.verify()); + assert!(value.verify_with_cache(&cache)); value.sign(wrong_keypair); - assert!(!value.verify()); - serialize_deserialize_value(value, correct_keypair); + assert!(!value.verify_with_cache(&cache)); + serialize_deserialize_value(value, correct_keypair, &cache); } #[test] @@ -395,16 +548,49 @@ mod test { CrdsValue::new(CrdsData::Vote(5, vote), &keypair) }, ]; - let bytes = bincode::serialize(&values).unwrap(); + let bytes = wincode::serialize(&values).unwrap(); + assert_eq!(bytes, bincode::serialize(&values).unwrap()); // Serialized bytes are fixed and should never change. assert_eq!( solana_sha256_hasher::hash(&bytes), Hash::from_str("BTg284TRo5S5PpbA9YZaab5rKeoLNAj7arwadvG6XVLT").unwrap() ); // serialize -> deserialize should round trip. + let wincode_values = wincode::deserialize::>(&bytes).unwrap(); assert_eq!( - bincode::deserialize::>(&bytes).unwrap(), - values + wincode_values, + bincode::deserialize::>(&bytes).unwrap() ); + assert_eq!(wincode_values, values); + } + + #[test] + fn test_wincode_compatibility_crds_value() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let value = CrdsValue::new_rand(&mut rng, None); + let bincode_bytes = bincode::serialize(&value).unwrap(); + let wincode_bytes = wincode::serialize(&value).unwrap(); + assert_eq!( + bincode_bytes, + wincode_bytes, + "bytes differ for {:?}", + value.label() + ); + // Deprecated types and Vote with test-only invalid transactions intentionally + // fail serde deserialization; skip those. + let Ok(bincode_decoded) = bincode::deserialize::(&bincode_bytes) else { + continue; + }; + let wincode_decoded: CrdsValue = wincode::deserialize(&bincode_bytes) + .unwrap_or_else(|e| panic!("wincode deser failed for {:?}: {e}", value.label())); + assert_eq!( + bincode_decoded, + wincode_decoded, + "deser mismatch for {:?}", + value.label() + ); + assert_eq!(value, bincode_decoded); + } } } diff --git a/gossip/src/duplicate_shred.rs b/gossip/src/duplicate_shred.rs index 14e2e408adc..8f4e4202a10 100644 --- a/gossip/src/duplicate_shred.rs +++ b/gossip/src/duplicate_shred.rs @@ -16,6 +16,7 @@ use { num::TryFromIntError, }, thiserror::Error, + wincode::{ReadError, SchemaRead, SchemaWrite, WriteError}, }; const DUPLICATE_SHRED_HEADER_SIZE: usize = 63; @@ -23,8 +24,16 @@ const DUPLICATE_SHRED_HEADER_SIZE: usize = 63; pub(crate) type DuplicateShredIndex = u16; pub(crate) const MAX_DUPLICATE_SHREDS: DuplicateShredIndex = 512; -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "9zVjcmgLcLv1YDhBwDFYgMMjtpoZjk77YoL3sLBnABTf", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, SchemaWrite, SchemaRead)] pub struct DuplicateShred { pub(crate) from: Pubkey, pub(crate) wallclock: u64, @@ -52,25 +61,26 @@ impl DuplicateShred { self.chunk_index } - #[cfg(any(test, feature = "dev-context-only-utils"))] + // Conformance-only accessors; unused under DCOU. + #[cfg(any(test, feature = "conformance"))] #[inline] pub(crate) fn from(&self) -> &Pubkey { &self.from } - #[cfg(any(test, feature = "dev-context-only-utils"))] + #[cfg(any(test, feature = "conformance"))] #[inline] pub(crate) fn wallclock(&self) -> u64 { self.wallclock } - #[cfg(any(test, feature = "dev-context-only-utils"))] + #[cfg(any(test, feature = "conformance"))] #[inline] pub(crate) fn slot(&self) -> Slot { self.slot } - #[cfg(any(test, feature = "dev-context-only-utils"))] + #[cfg(any(test, feature = "conformance"))] #[inline] pub(crate) fn chunk(&self) -> &[u8] { &self.chunk @@ -108,9 +118,9 @@ pub enum Error { #[error("missing data chunk")] MissingDataChunk, #[error("wincode deserialization error")] - WincodeReadError(#[from] wincode::ReadError), + WincodeReadError(#[from] ReadError), #[error("wincode serialization error")] - WincodeWriteError(#[from] wincode::WriteError), + WincodeWriteError(#[from] WriteError), #[error("shred type mismatch")] ShredTypeMismatch, #[error("slot mismatch")] @@ -378,6 +388,33 @@ pub(crate) mod tests { std::sync::Arc, }; + #[test] + fn test_wincode_compatibility_duplicate_shred() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let chunk_len = rng.random_range(0..64usize); + let dup = DuplicateShred { + from: Pubkey::new_unique(), + wallclock: rng.random(), + slot: rng.random(), + _unused: rng.random(), + _unused_shred_type: rng.random(), + num_chunks: rng.random(), + chunk_index: rng.random(), + chunk: (0..chunk_len).map(|_| rng.random::()).collect(), + }; + + let bincode_bytes = bincode::serialize(&dup).unwrap(); + let wincode_decoded: DuplicateShred = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(dup, wincode_decoded); + + let wincode_bytes = wincode::serialize(&dup).unwrap(); + assert_eq!(wincode_bytes, bincode_bytes); + let bincode_decoded: DuplicateShred = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(dup, bincode_decoded); + } + } + #[test] fn test_duplicate_shred_header_size() { let dup = DuplicateShred { @@ -390,14 +427,12 @@ pub(crate) mod tests { chunk: Vec::default(), _unused: 0, }; - assert_eq!( - bincode::serialize(&dup).unwrap().len(), - DUPLICATE_SHRED_HEADER_SIZE - ); - assert_eq!( - bincode::serialized_size(&dup).unwrap(), - DUPLICATE_SHRED_HEADER_SIZE as u64 - ); + let dup_bytes = wincode::serialize(&dup).unwrap(); + assert_eq!(dup_bytes, bincode::serialize(&dup).unwrap()); + assert_eq!(dup_bytes.len(), DUPLICATE_SHRED_HEADER_SIZE); + let dup_size = wincode::serialized_size(&dup).unwrap(); + assert_eq!(dup_size, bincode::serialized_size(&dup).unwrap()); + assert_eq!(dup_size, DUPLICATE_SHRED_HEADER_SIZE as u64); } pub(crate) fn new_rand_shred( diff --git a/gossip/src/duplicate_shred_handler.rs b/gossip/src/duplicate_shred_handler.rs index d4b67c4a309..3389bac5f7f 100644 --- a/gossip/src/duplicate_shred_handler.rs +++ b/gossip/src/duplicate_shred_handler.rs @@ -131,10 +131,16 @@ impl DuplicateShredHandler { shred1.into_payload(), shred2.into_payload(), )?; - // Notify duplicate consensus state machine - self.duplicate_slots_sender - .send(slot) - .map_err(|_| Error::DuplicateSlotSenderFailure)?; + + // Notify duplicate consensus state machine. Drop if channel is over 50% full + // to avoid blocking replay. + if self.duplicate_slots_sender.len() * 2 + < self.duplicate_slots_sender.capacity().unwrap_or(usize::MAX) + { + self.duplicate_slots_sender + .try_send(slot) + .map_err(|_| Error::DuplicateSlotSenderFailure)?; + } } self.consumed.insert(slot, true); } @@ -213,7 +219,7 @@ mod tests { epoch_specs::TestEpochSpecs, protocol::DUPLICATE_SHRED_MAX_PAYLOAD_SIZE, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, itertools::Itertools, solana_keypair::Keypair, solana_ledger::{ @@ -305,7 +311,7 @@ mod tests { let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank( &bank_forks_arc.read().unwrap().working_bank(), )); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let start_slot: Slot = 10; let mut duplicate_shred_handler = DuplicateShredHandler::new( @@ -409,7 +415,7 @@ mod tests { let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank( &bank_forks_arc.read().unwrap().working_bank(), )); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let mut duplicate_shred_handler = DuplicateShredHandler::new( blockstore.clone(), leader_schedule_cache, diff --git a/gossip/src/epoch_slots.rs b/gossip/src/epoch_slots.rs index 8ceeb16ea2c..09936113d9e 100644 --- a/gossip/src/epoch_slots.rs +++ b/gossip/src/epoch_slots.rs @@ -3,7 +3,6 @@ use { crds_data::{self, MAX_SLOT, MAX_WALLCLOCK}, protocol::MAX_CRDS_OBJECT_SIZE, }, - bincode::serialized_size, bv::BitVec, flate2::{Compress, Compression, Decompress, FlushCompress, FlushDecompress}, serde::{Deserialize, Serialize}, @@ -11,14 +10,21 @@ use { solana_pubkey::Pubkey, solana_sanitize::{Sanitize, SanitizeError}, std::{borrow::Cow, cell::RefCell, sync::Arc}, + wincode::{SchemaRead, SchemaWrite}, }; pub const MAX_SLOTS_PER_ENTRY: usize = 2048 * 8; -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] pub struct Uncompressed { pub first_slot: Slot, pub num: usize, + // `BitVec` has no `StableAbi` impl; sample a non-empty one (see `sample_bit_vec`). + #[cfg_attr( + feature = "frozen-abi", + stable_abi_sample(with = "std::sync::Arc::new(solana_bloom::bloom::sample_bit_vec(rng))") + )] pub slots: Arc>, } @@ -43,8 +49,8 @@ impl Sanitize for Uncompressed { } } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] pub struct Flate2 { pub first_slot: Slot, pub num: usize, @@ -197,8 +203,11 @@ impl Uncompressed { } } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, AbiEnumVisitor, StableAbi, StableAbiSample) +)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] pub enum CompressedSlots { Flate2(Flate2), Uncompressed(Uncompressed), @@ -264,8 +273,16 @@ impl CompressedSlots { } } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "7fXxq9nG7ywp9LhMiGTJ7MLNtghPo9mz5xQNE74RxWGj", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, SchemaRead, SchemaWrite)] pub struct EpochSlots { pub from: Pubkey, pub slots: Vec, @@ -340,8 +357,8 @@ impl EpochSlots { Ok(()) } pub fn max_compressed_slot_size(&self) -> isize { - let len_header = serialized_size(self).unwrap(); - let len_slot = serialized_size(&CompressedSlots::default()).unwrap(); + let len_header = wincode::serialized_size(self).unwrap(); + let len_slot = wincode::serialized_size(&CompressedSlots::default()).unwrap(); MAX_CRDS_OBJECT_SIZE as isize - (len_header + len_slot) as isize } @@ -529,6 +546,98 @@ mod tests { assert!(slots.to_slots(0).eq(range)); } + #[test] + fn test_wincode_compatibility_uncompressed() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let mut unc = Uncompressed::new(rng.random_range(1..=16usize)); + let first: Slot = rng.random_range(0..10_000); + let num: u64 = rng.random_range(0..64); + let slots: Vec<_> = (0..num).map(|i| first + i).collect(); + unc.add(&slots); + + let bincode_bytes = bincode::serialize(&unc).unwrap(); + let wincode_decoded: Uncompressed = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(unc, wincode_decoded); + + let wincode_bytes = wincode::serialize(&unc).unwrap(); + let bincode_decoded: Uncompressed = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(unc, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_flate2() { + let mut rng = rand::rng(); + for _ in 0..1000 { + // Arbitrary bytes — wire-format equivalence does not require a valid compressed stream. + let len = rng.random_range(0..64usize); + let flate2 = Flate2 { + first_slot: rng.random(), + num: rng.random_range(0..1000), + compressed: Arc::new((0..len).map(|_| rng.random::()).collect()), + }; + + let bincode_bytes = bincode::serialize(&flate2).unwrap(); + let wincode_decoded: Flate2 = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(flate2, wincode_decoded); + + let wincode_bytes = wincode::serialize(&flate2).unwrap(); + let bincode_decoded: Flate2 = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(flate2, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_compressed_slots() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let mut unc = Uncompressed::new(rng.random_range(1..=16usize)); + let first: Slot = rng.random_range(0..10_000); + let num: u64 = rng.random_range(0..64); + let slots: Vec<_> = (0..num).map(|i| first + i).collect(); + unc.add(&slots); + let cs = CompressedSlots::Uncompressed(unc); + + let bincode_bytes = bincode::serialize(&cs).unwrap(); + let wincode_decoded: CompressedSlots = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(cs, wincode_decoded); + let wincode_bytes = wincode::serialize(&cs).unwrap(); + let bincode_decoded: CompressedSlots = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(cs, bincode_decoded); + + let len = rng.random_range(0..64usize); + let cs = CompressedSlots::Flate2(Flate2 { + first_slot: rng.random(), + num: rng.random_range(0..1000), + compressed: Arc::new((0..len).map(|_| rng.random::()).collect()), + }); + + let bincode_bytes = bincode::serialize(&cs).unwrap(); + let wincode_decoded: CompressedSlots = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(cs, wincode_decoded); + let wincode_bytes = wincode::serialize(&cs).unwrap(); + let bincode_decoded: CompressedSlots = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(cs, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_epoch_slots() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let epoch_slots = EpochSlots::new_rand(&mut rng, None); + + let bincode_bytes = bincode::serialize(&epoch_slots).unwrap(); + let wincode_decoded: EpochSlots = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(epoch_slots, wincode_decoded); + + let wincode_bytes = wincode::serialize(&epoch_slots).unwrap(); + let bincode_decoded: EpochSlots = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(epoch_slots, bincode_decoded); + } + } + fn make_rand_slots(rng: &mut R) -> impl Iterator + '_ { repeat_with(|| rng.random_range(1..5)).scan(0, |slot, step| { *slot += step; diff --git a/gossip/src/gossip_error.rs b/gossip/src/gossip_error.rs index d9457b07091..89f439ca1b2 100644 --- a/gossip/src/gossip_error.rs +++ b/gossip/src/gossip_error.rs @@ -20,7 +20,7 @@ pub enum GossipError { #[error("send error")] SendError, #[error("serialization error")] - Serialize(#[from] Box), + Serialize(#[from] wincode::Error), } impl std::convert::From> for GossipError { diff --git a/gossip/src/gossip_service.rs b/gossip/src/gossip_service.rs index df29dd7e379..08e71bb386b 100644 --- a/gossip/src/gossip_service.rs +++ b/gossip/src/gossip_service.rs @@ -2,24 +2,33 @@ use { crate::{ + XdpSender, cluster_info::{ClusterInfo, GOSSIP_CHANNEL_CAPACITY}, cluster_info_metrics::submit_gossip_stats, contact_info::ContactInfo, epoch_specs::EpochSpecs, }, - crossbeam_channel::Sender, + crossbeam_channel::{Sender, TrySendError}, solana_keypair::Keypair, - solana_net_utils::{DEFAULT_IP_ECHO_SERVER_THREADS, SocketAddrSpace}, - solana_perf::recycler::Recycler, + solana_net_utils::{ + DEFAULT_IP_ECHO_SERVER_THREADS, SocketAddrSpace, + multihomed_sockets::{BindIpAddrs, MultihomedSocketProvider, SocketProvider}, + }, + solana_perf::{packet::PacketBatch, recycler::Recycler}, solana_pubkey::Pubkey, solana_signer::Signer, solana_streamer::{ evicting_sender::EvictingSender, - streamer::{self, StreamerReceiveStats}, + sendmmsg::{SendPktsError, batch_send}, + streamer::{ + self, PacketBatchReceiver, ResponseSender, StreamerReceiveStats, + filter_packets_by_socket_addr_space, responder_loop, + }, }, std::{ collections::HashSet, - net::{SocketAddr, TcpListener, UdpSocket}, + io, + net::{IpAddr, SocketAddr, TcpListener, UdpSocket}, sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -40,6 +49,7 @@ impl GossipService { cluster_info: &Arc, mut epoch_specs: Option>, gossip_sockets: Arc<[UdpSocket]>, + xdp_sender: Option, gossip_validators: Option>, should_check_duplicate_instance: bool, stats_reporter_sender: Option>>, @@ -50,7 +60,7 @@ impl GossipService { trace!( "GossipService: id: {}, listening on primary interface: {:?}, all available \ interfaces: {:?}", - &cluster_info.id(), + cluster_info.id(), gossip_sockets[0].local_addr().unwrap(), gossip_sockets, ); @@ -91,12 +101,18 @@ impl GossipService { gossip_validators, exit.clone(), ); - let t_responder = streamer::responder_atomic( + let gossip_responder_socket = match xdp_sender { + Some(xdp_sender) => GossipResponderSocket::Xdp(xdp_sender), + None => GossipResponderSocket::Udp { + sockets: gossip_sockets.clone(), + bind_ip_addrs: cluster_info.bind_ip_addrs(), + socket_addr_space, + }, + }; + let t_responder = run_responder( "Gossip", - gossip_sockets, - cluster_info.bind_ip_addrs(), + gossip_responder_socket, response_receiver, - socket_addr_space, stats_reporter_sender, ); let t_metrics = Builder::new() @@ -329,6 +345,7 @@ pub fn make_node( None, gossip_sockets, None, + None, should_check_duplicate_instance, None, exit, @@ -336,6 +353,129 @@ pub fn make_node( (gossip_service, ip_echo, cluster_info) } +enum GossipResponderSocket { + Udp { + sockets: Arc<[UdpSocket]>, + bind_ip_addrs: Arc, + socket_addr_space: SocketAddrSpace, + }, + Xdp(XdpSender), +} + +fn run_responder( + name: &'static str, + socket: GossipResponderSocket, + r: PacketBatchReceiver, + stats_reporter_sender: Option>>, +) -> JoinHandle<()> { + Builder::new() + .name(format!("solRspndr{name}")) + .spawn(move || match socket { + GossipResponderSocket::Udp { + sockets, + bind_ip_addrs, + socket_addr_space, + } => responder_loop( + name, + r, + GossipUdpSocketProvider::new(sockets, bind_ip_addrs, socket_addr_space), + stats_reporter_sender, + ), + GossipResponderSocket::Xdp(xdp_sender) => { + responder_loop(name, r, GossipXdpSender(xdp_sender), stats_reporter_sender) + } + }) + .unwrap() +} + +struct GossipUdpSocketProvider { + socket_provider: MultihomedSocketProvider, + socket_addr_space: SocketAddrSpace, +} + +impl GossipUdpSocketProvider { + pub fn new( + sockets: Arc<[UdpSocket]>, + bind_ip_addrs: Arc, + socket_addr_space: SocketAddrSpace, + ) -> Self { + Self { + socket_provider: MultihomedSocketProvider::new(sockets, bind_ip_addrs), + socket_addr_space, + } + } +} + +impl ResponseSender for GossipUdpSocketProvider { + fn send_batch(&self, batch: PacketBatch) -> std::result::Result<(), SendPktsError> { + let packets = filter_packets_by_socket_addr_space(batch.iter(), &self.socket_addr_space); + let sock = self.socket_provider.current_socket_ref(); + batch_send(sock, packets.collect::>()) + } +} + +struct GossipXdpSender(XdpSender); + +impl ResponseSender for GossipXdpSender { + fn send_batch(&self, batch: PacketBatch) -> std::result::Result<(), SendPktsError> { + let packets = batch.iter().filter_map(|pkt| { + let addr = pkt.meta().socket_addr(); + let data = pkt.data(..)?; + + // For XDP, we don't support IPv6 and no private or loopback IPv4 addresses. + match addr.ip() { + IpAddr::V4(ip) if !ip.is_private() && !ip.is_loopback() => Some((data, addr)), + _ => None, + } + }); + + let mut num_sent = 0; + let mut num_dropped_full = 0; + let mut num_dropped_disconnected = 0; + + for (idx, (payload, addr)) in packets.enumerate() { + match self + .0 + .try_send(idx, addr, bytes::Bytes::copy_from_slice(payload)) + { + Ok(()) => { + num_sent += 1; + } + Err(TrySendError::Full(_)) => { + num_dropped_full += 1; + continue; + } + Err(TrySendError::Disconnected(_)) => { + num_dropped_disconnected += 1; + continue; + } + } + } + + let num_failed = num_dropped_full + num_dropped_disconnected; + if num_failed > 0 { + let kind = if num_dropped_disconnected != 0 { + io::ErrorKind::BrokenPipe + } else { + io::ErrorKind::WouldBlock + }; + return Err(SendPktsError::IoError( + io::Error::new( + kind, + format!( + "XDP sender failed to enqueue {num_failed} out of {num_total} gossip \ + packets ({num_dropped_full} full queue, {num_dropped_disconnected} \ + disconnected)", + num_total = num_sent + num_failed + ), + ), + num_failed, + )); + } + Ok(()) + } +} + #[cfg(test)] mod tests { use { @@ -358,6 +498,7 @@ mod tests { None, tn.sockets.gossip, None, + None, true, // should_check_duplicate_instance None, exit.clone(), diff --git a/gossip/src/harness.rs b/gossip/src/harness.rs new file mode 100644 index 00000000000..1dda7277552 --- /dev/null +++ b/gossip/src/harness.rs @@ -0,0 +1,59 @@ +use {crate::protocol::gossip_decode_to_effects, prost::Message, std::ffi::c_int}; + +/// Conformance harness entry point for gossip message decoding. +/// +/// Deserializes a binary-encoded gossip message from the input buffer, +/// decodes it into effects, and writes the Protobuf-encoded result into +/// the output buffer. +/// +/// # Parameters +/// - `out_ptr`: Pointer to the output buffer where the Protobuf-encoded +/// result will be written. +/// - `out_psz`: Pointer to a `u64` that on entry holds the capacity of the +/// output buffer and on successful return is updated to the number of +/// bytes actually written. +/// - `in_ptr`: Pointer to the input buffer containing the binary-encoded +/// gossip message. May be null when `in_sz` is 0. +/// - `in_sz`: Length of the input buffer in bytes. +/// +/// # Returns +/// `1` on success, `0` on failure (null pointers, zero-length input, or +/// output buffer too small). +/// +/// # Safety +/// - `out_ptr` must point to a writable buffer whose capacity is stored at +/// `*out_psz`. +/// - `out_psz` must be a valid, aligned pointer to a `u64`. +/// - `in_ptr` must point to a readable buffer of at least `in_sz` bytes +/// (or be null/ignored when `in_sz` is 0). +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sol_compat_gossip_decode_v1( + out_ptr: *mut u8, + out_psz: *mut u64, + in_ptr: *const u8, + in_sz: u64, +) -> c_int { + if out_ptr.is_null() || out_psz.is_null() { + return 0; + } + + let in_slice = if in_sz == 0 { + &[] + } else if in_ptr.is_null() { + return 0; + } else { + unsafe { std::slice::from_raw_parts(in_ptr, in_sz as usize) } + }; + + let effects = gossip_decode_to_effects(in_slice); + let out_vec = effects.encode_to_vec(); + + let out_cap = unsafe { *out_psz } as usize; + if out_vec.len() > out_cap { + return 0; + } + let out_slice = unsafe { std::slice::from_raw_parts_mut(out_ptr, out_cap) }; + out_slice[..out_vec.len()].copy_from_slice(&out_vec); + unsafe { *out_psz = out_vec.len() as u64 }; + 1 +} diff --git a/gossip/src/lib.rs b/gossip/src/lib.rs index 056aa386144..4e77940a4fe 100644 --- a/gossip/src/lib.rs +++ b/gossip/src/lib.rs @@ -31,8 +31,11 @@ mod protocol; mod push_active_set; mod received_cache; pub mod restart_crds_values; +mod sigverify_cache; pub mod weighted_shuffle; +pub use solana_net_utils::PinnedXdpSender as XdpSender; + #[macro_use] extern crate log; @@ -47,7 +50,10 @@ extern crate solana_frozen_abi_macro; #[macro_use] extern crate solana_metrics; -#[cfg(feature = "dev-context-only-utils")] +#[cfg(feature = "conformance")] pub use protocol::gossip_decode_to_effects; +#[cfg(feature = "conformance")] +mod harness; + mod wire_format_tests; diff --git a/gossip/src/node.rs b/gossip/src/node.rs index 9dbe1c9a73d..c628aa9a7e0 100644 --- a/gossip/src/node.rs +++ b/gossip/src/node.rs @@ -401,7 +401,7 @@ impl Node { trace!("new ContactInfo: {info:?}"); let sockets = Sockets { - alpenglow: Some(alpenglow), + alpenglow, gossip: gossip_sockets.into_iter().collect(), tvu: tvu_sockets, tpu_vote: tpu_vote_sockets, @@ -421,7 +421,7 @@ impl Node { tpu_transaction_forwarding_clients, rpc_sts_client, }; - info!("Bound all network sockets as follows: {:?}", &sockets); + info!("Bound all network sockets as follows: {sockets:?}"); Node { info, sockets, diff --git a/gossip/src/ping_pong.rs b/gossip/src/ping_pong.rs index 6174e5f74a1..a5bdf5d8ddd 100644 --- a/gossip/src/ping_pong.rs +++ b/gossip/src/ping_pong.rs @@ -4,7 +4,6 @@ use { rand::{CryptoRng, Rng}, serde::{Deserialize, Serialize}, serde_big_array::BigArray, - siphasher::sip::SipHasher24, solana_hash::Hash, solana_keypair::{Keypair, signable::Signable}, solana_pubkey::Pubkey, @@ -13,21 +12,20 @@ use { solana_signer::Signer, std::{ borrow::Cow, - hash::{Hash as _, Hasher}, net::{IpAddr, SocketAddr}, time::{Duration, Instant}, }, + wincode::{SchemaRead, SchemaWrite}, }; -const KEY_REFRESH_CADENCE: Duration = Duration::from_secs(60); const PING_PONG_HASH_PREFIX: &[u8] = "SOLANA_PING_PONG".as_bytes(); const PONG_SIGNATURE_SAMPLE_LEADING_ZEROS: u32 = 5; // For backward compatibility we are using a const generic parameter here. // N should always be >= 8 and only the first 8 bytes are used. So the new code // should only use N == 8. -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Debug, Deserialize, Serialize)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Debug, Deserialize, PartialEq, Serialize, SchemaRead, SchemaWrite)] pub struct Ping { from: Pubkey, #[serde(with = "BigArray")] @@ -35,8 +33,18 @@ pub struct Ping { signature: Signature, } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Debug, Deserialize, Serialize)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "Gab1D5ug6ZAB5sRNmBpoM8JyxsixccLLaWxYZwmueVYA", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Debug, Deserialize, PartialEq, Serialize, SchemaRead, SchemaWrite)] +// repr(C) makes this struct zero-copy eligible in wincode. +#[repr(C)] pub struct Pong { from: Pubkey, hash: Hash, // Hash of received ping token. @@ -52,15 +60,9 @@ pub struct PingCache { ttl: Duration, // Rate limit delay to generate pings for a given address rate_limit_delay: Duration, - // Hashers initialized with random keys, rotated at KEY_REFRESH_CADENCE. - // Because at the moment that the keys are rotated some pings might already - // be in the flight, we need to keep the two most recent hashers. - hashers: [SipHasher24; 2], - // When hashers were last refreshed. - key_refresh: Instant, - // Timestamp of last ping message sent to a remote node. - // Used to rate limit pings to remote nodes. - pings: LruCache<(Pubkey, SocketAddr), Instant>, + // Timestamp and expected pong hash for each pinged remote node. + // Used for rate-limiting and pong validation. + pings: LruCache<(Pubkey, SocketAddr), (Instant, Hash)>, // Verified pong responses from remote nodes. pongs: LruCache<(Pubkey, SocketAddr), Instant>, // Timestamp of last ping message sent to a remote IP. @@ -154,38 +156,27 @@ impl Signable for Pong { } impl PingCache { - pub fn new( - rng: &mut R, - now: Instant, - ttl: Duration, - rate_limit_delay: Duration, - cap: usize, - ) -> Self { + pub fn new(ttl: Duration, rate_limit_delay: Duration, cap: usize) -> Self { // Sanity check ttl/rate_limit_delay assert!(rate_limit_delay <= ttl / 2); Self { ttl, rate_limit_delay, - hashers: std::array::from_fn(|_| SipHasher24::new_with_key(&rng.random())), - key_refresh: now, pings: LruCache::new(cap), pongs: LruCache::new(cap), ping_times: LruCache::new(cap), } } - /// Checks if the pong hash, pubkey and socket match a ping message sent - /// out previously. If so records current timestamp for the remote node and - /// returns true. + /// Checks if the pong hash matches a ping message sent out previously. + /// If so records current timestamp for the remote node and returns true. /// Note: Does not verify the signature. pub fn add(&mut self, pong: &Pong, socket: SocketAddr, now: Instant) -> bool { let remote_node = (pong.pubkey(), socket); - if !self.hashers.iter().copied().any(|hasher| { - let token = make_ping_token::(hasher, &remote_node); - hash_ping_token(&token) == pong.hash - }) { + if !matches!(self.pings.peek(&remote_node), Some((_, h)) if *h == pong.hash) { return false; }; + self.pings.pop(&remote_node); self.pongs.put(remote_node, now); if let Some(sent_time) = self.ping_times.pop(&socket.ip()) && should_report_message_signature( @@ -215,13 +206,23 @@ impl PingCache { ) -> Option> { // Rate limit consecutive pings sent to a remote node. if matches!(self.pings.peek(&remote_node), - Some(&t) if now.saturating_duration_since(t) < self.rate_limit_delay) + Some((t, _)) if now.saturating_duration_since(*t) < self.rate_limit_delay) { return None; } - self.pings.put(remote_node, now); - self.maybe_refresh_key(rng, now); - let token = make_ping_token::(self.hashers[0], &remote_node); + let token = { + let mut token = [0u8; N]; + const FILL: usize = std::mem::size_of::(); + const { assert!(N >= FILL, "N must be >= size_of::()") }; + let entropy: [u8; FILL] = rng.random(); + *token + .first_chunk_mut::() + .expect("token is known to fit FILL bytes") = entropy; + token + }; + // The hash we expect to see in the Pong message + let ping_hash = hash_ping_token(&token); + self.pings.put(remote_node, (now, ping_hash)); self.ping_times.put(remote_node.1.ip(), Instant::now()); Some(Ping::new(token, keypair)) } @@ -262,33 +263,12 @@ impl PingCache { (check, ping) } - fn maybe_refresh_key(&mut self, rng: &mut R, now: Instant) { - if now.checked_duration_since(self.key_refresh) > Some(KEY_REFRESH_CADENCE) { - let hasher = SipHasher24::new_with_key(&rng.random()); - self.hashers[1] = std::mem::replace(&mut self.hashers[0], hasher); - self.key_refresh = now; - } - } - /// Only for tests and simulations. pub fn mock_pong(&mut self, node: Pubkey, socket: SocketAddr, now: Instant) { self.pongs.put((node, socket), now); } } -fn make_ping_token( - mut hasher: SipHasher24, - remote_node: &(Pubkey, SocketAddr), -) -> [u8; N] { - // TODO: Consider including local node's (pubkey, socket-addr). - remote_node.hash(&mut hasher); - let hash = hasher.finish().to_le_bytes(); - debug_assert!(N >= std::mem::size_of::()); - let mut token = [0u8; N]; - token[..std::mem::size_of::()].copy_from_slice(&hash); - token -} - fn hash_ping_token(token: &[u8; N]) -> Hash { solana_sha256_hasher::hashv(&[PING_PONG_HASH_PREFIX, token]) } @@ -327,7 +307,7 @@ mod tests { let mut rng = rand::rng(); let ttl = Duration::from_millis(256); let delay = ttl / 64; - let mut cache = PingCache::new(&mut rng, Instant::now(), ttl, delay, /*cap=*/ 1000); + let mut cache = PingCache::new(ttl, delay, /*cap=*/ 1000); let this_node = Keypair::new(); let sockets: Vec<_> = (1u8..=3) .map(|i| { @@ -446,6 +426,45 @@ mod tests { } } + #[test] + fn test_wincode_compatibility_ping() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let ping = Ping::<32>::new(rng.random(), &keypair); + + let bincode_bytes = bincode::serialize(&ping).unwrap(); + let wincode_decoded: Ping<32> = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(ping, wincode_decoded); + + let wincode_bytes = wincode::serialize(&ping).unwrap(); + let bincode_decoded: Ping<32> = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(ping, bincode_decoded); + + assert_eq!(bincode_bytes, wincode_bytes); + } + } + + #[test] + fn test_wincode_compatibility_pong() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let ping = Ping::<32>::new(rng.random(), &keypair); + let pong = Pong::new(&ping, &keypair); + + let bincode_bytes = bincode::serialize(&pong).unwrap(); + let wincode_decoded: Pong = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(pong, wincode_decoded); + + let wincode_bytes = wincode::serialize(&pong).unwrap(); + let bincode_decoded: Pong = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(pong, bincode_decoded); + + assert_eq!(bincode_bytes, wincode_bytes); + } + } + #[test] fn test_expired_pong_returns_check_false() { let mut rng = rand::rng(); @@ -456,7 +475,7 @@ mod tests { let ttl = Duration::from_secs(20 * 60); // 20 minutes let delay = ttl / 64; let mut now = Instant::now(); - let mut cache = PingCache::<32>::new(&mut rng, now, ttl, delay, /*cap=*/ 1000); + let mut cache = PingCache::<32>::new(ttl, delay, /*cap=*/ 1000); // Add a pong for the remote node cache.mock_pong(remote_node.0, remote_node.1, now); diff --git a/gossip/src/protocol.rs b/gossip/src/protocol.rs index a667af5d80c..d6422d70e5f 100644 --- a/gossip/src/protocol.rs +++ b/gossip/src/protocol.rs @@ -5,8 +5,8 @@ use { crds_gossip_pull::CrdsFilter, crds_value::CrdsValue, ping_pong::{self, Pong}, + sigverify_cache::SigVerifyCache, }, - bincode::serialize, serde::{Deserialize, Serialize}, solana_keypair::signable::Signable, solana_perf::packet::PACKET_DATA_SIZE, @@ -18,6 +18,7 @@ use { fmt::Debug, result::Result, }, + wincode::{SchemaRead, SchemaWrite}, }; pub(crate) const MAX_CRDS_OBJECT_SIZE: usize = 928; @@ -43,9 +44,30 @@ const PRUNE_DATA_PREFIX: &[u8] = b"\xffSOLANA_PRUNE_DATA"; const GOSSIP_PING_TOKEN_SIZE: usize = 32; /// Minimum serialized size of a Protocol::PullResponse packet. pub(crate) const PULL_RESPONSE_MIN_SERIALIZED_SIZE: usize = 161; +const MIN_CRDS_VALUE_SERIALIZED_SIZE: usize = + PULL_RESPONSE_MIN_SERIALIZED_SIZE - (PACKET_DATA_SIZE - PULL_RESPONSE_MAX_PAYLOAD_SIZE); +const MAX_CRDS_VALUES_PER_PACKET: usize = + (PULL_RESPONSE_MAX_PAYLOAD_SIZE / MIN_CRDS_VALUE_SERIALIZED_SIZE) + 1; +// Wincode's preallocation limit is decoded collection memory, not input bytes. +// Bound it to the largest CRDS value vector that can fit in one gossip packet. +const GOSSIP_PROTOCOL_PREALLOC_LIMIT: usize = + MAX_CRDS_VALUES_PER_PACKET * std::mem::size_of::(); +type GossipProtocolWincodeConfig = + wincode::config::Configuration; /// Gossip protocol messages base enum -#[derive(Serialize, Deserialize, Debug)] +#[cfg_attr( + feature = "frozen-abi", + derive(StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "D3Hqum16i1KHnejUD65odaQSbQnJtTQnTJSUoUrjzY2a", + abi_serializer = ["bincode", "wincode"], + // `Protocol` has no `PartialEq` and embeds `CrdsValue` (whose `hash` is + // recomputed on deserialize), so verify the wire round-trip only. + test_roundtrip = "wire_only", + ) +)] +#[derive(Serialize, Deserialize, Debug, SchemaRead, SchemaWrite)] #[allow(clippy::large_enum_variant)] pub(crate) enum Protocol { PullRequest(CrdsFilter, CrdsValue), @@ -59,11 +81,34 @@ pub(crate) enum Protocol { // Update count_packets_received if new variants are added here. } +#[cfg_attr( + feature = "frozen-abi", + frozen_abi( + abi_digest = "Gab1D5ug6ZAB5sRNmBpoM8JyxsixccLLaWxYZwmueVYA", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] pub(crate) type Ping = ping_pong::Ping; pub(crate) type PingCache = ping_pong::PingCache; -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +pub(crate) fn deserialize_protocol(input: &[u8]) -> wincode::ReadResult { + wincode::config::deserialize_exact::( + input, + GossipProtocolWincodeConfig::new(), + ) +} + +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "GomZf5rFL743zPKH71UShh64JfNvrDBBEC2o2VehinsT", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, SchemaRead, SchemaWrite)] pub(crate) struct PruneData { /// Pubkey of the node that sent this prune data pub(crate) pubkey: Pubkey, @@ -78,10 +123,10 @@ pub(crate) struct PruneData { } impl Protocol { - /// Returns the bincode serialized size (in bytes) of the Protocol. + /// Returns the serialized size (in bytes) of the Protocol. #[cfg(test)] - fn bincode_serialized_size(&self) -> usize { - bincode::serialized_size(self) + fn serialized_size(&self) -> usize { + wincode::serialized_size(self) .map(usize::try_from) .unwrap() .unwrap() @@ -89,11 +134,11 @@ impl Protocol { // Returns true if all signatures verify. #[must_use] - pub(crate) fn verify(&self) -> bool { + pub(crate) fn verify(&self, cache: &SigVerifyCache) -> bool { match self { - Self::PullRequest(_, caller) => caller.verify(), - Self::PullResponse(_, data) => data.iter().all(CrdsValue::verify), - Self::PushMessage(_, data) => data.iter().all(CrdsValue::verify), + Self::PullRequest(_, caller) => caller.verify_with_cache(cache), + Self::PullResponse(_, data) => data.iter().all(|value| value.verify_with_cache(cache)), + Self::PushMessage(_, data) => data.iter().all(|value| value.verify_with_cache(cache)), Self::PruneMessage(_, data) => data.verify(), Self::PingMessage(ping) => ping.verify(), Self::PongMessage(pong) => pong.verify(), @@ -103,7 +148,7 @@ impl Protocol { impl PruneData { fn signable_data_without_prefix(&self) -> Cow<'static, [u8]> { - #[derive(Serialize)] + #[derive(Serialize, SchemaWrite)] struct SignData<'a> { pubkey: &'a Pubkey, prunes: &'a [Pubkey], @@ -116,11 +161,11 @@ impl PruneData { destination: &self.destination, wallclock: self.wallclock, }; - Cow::Owned(serialize(&data).expect("should serialize PruneData")) + Cow::Owned(wincode::serialize(&data).expect("should serialize PruneData")) } fn signable_data_with_prefix(&self) -> Cow<'static, [u8]> { - #[derive(Serialize)] + #[derive(Serialize, SchemaWrite)] struct SignDataWithPrefix<'a> { prefix: &'a [u8], pubkey: &'a Pubkey, @@ -135,7 +180,7 @@ impl PruneData { destination: &self.destination, wallclock: self.wallclock, }; - Cow::Owned(serialize(&data).expect("should serialize PruneDataWithPrefix")) + Cow::Owned(wincode::serialize(&data).expect("should serialize PruneDataWithPrefix")) } fn verify_data(&self, use_prefix: bool) -> bool { @@ -223,7 +268,9 @@ impl Signable for PruneData { /// max_chunk_size. /// Note: some messages cannot be contained within that size so in the worst case this returns /// N nested Vecs with 1 item each. -pub(crate) fn split_gossip_messages( +pub(crate) fn split_gossip_messages< + T: Serialize + Debug + SchemaWrite, +>( max_chunk_size: usize, data_feed: impl IntoIterator, ) -> impl Iterator> { @@ -235,7 +282,7 @@ pub(crate) fn split_gossip_messages( let Some(data) = data_feed.next() else { return (!buffer.is_empty()).then(|| std::mem::take(&mut buffer)); }; - let data_size = match bincode::serialized_size(&data) { + let data_size = match wincode::serialized_size(&data) { Ok(size) => size as usize, Err(err) => { error!("serialized_size failed: {err:?}"); @@ -255,14 +302,13 @@ pub(crate) fn split_gossip_messages( }) } -#[cfg(feature = "dev-context-only-utils")] +#[cfg(feature = "conformance")] pub fn gossip_decode_to_effects(input: &[u8]) -> protosol::protos::GossipEffects { use { crate::{ crds_data::CrdsData, crds_gossip_pull::CrdsFilter as NativeCrdsFilter, crds_value::CrdsValue as NativeCrdsValue, ping_pong::Pong, }, - bincode::Options as _, bv::Bits, protosol::protos::{ GossipBloom, GossipCompressedSlots, GossipContactInfo, GossipCrdsData, @@ -361,7 +407,7 @@ pub fn gossip_decode_to_effects(input: &[u8]) -> protosol::protos::GossipEffects } CrdsData::Vote(idx, vote) => { let tx_bytes = - bincode::serialize(vote.transaction()).expect("vote transaction serialization"); + wincode::serialize(vote.transaction()).expect("vote transaction serialization"); Some(gossip_crds_data::Data::Vote(GossipVote { index: *idx as u32, from: vote.from().to_bytes().to_vec(), @@ -499,11 +545,16 @@ pub fn gossip_decode_to_effects(input: &[u8]) -> protosol::protos::GossipEffects } } - let result = bincode::options() - .with_limit(PACKET_DATA_SIZE as u64) - .with_fixint_encoding() - .reject_trailing_bytes() - .deserialize::(input); + // Reject over-long inputs upfront so an over-long input whose prefix + // encodes a valid Protocol can't slip through; deserialize_exact rejects + // any trailing bytes that remain within the packet window. + if input.len() > PACKET_DATA_SIZE { + return GossipEffects { + valid: false, + msg: None, + }; + } + let result = deserialize_protocol(input); match result { Ok(proto) if proto.sanitize().is_ok() => { @@ -526,15 +577,17 @@ pub(crate) mod tests { super::*, crate::{ contact_info::ContactInfo, - crds_data::{self, CrdsData, LowestSlot, SnapshotHashes, Vote as CrdsVote}, + crds_data::{self, CrdsData, Deprecated, LowestSlot, SnapshotHashes, Vote as CrdsVote}, duplicate_shred::{self, MAX_DUPLICATE_SHREDS, tests::new_rand_shred}, + epoch_slots::EpochSlots, + restart_crds_values::{RestartHeaviestFork, RestartLastVotedForkSlots}, }, rand::Rng, solana_clock::Slot, solana_hash::Hash, solana_keypair::Keypair, solana_ledger::shred::Shredder, - solana_perf::packet::Packet, + solana_perf::{packet::Packet, test_tx::new_test_vote_tx}, solana_signer::Signer, solana_time_utils::timestamp, solana_transaction::Transaction, @@ -599,6 +652,20 @@ pub(crate) mod tests { prune_data } + #[test] + fn test_deserialize_protocol_rejects_large_vec_preallocation() { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&2u32.to_le_bytes()); // Protocol::PushMessage. + bytes.extend_from_slice(&Pubkey::new_unique().to_bytes()); + bytes.extend_from_slice(&u64::MAX.to_le_bytes()); // Vec length. + + let err = deserialize_protocol(&bytes).unwrap_err(); + assert!(matches!( + err, + wincode::ReadError::PreallocationSizeLimit { .. } + )); + } + #[test] fn test_max_snapshot_hashes_with_push_messages() { let mut rng = rand::rng(); @@ -654,7 +721,7 @@ pub(crate) mod tests { let header = Protocol::PushMessage(Pubkey::default(), Vec::default()); assert_eq!( PUSH_MESSAGE_MAX_PAYLOAD_SIZE, - PACKET_DATA_SIZE - header.bincode_serialized_size() + PACKET_DATA_SIZE - header.serialized_size() ); } @@ -663,7 +730,7 @@ pub(crate) mod tests { let header = Protocol::PullResponse(Pubkey::default(), Vec::default()); assert_eq!( PULL_RESPONSE_MAX_PAYLOAD_SIZE, - PACKET_DATA_SIZE - header.bincode_serialized_size() + PACKET_DATA_SIZE - header.serialized_size() ); } @@ -703,9 +770,9 @@ pub(crate) mod tests { let data = CrdsData::DuplicateShred(MAX_DUPLICATE_SHREDS - 1, chunk); let value = CrdsValue::new(data, &keypair); let pull_response = Protocol::PullResponse(keypair.pubkey(), vec![value.clone()]); - assert!(pull_response.bincode_serialized_size() < PACKET_DATA_SIZE); + assert!(pull_response.serialized_size() < PACKET_DATA_SIZE); let push_message = Protocol::PushMessage(keypair.pubkey(), vec![value.clone()]); - assert!(push_message.bincode_serialized_size() < PACKET_DATA_SIZE); + assert!(push_message.serialized_size() < PACKET_DATA_SIZE); } } @@ -715,7 +782,7 @@ pub(crate) mod tests { for _ in 0..100 { let crds_values = vec![CrdsValue::new_rand(&mut rng, None)]; let pull_response = Protocol::PullResponse(Pubkey::new_unique(), crds_values); - let size = pull_response.bincode_serialized_size(); + let size = pull_response.serialized_size(); assert!( PULL_RESPONSE_MIN_SERIALIZED_SIZE <= size, "pull-response serialized size: {size}" @@ -723,6 +790,85 @@ pub(crate) mod tests { } } + #[test] + fn test_min_crds_value_serialized_size_holds() { + let mut rng = rand::rng(); + let keypair = Keypair::new(); + + // Build a DuplicateShred for the corresponding variant. + let leader = Arc::new(Keypair::new()); + let (slot, parent_slot, reference_tick, version) = (53084024, 53084023, 0, 0); + let shredder = Shredder::new(slot, parent_slot, reference_tick, version).unwrap(); + let next_shred_index = rng.random_range(0..32_000); + let shred = new_rand_shred(&mut rng, next_shred_index, &shredder, &leader); + let other_payload = { + let other = new_rand_shred(&mut rng, next_shred_index, &shredder, &leader); + other.into_payload() + }; + let leader_schedule = |s| (s == slot).then_some(leader.pubkey()); + let dup_shred = duplicate_shred::from_shred( + shred, + keypair.pubkey(), + other_payload, + Some(leader_schedule), + timestamp(), + DUPLICATE_SHRED_MAX_PAYLOAD_SIZE, + version, + ) + .unwrap() + .next() + .unwrap(); + + let vote = + CrdsVote::new(keypair.pubkey(), new_test_vote_tx(&mut rng), timestamp()).unwrap(); + + // One representative per CrdsData variant. The array length is keyed to + // strum::EnumCount, so adding a new CrdsData variant without listing it + // here is a compile error (wrong array length). + use strum::EnumCount; + let variants: [CrdsData; CrdsData::COUNT] = [ + CrdsData::LegacyContactInfo(Deprecated {}), + CrdsData::Vote(0, vote), + CrdsData::LowestSlot(0, LowestSlot::new(Pubkey::new_unique(), 0, timestamp())), + CrdsData::LegacySnapshotHashes(Deprecated {}), + CrdsData::AccountsHashes(Deprecated {}), + CrdsData::EpochSlots(0, EpochSlots::new_rand(&mut rng, None)), + CrdsData::LegacyVersion(Deprecated {}), + CrdsData::Version(Deprecated {}), + CrdsData::NodeInstance(Deprecated {}), + CrdsData::DuplicateShred(0, dup_shred), + CrdsData::SnapshotHashes(SnapshotHashes { + from: Pubkey::new_unique(), + full: (0, Hash::default()), + incremental: vec![], + wallclock: timestamp(), + }), + CrdsData::ContactInfo(ContactInfo::new_localhost( + &Pubkey::new_unique(), + timestamp(), + )), + CrdsData::RestartLastVotedForkSlots(RestartLastVotedForkSlots::new_rand( + &mut rng, None, + )), + CrdsData::RestartHeaviestFork(RestartHeaviestFork::new_rand(&mut rng, None)), + ]; + + for data in variants { + let value = CrdsValue::new_unsigned(data); + let bytes = wincode::serialize(&value).unwrap(); + // Deprecated variants fail to deserialize; only assert the bound + // for variants that can appear in a successfully-parsed Protocol. + if wincode::deserialize::(&bytes).is_ok() { + assert!( + bytes.len() >= MIN_CRDS_VALUE_SERIALIZED_SIZE, + "MIN_CRDS_VALUE_SERIALIZED_SIZE ({MIN_CRDS_VALUE_SERIALIZED_SIZE}) \ + underestimates serialized size {}", + bytes.len(), + ); + } + } + } + #[test] fn test_split_messages_small() { let value = CrdsValue::new_unsigned(CrdsData::from(ContactInfo::default())); @@ -763,13 +909,9 @@ pub(crate) mod tests { let header_size = PACKET_DATA_SIZE - PUSH_MESSAGE_MAX_PAYLOAD_SIZE; for values in splits { // Assert that sum of parts equals the whole. - let size = header_size - + values - .iter() - .map(CrdsValue::bincode_serialized_size) - .sum::(); + let size = header_size + values.iter().map(CrdsValue::serialized_size).sum::(); let message = Protocol::PushMessage(self_pubkey, values); - assert_eq!(message.bincode_serialized_size(), size); + assert_eq!(message.serialized_size(), size); // Assert that the message fits into a packet. assert!(Packet::from_data(Some(&socket), message).is_ok()); } @@ -801,13 +943,9 @@ pub(crate) mod tests { let header_size = PACKET_DATA_SIZE - PULL_RESPONSE_MAX_PAYLOAD_SIZE; for values in splits { // Assert that sum of parts equals the whole. - let size = header_size - + values - .iter() - .map(CrdsValue::bincode_serialized_size) - .sum::(); + let size = header_size + values.iter().map(CrdsValue::serialized_size).sum::(); let message = Protocol::PullResponse(self_pubkey, values); - assert_eq!(message.bincode_serialized_size(), size); + assert_eq!(message.serialized_size(), size); // Assert that the message fits into a packet. assert!(Packet::from_data(Some(&socket), message).is_ok()); } @@ -824,7 +962,7 @@ pub(crate) mod tests { incremental: incremental.clone(), wallclock: 0, })); - while value.bincode_serialized_size() < PUSH_MESSAGE_MAX_PAYLOAD_SIZE { + while value.serialized_size() < PUSH_MESSAGE_MAX_PAYLOAD_SIZE { incremental.push((0, Hash::default())); value = CrdsValue::new_unsigned(CrdsData::SnapshotHashes(SnapshotHashes { from: Pubkey::default(), @@ -840,7 +978,7 @@ pub(crate) mod tests { fn test_split_messages(value: CrdsValue) { const NUM_VALUES: usize = 30; - let value_size = value.bincode_serialized_size(); + let value_size = value.serialized_size(); let num_values_per_payload = (PUSH_MESSAGE_MAX_PAYLOAD_SIZE / value_size).max(1); // Expected len is the ceiling of the division @@ -902,7 +1040,7 @@ pub(crate) mod tests { ) .unwrap(); let vote = CrdsValue::new(CrdsData::Vote(1, vote), &Keypair::new()); - assert!(vote.bincode_serialized_size() <= PUSH_MESSAGE_MAX_PAYLOAD_SIZE); + assert!(vote.serialized_size() <= PUSH_MESSAGE_MAX_PAYLOAD_SIZE); } #[test] @@ -932,6 +1070,172 @@ pub(crate) mod tests { assert!(is_valid, "Signature should be valid with prefix"); } + #[test] + fn test_wincode_compatibility_prune_data() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let prune_data = new_rand_prune_data(&mut rng, &keypair, None); + + let bincode_bytes = bincode::serialize(&prune_data).unwrap(); + let wincode_decoded: PruneData = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(prune_data, wincode_decoded); + + let wincode_bytes = wincode::serialize(&prune_data).unwrap(); + let bincode_decoded: PruneData = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(prune_data, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_vote_transaction() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let slots = (0..rng.random_range(1..32)).map(|_| rng.random()).collect(); + let vote = Vote::new(slots, Hash::new_unique()); + let vote_ix = vote_instruction::vote_switch( + &keypair.pubkey(), + &keypair.pubkey(), + vote, + Hash::new_unique(), + ); + let mut vote_tx = Transaction::new_with_payer(&[vote_ix], Some(&keypair.pubkey())); + vote_tx.partial_sign(&[&keypair], Hash::new_unique()); + + let bincode_bytes = bincode::serialize(&vote_tx).unwrap(); + let wincode_bytes = wincode::serialize(&vote_tx).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + } + } + + #[test] + fn test_wincode_compatibility_prune_signable_data() { + // Signed/verified bytes — any divergence would silently break signatures. + #[derive(Serialize)] + struct SignDataMirror<'a> { + pubkey: &'a Pubkey, + prunes: &'a [Pubkey], + destination: &'a Pubkey, + wallclock: u64, + } + #[derive(Serialize)] + struct SignDataWithPrefixMirror<'a> { + prefix: &'a [u8], + pubkey: &'a Pubkey, + prunes: &'a [Pubkey], + destination: &'a Pubkey, + wallclock: u64, + } + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let prune_data = new_rand_prune_data(&mut rng, &keypair, None); + + let wincode_no_prefix = prune_data.signable_data_without_prefix(); + let bincode_no_prefix = bincode::serialize(&SignDataMirror { + pubkey: &prune_data.pubkey, + prunes: &prune_data.prunes, + destination: &prune_data.destination, + wallclock: prune_data.wallclock, + }) + .unwrap(); + assert_eq!(wincode_no_prefix.as_ref(), bincode_no_prefix.as_slice()); + + let wincode_with_prefix = prune_data.signable_data_with_prefix(); + let bincode_with_prefix = bincode::serialize(&SignDataWithPrefixMirror { + prefix: PRUNE_DATA_PREFIX, + pubkey: &prune_data.pubkey, + prunes: &prune_data.prunes, + destination: &prune_data.destination, + wallclock: prune_data.wallclock, + }) + .unwrap(); + assert_eq!(wincode_with_prefix.as_ref(), bincode_with_prefix.as_slice()); + } + } + + #[test] + fn test_wincode_compatibility_protocol_prune_message() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let prune_data = new_rand_prune_data(&mut rng, &keypair, None); + let protocol = Protocol::PruneMessage(keypair.pubkey(), prune_data); + + let bincode_bytes = bincode::serialize(&protocol).unwrap(); + let wincode_bytes = wincode::serialize(&protocol).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + + let wincode_decoded: Protocol = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(wincode::serialize(&wincode_decoded).unwrap(), bincode_bytes); + + let bincode_decoded: Protocol = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(bincode::serialize(&bincode_decoded).unwrap(), wincode_bytes); + } + } + + fn new_rand_contact_info_crds_value(rng: &mut R) -> CrdsValue { + let keypair = Keypair::new(); + let ci = ContactInfo::new_rand(rng, Some(keypair.pubkey())); + CrdsValue::new(CrdsData::ContactInfo(ci), &keypair) + } + + fn assert_protocol_wincode_compat(protocol: &Protocol) { + let bincode_bytes = bincode::serialize(protocol).unwrap(); + let wincode_bytes = wincode::serialize(protocol).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + let wincode_decoded: Protocol = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(wincode::serialize(&wincode_decoded).unwrap(), bincode_bytes); + let bincode_decoded: Protocol = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(bincode::serialize(&bincode_decoded).unwrap(), wincode_bytes); + } + + #[test] + fn test_wincode_compatibility_protocol_pull_request() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let filter = CrdsFilter::new_rand(rng.random_range(0..1000), rng.random_range(32..512)); + let crds_value = new_rand_contact_info_crds_value(&mut rng); + assert_protocol_wincode_compat(&Protocol::PullRequest(filter, crds_value)); + } + } + + #[test] + fn test_wincode_compatibility_protocol_pull_response() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let values: Vec<_> = (0..rng.random_range(1usize..8)) + .map(|_| new_rand_contact_info_crds_value(&mut rng)) + .collect(); + assert_protocol_wincode_compat(&Protocol::PullResponse(Pubkey::new_unique(), values)); + } + } + + #[test] + fn test_wincode_compatibility_protocol_push_message() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let values: Vec<_> = (0..rng.random_range(1usize..8)) + .map(|_| new_rand_contact_info_crds_value(&mut rng)) + .collect(); + assert_protocol_wincode_compat(&Protocol::PushMessage(Pubkey::new_unique(), values)); + } + } + + #[test] + fn test_wincode_compatibility_protocol_ping_pong() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let keypair = Keypair::new(); + let token: [u8; GOSSIP_PING_TOKEN_SIZE] = rng.random(); + let ping = Ping::new(token, &keypair); + let pong = Pong::new(&ping, &keypair); + assert_protocol_wincode_compat(&Protocol::PingMessage(ping)); + assert_protocol_wincode_compat(&Protocol::PongMessage(pong)); + } + } + #[test] fn test_prune_data_verify_with_and_without_prefix() { let mut rng = rand::rng(); diff --git a/gossip/src/restart_crds_values.rs b/gossip/src/restart_crds_values.rs index 46b668893cd..44ffc65a01e 100644 --- a/gossip/src/restart_crds_values.rs +++ b/gossip/src/restart_crds_values.rs @@ -9,11 +9,21 @@ use { solana_pubkey::Pubkey, solana_sanitize::{Sanitize, SanitizeError}, solana_serde_varint as serde_varint, + solana_wincode_varint::Leb128Int, thiserror::Error, + wincode::{SchemaRead, SchemaWrite}, }; -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "3mueGPQfnZCXQfb5bdTU1afXeFToiJxpFGkckSSjH3RD", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] pub struct RestartLastVotedForkSlots { pub from: Pubkey, pub wallclock: u64, @@ -29,8 +39,18 @@ pub enum RestartLastVotedForkSlotsError { LastVotedForkEmpty, } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "9uCfdqWTWRZoxarLXEJ2HcJgvNs65jfFYK4fBReMkJzJ", + abi_serializer = ["bincode", "wincode"], + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, SchemaWrite, SchemaRead)] +// repr(C) makes this struct zero-copy eligible in wincode. +#[repr(C)] pub struct RestartHeaviestFork { pub from: Pubkey, pub wallclock: u64, @@ -40,26 +60,40 @@ pub struct RestartHeaviestFork { pub shred_version: u16, } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, AbiEnumVisitor, StableAbi, StableAbiSample) +)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] enum SlotsOffsets { RunLengthEncoding(RunLengthEncoding), RawOffsets(RawOffsets), } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] -struct U16(#[serde(with = "serde_varint")] u16); +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] +struct U16( + #[serde(with = "serde_varint")] + #[wincode(with = "Leb128Int")] + u16, +); // The vector always starts with 1. Encode number of 1's and 0's consecutively. // For example, 110000111 is [2, 4, 3]. -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] struct RunLengthEncoding(Vec); -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)] -struct RawOffsets(BitVec); +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] +struct RawOffsets( + // `BitVec` has no `StableAbi` impl; sample a non-empty one (see `sample_bit_vec`). + #[cfg_attr( + feature = "frozen-abi", + stable_abi_sample(with = "solana_bloom::bloom::sample_bit_vec(rng)") + )] + BitVec, +); impl Sanitize for RestartLastVotedForkSlots { fn sanitize(&self) -> std::result::Result<(), SanitizeError> { @@ -225,7 +259,7 @@ mod test { crds_value::{CrdsValue, CrdsValueLabel}, protocol::MAX_CRDS_OBJECT_SIZE, }, - bincode::serialized_size, + bv::BitsPush, solana_keypair::Keypair, solana_signer::Signer, solana_time_utils::timestamp, @@ -251,9 +285,11 @@ mod test { ) .unwrap(); // If the following assert fails, please update RestartLastVotedForkSlots::MAX_BYTES + let header_size = wincode::serialized_size(&header).unwrap(); + assert_eq!(header_size, bincode::serialized_size(&header).unwrap()); assert_eq!( RestartLastVotedForkSlots::MAX_BYTES, - MAX_CRDS_OBJECT_SIZE - serialized_size(&header).unwrap() as usize + MAX_CRDS_OBJECT_SIZE - header_size as usize ); // Create large enough slots to make sure we are discarding some to make slots fit. @@ -268,7 +304,12 @@ mod test { 0, ) .unwrap(); - assert!(serialized_size(&large_slots).unwrap() <= MAX_CRDS_OBJECT_SIZE as u64); + let large_slots_size = wincode::serialized_size(&large_slots).unwrap(); + assert_eq!( + large_slots_size, + bincode::serialized_size(&large_slots).unwrap() + ); + assert!(large_slots_size <= MAX_CRDS_OBJECT_SIZE as u64); let retrieved_slots = large_slots.to_slots(0); assert!(retrieved_slots.len() <= range.len()); assert!(retrieved_slots.last().unwrap() - retrieved_slots.first().unwrap() > 5000); @@ -322,7 +363,12 @@ mod test { shred_version, ) .unwrap(); - assert!(serialized_size(&large_slots).unwrap() < MAX_CRDS_OBJECT_SIZE as u64); + let large_slots_size = wincode::serialized_size(&large_slots).unwrap(); + assert_eq!( + large_slots_size, + bincode::serialized_size(&large_slots).unwrap() + ); + assert!(large_slots_size < MAX_CRDS_OBJECT_SIZE as u64); let retrieved_slots = large_slots.to_slots(0); assert_eq!(retrieved_slots, large_slots_vec); } @@ -357,6 +403,168 @@ mod test { check_run_length_encoding(range); } + #[test] + fn test_wincode_compatibility_run_length_encoding() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let len = rng.random_range(0..32usize); + let rle = RunLengthEncoding((0..len).map(|_| U16(rng.random())).collect()); + let bincode_bytes = bincode::serialize(&rle).unwrap(); + let wincode_bytes = wincode::serialize(&rle).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + assert_eq!( + rle, + wincode::deserialize::(&bincode_bytes).unwrap() + ); + assert_eq!( + rle, + bincode::deserialize::(&wincode_bytes).unwrap() + ); + } + } + + #[test] + fn test_wincode_compatibility_raw_offsets() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let nblocks = rng.random_range(0usize..64); + let mut bitvec = BitVec::::new(); + for _ in 0..nblocks { + bitvec.push_block(rng.random::()); + } + let nbits = if nblocks == 0 { + 0 + } else { + rng.random_range(0u64..nblocks as u64 * 8 + 1) + }; + bitvec.truncate(nbits); + // shrink_to_fit matches RawOffsets::new behavior: serde serializes + // the backing block count, so it must equal block_len() for the + // two wire formats to be identical. + bitvec.shrink_to_fit(); + let raw = RawOffsets(bitvec); + let bincode_bytes = bincode::serialize(&raw).unwrap(); + let wincode_bytes = wincode::serialize(&raw).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + assert_eq!( + raw, + wincode::deserialize::(&bincode_bytes).unwrap() + ); + assert_eq!( + raw, + bincode::deserialize::(&wincode_bytes).unwrap() + ); + } + } + + #[test] + fn test_wincode_compatibility_slots_offsets() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let len = rng.random_range(0..32usize); + let offsets = SlotsOffsets::RunLengthEncoding(RunLengthEncoding( + (0..len).map(|_| U16(rng.random())).collect(), + )); + let bincode_bytes = bincode::serialize(&offsets).unwrap(); + let wincode_bytes = wincode::serialize(&offsets).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + assert_eq!( + offsets, + wincode::deserialize::(&bincode_bytes).unwrap() + ); + assert_eq!( + offsets, + bincode::deserialize::(&wincode_bytes).unwrap() + ); + } + for _ in 0..1000 { + let nblocks = rng.random_range(0usize..64); + let mut bitvec = BitVec::::new(); + for _ in 0..nblocks { + bitvec.push_block(rng.random::()); + } + let nbits = if nblocks == 0 { + 0 + } else { + rng.random_range(0u64..nblocks as u64 * 8 + 1) + }; + bitvec.truncate(nbits); + // shrink_to_fit matches RawOffsets::new behavior: serde serializes + // the backing block count, so it must equal block_len() for the + // two wire formats to be identical. + bitvec.shrink_to_fit(); + let offsets = SlotsOffsets::RawOffsets(RawOffsets(bitvec)); + let bincode_bytes = bincode::serialize(&offsets).unwrap(); + let wincode_bytes = wincode::serialize(&offsets).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + assert_eq!( + offsets, + wincode::deserialize::(&bincode_bytes).unwrap() + ); + assert_eq!( + offsets, + bincode::deserialize::(&wincode_bytes).unwrap() + ); + } + } + + #[test] + fn test_wincode_compatibility_restart_last_voted_fork_slots() { + let mut rng = rand::rng(); + // Test RawOffsets variant (small slot ranges from new_rand). + for _ in 0..1000 { + let slots = RestartLastVotedForkSlots::new_rand(&mut rng, None); + let bincode_bytes = bincode::serialize(&slots).unwrap(); + let wincode_decoded: RestartLastVotedForkSlots = + wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(slots, wincode_decoded); + let wincode_bytes = wincode::serialize(&slots).unwrap(); + let bincode_decoded: RestartLastVotedForkSlots = + bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(slots, bincode_decoded); + } + // Test RunLengthEncoding variant: consecutive slots produce a single large run + // (num_encoded_slots > MAX_BYTES * 8), forcing RunLengthEncoding to be chosen. + for _ in 0..100 { + let base: Slot = rng.random_range(0..100_000); + let range: Vec = (base..base + 8001).collect(); + let keypair = solana_keypair::Keypair::new(); + let slots = RestartLastVotedForkSlots::new( + keypair.pubkey(), + new_rand_timestamp(&mut rng), + &range, + Hash::new_unique(), + 1, + ) + .unwrap(); + assert!(matches!(slots.offsets, SlotsOffsets::RunLengthEncoding(_))); + let bincode_bytes = bincode::serialize(&slots).unwrap(); + let wincode_decoded: RestartLastVotedForkSlots = + wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(slots, wincode_decoded); + let wincode_bytes = wincode::serialize(&slots).unwrap(); + let bincode_decoded: RestartLastVotedForkSlots = + bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(slots, bincode_decoded); + } + } + + #[test] + fn test_wincode_compatibility_restart_heaviest_fork() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let fork = RestartHeaviestFork::new_rand(&mut rng, None); + let bincode_bytes = bincode::serialize(&fork).unwrap(); + let wincode_decoded: RestartHeaviestFork = + wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(fork, wincode_decoded); + let wincode_bytes = wincode::serialize(&fork).unwrap(); + let bincode_decoded: RestartHeaviestFork = + bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(fork, bincode_decoded); + } + } + #[test] fn test_restart_heaviest_fork() { let keypair = Keypair::new(); diff --git a/gossip/src/sigverify_cache.rs b/gossip/src/sigverify_cache.rs new file mode 100644 index 00000000000..aca0bb2d74d --- /dev/null +++ b/gossip/src/sigverify_cache.rs @@ -0,0 +1,78 @@ +use { + crate::cluster_info::CRDS_UNIQUE_PUBKEY_CAPACITY, lazy_lru::LruCache, solana_hash::Hash, + solana_pubkey::Pubkey, std::sync::RwLock, +}; + +/// Bundles the two caches consulted on the gossip signature-verification path: +/// `verifying_keys` memoises decompressed Ed25519 points per pubkey, and +/// `verified_values` short-circuits sigverify entirely for CrdsValue hashes that +/// were already verified. +pub(crate) struct SigVerifyCache { + pub(crate) verifying_keys: VerifyingKeyCache, + pub(crate) verified_values: VerifiedCrdsCache, +} + +impl SigVerifyCache { + pub(crate) fn new() -> Self { + Self { + verifying_keys: VerifyingKeyCache::new(), + verified_values: VerifiedCrdsCache::new(), + } + } +} + +/// Benchmarks show no meaningful improvements beyond this value. +const CAPACITY: usize = 16384; + +/// LRU of CrdsValue hashes whose Ed25519 signature has already been verified, +/// letting duplicate gossip floods skip sigverify. Keyed by the CrdsValue hash +/// (`sha256(signature || serialized_data)`), so a hit guarantees identical bytes. +/// Insert only after a successful verification. +pub(crate) struct VerifiedCrdsCache { + cache: RwLock>, +} + +impl VerifiedCrdsCache { + pub(crate) fn new() -> Self { + Self { + cache: RwLock::new(LruCache::new(CAPACITY)), + } + } + + /// Returns true if a CrdsValue with this hash has already been verified + /// (modulo LRU eviction). + pub(crate) fn contains(&self, hash: &Hash) -> bool { + self.cache.read().unwrap().get(hash).is_some() + } + + /// Marks `hash` as verified. Call only after the surrounding signature has + /// actually verified — see the struct-level note on insertion policy. + pub(crate) fn insert(&self, hash: Hash) { + self.cache.write().unwrap().put(hash, ()); + } +} + +/// LRU of decompressed Ed25519 verifying keys, memoising the non-trivial pubkey +/// decompression on the gossip receive path. Sized to the CRDS unique-pubkey +/// capacity. Insert only after the signature verifies, so the cache can't be +/// seeded with arbitrary pubkeys to evict useful entries. +pub(crate) struct VerifyingKeyCache { + cache: RwLock>, +} + +impl VerifyingKeyCache { + pub(crate) fn new() -> Self { + Self { + cache: RwLock::new(LruCache::new(CRDS_UNIQUE_PUBKEY_CAPACITY)), + } + } + + pub(crate) fn get(&self, pubkey: &Pubkey) -> Option { + self.cache.read().unwrap().get(pubkey).copied() + } + + /// Insert only after a signature against `vk` has verified. + pub(crate) fn insert(&self, pubkey: Pubkey, vk: ed25519_dalek::VerifyingKey) { + self.cache.write().unwrap().put(pubkey, vk); + } +} diff --git a/gossip/src/tlv.rs b/gossip/src/tlv.rs index 521cae7e5d9..02cfc14b6b4 100644 --- a/gossip/src/tlv.rs +++ b/gossip/src/tlv.rs @@ -1,15 +1,17 @@ use { serde::{Deserialize, Serialize}, solana_short_vec as short_vec, + wincode::{ReadError, SchemaRead, SchemaWrite, WriteError}, }; -/// Type-Length-Value encoding wrapper for bincode -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +/// Type-Length-Value encoding wrapper +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize, SchemaWrite, SchemaRead)] pub(crate) struct TlvRecord { // type pub(crate) typ: u8, // length and serialized bytes of the value #[serde(with = "short_vec")] + #[wincode(with = "wincode::containers::Vec")] pub(crate) bytes: Vec, } @@ -32,6 +34,20 @@ macro_rules! define_tlv_enum { )* } + unsafe impl wincode::SchemaWrite for $enum_name { + type Src = Self; + + fn size_of(value: &Self::Src) -> wincode::WriteResult { + let tlv_rec = TlvRecord::try_from(value).map_err(|_| wincode::WriteError::Custom("invalid as tlv_rec"))?; + >::size_of(&tlv_rec) + } + + fn write(writer: impl wincode::io::Writer, value: &Self::Src) -> wincode::WriteResult<()> { + let tlv_rec = TlvRecord::try_from(value).map_err(|_| wincode::WriteError::Custom("invalid as tlv_rec"))?; + >::write(writer, &tlv_rec) + } + } + // Serialize enum by first converting into TlvRecord, and then serializing that impl serde::Serialize for $enum_name { fn serialize(&self, serializer: S) -> Result @@ -49,7 +65,7 @@ macro_rules! define_tlv_enum { fn try_from(value: &TlvRecord) -> Result { match value.typ { $( - $typ => Ok(Self::$variant(bincode::deserialize::<$inner>(&value.bytes)?)), + $typ => Ok(Self::$variant(wincode::deserialize::<$inner>(&value.bytes)?)), )* _ => Err(TlvDecodeError::UnknownType(value.typ)), } @@ -57,21 +73,35 @@ macro_rules! define_tlv_enum { } // define conversion into TLV wire format impl TryFrom<&$enum_name> for TlvRecord { - type Error = bincode::Error; + type Error = $crate::tlv::TlvEncodeError; fn try_from(value: &$enum_name) -> Result { - use serde::ser::Error; match value { $( $enum_name::$variant(inner) => Ok(TlvRecord { typ: $typ, - bytes: bincode::serialize(inner)?, + bytes: wincode::serialize(inner)?, }), )* #[allow(unreachable_patterns)] - _ => Err(bincode::Error::custom("Unsupported enum variant")), + _ => Err($crate::tlv::TlvEncodeError::UnsupportedVariant), } } } + + unsafe impl<'de, C: wincode::config::Config> wincode::SchemaRead<'de, C> for $enum_name { + type Dst = Self; + + fn read( + reader: impl wincode::io::Reader<'de>, + dst: &mut std::mem::MaybeUninit, + ) -> wincode::ReadResult<()> { + let rec = <$crate::tlv::TlvRecord as wincode::SchemaRead<'de, C>>::get(reader)?; + let value = Self::try_from(&rec) + .map_err(|_| wincode::ReadError::Custom("TlvRecord conversion failed"))?; + dst.write(value); + Ok(()) + } + } }; } @@ -80,7 +110,15 @@ pub enum TlvDecodeError { #[error("Unknown type: {0}")] UnknownType(u8), #[error("Malformed payload: {0}")] - MalformedPayload(#[from] bincode::Error), + MalformedPayload(#[from] ReadError), +} + +#[derive(Debug, thiserror::Error)] +pub enum TlvEncodeError { + #[error("Serialization failed: {0}")] + Wincode(#[from] WriteError), + #[error("Unsupported enum variant")] + UnsupportedVariant, } /// Parses a slice of serialized TLV records into a provided type. Unsupported @@ -91,7 +129,10 @@ pub(crate) fn parse<'a, T: TryFrom<&'a TlvRecord>>(entries: &'a [TlvRecord]) -> #[cfg(test)] mod tests { - use crate::tlv::{TlvDecodeError, TlvRecord}; + use { + crate::tlv::{TlvDecodeError, TlvRecord}, + rand::Rng, + }; define_tlv_enum! (pub(crate) enum ExtensionNew { 1=>Test(u64), @@ -114,8 +155,13 @@ mod tests { ExtensionNew::NewString(String::from("bla")), ]; - let new_bytes = bincode::serialize(&new_tlv_data).unwrap(); - let tlv_vec: Vec = bincode::deserialize(&new_bytes).unwrap(); + let new_bytes = wincode::serialize(&new_tlv_data).unwrap(); + assert_eq!(new_bytes, bincode::serialize(&new_tlv_data).unwrap()); + let tlv_vec: Vec = wincode::deserialize(&new_bytes).unwrap(); + assert_eq!( + tlv_vec, + bincode::deserialize::>(&new_bytes).unwrap() + ); // check that both TLV are encoded correctly let new: Vec = crate::tlv::parse(&tlv_vec); assert!(matches!(new[0], ExtensionNew::Test(42))); @@ -143,9 +189,14 @@ mod tests { ExtensionLegacy::Test(42), ExtensionLegacy::LegacyString(String::from("foo")), ]; - let legacy_bytes = bincode::serialize(&legacy_tlv_data).unwrap(); + let legacy_bytes = wincode::serialize(&legacy_tlv_data).unwrap(); + assert_eq!(legacy_bytes, bincode::serialize(&legacy_tlv_data).unwrap()); - let tlv_vec: Vec = bincode::deserialize(&legacy_bytes).unwrap(); + let tlv_vec: Vec = wincode::deserialize(&legacy_bytes).unwrap(); + assert_eq!( + tlv_vec, + bincode::deserialize::>(&legacy_bytes).unwrap() + ); // Just in case make sure that legacy data is serialized correctly let legacy: Vec = crate::tlv::parse(&tlv_vec); assert!(matches!(legacy[0], ExtensionLegacy::Test(42))); @@ -163,4 +214,42 @@ mod tests { panic!("Wrong deserialization") }; } + + #[test] + fn test_wincode_compatibility_tlv_record() { + let mut rng = rand::rng(); + // Test various byte lengths to exercise all ShortU16 varint widths: + // 0-127: 1-byte varint + // 128-16383: 2-byte varint + let lengths: &[usize] = &[0, 1, 64, 127, 128, 255, 1000, 16383]; + for &len in lengths { + let record = TlvRecord { + typ: rng.random::(), + bytes: (0..len).map(|_| rng.random::()).collect(), + }; + let bincode_bytes = bincode::serialize(&record).unwrap(); + let wincode_bytes = wincode::serialize(&record).unwrap(); + assert_eq!( + bincode_bytes, wincode_bytes, + "bytes differ for TlvRecord with len={len}" + ); + let wincode_decoded: TlvRecord = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(record, wincode_decoded); + let bincode_decoded: TlvRecord = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(record, bincode_decoded); + } + // Also fuzz with random lengths and types + for _ in 0..1000 { + let len = rng.random_range(0usize..256); + let record = TlvRecord { + typ: rng.random::(), + bytes: (0..len).map(|_| rng.random::()).collect(), + }; + let bincode_bytes = bincode::serialize(&record).unwrap(); + let wincode_bytes = wincode::serialize(&record).unwrap(); + assert_eq!(bincode_bytes, wincode_bytes); + let wincode_decoded: TlvRecord = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(record, wincode_decoded); + } + } } diff --git a/gossip/src/wire_format_tests.rs b/gossip/src/wire_format_tests.rs index 440dbbe44da..2c6db284f28 100644 --- a/gossip/src/wire_format_tests.rs +++ b/gossip/src/wire_format_tests.rs @@ -4,7 +4,7 @@ mod tests { use { - crate::protocol::Protocol, + crate::protocol::{Protocol, deserialize_protocol}, serde::Serialize, solana_net_utils::tooling_for_tests::{hexdump, validate_packet_format}, solana_sanitize::Sanitize, @@ -12,13 +12,17 @@ mod tests { }; fn parse_gossip(bytes: &[u8]) -> anyhow::Result { - let pkt: Protocol = solana_perf::packet::deserialize_from_with_limit(bytes)?; + let pkt = deserialize_protocol(bytes)?; pkt.sanitize()?; Ok(pkt) } - fn serialize(pkt: T) -> Vec { - bincode::serialize(&pkt).unwrap() + fn serialize>( + pkt: T, + ) -> Vec { + let wincode_bytes = wincode::serialize(&pkt).unwrap(); + assert_eq!(wincode_bytes, bincode::serialize(&pkt).unwrap()); + wincode_bytes } fn find_differences(a: &[u8], b: &[u8]) -> Option { diff --git a/gossip/tests/conformance/main.rs b/gossip/tests/conformance/main.rs index 970d3c9796d..1c24bfa73e8 100644 --- a/gossip/tests/conformance/main.rs +++ b/gossip/tests/conformance/main.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "dev-context-only-utils")] +#![cfg(feature = "conformance")] //! Gossip conformance tests and fixture generation. diff --git a/gossip/tests/crds_gossip.rs b/gossip/tests/crds_gossip.rs index 0be89ba29a1..28978a7ebff 100644 --- a/gossip/tests/crds_gossip.rs +++ b/gossip/tests/crds_gossip.rs @@ -1,6 +1,5 @@ #![allow(clippy::arithmetic_side_effects)] use { - bincode::serialized_size, itertools::Itertools, log::*, rayon::{ThreadPool, ThreadPoolBuilder, prelude::*}, @@ -393,10 +392,7 @@ fn network_run_push( let mut pruned: HashSet<(Pubkey, Pubkey)> = HashSet::new(); for (to, msgs) in push_messages { // 8 bytes for encoding the length of the vector. - bytes += 8 + msgs - .iter() - .map(CrdsValue::bincode_serialized_size) - .sum::(); + bytes += 8 + msgs.iter().map(CrdsValue::serialized_size).sum::(); num_msgs += 1; let origins: HashSet<_> = network .get(&to) @@ -420,7 +416,12 @@ fn network_run_push( pruned.insert((from, *prune_key)); } - bytes += serialized_size(&prune_keys).unwrap() as usize; + let prune_keys_size = wincode::serialized_size(&prune_keys).unwrap(); + assert_eq!( + prune_keys_size, + bincode::serialized_size(&prune_keys).unwrap() + ); + bytes += prune_keys_size as usize; delivered += 1; network @@ -575,7 +576,7 @@ fn network_run_pull( .iter() .map(|f| f.filter.bits.len() as usize / 8) .sum::(); - bytes += caller_info.bincode_serialized_size(); + bytes += caller_info.serialized_size(); let requests: Vec<_> = filters .into_iter() .map(|filter| PullRequest { @@ -594,7 +595,8 @@ fn network_run_pull( &requests, usize::MAX, // output_size_limit now, - |_| true, // should_retain_crds_value + |_| true, // should_retain_crds_value + |_, _| true, // try_consume_scan_budget &GossipStats::default(), ) .into_iter() @@ -603,10 +605,7 @@ fn network_run_pull( }) .unwrap(); // 8 bytes for encoding the length of the vector. - bytes += 8 + rsp - .iter() - .map(CrdsValue::bincode_serialized_size) - .sum::(); + bytes += 8 + rsp.iter().map(CrdsValue::serialized_size).sum::(); msgs += rsp.len(); if let Some(node) = network.get(&from) { let mut stats = ProcessPullStats::default(); @@ -663,8 +662,6 @@ fn build_gossip_thread_pool() -> ThreadPool { fn new_ping_cache() -> Mutex { let ping_cache = PingCache::new( - &mut rand::rng(), - Instant::now(), Duration::from_secs(20 * 60), // ttl Duration::from_secs(20 * 60) / 64, // rate_limit_delay 2048, // capacity diff --git a/gossip/tests/gossip.rs b/gossip/tests/gossip.rs index d9c1a2c0645..9d6f3f0c7b1 100644 --- a/gossip/tests/gossip.rs +++ b/gossip/tests/gossip.rs @@ -46,6 +46,7 @@ fn test_node(exit: Arc) -> (Arc, GossipService, UdpSock None, test_node.sockets.gossip, None, + None, true, // should_check_duplicate_instance None, exit, @@ -74,6 +75,7 @@ fn test_node_with_bank( Some(epoch_specs), test_node.sockets.gossip, None, + None, true, // should_check_duplicate_instance None, exit, @@ -198,7 +200,7 @@ fn gossip_star() { yd.set_wallclock(timestamp()); let xv = &listen[x].0; xv.insert_info(yd); - trace!("star leader {}", &xv.id()); + trace!("star leader {}", xv.id()); } }); } diff --git a/install/Cargo.toml b/install/Cargo.toml index dd21b5d36fa..1e6ab35eaf4 100644 --- a/install/Cargo.toml +++ b/install/Cargo.toml @@ -11,15 +11,15 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] [dependencies] agave-logger = { workspace = true } -bincode = { workspace = true } bzip2 = { workspace = true } -chrono = { workspace = true, features = ["default", "serde"] } clap = { workspace = true } console = { workspace = true } crossbeam-channel = { workspace = true } @@ -39,17 +39,8 @@ scopeguard = { workspace = true } semver = { workspace = true } serde = { workspace = true } serde_yaml = { workspace = true } -solana-clap-utils = { workspace = true } -solana-config-interface = { version = "=2.0.0", features = ["bincode"] } -solana-hash = "=4.3.0" -solana-keypair = "=3.1.2" -solana-message = "=4.1.1" -solana-pubkey = { version = "=4.2.0", default-features = false } -solana-rpc-client = { workspace = true } +solana-hash = "=4.4.0" solana-sha256-hasher = { workspace = true } -solana-signature = { version = "=3.4.0", default-features = false } -solana-signer = "=3.0.0" -solana-transaction = { version = "=4.1.1", features = ["wincode"] } solana-version = { workspace = true } tar = { workspace = true } tempfile = { workspace = true } @@ -58,3 +49,6 @@ url = { workspace = true } [target."cfg(windows)".dependencies] winapi = { workspace = true, features = ["minwindef", "winuser"] } winreg = { workspace = true } + +[lints] +workspace = true diff --git a/install/agave-install-init.sh b/install/agave-install-init.sh index cf2d1babf3c..fcb0ed11e1f 100755 --- a/install/agave-install-init.sh +++ b/install/agave-install-init.sh @@ -28,7 +28,7 @@ agave-install-init initializes a new installation USAGE: - agave-install-init [FLAGS] [OPTIONS] --data_dir --pubkey + agave-install-init [FLAGS] [OPTIONS] FLAGS: -h, --help Prints help information @@ -36,8 +36,7 @@ FLAGS: OPTIONS: -d, --data-dir Directory to store install data - -u, --url JSON RPC URL for the solana cluster - -p, --pubkey Public key of the update manifest + EOF } diff --git a/install/build.rs b/install/build.rs index bc43404b0b0..81caa36d691 100644 --- a/install/build.rs +++ b/install/build.rs @@ -1,15 +1,6 @@ -use std::time::SystemTime; - fn main() { println!( "cargo:rustc-env=TARGET={}", std::env::var("TARGET").unwrap() ); - println!( - "cargo:rustc-env=BUILD_SECONDS_SINCE_UNIX_EPOCH={}", - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(), - ); } diff --git a/install/src/build_env.rs b/install/src/build_env.rs index 2ec8581b843..327297ae93c 100644 --- a/install/src/build_env.rs +++ b/install/src/build_env.rs @@ -1,2 +1 @@ pub const TARGET: &str = env!("TARGET"); -pub const BUILD_SECONDS_SINCE_UNIX_EPOCH: &str = env!("BUILD_SECONDS_SINCE_UNIX_EPOCH"); diff --git a/install/src/command.rs b/install/src/command.rs index fec5fbd28eb..26d6a2d3853 100644 --- a/install/src/command.rs +++ b/install/src/command.rs @@ -2,30 +2,18 @@ use { crate::{ config::{Config, ExplicitRelease}, stop_process::stop_process, - update_manifest::{SignedUpdateManifest, UpdateManifest}, }, - chrono::{Local, TimeZone}, console::{Emoji, style}, crossbeam_channel::unbounded, indicatif::{ProgressBar, ProgressStyle}, serde::{Deserialize, Serialize}, - solana_config_interface::{ - instruction::{self as config_instruction}, - state::get_config_data, - }, solana_hash::Hash, - solana_keypair::{Keypair, read_keypair_file, signable::Signable}, - solana_message::Message, - solana_pubkey::Pubkey, - solana_rpc_client::rpc_client::RpcClient, solana_sha256_hasher::Hasher, - solana_signer::Signer, - solana_transaction::Transaction, std::{ fs::{self, File}, io::{self, BufReader, Read}, path::{Path, PathBuf}, - time::{Duration, Instant, SystemTime}, + time::{Duration, Instant}, }, tempfile::TempDir, url::Url, @@ -44,7 +32,6 @@ static BULLET: Emoji = Emoji("• ", "* "); static SPARKLE: Emoji = Emoji("✨ ", ""); static WRAPPED_PRESENT: Emoji = Emoji("🎁 ", ""); static PACKAGE: Emoji = Emoji("📦 ", ""); -static INFORMATION: Emoji = Emoji("ℹ️ ", ""); static RECYCLING: Emoji = Emoji("♻️ ", ""); /// Creates a new process bar for processing that will take an unknown amount of time @@ -207,86 +194,6 @@ fn load_release_target(release_dir: &Path) -> Result { Ok(version.target) } -/// Time in seconds since the UNIX_EPOCH -fn timestamp_secs() -> u64 { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs() -} - -/// Create an empty update manifest for the given `update_manifest_keypair` if it doesn't already -/// exist on the cluster -fn new_update_manifest( - rpc_client: &RpcClient, - from_keypair: &Keypair, - update_manifest_keypair: &Keypair, -) -> Result<(), Box> { - if rpc_client - .get_account_data(&update_manifest_keypair.pubkey()) - .is_err() - { - let recent_blockhash = rpc_client.get_latest_blockhash()?; - - let lamports = rpc_client - .get_minimum_balance_for_rent_exemption(SignedUpdateManifest::MAX_SPACE as usize)?; - - let instructions = - config_instruction::create_account_with_max_config_space::( - &from_keypair.pubkey(), - &update_manifest_keypair.pubkey(), - lamports, - SignedUpdateManifest::MAX_SPACE, - vec![], // additional keys - ); - let message = Message::new(&instructions, Some(&from_keypair.pubkey())); - let signers = [from_keypair, update_manifest_keypair]; - let transaction = Transaction::new(&signers, message, recent_blockhash); - rpc_client.send_and_confirm_transaction(&transaction)?; - } - Ok(()) -} - -/// Update the update manifest on the cluster with new content -fn store_update_manifest( - rpc_client: &RpcClient, - from_keypair: &Keypair, - update_manifest_keypair: &Keypair, - update_manifest: &SignedUpdateManifest, -) -> Result<(), Box> { - let recent_blockhash = rpc_client.get_latest_blockhash()?; - - let signers = [from_keypair, update_manifest_keypair]; - let instruction = config_instruction::store::( - &update_manifest_keypair.pubkey(), - true, // update_manifest_keypair is signer - vec![], // additional keys - update_manifest, - ); - - let message = Message::new(&[instruction], Some(&from_keypair.pubkey())); - let transaction = Transaction::new(&signers, message, recent_blockhash); - rpc_client.send_and_confirm_transaction(&transaction)?; - Ok(()) -} - -/// Read the current contents of the update manifest from the cluster -fn get_update_manifest( - rpc_client: &RpcClient, - update_manifest_pubkey: &Pubkey, -) -> Result { - let data = rpc_client - .get_account_data(update_manifest_pubkey) - .map_err(|err| format!("Unable to fetch update manifest: {err}"))?; - - let config_data = get_config_data(&data) - .map_err(|err| format!("Unable to get at config_data to update manifest: {err}"))?; - let signed_update_manifest = - SignedUpdateManifest::deserialize(update_manifest_pubkey, config_data) - .map_err(|err| format!("Unable to deserialize update manifest: {err}"))?; - Ok(signed_update_manifest.manifest) -} - /// Bug the user if active_release_bin_dir is not in their PATH fn check_env_path_for_bin_dir(config: &Config) { use std::env; @@ -297,10 +204,10 @@ fn check_env_path_for_bin_dir(config: &Config) { .unwrap_or_default(); let found = match env::var_os("PATH") { Some(paths) => env::split_paths(&paths).any(|path| { - if let Ok(path) = path.canonicalize() { - if path == bin_dir { - return true; - } + if let Ok(path) = path.canonicalize() + && path == bin_dir + { + return true; } false }), @@ -460,15 +367,15 @@ fn add_to_path(new_path: &str) -> bool { // Look for sh, bash, and zsh rc files let mut rcfiles = vec![dirs_next::home_dir().map(|p| p.join(".profile"))]; - if let Ok(shell) = std::env::var("SHELL") { - if shell.contains("zsh") { - let zdotdir = std::env::var("ZDOTDIR") - .ok() - .map(PathBuf::from) - .or_else(dirs_next::home_dir); - let zprofile = zdotdir.map(|p| p.join(".zprofile")); - rcfiles.push(zprofile); - } + if let Ok(shell) = std::env::var("SHELL") + && shell.contains("zsh") + { + let zdotdir = std::env::var("ZDOTDIR") + .ok() + .map(PathBuf::from) + .or_else(dirs_next::home_dir); + let zprofile = zdotdir.map(|p| p.join(".zprofile")); + rcfiles.push(zprofile); } if let Some(bash_profile) = dirs_next::home_dir().map(|p| p.join(".bash_profile")) { @@ -546,27 +453,19 @@ fn add_to_path(new_path: &str) -> bool { pub fn init( config_file: &str, data_dir: &str, - json_rpc_url: &str, - update_manifest_pubkey: &Pubkey, no_modify_path: bool, - explicit_release: Option, + explicit_release: ExplicitRelease, ) -> Result<(), String> { - let config = { + let config = Config::new(data_dir, explicit_release); + if let Ok(current_config) = Config::load(config_file) { // Write new config file only if different, so that running |agave-install init| // repeatedly doesn't unnecessarily re-download - let mut current_config = Config::load(config_file).unwrap_or_default(); - current_config.current_update_manifest = None; - let config = Config::new( - data_dir, - json_rpc_url, - update_manifest_pubkey, - explicit_release, - ); if current_config != config { config.save(config_file)?; } - config - }; + } else { + config.save(config_file)?; + } init_or_update(config_file, true, false)?; @@ -606,35 +505,19 @@ fn release_channel_version_url(release_channel: &str) -> String { ) } -fn print_update_manifest(update_manifest: &UpdateManifest) { - let when = Local - .timestamp_opt(update_manifest.timestamp_secs as i64, 0) - .unwrap(); - println_name_value(&format!("{BULLET}release date:"), &when.to_string()); - println_name_value( - &format!("{BULLET}download URL:"), - &update_manifest.download_url, - ); -} - pub fn info(config_file: &str, local_info_only: bool, eval: bool) -> Result<(), String> { let config = Config::load(config_file)?; if eval { println!( "SOLANA_INSTALL_ACTIVE_RELEASE={}", - &config.active_release_dir().to_str().unwrap_or("") + config.active_release_dir().to_str().unwrap_or("") ); - config - .explicit_release - .map(|er| match er { - ExplicitRelease::Semver(semver) => semver, - ExplicitRelease::Channel(channel) => channel, - }) - .and_then(|channel| { - println!("SOLANA_INSTALL_ACTIVE_CHANNEL={channel}",); - Option::::None - }); + let channel = match &config.explicit_release { + ExplicitRelease::Semver(semver) => semver, + ExplicitRelease::Channel(channel) => channel, + }; + println!("SOLANA_INSTALL_ACTIVE_CHANNEL={channel}",); return Ok(()); } @@ -655,42 +538,23 @@ pub fn info(config_file: &str, local_info_only: bool, eval: bool) -> Result<(), } } - if let Some(explicit_release) = &config.explicit_release { - match explicit_release { - ExplicitRelease::Semver(release_semver) => { - println_name_value(&format!("{BULLET}Release version:"), release_semver); - println_name_value( - &format!("{BULLET}Release URL:"), - &github_release_download_url(release_semver), - ); - } - ExplicitRelease::Channel(release_channel) => { - println_name_value(&format!("{BULLET}Release channel:"), release_channel); - println_name_value( - &format!("{BULLET}Release URL:"), - &release_channel_download_url(release_channel), - ); - } + match &config.explicit_release { + ExplicitRelease::Semver(release_semver) => { + println_name_value(&format!("{BULLET}Release version:"), release_semver); + println_name_value( + &format!("{BULLET}Release URL:"), + &github_release_download_url(release_semver), + ); } - print_release_version(&config); - } else { - println_name_value("JSON RPC URL:", &config.json_rpc_url); - println_name_value( - "Update manifest pubkey:", - &config.update_manifest_pubkey.to_string(), - ); - - match config.current_update_manifest { - Some(ref update_manifest) => { - println_name_value("Installed version:", ""); - print_release_version(&config); - print_update_manifest(update_manifest); - } - None => { - println_name_value("Installed version:", "None"); - } + ExplicitRelease::Channel(release_channel) => { + println_name_value(&format!("{BULLET}Release channel:"), release_channel); + println_name_value( + &format!("{BULLET}Release URL:"), + &release_channel_download_url(release_channel), + ); } } + print_release_version(&config); if local_info_only { Ok(()) @@ -699,96 +563,6 @@ pub fn info(config_file: &str, local_info_only: bool, eval: bool) -> Result<(), } } -pub fn deploy( - json_rpc_url: &str, - from_keypair_file: &str, - download_url: &str, - update_manifest_keypair_file: &str, -) -> Result<(), String> { - let from_keypair = read_keypair_file(from_keypair_file) - .map_err(|err| format!("Unable to read {from_keypair_file}: {err}"))?; - let update_manifest_keypair = read_keypair_file(update_manifest_keypair_file) - .map_err(|err| format!("Unable to read {update_manifest_keypair_file}: {err}"))?; - - println_name_value("JSON RPC URL:", json_rpc_url); - println_name_value( - "Update manifest pubkey:", - &update_manifest_keypair.pubkey().to_string(), - ); - - // Confirm the `json_rpc_url` is good and that `from_keypair` is a valid account - let rpc_client = RpcClient::new(json_rpc_url.to_string()); - let progress_bar = new_spinner_progress_bar(); - progress_bar.set_message(format!("{LOOKING_GLASS}Checking cluster...")); - let balance = rpc_client - .get_balance(&from_keypair.pubkey()) - .map_err(|err| { - format!("Unable to get the account balance of {from_keypair_file}: {err}") - })?; - progress_bar.finish_and_clear(); - if balance == 0 { - return Err(format!("{from_keypair_file} account balance is empty")); - } - - // Download the release - let (temp_dir, temp_archive, temp_archive_sha256) = download_to_temp(download_url, None) - .map_err(|err| format!("Unable to download {download_url}: {err}"))?; - - if let Ok(update_manifest) = get_update_manifest(&rpc_client, &update_manifest_keypair.pubkey()) - { - if temp_archive_sha256 == update_manifest.download_sha256 { - println!( - " {}{}", - INFORMATION, - style("Update is already deployed").bold() - ); - return Ok(()); - } - } - - // Extract it and load the release version metadata - let temp_release_dir = temp_dir.path().join("archive"); - extract_release_archive(&temp_archive, &temp_release_dir).map_err(|err| { - format!("Unable to extract {temp_archive:?} into {temp_release_dir:?}: {err}") - })?; - - let release_target = load_release_target(&temp_release_dir) - .map_err(|err| format!("Unable to load release target from {temp_release_dir:?}: {err}"))?; - - println_name_value("Update target:", &release_target); - - let progress_bar = new_spinner_progress_bar(); - progress_bar.set_message(format!("{PACKAGE}Deploying update...")); - - // Construct an update manifest for the release - let mut update_manifest = SignedUpdateManifest { - account_pubkey: update_manifest_keypair.pubkey(), - ..SignedUpdateManifest::default() - }; - - update_manifest.manifest.timestamp_secs = timestamp_secs(); - update_manifest.manifest.download_url = download_url.to_string(); - update_manifest.manifest.download_sha256 = temp_archive_sha256; - - update_manifest.sign(&update_manifest_keypair); - assert!(update_manifest.verify()); - - // Store the new update manifest on the cluster - new_update_manifest(&rpc_client, &from_keypair, &update_manifest_keypair) - .map_err(|err| format!("Unable to create update manifest: {err}"))?; - store_update_manifest( - &rpc_client, - &from_keypair, - &update_manifest_keypair, - &update_manifest, - ) - .map_err(|err| format!("Unable to store update manifest: {err:?}"))?; - - progress_bar.finish_and_clear(); - println!(" {}{}", SPARKLE, style("Deployment successful").bold()); - Ok(()) -} - #[cfg(windows)] fn symlink_dir, Q: AsRef>(src: P, dst: Q) -> std::io::Result<()> { std::os::windows::fs::symlink_dir(src, dst) @@ -938,14 +712,13 @@ fn check_for_newer_github_release( tag_name, prerelease, }| { - if let Ok(version) = semver_of(&tag_name) { - if (prerelease_allowed || !prerelease) - && version_filter - .as_ref() - .is_none_or(|version_filter| version_filter.matches(&version)) - { - return Some(version); - } + if let Ok(version) = semver_of(&tag_name) + && (prerelease_allowed || !prerelease) + && version_filter + .as_ref() + .is_none_or(|version_filter| version_filter.matches(&version)) + { + return Some(version); } None }, @@ -979,172 +752,124 @@ pub fn update(config_file: &str, check_only: bool) -> Result { pub fn init_or_update(config_file: &str, is_init: bool, check_only: bool) -> Result { let mut config = Config::load(config_file)?; - let (updated_version, download_url_and_sha256, release_dir) = if let Some(explicit_release) = - &config.explicit_release - { - match explicit_release { - ExplicitRelease::Semver(current_release_semver) => { - let release_dir = config.release_dir(current_release_semver); - if is_init && release_dir.exists() { - (current_release_semver.to_owned(), None, release_dir) + let (updated_version, download_url_and_sha256, release_dir) = match &config.explicit_release { + ExplicitRelease::Semver(current_release_semver) => { + let release_dir = config.release_dir(current_release_semver); + if is_init && release_dir.exists() { + (current_release_semver.to_owned(), None, release_dir) + } else { + let progress_bar = new_spinner_progress_bar(); + progress_bar.set_message(format!("{LOOKING_GLASS}Checking for updates...")); + + let semver_update_type = if is_init { + SemverUpdateType::Fixed } else { - let progress_bar = new_spinner_progress_bar(); - progress_bar.set_message(format!("{LOOKING_GLASS}Checking for updates...")); - - let semver_update_type = if is_init { - SemverUpdateType::Fixed - } else { - SemverUpdateType::Patch - }; - let github_release = check_for_newer_github_release( - current_release_semver, - semver_update_type, - is_init, - )?; - - progress_bar.finish_and_clear(); - - match github_release { - None => { - return Err(format!("Unknown release: {current_release_semver}")); - } - Some(release_semver) => { - if release_semver == *current_release_semver { - if let Ok(active_release_version) = load_release_version( - &config.active_release_dir().join("version.yml"), - ) { - if format!("v{current_release_semver}") - == active_release_version.channel - { - println!( - "Install is up to date. {release_semver} is the \ - latest compatible release" - ); - return Ok(false); - } - } - } - config.explicit_release = - Some(ExplicitRelease::Semver(release_semver.clone())); - - let release_dir = config.release_dir(&release_semver); - let download_url_and_sha256 = if release_dir.exists() { - // Release already present in the cache - None - } else { - Some((github_release_download_url(&release_semver), None)) - }; - (release_semver, download_url_and_sha256, release_dir) + SemverUpdateType::Patch + }; + let github_release = check_for_newer_github_release( + current_release_semver, + semver_update_type, + is_init, + )?; + + progress_bar.finish_and_clear(); + + match github_release { + None => { + return Err(format!("Unknown release: {current_release_semver}")); + } + Some(release_semver) => { + if release_semver == *current_release_semver + && let Ok(active_release_version) = load_release_version( + &config.active_release_dir().join("version.yml"), + ) + && format!("v{current_release_semver}") + == active_release_version.channel + { + println!( + "Install is up to date. {release_semver} is the latest compatible \ + release" + ); + return Ok(false); } + config.explicit_release = ExplicitRelease::Semver(release_semver.clone()); + + let release_dir = config.release_dir(&release_semver); + let download_url_and_sha256 = if release_dir.exists() { + // Release already present in the cache + None + } else { + Some((github_release_download_url(&release_semver), None)) + }; + (release_semver, download_url_and_sha256, release_dir) } } } - ExplicitRelease::Channel(release_channel) => { - let version_url = release_channel_version_url(release_channel); - - let (_temp_dir, temp_file, _temp_archive_sha256) = - download_to_temp(&version_url, None) - .map_err(|err| format!("Unable to download {version_url}: {err}"))?; - - let update_release_version = load_release_version(&temp_file)?; - - let release_id = format!("{}-{}", release_channel, update_release_version.commit); - let release_dir = config.release_dir(&release_id); - let current_release_version_yml = - release_dir.join("solana-release").join("version.yml"); - - let download_url = release_channel_download_url(release_channel); + } + ExplicitRelease::Channel(release_channel) => { + let version_url = release_channel_version_url(release_channel); + + let (_temp_dir, temp_file, _temp_archive_sha256) = download_to_temp(&version_url, None) + .map_err(|err| format!("Unable to download {version_url}: {err}"))?; + + let update_release_version = load_release_version(&temp_file)?; + + let release_id = format!("{}-{}", release_channel, update_release_version.commit); + let release_dir = config.release_dir(&release_id); + let current_release_version_yml = + release_dir.join("solana-release").join("version.yml"); + + let download_url = release_channel_download_url(release_channel); + + if !current_release_version_yml.exists() { + ( + format!( + "{} commit {}", + release_channel, + &update_release_version.commit[0..7] + ), + Some((download_url, None)), + release_dir, + ) + } else { + let current_release_version = load_release_version(¤t_release_version_yml)?; + if update_release_version.commit == current_release_version.commit { + if let Ok(active_release_version) = + load_release_version(&config.active_release_dir().join("version.yml")) + && current_release_version.commit == active_release_version.commit + { + // Same version, no update required + println!( + "Install is up to date. {} is the latest commit for {}", + &active_release_version.commit[0..7], + release_channel + ); + return Ok(false); + } - if !current_release_version_yml.exists() { + // Release already present in the cache ( format!( "{} commit {}", release_channel, &update_release_version.commit[0..7] ), - Some((download_url, None)), + None, release_dir, ) } else { - let current_release_version = - load_release_version(¤t_release_version_yml)?; - if update_release_version.commit == current_release_version.commit { - if let Ok(active_release_version) = - load_release_version(&config.active_release_dir().join("version.yml")) - { - if current_release_version.commit == active_release_version.commit { - // Same version, no update required - println!( - "Install is up to date. {} is the latest commit for {}", - &active_release_version.commit[0..7], - release_channel - ); - return Ok(false); - } - } - - // Release already present in the cache - ( - format!( - "{} commit {}", - release_channel, - &update_release_version.commit[0..7] - ), - None, - release_dir, - ) - } else { - ( - format!( - "{} (from {})", - &update_release_version.commit[0..7], - ¤t_release_version.commit[0..7], - ), - Some((download_url, None)), - release_dir, - ) - } + ( + format!( + "{} (from {})", + &update_release_version.commit[0..7], + ¤t_release_version.commit[0..7], + ), + Some((download_url, None)), + release_dir, + ) } } } - } else { - let progress_bar = new_spinner_progress_bar(); - progress_bar.set_message(format!("{LOOKING_GLASS}Checking for updates...")); - let rpc_client = RpcClient::new(config.json_rpc_url.clone()); - let update_manifest = get_update_manifest(&rpc_client, &config.update_manifest_pubkey)?; - progress_bar.finish_and_clear(); - - if Some(&update_manifest) == config.current_update_manifest.as_ref() { - println!("Install is up to date"); - return Ok(false); - } - println!("\n{}", style("An update is available:").bold()); - print_update_manifest(&update_manifest); - - if timestamp_secs() - < crate::build_env::BUILD_SECONDS_SINCE_UNIX_EPOCH - .parse::() - .unwrap() - { - return Err("Unable to update as system time seems unreliable".to_string()); - } - - if let Some(ref current_update_manifest) = config.current_update_manifest { - if update_manifest.timestamp_secs < current_update_manifest.timestamp_secs { - return Err("Unable to update to an older version".to_string()); - } - } - config.current_update_manifest = Some(update_manifest.clone()); - - let release_dir = config.release_dir(&update_manifest.download_sha256.to_string()); - - let download_url = update_manifest.download_url; - let archive_sha256 = Some(update_manifest.download_sha256); - ( - "latest manifest".to_string(), - Some((download_url, archive_sha256)), - release_dir, - ) }; if check_only { @@ -1277,7 +1002,7 @@ pub fn run( } }; - if config.explicit_release.is_none() && now.elapsed().as_secs() > config.update_poll_secs { + if now.elapsed().as_secs() > config.update_poll_secs { match update(config_file, false) { Ok(true) => { // Update successful, kill current process so it will be restart diff --git a/install/src/config.rs b/install/src/config.rs index eafbf20403a..70d210cd4e5 100644 --- a/install/src/config.rs +++ b/install/src/config.rs @@ -1,7 +1,5 @@ use { - crate::update_manifest::UpdateManifest, serde::{Deserialize, Serialize}, - solana_pubkey::Pubkey, std::{ fs::{File, create_dir_all}, io::{self, Write}, @@ -15,28 +13,17 @@ pub enum ExplicitRelease { Channel(String), } -#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Config { - pub json_rpc_url: String, - pub update_manifest_pubkey: Pubkey, - pub current_update_manifest: Option, pub update_poll_secs: u64, - pub explicit_release: Option, + pub explicit_release: ExplicitRelease, pub releases_dir: PathBuf, active_release_dir: PathBuf, } impl Config { - pub fn new( - data_dir: &str, - json_rpc_url: &str, - update_manifest_pubkey: &Pubkey, - explicit_release: Option, - ) -> Self { + pub fn new(data_dir: &str, explicit_release: ExplicitRelease) -> Self { Self { - json_rpc_url: json_rpc_url.to_string(), - update_manifest_pubkey: *update_manifest_pubkey, - current_update_manifest: None, update_poll_secs: 60 * 60, // check for updates once an hour explicit_release, releases_dir: PathBuf::from(data_dir).join("releases"), @@ -99,12 +86,10 @@ mod test { #[test] fn test_save() { let root_dir = env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR"); - let json_rpc_url = "https://api.mainnet-beta.solana.com"; - let pubkey = Pubkey::default(); let config_name = "config.yaml"; let config_path = format!("{root_dir}/{config_name}"); - let config = Config::new(&root_dir, json_rpc_url, &pubkey, None); + let config = Config::new(&root_dir, ExplicitRelease::Channel("stable".to_string())); assert_eq!(config.save(config_name), Ok(())); defer! { @@ -115,43 +100,8 @@ mod test { read_to_string(&config_path).unwrap(), format!( "--- -json_rpc_url: https://api.mainnet-beta.solana.com -update_manifest_pubkey: -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -- 0 -current_update_manifest: null update_poll_secs: 3600 -explicit_release: null +explicit_release: !Channel stable releases_dir: {root_dir}/releases active_release_dir: {root_dir}/active_release " diff --git a/install/src/defaults.rs b/install/src/defaults.rs index fe7131a389f..535cd06b4e2 100644 --- a/install/src/defaults.rs +++ b/install/src/defaults.rs @@ -1,7 +1,5 @@ use std::sync::LazyLock; -pub const JSON_RPC_URL: &str = "http://api.devnet.solana.com"; - pub static CONFIG_FILE: LazyLock> = LazyLock::new(|| { dirs_next::home_dir().map(|mut path| { path.extend([".config", "solana", "install", "config.yml"]); @@ -9,13 +7,6 @@ pub static CONFIG_FILE: LazyLock> = LazyLock::new(|| { }) }); -pub static USER_KEYPAIR: LazyLock> = LazyLock::new(|| { - dirs_next::home_dir().map(|mut path| { - path.extend([".config", "solana", "id.json"]); - path.to_str().unwrap().to_string() - }) -}); - pub static DATA_DIR: LazyLock> = LazyLock::new(|| { dirs_next::home_dir().map(|mut path| { path.extend([".local", "share", "solana", "install"]); diff --git a/install/src/lib.rs b/install/src/lib.rs index fd097df16a6..bf72488687e 100644 --- a/install/src/lib.rs +++ b/install/src/lib.rs @@ -1,18 +1,11 @@ #![allow(clippy::arithmetic_side_effects)] -use { - clap::{App, AppSettings, Arg, ArgMatches, SubCommand, crate_description, crate_name}, - solana_clap_utils::{ - input_parsers::pubkey_of, - input_validators::{is_pubkey, is_url}, - }, -}; +use clap::{App, AppSettings, Arg, ArgMatches, SubCommand, crate_description, crate_name}; mod build_env; mod command; mod config; mod defaults; mod stop_process; -mod update_manifest; pub fn is_semver(semver: &str) -> Result<(), String> { match semver::Version::parse(semver) { @@ -35,48 +28,23 @@ pub fn is_explicit_release(string: String) -> Result<(), String> { is_semver(&string).or_else(|_| is_release_channel(&string)) } -pub fn explicit_release_of( - matches: &ArgMatches<'_>, - name: &str, -) -> Option { - matches - .value_of(name) - .map(ToString::to_string) - .map(|explicit_release| { - if explicit_release.starts_with('v') - && is_semver(explicit_release.split_at(1).1).is_ok() - { - config::ExplicitRelease::Semver(explicit_release.split_at(1).1.to_string()) - } else if is_semver(&explicit_release).is_ok() { - config::ExplicitRelease::Semver(explicit_release) - } else { - config::ExplicitRelease::Channel(explicit_release) - } - }) +pub fn explicit_release_of(matches: &ArgMatches<'_>, name: &str) -> config::ExplicitRelease { + let explicit_release = matches.value_of(name).unwrap().to_string(); + if explicit_release.starts_with('v') && is_semver(explicit_release.split_at(1).1).is_ok() { + config::ExplicitRelease::Semver(explicit_release.split_at(1).1.to_string()) + } else if is_semver(&explicit_release).is_ok() { + config::ExplicitRelease::Semver(explicit_release.to_owned()) + } else { + config::ExplicitRelease::Channel(explicit_release.to_owned()) + } } fn handle_init(matches: &ArgMatches<'_>, config_file: &str) -> Result<(), String> { - let json_rpc_url = matches.value_of("json_rpc_url").unwrap(); - let update_manifest_pubkey = pubkey_of(matches, "update_manifest_pubkey"); let data_dir = matches.value_of("data_dir").unwrap(); let no_modify_path = matches.is_present("no_modify_path"); let explicit_release = explicit_release_of(matches, "explicit_release"); - if update_manifest_pubkey.is_none() && explicit_release.is_none() { - Err(format!( - "Please specify the release to install for {}. See --help for more", - build_env::TARGET - )) - } else { - command::init( - config_file, - data_dir, - json_rpc_url, - &update_manifest_pubkey.unwrap_or_default(), - no_modify_path, - explicit_release, - ) - } + command::init(config_file, data_dir, no_modify_path, explicit_release) } pub fn main() -> Result<(), String> { @@ -116,36 +84,17 @@ pub fn main() -> Result<(), String> { None => arg, } }) - .arg( - Arg::with_name("json_rpc_url") - .short("u") - .long("url") - .value_name("URL") - .takes_value(true) - .default_value(defaults::JSON_RPC_URL) - .validator(is_url) - .help("JSON RPC URL for the solana cluster"), - ) .arg( Arg::with_name("no_modify_path") .long("no-modify-path") .help("Don't configure the PATH environment variable"), ) - .arg( - Arg::with_name("update_manifest_pubkey") - .short("p") - .long("pubkey") - .value_name("PUBKEY") - .takes_value(true) - .validator(is_pubkey) - .help("Public key of the update manifest"), - ) .arg( Arg::with_name("explicit_release") .value_name("release") .index(1) - .conflicts_with_all(&["json_rpc_url", "update_manifest_pubkey"]) .validator(is_explicit_release) + .required(true) .help("The release version or channel to install"), ), ) @@ -165,47 +114,6 @@ pub fn main() -> Result<(), String> { .help("Display information in a format that can be used with `eval`"), ), ) - .subcommand( - SubCommand::with_name("deploy") - .about("Deploys a new update") - .setting(AppSettings::DisableVersion) - .arg({ - let arg = Arg::with_name("from_keypair_file") - .short("k") - .long("keypair") - .value_name("PATH") - .takes_value(true) - .required(true) - .help("Keypair file of the account that funds the deployment"); - match *defaults::USER_KEYPAIR { - Some(ref config_file) => arg.default_value(config_file), - None => arg, - } - }) - .arg( - Arg::with_name("json_rpc_url") - .short("u") - .long("url") - .value_name("URL") - .takes_value(true) - .default_value(defaults::JSON_RPC_URL) - .validator(is_url) - .help("JSON RPC URL for the solana cluster"), - ) - .arg( - Arg::with_name("download_url") - .index(1) - .required(true) - .validator(is_url) - .help("URL to the solana release archive"), - ) - .arg( - Arg::with_name("update_manifest_keypair_file") - .index(2) - .required(true) - .help("Keypair file for the update manifest (/path/to/keypair.json)"), - ), - ) .subcommand( SubCommand::with_name("gc") .about("Delete older releases from the install cache to reclaim disk space") @@ -246,19 +154,6 @@ pub fn main() -> Result<(), String> { let eval = matches.is_present("eval"); command::info(config_file, local_info_only, eval).map(|_| ()) } - ("deploy", Some(matches)) => { - let from_keypair_file = matches.value_of("from_keypair_file").unwrap(); - let json_rpc_url = matches.value_of("json_rpc_url").unwrap(); - let download_url = matches.value_of("download_url").unwrap(); - let update_manifest_keypair_file = - matches.value_of("update_manifest_keypair_file").unwrap(); - command::deploy( - json_rpc_url, - from_keypair_file, - download_url, - update_manifest_keypair_file, - ) - } ("gc", Some(_matches)) => command::gc(config_file), ("update", Some(_matches)) => command::update(config_file, false).map(|_| ()), ("run", Some(matches)) => { @@ -306,36 +201,17 @@ pub fn main_init() -> Result<(), String> { None => arg, } }) - .arg( - Arg::with_name("json_rpc_url") - .short("u") - .long("url") - .value_name("URL") - .takes_value(true) - .default_value(defaults::JSON_RPC_URL) - .validator(is_url) - .help("JSON RPC URL for the solana cluster"), - ) .arg( Arg::with_name("no_modify_path") .long("no-modify-path") .help("Don't configure the PATH environment variable"), ) - .arg( - Arg::with_name("update_manifest_pubkey") - .short("p") - .long("pubkey") - .value_name("PUBKEY") - .takes_value(true) - .validator(is_pubkey) - .help("Public key of the update manifest"), - ) .arg( Arg::with_name("explicit_release") .value_name("release") .index(1) - .conflicts_with_all(&["json_rpc_url", "update_manifest_pubkey"]) .validator(is_explicit_release) + .required(true) .help("The release version or channel to install"), ) .get_matches(); diff --git a/install/src/update_manifest.rs b/install/src/update_manifest.rs deleted file mode 100644 index 97b842dee5f..00000000000 --- a/install/src/update_manifest.rs +++ /dev/null @@ -1,58 +0,0 @@ -use { - serde::{Deserialize, Serialize}, - solana_hash::Hash, - solana_keypair::signable::Signable, - solana_pubkey::Pubkey, - solana_signature::Signature, - std::{borrow::Cow, error, io}, -}; - -/// Information required to download and apply a given update -#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)] -pub struct UpdateManifest { - pub timestamp_secs: u64, // When the release was deployed in seconds since UNIX EPOCH - pub download_url: String, // Download URL to the release tar.bz2 - pub download_sha256: Hash, // SHA256 digest of the release tar.bz2 file -} - -/// Data of an Update Manifest program Account. -#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)] -pub struct SignedUpdateManifest { - pub manifest: UpdateManifest, - pub manifest_signature: Signature, - #[serde(skip)] - pub account_pubkey: Pubkey, -} - -impl Signable for SignedUpdateManifest { - fn pubkey(&self) -> Pubkey { - self.account_pubkey - } - - fn signable_data(&self) -> Cow<'_, [u8]> { - Cow::Owned(bincode::serialize(&self.manifest).expect("serialize")) - } - fn get_signature(&self) -> Signature { - self.manifest_signature - } - fn set_signature(&mut self, signature: Signature) { - self.manifest_signature = signature - } -} - -impl SignedUpdateManifest { - pub const MAX_SPACE: u64 = 256; // Enough space for a fully populated SignedUpdateManifest - - pub fn deserialize( - account_pubkey: &Pubkey, - input: &[u8], - ) -> Result> { - let mut manifest: SignedUpdateManifest = bincode::deserialize(input)?; - manifest.account_pubkey = *account_pubkey; - if !manifest.verify() { - Err(io::Error::other("Manifest failed to verify").into()) - } else { - Ok(manifest) - } - } -} diff --git a/io-uring/Cargo.toml b/io-uring/Cargo.toml index 9891cb807a2..2ae5cf2e08d 100644 --- a/io-uring/Cargo.toml +++ b/io-uring/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] @@ -20,3 +25,6 @@ smallvec = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] io-uring = { workspace = true } + +[lints] +workspace = true diff --git a/keygen/Cargo.toml b/keygen/Cargo.toml index 26130ba34d9..abc008da22c 100644 --- a/keygen/Cargo.toml +++ b/keygen/Cargo.toml @@ -11,6 +11,11 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +# `all-features` can't be used: remote-wallet-hidraw and remote-wallet-libusb +# forward to hidapi's mutually-exclusive linux backends. Document the default +# (hidraw) backend plus the unstable API. +features = ["agave-unstable-api"] +rustdoc-args = ["--cfg=docsrs"] [[bin]] name = "solana-keygen" @@ -24,6 +29,7 @@ remote-wallet-libusb = ["solana-remote-wallet/linux-static-libusb"] [dependencies] agave-votor-messages = { workspace = true } +bip39 = { workspace = true } bs58 = { workspace = true, features = ["std"] } clap = { version = "3.1.5", default-features = false, features = ["cargo", "std", "suggestions"] } dirs-next = { workspace = true } @@ -35,14 +41,16 @@ solana-cli-config = { workspace = true } solana-derivation-path = { workspace = true } solana-instruction = { version = "=3.4.0", features = ["bincode"] } solana-keypair = "=3.1.2" -solana-message = { version = "=4.1.1", features = ["wincode"] } +solana-message = { version = "=4.2.4", features = ["wincode"] } solana-pubkey = { version = "=4.2.0", default-features = false } -solana-remote-wallet = { workspace = true } +solana-remote-wallet = { workspace = true, features = ["keystone"] } solana-seed-derivable = { workspace = true } -solana-signer = "=3.0.0" +solana-signer = "=3.0.1" solana-version = { workspace = true } -tiny-bip39 = { workspace = true } [dev-dependencies] solana-pubkey = { workspace = true, features = ["rand"] } tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/keygen/src/keygen.rs b/keygen/src/keygen.rs index d893cac0e8d..abfe0275f98 100644 --- a/keygen/src/keygen.rs +++ b/keygen/src/keygen.rs @@ -1,12 +1,12 @@ #![allow(clippy::arithmetic_side_effects)] use { agave_votor_messages::consensus_message::BLS_KEYPAIR_DERIVE_SEED, - bip39::{Mnemonic, MnemonicType, Seed}, + bip39::Mnemonic, clap::{ Arg, ArgAction, ArgMatches, Command, builder::ValueParser, crate_description, crate_name, value_parser, }, - solana_bls_signatures::{Pubkey as BLSPubkey, keypair::Keypair as BLSKeypair}, + solana_bls_signatures::keypair::Keypair as BLSKeypair, solana_clap_v3_utils::{ DisplayError, input_parsers::{ @@ -394,7 +394,7 @@ fn app<'a>(num_threads: &'a str, crate_version: &'a str) -> Command<'a> { ) .subcommand( Command::new("bls_pubkey") - .about("Display the BLS pubkey derived from given ed25519 keypair file") + .about("Display the compressed BLS pubkey derived from given ed25519 keypair file") .disable_version_flag(true) .arg( Arg::new("keypair") @@ -485,13 +485,17 @@ fn write_pubkey_file(outfile: &str, pubkey: Pubkey) -> Result<(), Box String { + bs58::encode(bls_pubkey_compressed).into_string() +} + fn write_bls_pubkey_file( outfile: &str, - bls_pubkey: BLSPubkey, + bls_pubkey_compressed: &[u8], ) -> Result<(), Box> { use std::io::Write; - let printable = format!("{bls_pubkey}"); + let printable = format_bls_pubkey_compressed(bls_pubkey_compressed); let serialized = serde_json::to_string(&printable)?; if let Some(outdir) = std::path::Path::new(&outfile).parent() { @@ -538,14 +542,14 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { ("bls_pubkey", matches) => { let keypair = get_keypair_from_matches(matches, config, &mut wallet_manager)?; let bls_keypair = BLSKeypair::derive_from_signer(&keypair, BLS_KEYPAIR_DERIVE_SEED)?; - let bls_pubkey: BLSPubkey = bls_keypair.public.into_inner().into(); + let bls_pubkey_compressed = bls_keypair.public.to_bytes_compressed(); if matches.try_contains_id("outfile")? { let outfile = matches.get_one::("outfile").unwrap(); check_for_overwrite(outfile, matches)?; - write_bls_pubkey_file(outfile, bls_pubkey)?; + write_bls_pubkey_file(outfile, &bls_pubkey_compressed)?; } else { - println!("{bls_pubkey}"); + println!("{}", format_bls_pubkey_compressed(&bls_pubkey_compressed)); } } ("new", matches) => { @@ -566,7 +570,6 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { } let word_count = try_get_word_count(matches)?.unwrap(); - let mnemonic_type = MnemonicType::for_word_count(word_count)?; let language = try_get_language(matches)?.unwrap(); let silent = matches.try_contains_id("silent")?; @@ -576,14 +579,15 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { let derivation_path = acquire_derivation_path(matches)?; - let mnemonic = Mnemonic::new(mnemonic_type, language); + let mnemonic = Mnemonic::generate_in(language, word_count) + .map_err(|e| format!("Unable to generate mnemonic: {e}"))?; let (passphrase, passphrase_message) = acquire_passphrase_and_message(matches) .map_err(|err| format!("Unable to acquire passphrase: {err}"))?; - let seed = Seed::new(&mnemonic, &passphrase); + let seed = mnemonic.to_seed(&passphrase); let keypair = match derivation_path { - Some(_) => keypair_from_seed_and_derivation_path(seed.as_bytes(), derivation_path), - None => keypair_from_seed(seed.as_bytes()), + Some(_) => keypair_from_seed_and_derivation_path(&seed, derivation_path), + None => keypair_from_seed(&seed), }?; if let Some(outfile) = outfile { @@ -592,17 +596,17 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { } if !silent { - let phrase: &str = mnemonic.phrase(); + let phrase = mnemonic.to_string(); let divider = String::from_utf8(vec![b'='; phrase.len()]).unwrap(); println!( "{}\npubkey: {}\n{}\nSave this seed phrase{} to recover your new \ keypair:\n{}\n{}", - ÷r, + divider, keypair.pubkey(), - ÷r, + divider, passphrase_message, phrase, - ÷r + divider ); } } @@ -705,7 +709,6 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { let derivation_path = acquire_derivation_path(matches)?; let word_count = try_get_word_count(matches)?.unwrap(); - let mnemonic_type = MnemonicType::for_word_count(word_count)?; let language = try_get_language(matches)?.unwrap(); let (passphrase, passphrase_message) = if use_mnemonic { @@ -773,17 +776,18 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { ); } let (keypair, phrase) = if use_mnemonic { - let mnemonic = Mnemonic::new(mnemonic_type, language); - let seed = Seed::new(&mnemonic, &passphrase); + let mnemonic = Mnemonic::generate_in(language, word_count) + .expect("CLI word count is always a valid BIP39 length"); + let seed = mnemonic.to_seed(&passphrase); let keypair = match derivation_path { Some(_) => keypair_from_seed_and_derivation_path( - seed.as_bytes(), + &seed, derivation_path.clone(), ), - None => keypair_from_seed(seed.as_bytes()), + None => keypair_from_seed(&seed), } .unwrap(); - (keypair, mnemonic.phrase().to_string()) + (keypair, mnemonic.to_string()) } else { (Keypair::new(), "".to_string()) }; @@ -841,13 +845,12 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { String::from_utf8(vec![b'='; phrase.len()]).unwrap(); println!( "{}\nFound matching key {}", - ÷r, + divider, keypair.pubkey() ); println!( - "\nSave this seed phrase{} to recover your new \ - keypair:\n{}\n{}", - passphrase_message, phrase, ÷r + "\nSave this seed phrase{passphrase_message} to \ + recover your new keypair:\n{phrase}\n{divider}" ); } } @@ -907,12 +910,11 @@ mod tests { Ok(Pubkey::from_str(&printable)?) } - fn read_bls_pubkey_file(infile: &str) -> Result> { + fn read_bls_pubkey_file(infile: &str) -> Result, Box> { let f = std::fs::File::open(infile)?; let printable: String = serde_json::from_reader(f)?; - use std::str::FromStr; - Ok(BLSPubkey::from_str(&printable)?) + Ok(bs58::decode(printable).into_vec()?) } fn process_test_command(args: &[&str]) -> Result<(), Box> { @@ -1292,8 +1294,8 @@ mod tests { fn test_read_write_bls_pubkey() -> Result<(), std::boxed::Box> { let filename = "test_bls_pubkey.json"; let bls_keypair = BLSKeypair::new(); - let bls_pubkey: BLSPubkey = bls_keypair.public.into_inner().into(); - write_bls_pubkey_file(filename, bls_pubkey)?; + let bls_pubkey = bls_keypair.public.to_bytes_compressed(); + write_bls_pubkey_file(filename, &bls_pubkey)?; let read = read_bls_pubkey_file(filename)?; assert_eq!(read, bls_pubkey); std::fs::remove_file(filename)?; @@ -1323,7 +1325,7 @@ mod tests { let bls_keypair = BLSKeypair::derive_from_signer(&my_keypair, BLS_KEYPAIR_DERIVE_SEED).unwrap(); let read_bls_pubkey = read_bls_pubkey_file(&outfile_path).unwrap(); - assert_eq!(read_bls_pubkey, bls_keypair.public.into_inner().into()); + assert_eq!(read_bls_pubkey, bls_keypair.public.to_bytes_compressed()); } #[test] diff --git a/lattice-hash/Cargo.toml b/lattice-hash/Cargo.toml index b0706d7c92b..b100c4d76dc 100644 --- a/lattice-hash/Cargo.toml +++ b/lattice-hash/Cargo.toml @@ -8,6 +8,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] @@ -26,3 +31,6 @@ solana-lattice-hash = { path = ".", features = ["agave-unstable-api"] } [[bench]] name = "bench_lt_hash" harness = false + +[lints] +workspace = true diff --git a/leader-schedule/Cargo.toml b/leader-schedule/Cargo.toml index 4c739f4bd71..03ea28319f5 100644 --- a/leader-schedule/Cargo.toml +++ b/leader-schedule/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] diff --git a/ledger-tool/Cargo.toml b/ledger-tool/Cargo.toml index a15e5839f9d..8c8162efb8a 100644 --- a/ledger-tool/Cargo.toml +++ b/ledger-tool/Cargo.toml @@ -24,6 +24,7 @@ agave-feature-set = { workspace = true } agave-logger = { workspace = true } agave-reserved-account-keys = { workspace = true } agave-snapshots = { workspace = true } +agave-votor = { workspace = true } chrono = { workspace = true, features = ["default"] } clap = { workspace = true } crossbeam-channel = { workspace = true } @@ -73,6 +74,7 @@ solana-sbpf = { workspace = true, features = ["debugger", "jit"] } solana-sdk-ids = { workspace = true } solana-shred-version = { workspace = true } solana-signature = { workspace = true } +solana-signer-store = { workspace = true } solana-stake-interface = { workspace = true } solana-storage-bigtable = { workspace = true } solana-svm-callback = { workspace = true } @@ -99,3 +101,6 @@ signal-hook = { workspace = true } [dev-dependencies] assert_cmd = { workspace = true } tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/ledger-tool/src/args.rs b/ledger-tool/src/args.rs index 70a4ba34939..9d17cac53b6 100644 --- a/ledger-tool/src/args.rs +++ b/ledger-tool/src/args.rs @@ -251,6 +251,7 @@ pub fn parse_process_options(ledger_path: &Path, arg_matches: &ArgMatches<'_>) - let allow_dead_slots = arg_matches.is_present("allow_dead_slots"); let abort_on_invalid_block = arg_matches.is_present("abort_on_invalid_block"); let no_block_cost_limits = arg_matches.is_present("no_block_cost_limits"); + let skip_inter_slot_verification = arg_matches.is_present("skip_inter_slot_verification"); ProcessOptions { new_hard_forks, @@ -267,6 +268,7 @@ pub fn parse_process_options(ledger_path: &Path, arg_matches: &ArgMatches<'_>) - use_snapshot_archives_at_startup, abort_on_invalid_block, no_block_cost_limits, + skip_inter_slot_verification, ..ProcessOptions::default() } } @@ -362,7 +364,6 @@ pub fn get_accounts_db_config( index: Some(accounts_index_config), account_indexes: None, bank_hash_details_dir: ledger_tool_ledger_path, - shrink_paths: None, shrink_ratio: AccountShrinkThreshold::default(), read_cache_limit_bytes: None, read_cache_evict_sample_size: None, diff --git a/ledger-tool/src/bigtable.rs b/ledger-tool/src/bigtable.rs index 147eca4eb42..84c439906d5 100644 --- a/ledger-tool/src/bigtable.rs +++ b/ledger-tool/src/bigtable.rs @@ -392,7 +392,7 @@ async fn shreds( ) .filter(Shred::is_data) .collect(); - blockstore.insert_shreds(data_shreds, None, false)?; + blockstore.insert_shreds(data_shreds, false)?; } Ok(()) } @@ -568,32 +568,29 @@ pub async fn transaction_history( // and keep it around. This helps reduce BigTable query traffic and speeds up the // results for high-volume addresses loop { - if let Some((slot, block)) = &loaded_block { - if *slot == result.slot { - match block.transactions.get(index as usize).map(|tx_with_meta| { - ( - tx_with_meta.get_transaction(), - tx_with_meta.get_status_meta(), - ) - }) { - None => { - println!( - " Transaction info for {} is corrupt", - result.signature - ); - } - Some((transaction, meta)) => { - println_transaction( - &transaction, - meta.map(|m| m.into()).as_ref(), - " ", - None, - None, - ); - } + if let Some((slot, block)) = &loaded_block + && *slot == result.slot + { + match block.transactions.get(index as usize).map(|tx_with_meta| { + ( + tx_with_meta.get_transaction(), + tx_with_meta.get_status_meta(), + ) + }) { + None => { + println!(" Transaction info for {} is corrupt", result.signature); + } + Some((transaction, meta)) => { + println_transaction( + &transaction, + meta.map(|m| m.into()).as_ref(), + " ", + None, + None, + ); } - break; } + break; } match bigtable.get_confirmed_block(result.slot).await { Err(err) => { diff --git a/ledger-tool/src/blockstore.rs b/ledger-tool/src/blockstore.rs index b1771a5a556..920f44b63a0 100644 --- a/ledger-tool/src/blockstore.rs +++ b/ledger-tool/src/blockstore.rs @@ -642,7 +642,7 @@ fn do_blockstore_process_command(ledger_path: &Path, matches: &ArgMatches<'_>) - } let shreds = source.get_data_shreds_for_slot(slot, 0)?; let shreds = shreds.into_iter().map(Cow::Owned); - if target.insert_cow_shreds(shreds, None, true).is_err() { + if target.insert_cow_shreds(shreds, true).is_err() { warn!("error inserting shreds for slot {slot}"); } } @@ -1009,7 +1009,7 @@ pub mod tests { let num_slots = 5; let entries_per_shred = 5; let (shreds, _) = make_many_slot_entries(start_slot, num_slots, entries_per_shred); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // Mark even shreds as optimistically confirmed (0..num_slots).step_by(2).for_each(|slot| { diff --git a/ledger-tool/src/error.rs b/ledger-tool/src/error.rs index 4f03c905acb..6cb9d499389 100644 --- a/ledger-tool/src/error.rs +++ b/ledger-tool/src/error.rs @@ -13,6 +13,9 @@ pub enum LedgerToolError { #[error("{0}")] TransactionEncode(#[from] solana_transaction_status::EncodeError), + #[error("{0}")] + BitmapDecode(String), + #[error("{0}")] Io(#[from] std::io::Error), diff --git a/ledger-tool/src/ledger_utils.rs b/ledger-tool/src/ledger_utils.rs index 144fffce662..026d06af1c0 100644 --- a/ledger-tool/src/ledger_utils.rs +++ b/ledger-tool/src/ledger_utils.rs @@ -10,10 +10,7 @@ use { log::*, solana_accounts_db::{ accounts_db::TOTAL_IO_URING_BUFFERS_SIZE_LIMIT, - utils::{ - create_all_accounts_run_and_snapshot_dirs, move_and_async_delete_path_contents, - validate_account_paths_for_direct_io, - }, + utils::{create_all_accounts_run_and_snapshot_dirs, validate_account_paths_for_direct_io}, }, solana_clock::Slot, solana_core::{ @@ -35,7 +32,6 @@ use { leader_schedule_cache::LeaderScheduleCache, use_snapshot_archives_at_startup::UseSnapshotArchivesAtStartup, }, - solana_measure::measure_time, solana_pubkey::Pubkey, solana_rpc::transaction_status_service::TransactionStatusService, solana_runtime::{ @@ -45,8 +41,9 @@ use { }, bank_forks::BankForks, snapshot_controller::SnapshotController, - snapshot_utils::{self, clean_orphaned_account_snapshot_dirs}, + snapshot_utils, }, + solana_shred_version::compute_shred_version, solana_transaction::versioned::VersionedTransaction, solana_unified_scheduler_pool::DefaultSchedulerPool, std::{ @@ -81,9 +78,6 @@ const PROCESS_SLOTS_HELP_STRING: &str = #[derive(Error, Debug)] pub(crate) enum LoadAndProcessLedgerError { - #[error("failed to clean orphaned account snapshot directories: {0}")] - CleanOrphanedAccountSnapshotDirectories(#[source] std::io::Error), - #[error("failed to create all run and snapshot directories: {0}")] CreateAllAccountsRunAndSnapshotDirectories(#[source] std::io::Error), @@ -279,26 +273,8 @@ pub fn load_and_process_ledger( ) .map_err(LoadAndProcessLedgerError::ValidateAccountPaths)?; - let (_, measure_clean_account_paths) = measure_time!( - account_paths.iter().for_each(|path| { - if path.exists() { - info!("Cleaning contents of account path: {}", path.display()); - move_and_async_delete_path_contents(path); - } - }), - "Cleaning account paths" - ); - info!("{measure_clean_account_paths}"); - snapshot_utils::purge_incomplete_bank_snapshots(&snapshot_config.bank_snapshots_dir); - info!("Cleaning contents of account snapshot paths: {account_snapshot_paths:?}"); - clean_orphaned_account_snapshot_dirs( - &snapshot_config.bank_snapshots_dir, - &account_snapshot_paths, - ) - .map_err(LoadAndProcessLedgerError::CleanOrphanedAccountSnapshotDirectories)?; - let geyser_plugin_active = arg_matches.is_present("geyser_plugin_config"); let (accounts_update_notifier, transaction_notifier) = if geyser_plugin_active { let geyser_config_files = values_t_or_exit!(arg_matches, "geyser_plugin_config", String) @@ -375,6 +351,11 @@ pub fn load_and_process_ledger( ) .transpose() .unwrap_or_else(|| { + // Clean run from genesis — must not use any existing state from previous runs. + bank_forks_utils::discard_previous_run_state( + &snapshot_config.bank_snapshots_dir, + &account_paths, + ); bank_forks_utils::load_bank_forks_from_genesis( genesis_config, &blockstore, @@ -387,6 +368,26 @@ pub fn load_and_process_ledger( ) }) .map_err(LoadAndProcessLedgerError::LoadBankForks)?; + + // With hard-linking gone, an AppendVec's Drop removes the only copy of the storage file by + // default. For a fastboot load we don't want that — the files belong to the validator's + // local state and need to outlive this process. Storages ledger-tool creates itself (archive + // or genesis load) can be cleaned up normally. We detect fastboot via the bank snapshot dir + // at the root slot: archive/genesis paths went through `discard_previous_run_state`, which + // purges those dirs, so its presence here means we loaded from it. + { + let root_bank = bank_forks.read().unwrap().root_bank(); + let loaded_from_fastboot = snapshot_config + .bank_snapshots_dir + .join(root_bank.slot().to_string()) + .is_dir(); + if loaded_from_fastboot { + for storage in root_bank.get_snapshot_storages(None) { + storage.disable_remove_on_drop(); + } + } + } + let leader_schedule_cache = LeaderScheduleCache::new_from_bank(&bank_forks.read().unwrap().root_bank()); @@ -460,10 +461,15 @@ pub fn load_and_process_ledger( }; let accounts_background_service = AccountsBackgroundService::new(bank_forks.clone(), exit.clone(), abs_request_handler); + let shred_version = { + let hard_forks = bank_forks.read().unwrap().root_bank().hard_forks(); + compute_shred_version(&genesis_config.hash(), Some(&hard_forks)) + }; let result = blockstore_processor::process_blockstore_from_root( blockstore.as_ref(), &bank_forks, + shred_version, &leader_schedule_cache, &process_options, transaction_status_sender.as_ref(), diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index be6b2a2a048..d41b3cc5af6 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -1017,6 +1017,13 @@ fn main() { .global(true) .help(BlockVerificationMethod::cli_message()), ) + .arg( + Arg::with_name("skip_inter_slot_verification") + .long("skip-inter-slot-verification") + .takes_value(false) + .global(true) + .help("Skip inter-slot parent block ID verification while processing the ledger"), + ) .arg( Arg::with_name("unified_scheduler_handler_threads") .long("unified-scheduler-handler-threads") @@ -1516,14 +1523,6 @@ fn main() { .long("enable-capitalization-change") .takes_value(false) .help("If snapshot creation should succeed with a capitalization delta."), - ) - .arg( - Arg::with_name("fix_testnet_ed25519_precompile_account") - .long("fix-testnet-ed25519-precompile-account") - .help( - "correct misassigned owner and data on testnet ed25519 precompile \ - account deployment", - ), ), ) .subcommand( @@ -1852,6 +1851,7 @@ fn main() { SystemMonitorStatsReportConfig { report_os_memory_stats, report_os_network_stats: false, + xdp_network_config_report: None, report_os_cpu_stats: false, report_os_disk_stats: false, }, @@ -1990,6 +1990,7 @@ fn main() { SystemMonitorStatsReportConfig { report_os_memory_stats: true, report_os_network_stats: false, + xdp_network_config_report: None, report_os_cpu_stats: false, report_os_disk_stats: false, }, @@ -2083,9 +2084,6 @@ fn main() { archive_format }; - let fix_testnet_ed25519_precompile_account = - arg_matches.is_present("fix_testnet_ed25519_precompile_account"); - let genesis_config = open_genesis_config_by(&ledger_path, arg_matches); let mut process_options = parse_process_options(&ledger_path, arg_matches); @@ -2197,8 +2195,7 @@ fn main() { || !feature_gates_to_deactivate.is_empty() || !vote_accounts_to_destake.is_empty() || faucet_pubkey.is_some() - || bootstrap_validator_pubkeys.is_some() - || fix_testnet_ed25519_precompile_account; + || bootstrap_validator_pubkeys.is_some(); if child_bank_required { let mut child_bank = @@ -2285,64 +2282,21 @@ fn main() { .unwrap() .into_iter() { - if let Ok(StakeStateV2::Stake(meta, stake, _)) = account.state() { - if vote_accounts_to_destake.contains(&stake.delegation.voter_pubkey) - { - if verbose_level > 0 { - warn!( - "Undelegating stake account {} from {}", - address, stake.delegation.voter_pubkey, - ); - } - account.set_state(&StakeStateV2::Initialized(meta)).unwrap(); - bank.store_account(&address, &account); + if let Ok(StakeStateV2::Stake(meta, stake, _)) = account.state() + && vote_accounts_to_destake.contains(&stake.delegation.voter_pubkey) + { + if verbose_level > 0 { + warn!( + "Undelegating stake account {} from {}", + address, stake.delegation.voter_pubkey, + ); } + account.set_state(&StakeStateV2::Initialized(meta)).unwrap(); + bank.store_account(&address, &account); } } } - if fix_testnet_ed25519_precompile_account { - use solana_sdk_ids::{ed25519_program, native_loader, system_program}; - - if bank.cluster_type() != ClusterType::Testnet { - eprintln!( - "--fix-testnet-ed25519-precompile-account is incompatible with \ - the supplied base snapshot" - ); - std::process::exit(1); - } - - let mut ed25519_program_account = - bank.get_account(&ed25519_program::id()).unwrap_or_else(|| { - eprintln!("Error: `{}` is not deployed", ed25519_program::id()); - exit(1); - }); - - if ed25519_program_account.owner() != &system_program::id() { - eprintln!( - "Error: expected `{}` to be owned by `{}`, found `{}`", - ed25519_program::id(), - system_program::id(), - ed25519_program_account.owner(), - ); - exit(1); - } - - if !ed25519_program_account.data().is_empty() { - eprintln!( - "Error: expected `{}` account data to be empty, found {} bytes", - ed25519_program::id(), - ed25519_program_account.data().len(), - ); - exit(1); - } - - ed25519_program_account.set_owner(native_loader::id()); - ed25519_program_account.set_data_from_slice(b"ed25519_program"); - - bank.store_account(&ed25519_program::id(), &ed25519_program_account); - } - if let Some(bootstrap_validator_pubkeys) = bootstrap_validator_pubkeys { assert_eq!(bootstrap_validator_pubkeys.len() % 3, 0); diff --git a/ledger-tool/src/output.rs b/ledger-tool/src/output.rs index 65c10fd524e..d9c69e17546 100644 --- a/ledger-tool/src/output.rs +++ b/ledger-tool/src/output.rs @@ -3,7 +3,7 @@ use { error::{LedgerToolError, Result}, ledger_utils::get_program_ids, }, - chrono::{Local, TimeZone}, + agave_votor::consensus_pool::certificate_builder::MAXIMUM_VALIDATORS, itertools::Either, pretty_hex::PrettyHex, serde::{ @@ -13,24 +13,31 @@ use { solana_account::{AccountSharedData, ReadableAccount}, solana_accounts_db::is_loadable::IsLoadable as _, solana_cli_output::{ - CliAccount, CliAccountNewConfig, OutputFormat, QuietDisplay, VerboseDisplay, - display::{build_balance_message, writeln_transaction}, + CliAccount, CliAccountNewConfig, CliBlock, OutputFormat, QuietDisplay, VerboseDisplay, + display::writeln_transaction, }, solana_clock::{Slot, UnixTimestamp}, + solana_entry::block_component::{ + BlockComponent, BlockFooterV1, BlockMarkerV1, VersionedBlockFooter, VersionedBlockHeader, + VersionedBlockMarker, VersionedUpdateParent, + }, solana_hash::Hash, solana_ledger::{ - blockstore::{Blockstore, BlockstoreError}, + blockstore::{ + Blockstore, BlockstoreError, ConfirmedBlockComponent, + VersionedConfirmedBlockWithComponents, + }, blockstore_meta::{DuplicateSlotProof, ErasureMeta}, shred::{Shred, ShredType}, }, solana_pubkey::Pubkey, solana_runtime::bank::Bank, + solana_signer_store::{Decoded, decode}, solana_transaction::versioned::VersionedTransaction, solana_transaction_status::{ BlockEncodingOptions, ConfirmedBlock, Encodable, EncodedConfirmedBlock, EncodedTransactionWithStatusMeta, EntrySummary, Rewards, TransactionDetails, - UiTransactionEncoding, VersionedConfirmedBlock, VersionedConfirmedBlockWithEntries, - VersionedTransactionWithStatusMeta, + UiTransactionEncoding, VersionedConfirmedBlock, VersionedTransactionWithStatusMeta, }, std::{ cell::RefCell, @@ -132,6 +139,65 @@ fn writeln_entry(f: &mut dyn fmt::Write, i: usize, entry: &CliEntry, prefix: &st ) } +fn writeln_block_marker( + f: &mut dyn fmt::Write, + marker: &CliPopulatedBlockMarker, + prefix: &str, +) -> fmt::Result { + match marker { + CliPopulatedBlockMarker::BlockFooter(footer) => { + writeln!( + f, + "{prefix}Block Marker - BlockFooter: bank_hash: {}, block_producer_time_nanos: \ + {}, block_user_agent: {}", + footer.bank_hash, footer.block_producer_time_nanos, footer.block_user_agent, + )?; + + let final_cert_log = match &footer.final_cert { + Some(cert) => format!( + "FinalCertificate: slot: {}, block_id: {}, final_aggregate validators: {}, \ + notar_aggregate validators: {}", + cert.slot, cert.block_id, cert.final_validators, cert.notar_validators, + ), + None => "No finalization certificate".to_string(), + }; + let skip_reward_log = match &footer.skip_reward { + Some(cert) => format!( + "SkipRewardCertificate: slot: {}, validators: {}", + cert.slot, cert.validators, + ), + None => "SkipRewardCertificate: None".to_string(), + }; + let notar_reward_log = match &footer.notar_reward { + Some(cert) => format!( + "NotarRewardCertificate: slot: {}, validators: {}, block_id: {}", + cert.slot, cert.validators, cert.block_id, + ), + None => "NotarRewardCertificate: None".to_string(), + }; + + writeln!(f, "{prefix} {final_cert_log}")?; + writeln!(f, "{prefix} {skip_reward_log}")?; + writeln!(f, "{prefix} {notar_reward_log}") + } + CliPopulatedBlockMarker::BlockHeader(header) => writeln!( + f, + "{prefix}Block Marker - BlockHeader: parent_slot: {}, parent_block_id: {}", + header.parent_slot, header.parent_block_id, + ), + CliPopulatedBlockMarker::UpdateParent(update_parent) => writeln!( + f, + "{prefix}Block Marker - UpdateParent: new_parent_slot: {}, new_parent_block_id: {}", + update_parent.new_parent_slot, update_parent.new_parent_block_id, + ), + CliPopulatedBlockMarker::GenesisCertificate(certificate) => writeln!( + f, + "{prefix}Block Marker - GenesisCertificate: slot: {}, block_id: {}", + certificate.slot, certificate.block_id, + ), + } +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CliEntries { @@ -184,6 +250,22 @@ impl From<&CliPopulatedEntry> for CliEntry { } } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum CliPopulatedComponent { + EntryBatch(Vec), + BlockMarker(CliPopulatedBlockMarker), +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum CliPopulatedBlockMarker { + BlockFooter(CliPopulatedFooter), + BlockHeader(CliPopulatedHeader), + UpdateParent(CliPopulatedUpdateParent), + GenesisCertificate(CliPopulatedGenesisCertificate), +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CliPopulatedEntry { @@ -194,6 +276,117 @@ pub struct CliPopulatedEntry { transactions: Vec, } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliFinalCert { + slot: Slot, + final_validators: usize, + notar_validators: usize, + block_id: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliSkipRewards { + slot: Slot, + validators: usize, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliNotarRewards { + slot: Slot, + validators: usize, + block_id: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliPopulatedFooter { + bank_hash: String, + block_producer_time_nanos: u64, + block_user_agent: String, + final_cert: Option, + skip_reward: Option, + notar_reward: Option, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliPopulatedHeader { + parent_slot: Slot, + parent_block_id: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliPopulatedUpdateParent { + new_parent_slot: Slot, + new_parent_block_id: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliPopulatedGenesisCertificate { + slot: Slot, + block_id: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliBlockWithComponents { + #[serde(flatten)] + pub encoded_confirmed_block: EncodedConfirmedBlockWithComponents, + #[serde(skip_serializing)] + pub slot: Slot, +} + +impl QuietDisplay for CliBlockWithComponents {} +impl VerboseDisplay for CliBlockWithComponents {} + +impl fmt::Display for CliBlockWithComponents { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + CliBlock::display_block_meta( + f, + self.slot, + self.encoded_confirmed_block.parent_slot, + &self.encoded_confirmed_block.blockhash, + &self.encoded_confirmed_block.previous_blockhash, + self.encoded_confirmed_block.block_time, + self.encoded_confirmed_block.block_height, + &self.encoded_confirmed_block.rewards, + )?; + + let mut entry_index = 0; + for component in &self.encoded_confirmed_block.components { + match component { + CliPopulatedComponent::EntryBatch(entries) => { + for entry in entries { + writeln_entry(f, entry_index, &entry.into(), "")?; + for (index, transaction_with_meta) in entry.transactions.iter().enumerate() + { + writeln!(f, " Transaction {index}:")?; + writeln_transaction( + f, + &transaction_with_meta.transaction.decode().unwrap(), + transaction_with_meta.meta.as_ref(), + " ", + None, + None, + )?; + } + entry_index += 1; + } + } + CliPopulatedComponent::BlockMarker(marker) => { + writeln_block_marker(f, marker, "")?; + } + } + } + Ok(()) + } +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CliBlockWithEntries { @@ -208,84 +401,17 @@ impl VerboseDisplay for CliBlockWithEntries {} impl fmt::Display for CliBlockWithEntries { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "Slot: {}", self.slot)?; - writeln!( + CliBlock::display_block_meta( f, - "Parent Slot: {}", - self.encoded_confirmed_block.parent_slot + self.slot, + self.encoded_confirmed_block.parent_slot, + &self.encoded_confirmed_block.blockhash, + &self.encoded_confirmed_block.previous_blockhash, + self.encoded_confirmed_block.block_time, + self.encoded_confirmed_block.block_height, + &self.encoded_confirmed_block.rewards, )?; - writeln!(f, "Blockhash: {}", self.encoded_confirmed_block.blockhash)?; - writeln!( - f, - "Previous Blockhash: {}", - self.encoded_confirmed_block.previous_blockhash - )?; - if let Some(block_time) = self.encoded_confirmed_block.block_time { - writeln!( - f, - "Block Time: {:?}", - Local.timestamp_opt(block_time, 0).unwrap() - )?; - } - if let Some(block_height) = self.encoded_confirmed_block.block_height { - writeln!(f, "Block Height: {block_height:?}")?; - } - if !self.encoded_confirmed_block.rewards.is_empty() { - let mut rewards = self.encoded_confirmed_block.rewards.clone(); - rewards.sort_by(|a, b| a.pubkey.cmp(&b.pubkey)); - let mut total_rewards = 0; - writeln!(f, "Rewards:")?; - writeln!( - f, - " {:<44} {:^15} {:<15} {:<20} {:>14} {:>10}", - "Address", "Type", "Amount", "New Balance", "Percent Change", "Commission" - )?; - for reward in rewards { - let sign = if reward.lamports < 0 { "-" } else { "" }; - total_rewards += reward.lamports; - #[allow(clippy::format_in_format_args)] - writeln!( - f, - " {:<44} {:^15} {:>15} {} {}", - reward.pubkey, - if let Some(reward_type) = reward.reward_type { - format!("{reward_type}") - } else { - "-".to_string() - }, - format!( - "{}◎{:<14.9}", - sign, - build_balance_message(reward.lamports.unsigned_abs(), false, false) - ), - if reward.post_balance == 0 { - " - -".to_string() - } else { - format!( - "◎{:<19.9} {:>13.9}%", - build_balance_message(reward.post_balance, false, false), - (reward.lamports.abs() as f64 - / (reward.post_balance as f64 - reward.lamports as f64)) - * 100.0 - ) - }, - reward - .commission_bps - .map(|bps| format!("{:>8}.{:02}%", bps / 100, bps % 100)) - .or_else(|| reward.commission.map(|c| format!("{c:>9}%"))) - .unwrap_or_else(|| " -".to_string()) - )?; - } - - let sign = if total_rewards < 0 { "-" } else { "" }; - writeln!( - f, - "Total Rewards: {}◎{:<12.9}", - sign, - build_balance_message(total_rewards.unsigned_abs(), false, false) - )?; - } for (index, entry) in self.encoded_confirmed_block.entries.iter().enumerate() { writeln_entry(f, index, &entry.into(), "")?; for (index, transaction_with_meta) in entry.transactions.iter().enumerate() { @@ -415,6 +541,172 @@ impl From for CliDuplicateShred { } } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EncodedConfirmedBlockWithComponents { + pub previous_blockhash: String, + pub blockhash: String, + pub parent_slot: Slot, + pub components: Vec, + pub rewards: Rewards, + pub block_time: Option, + pub block_height: Option, +} + +fn cli_populated_block_marker_from( + marker: VersionedBlockMarker, +) -> Result { + let VersionedBlockMarker::V1(block_marker) = marker; + match block_marker { + BlockMarkerV1::BlockFooter(footer) => { + let VersionedBlockFooter::V1(footer) = footer.into_inner(); + Ok(CliPopulatedBlockMarker::BlockFooter( + cli_populated_footer_from_marker(footer)?, + )) + } + BlockMarkerV1::BlockHeader(header) => { + let VersionedBlockHeader::V1(header) = header.into_inner(); + Ok(CliPopulatedBlockMarker::BlockHeader(CliPopulatedHeader { + parent_slot: header.parent_slot, + parent_block_id: header.parent_block_id.to_string(), + })) + } + BlockMarkerV1::UpdateParent(update_parent) => { + let VersionedUpdateParent::V1(update_parent) = update_parent.into_inner(); + Ok(CliPopulatedBlockMarker::UpdateParent( + CliPopulatedUpdateParent { + new_parent_slot: update_parent.new_parent_slot, + new_parent_block_id: update_parent.new_parent_block_id.to_string(), + }, + )) + } + BlockMarkerV1::GenesisCertificate(certificate) => { + let certificate = certificate.into_inner(); + Ok(CliPopulatedBlockMarker::GenesisCertificate( + CliPopulatedGenesisCertificate { + slot: certificate.slot, + block_id: certificate.block_id.to_string(), + }, + )) + } + } +} + +fn cli_populated_footer_from_marker(footer: BlockFooterV1) -> Result { + let BlockFooterV1 { + bank_hash, + block_producer_time_nanos, + block_user_agent, + block_final_cert, + skip_reward_cert, + notar_reward_cert, + } = footer; + + let final_cert = if let Some(cert) = block_final_cert { + Some(CliFinalCert { + slot: cert.slot, + block_id: cert.block_id.to_string(), + final_validators: validator_count_log(&cert.final_aggregate.into_bitmap())?, + notar_validators: match cert.notar_aggregate { + Some(notar_aggregate) => validator_count_log(¬ar_aggregate.into_bitmap())?, + None => 0, + }, + }) + } else { + None + }; + + let skip_reward = if let Some(cert) = skip_reward_cert { + Some(CliSkipRewards { + slot: cert.slot, + validators: validator_count_log(cert.to_bitmap())?, + }) + } else { + None + }; + + let notar_reward = if let Some(cert) = notar_reward_cert { + Some(CliNotarRewards { + slot: cert.slot, + validators: validator_count_log(cert.bitmap())?, + block_id: cert.block_id.to_string(), + }) + } else { + None + }; + + Ok(CliPopulatedFooter { + bank_hash: bank_hash.to_string(), + block_producer_time_nanos, + block_user_agent: String::from_utf8_lossy(&block_user_agent).into_owned(), + final_cert, + skip_reward, + notar_reward, + }) +} + +fn validator_count_log(bitmap: &[u8]) -> Result { + match decode(bitmap, MAXIMUM_VALIDATORS) + .map_err(|err| LedgerToolError::BitmapDecode(format!("unable to decode ({err:?})")))? + { + Decoded::Base2(validators) => Ok(validators.count_ones()), + Decoded::Base3(first, second) => Ok(first.count_ones().saturating_add(second.count_ones())), + } +} + +impl EncodedConfirmedBlockWithComponents { + pub fn try_from( + block: EncodedConfirmedBlock, + components_iterator: impl IntoIterator, + ) -> Result { + let components = components_iterator + .into_iter() + .enumerate() + .map(|(i, component)| match component { + ConfirmedBlockComponent::EntryBatch(entry_batch) => { + let entries = entry_batch + .into_iter() + .enumerate() + .map(|(j, entry)| { + let ending_transaction_index = entry + .starting_transaction_index + .saturating_add(entry.num_transactions as usize); + let transactions = block + .transactions + .get(entry.starting_transaction_index..ending_transaction_index) + .ok_or(LedgerToolError::Generic(format!( + "Mismatched entry data and transactions: component {i:?}, \ + entry {j:?}" + )))?; + Ok(CliPopulatedEntry { + num_hashes: entry.num_hashes, + hash: entry.hash.to_string(), + num_transactions: entry.num_transactions, + starting_transaction_index: entry.starting_transaction_index, + transactions: transactions.to_vec(), + }) + }) + .collect::>>()?; + Ok(CliPopulatedComponent::EntryBatch(entries)) + } + ConfirmedBlockComponent::BlockMarker(marker) => Ok( + CliPopulatedComponent::BlockMarker(cli_populated_block_marker_from(marker)?), + ), + }) + .collect::>>()?; + + Ok(Self { + previous_blockhash: block.previous_blockhash, + blockhash: block.blockhash, + parent_slot: block.parent_slot, + components, + rewards: block.rewards, + block_time: block.block_time, + block_height: block.block_height, + }) + } +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EncodedConfirmedBlockWithEntries { @@ -432,25 +724,29 @@ impl EncodedConfirmedBlockWithEntries { block: EncodedConfirmedBlock, entries_iterator: impl IntoIterator, ) -> Result { - let mut entries = vec![]; - for (i, entry) in entries_iterator.into_iter().enumerate() { - let ending_transaction_index = entry - .starting_transaction_index - .saturating_add(entry.num_transactions as usize); - let transactions = block - .transactions - .get(entry.starting_transaction_index..ending_transaction_index) - .ok_or(LedgerToolError::Generic(format!( - "Mismatched entry data and transactions: entry {i:?}" - )))?; - entries.push(CliPopulatedEntry { - num_hashes: entry.num_hashes, - hash: entry.hash.to_string(), - num_transactions: entry.num_transactions, - starting_transaction_index: entry.starting_transaction_index, - transactions: transactions.to_vec(), - }); - } + let entries = entries_iterator + .into_iter() + .enumerate() + .map(|(i, entry)| { + let ending_transaction_index = entry + .starting_transaction_index + .saturating_add(entry.num_transactions as usize); + let transactions = block + .transactions + .get(entry.starting_transaction_index..ending_transaction_index) + .ok_or(LedgerToolError::Generic(format!( + "Mismatched entry data and transactions: entry {i:?}" + )))?; + Ok(CliPopulatedEntry { + num_hashes: entry.num_hashes, + hash: entry.hash.to_string(), + num_transactions: entry.num_transactions, + starting_transaction_index: entry.starting_transaction_index, + transactions: transactions.to_vec(), + }) + }) + .collect::>>()?; + Ok(Self { previous_blockhash: block.previous_blockhash, blockhash: block.blockhash, @@ -575,45 +871,61 @@ pub fn output_slot( let Some(meta) = blockstore.meta(slot)? else { return Ok(()); }; - let (block_contents, entries) = match blockstore.get_complete_block_with_entries( + let (block_contents, components) = match blockstore.get_complete_block_with_components( slot, /*require_previous_blockhash:*/ false, - /*populate_entries:*/ true, + /*populate_components:*/ true, allow_dead_slots, ) { - Ok(VersionedConfirmedBlockWithEntries { block, entries }) => { - (BlockContents::VersionedConfirmedBlock(block), entries) + Ok(VersionedConfirmedBlockWithComponents { block, components }) => { + (BlockContents::VersionedConfirmedBlock(block), components) } Err(_) => { // Transaction metadata could be missing, try to fetch just the // entries and leave the metadata fields empty - let (entries, _, _) = blockstore.get_slot_entries_with_shred_info( + let (components, _, _) = blockstore.get_slot_components_with_shred_info( slot, /*shred_start_index:*/ 0, allow_dead_slots, )?; - let blockhash = entries - .last() + let blockhash = components + .iter() + .rev() + .find_map(|component| match component { + BlockComponent::EntryBatch(entries) => entries.last().map(|entry| entry.hash), + BlockComponent::BlockMarker(_) => None, + }) .filter(|_| meta.is_full()) - .map(|entry| entry.hash) - .unwrap_or(Hash::default()); + .unwrap_or_default(); let parent_slot = meta.parent_slot.unwrap_or(0); - - let mut entry_summaries = Vec::with_capacity(entries.len()); let mut starting_transaction_index = 0; - let transactions = entries + let mut transactions = Vec::new(); + + let components = components .into_iter() - .flat_map(|entry| { - entry_summaries.push(EntrySummary { - num_hashes: entry.num_hashes, - hash: entry.hash, - num_transactions: entry.transactions.len() as u64, - starting_transaction_index, - }); - starting_transaction_index += entry.transactions.len(); - - entry.transactions + .map(|component| match component { + BlockComponent::EntryBatch(entries) => { + let entry_summaries = entries + .into_iter() + .map(|entry| { + let entry_summary = EntrySummary { + num_hashes: entry.num_hashes, + hash: entry.hash, + num_transactions: entry.transactions.len() as u64, + starting_transaction_index, + }; + starting_transaction_index += entry.transactions.len(); + transactions.extend(entry.transactions); + entry_summary + }) + .collect(); + + ConfirmedBlockComponent::EntryBatch(entry_summaries) + } + BlockComponent::BlockMarker(marker) => { + ConfirmedBlockComponent::BlockMarker(marker) + } }) .collect(); @@ -622,10 +934,20 @@ pub fn output_slot( parent_slot, transactions, }; - (BlockContents::BlockWithoutMetadata(block), entry_summaries) + (BlockContents::BlockWithoutMetadata(block), components) } }; + let entries: Vec = components + .iter() + .filter_map(|component| match component { + ConfirmedBlockComponent::EntryBatch(entries) => Some(entries), + ConfirmedBlockComponent::BlockMarker(_) => None, + }) + .flatten() + .copied() + .collect(); + if verbose_level == 0 { if *output_format == OutputFormat::Display { // Given that Blockstore::get_complete_block_with_entries() returned Ok(_), we know @@ -652,7 +974,7 @@ pub fn output_slot( .last() .filter(|_| meta.is_full()) .map(|entry| entry.hash) - .unwrap_or(Hash::default()); + .unwrap_or_default(); let mut num_transactions = 0; let mut program_ids = HashMap::new(); @@ -672,7 +994,7 @@ pub fn output_slot( println!(" Programs:"); output_sorted_program_ids(program_ids); } - } else { + } else if verbose_level == 2 { let encoded_block = EncodedConfirmedBlock::try_from(block_contents)?; let cli_block = CliBlockWithEntries { encoded_confirmed_block: EncodedConfirmedBlockWithEntries::try_from( @@ -682,6 +1004,17 @@ pub fn output_slot( slot, }; + println!("{}", output_format.formatted_string(&cli_block)); + } else { + let encoded_block = EncodedConfirmedBlock::try_from(block_contents)?; + let cli_block = CliBlockWithComponents { + encoded_confirmed_block: EncodedConfirmedBlockWithComponents::try_from( + encoded_block, + components, + )?, + slot, + }; + println!("{}", output_format.formatted_string(&cli_block)); } @@ -880,10 +1213,10 @@ impl AccountsScanner { // output_config.data_slice_config will have been created to // yield an empty slice which will make data.empty() below true let account_data = cli_account.keyed_account.account.data.decode(); - if let Some(data) = account_data { - if !data.is_empty() { - println!("{:?}", data.hex_dump()); - } + if let Some(data) = account_data + && !data.is_empty() + { + println!("{:?}", data.hex_dump()); } } } @@ -952,10 +1285,10 @@ impl fmt::Display for CliAccounts { for account in &self.accounts { write!(f, "{account}")?; let account_data = account.keyed_account.account.data.decode(); - if let Some(data) = account_data { - if !data.is_empty() { - writeln!(f, "{:?}", data.hex_dump())?; - } + if let Some(data) = account_data + && !data.is_empty() + { + writeln!(f, "{:?}", data.hex_dump())?; } } Ok(()) diff --git a/ledger-tool/src/program.rs b/ledger-tool/src/program.rs index a5d6c864c2e..b06118f3e65 100644 --- a/ledger-tool/src/program.rs +++ b/ledger-tool/src/program.rs @@ -412,18 +412,17 @@ pub fn program(ledger_path: &Path, matches: &ArgMatches<'_>) { let space = data.len(); let account = if let Some(account) = bank.get_account_with_fixed_root(&pubkey) { let owner = *account.owner(); - if bpf_loader_upgradeable::check_id(&owner) { - if let Ok(UpgradeableLoaderState::Program { + if bpf_loader_upgradeable::check_id(&owner) + && let Ok(UpgradeableLoaderState::Program { programdata_address, }) = account.state() + { + debug!("Program data address {programdata_address}"); + if bank + .get_account_with_fixed_root(&programdata_address) + .is_some() { - debug!("Program data address {programdata_address}"); - if bank - .get_account_with_fixed_root(&programdata_address) - .is_some() - { - cached_account_keys.push(pubkey); - } + cached_account_keys.push(pubkey); } } // Override account data and lamports from input file if provided diff --git a/ledger-tool/tests/basic.rs b/ledger-tool/tests/basic.rs index b172a268182..810e3eaf10b 100644 --- a/ledger-tool/tests/basic.rs +++ b/ledger-tool/tests/basic.rs @@ -51,7 +51,7 @@ fn insert_test_shreds(ledger_path: &Path, ending_slot: u64) { ending_slot, /*entries_per_slot:*/ 10, ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); } #[test] diff --git a/ledger/Cargo.toml b/ledger/Cargo.toml index f704c82b36b..04050826dea 100644 --- a/ledger/Cargo.toml +++ b/ledger/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -32,7 +34,6 @@ agave-snapshots = { workspace = true } agave-votor-messages = { workspace = true } anyhow = { workspace = true } assert_matches = { workspace = true } -bincode = { workspace = true } bitflags = { workspace = true } bytes = { workspace = true } bzip2 = { workspace = true } @@ -61,7 +62,7 @@ serde = { workspace = true } solana-account = { workspace = true } solana-account-decoder = { workspace = true } solana-accounts-db = { workspace = true } -solana-address-lookup-table-interface = { workspace = true } +solana-address-lookup-table-interface = { workspace = true, features = ["wincode"] } solana-clock = { workspace = true } solana-cost-model = { workspace = true } solana-entry = { workspace = true } @@ -95,7 +96,7 @@ solana-sha256-hasher = { workspace = true } solana-shred-version = { workspace = true } solana-signature = { workspace = true, features = ["wincode"] } solana-signer = { workspace = true } -solana-stake-interface = { workspace = true } +solana-stake-interface = { workspace = true, features = ["wincode"] } solana-storage-bigtable = { workspace = true } solana-storage-proto = { workspace = true } solana-streamer = { workspace = true } diff --git a/ledger/benches/blockstore.rs b/ledger/benches/blockstore.rs index b74c8f41d4f..ea0ca327756 100644 --- a/ledger/benches/blockstore.rs +++ b/ledger/benches/blockstore.rs @@ -40,7 +40,7 @@ fn setup_read_bench( 0, // version ); blockstore - .insert_shreds(shreds, None, false) + .insert_shreds(shreds, false) .expect("Expected successful insertion of shreds into ledger"); } @@ -55,7 +55,7 @@ fn bench_write_shreds(bench: &mut Bencher) { Blockstore::open(ledger_path.path()).expect("Expected to be able to open database ledger"); bench.iter(move || { let shreds = entries_to_test_shreds(&entries, 0, 0, true, 0); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); }); } @@ -123,7 +123,7 @@ fn bench_insert_data_shred(bench: &mut Bencher) { let entries = create_ticks(num_entries, 0, Hash::default()); bench.iter(move || { let shreds = entries_to_test_shreds(&entries, 0, 0, true, 0); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); }); } diff --git a/ledger/benches/protobuf.rs b/ledger/benches/protobuf.rs deleted file mode 100644 index 122f58ce0e5..00000000000 --- a/ledger/benches/protobuf.rs +++ /dev/null @@ -1,95 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] -#![feature(test)] -extern crate test; - -use { - bincode::{deserialize, serialize}, - prost::Message, - solana_runtime::bank::RewardType, - solana_transaction_status::{Reward, Rewards}, - test::Bencher, -}; - -fn create_rewards() -> Rewards { - (0..100) - .map(|i| Reward { - pubkey: solana_pubkey::new_rand().to_string(), - lamports: 42 + i, - post_balance: u64::MAX, - reward_type: Some(RewardType::Fee), - commission: None, - commission_bps: None, - }) - .collect() -} - -fn bincode_serialize_rewards(rewards: Rewards) -> Vec { - serialize(&rewards).unwrap() -} - -fn protobuf_serialize_rewards(rewards: Rewards) -> Vec { - let rewards: solana_storage_proto::convert::generated::Rewards = rewards.into(); - let mut buffer = Vec::with_capacity(rewards.encoded_len()); - rewards.encode(&mut buffer).unwrap(); - buffer -} - -fn bincode_deserialize_rewards(bytes: &[u8]) -> Rewards { - deserialize(bytes).unwrap() -} - -fn protobuf_deserialize_rewards(bytes: &[u8]) -> Rewards { - solana_storage_proto::convert::generated::Rewards::decode(bytes) - .unwrap() - .into() -} - -fn bench_serialize_rewards(bench: &mut Bencher, serialize_method: S) -where - S: Fn(Rewards) -> Vec, -{ - let rewards = create_rewards(); - bench.iter(move || { - let _ = serialize_method(rewards.clone()); - }); -} - -fn bench_deserialize_rewards(bench: &mut Bencher, serialize_method: S, deserialize_method: D) -where - S: Fn(Rewards) -> Vec, - D: Fn(&[u8]) -> Rewards, -{ - let rewards = create_rewards(); - let rewards_bytes = serialize_method(rewards); - bench.iter(move || { - let _ = deserialize_method(&rewards_bytes); - }); -} - -#[bench] -fn bench_serialize_bincode(bencher: &mut Bencher) { - bench_serialize_rewards(bencher, bincode_serialize_rewards); -} - -#[bench] -fn bench_serialize_protobuf(bencher: &mut Bencher) { - bench_serialize_rewards(bencher, protobuf_serialize_rewards); -} - -#[bench] -fn bench_deserialize_bincode(bencher: &mut Bencher) { - bench_deserialize_rewards( - bencher, - bincode_serialize_rewards, - bincode_deserialize_rewards, - ); -} - -#[bench] -fn bench_deserialize_protobuf(bencher: &mut Bencher) { - bench_deserialize_rewards( - bencher, - protobuf_serialize_rewards, - protobuf_deserialize_rewards, - ); -} diff --git a/ledger/src/ancestor_iterator.rs b/ledger/src/ancestor_iterator.rs index d736f578c46..d3b4763ce7f 100644 --- a/ledger/src/ancestor_iterator.rs +++ b/ledger/src/ancestor_iterator.rs @@ -121,11 +121,11 @@ mod tests { let blockstore = Blockstore::open(ledger_path.path()).unwrap(); let (shreds, _) = make_slot_entries(0, 0, 42); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let (shreds, _) = make_slot_entries(1, 0, 42); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let (shreds, _) = make_slot_entries(2, 1, 42); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert_eq!( AncestorIterator::new(2, &blockstore).collect::>(), diff --git a/ledger/src/bank_forks_utils.rs b/ledger/src/bank_forks_utils.rs index ce06cd87582..a5162c4015d 100644 --- a/ledger/src/bank_forks_utils.rs +++ b/ledger/src/bank_forks_utils.rs @@ -17,11 +17,14 @@ use { snapshot_hash::{FullSnapshotHash, IncrementalSnapshotHash, StartingSnapshotHashes}, }, log::*, - solana_accounts_db::accounts_update_notifier_interface::AccountsUpdateNotifier, + solana_accounts_db::{ + accounts_update_notifier_interface::AccountsUpdateNotifier, + utils::move_and_async_delete_path_contents, + }, solana_genesis_config::GenesisConfig, solana_runtime::{bank_forks::BankForks, snapshot_bank_utils, snapshot_utils}, std::{ - path::PathBuf, + path::{Path, PathBuf}, sync::{Arc, RwLock, atomic::AtomicBool}, }, thiserror::Error, @@ -60,6 +63,19 @@ pub enum BankForksUtilsError { pub type BankAndHashes = (Arc>, Option); +/// Tear down state left over from a previous run: purges old bank snapshots, wipes the account +/// run dirs, and wipes the legacy snapshot sibling trees. +/// +/// Call from any load path that commits to NOT fastbooting (archive load, genesis boot) — +/// i.e. anywhere the existing storages must not be loaded. +pub fn discard_previous_run_state(bank_snapshots_dir: &Path, account_run_paths: &[PathBuf]) { + snapshot_utils::purge_all_bank_snapshots(bank_snapshots_dir); + for account_run_path in account_run_paths { + move_and_async_delete_path_contents(account_run_path); + } + snapshot_utils::wipe_account_snapshot_dirs(account_run_paths); +} + /// Load the banks via genesis pub fn load_bank_forks_from_genesis( genesis_config: &GenesisConfig, @@ -209,11 +225,10 @@ pub fn try_load_bank_forks_from_snapshot( path: fastboot_snapshot.snapshot_path(), })? } else { - // Given that we are going to boot from an archive, the append vecs held in the snapshot dirs for fast-boot should - // be released. They will be released by the account_background_service anyway. But in the case of the account_paths - // using memory-mounted file system, they are not released early enough to give space for the new append-vecs from - // the archives, causing the out-of-memory problem. So, purge the snapshot dirs upfront before loading from the archive. - snapshot_utils::purge_all_bank_snapshots(&snapshot_config.bank_snapshots_dir); + // Committed to loading from a snapshot archive — the existing storages a previous run + // left around (kept for fastboot) are now orphans, and the archive will be extracted + // into the (cleared) run dirs. + discard_previous_run_state(&snapshot_config.bank_snapshots_dir, account_paths); snapshot_bank_utils::bank_from_snapshot_archives( account_paths, @@ -252,6 +267,13 @@ pub fn try_load_bank_forks_from_snapshot( .accounts .accounts_db .set_latest_full_snapshot_slot(full_snapshot_archive_info.slot()); + // Set the last swept slot so the first full snapshot only triggers + // cleaning of zero lamport single ref accounts between the previous + // full snapshot and the new full snapshot + bank.rc + .accounts + .accounts_db + .set_last_swept_full_snapshot_slot(full_snapshot_archive_info.slot()); } else { assert!( bank.rc diff --git a/ledger/src/bit_vec.rs b/ledger/src/bit_vec.rs index e2eedd1b301..b1c74e96141 100644 --- a/ledger/src/bit_vec.rs +++ b/ledger/src/bit_vec.rs @@ -12,6 +12,10 @@ use { type Word = u8; const BITS_PER_WORD: usize = std::mem::size_of::() * 8; +const fn num_words() -> usize { + NUM_BITS.div_ceil(BITS_PER_WORD) +} + /// A bit vector implementation optimized for efficient bidirectional range /// scanning and iteration. /// @@ -38,6 +42,34 @@ impl Default for BitVec { } } +#[inline] +fn location_of(idx: usize) -> (usize, usize) { + let word_idx = idx / BITS_PER_WORD; + let bit_idx = idx & (BITS_PER_WORD - 1); + (word_idx, bit_idx) +} + +#[inline] +fn check_bounds(idx: usize) -> Result<(), BitVecError> { + if idx >= NUM_BITS { + return Err(BitVecError::OutOfBounds { + index: idx, + num_bits: NUM_BITS, + }); + } + Ok(()) +} + +#[inline] +fn contains(words: &[Word], idx: usize) -> bool { + if check_bounds::(idx).is_err() { + return false; + } + + let (word_idx, bit_idx) = location_of(idx); + (words[word_idx] & (1 << bit_idx)) != 0 +} + // Note: bincode/wincode would construct a variable-length buffer, // which violates `BitVec`'s invariant that its backing vector length (in words) // is exactly `NUM_WORDS`. Bounds checks and performance rely on this fixed size. @@ -66,7 +98,7 @@ pub enum BitVecError { } impl BitVec { - const NUM_WORDS: usize = NUM_BITS.div_ceil(BITS_PER_WORD); + const NUM_WORDS: usize = num_words::(); /// Get the word and bit offset for the given index. /// @@ -78,20 +110,14 @@ impl BitVec { /// assert_eq!(word_idx, 7); /// assert_eq!(bit_idx, 7); /// ``` + #[inline] pub fn location_of(idx: usize) -> (usize, usize) { - let word_idx = idx / BITS_PER_WORD; - let bit_idx = idx & (BITS_PER_WORD - 1); - (word_idx, bit_idx) + location_of(idx) } + #[inline] fn check_bounds(&self, idx: usize) -> Result<(), BitVecError> { - if idx >= NUM_BITS { - return Err(BitVecError::OutOfBounds { - index: idx, - num_bits: NUM_BITS, - }); - } - Ok(()) + check_bounds::(idx) } /// Remove a bit at the given index. @@ -228,13 +254,9 @@ impl BitVec { /// bit_vec.insert(63); /// assert!(bit_vec.contains(63)); /// ``` + #[inline] pub fn contains(&self, idx: usize) -> bool { - if self.check_bounds(idx).is_err() { - return false; - } - - let (word_idx, bit_idx) = Self::location_of(idx); - (self.words[word_idx] & (1 << bit_idx)) != 0 + contains::(&self.words, idx) } /// Get an iterator over the bits in the array within the given range. @@ -253,7 +275,7 @@ impl BitVec { /// assert_eq!(bit_vec.range(1..).count_ones(), 1); /// ``` pub fn range(&self, bounds: impl RangeBounds) -> BitVecSlice<'_, NUM_BITS> { - BitVecSlice::from_range_bounds(self, bounds) + BitVecSlice::from_range_bounds(&self.words, bounds) } /// Get an iterator over the positions of the set bits in the array. @@ -310,6 +332,47 @@ impl FromIterator for BitVec { } } +/// A reference to a [`BitVec`] that does not own the underlying data. +#[derive(Debug, PartialEq, Eq)] +pub struct BitVecRef<'a, const NUM_BITS: usize> { + words: &'a [Word], +} + +impl BitVecRef<'_, NUM_BITS> { + const NUM_WORDS: usize = num_words::(); + + /// Check if a bit is set at the given index. + /// + /// See [`BitVec::contains`]. + #[inline] + pub fn contains(&self, idx: usize) -> bool { + contains::(self.words, idx) + } + + /// Get an iterator over the bits in the array within the given range. + /// + /// See [`BitVec::range`]. + pub fn range(&self, bounds: impl RangeBounds) -> BitVecSlice<'_, NUM_BITS> { + BitVecSlice::from_range_bounds(self.words, bounds) + } +} + +unsafe impl<'de, const NUM_BITS: usize, C: Config> SchemaRead<'de, C> for BitVecRef<'de, NUM_BITS> { + type Dst = Self; + + #[inline] + fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + use wincode::ReadError; + + let words = <&'de [Word] as SchemaRead>::get(reader)?; + if words.len() != Self::NUM_WORDS { + return Err(ReadError::Custom("Invalid BitVec length")); + } + dst.write(Self { words }); + Ok(()) + } +} + /// A slice of a [`BitVec`] that provides efficient bit-level iteration. pub struct BitVecSlice<'a, const NUM_BITS: usize> { mask_iter: BitVecMaskIter<'a, NUM_BITS>, @@ -319,7 +382,7 @@ impl<'a, const NUM_BITS: usize> BitVecSlice<'a, NUM_BITS> { /// Construct a new [`BitVecSlice`] from a [`BitVec`] and a range. /// /// Internal function -- use [`BitVec::range`]. - fn from_range_bounds(bit_vec: &'a BitVec, bounds: impl RangeBounds) -> Self { + fn from_range_bounds(bit_vec: &'a [u8], bounds: impl RangeBounds) -> Self { let start = match bounds.start_bound() { Bound::Included(&n) => n, Bound::Excluded(&n) => n + 1, @@ -340,7 +403,7 @@ impl<'a, const NUM_BITS: usize> BitVecSlice<'a, NUM_BITS> { start, end, start_word, - iter: bit_vec.words[start_word..end_word].iter().enumerate(), + iter: bit_vec[start_word..end_word].iter().enumerate(), }, } } diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 0d13749e37b..f192ed693b1 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -7,13 +7,14 @@ use trees::{Tree, TreeWalk}; use { crate::{ ancestor_iterator::AncestorIterator, - blockstore::column::{Column, TypedColumn, columns as cf}, - blockstore_db::{IteratorDirection, IteratorMode, LedgerColumn, Rocks, WriteBatch}, + blockstore::column::{TypedColumn, columns as cf}, + blockstore_db::{ + DBPinnedT, IteratorDirection, IteratorMode, LedgerColumn, Rocks, WriteBatch, + }, blockstore_meta::*, blockstore_options::{ BLOCKSTORE_DIRECTORY_ROCKS_LEVEL, BlockstoreOptions, LedgerColumnOptions, }, - leader_schedule_cache::LeaderScheduleCache, next_slots_iterator::NextSlotsIterator, shred::{ self, DATA_SHREDS_PER_FEC_BLOCK, ErasureSetId, Payload, ProcessShredsStats, @@ -34,7 +35,7 @@ use { lru::LruCache, rand::Rng, rayon::iter::{IntoParallelIterator, ParallelIterator}, - rocksdb::{DBRawIterator, LiveFile}, + rocksdb::LiveFile, solana_account::ReadableAccount, solana_address_lookup_table_interface::state::AddressLookupTable, solana_clock::{Slot, UnixTimestamp}, @@ -42,26 +43,25 @@ use { block_component::{ BlockComponent, VersionedBlockHeader, VersionedBlockMarker, VersionedUpdateParent, }, - entry::{Entry, MaxDataShredsLen, create_ticks}, + entry::{Entry, create_ticks}, }, solana_genesis_config::{DEFAULT_GENESIS_ARCHIVE, DEFAULT_GENESIS_FILE, GenesisConfig}, solana_hash::{HASH_BYTES, Hash}, solana_keypair::Keypair, - solana_measure::measure::Measure, + solana_measure::{measure::Measure, measure_us}, solana_metrics::datapoint_error, solana_pubkey::Pubkey, - solana_runtime::bank::Bank, + solana_runtime::{bank::Bank, leader_schedule_utils::leader_slot_index}, solana_sha256_hasher::hashv, solana_signature::Signature, solana_signer::Signer, - solana_storage_proto::{StoredExtendedRewards, StoredTransactionStatusMeta}, solana_time_utils::timestamp, solana_transaction::{ TransactionVerificationMode, versioned::{VersionedTransaction, sanitized::SanitizedVersionedTransaction}, }, solana_transaction_status::{ - ConfirmedTransactionStatusWithSignature, ConfirmedTransactionWithStatusMeta, Rewards, + ConfirmedTransactionStatusWithSignature, ConfirmedTransactionWithStatusMeta, EntrySummary, RewardsAndNumPartitions, TransactionStatusMeta, TransactionWithStatusMeta, VersionedConfirmedBlock, VersionedConfirmedBlockWithEntries, VersionedTransactionWithStatusMeta, @@ -90,7 +90,7 @@ use { tar, tempfile::{Builder, TempDir}, thiserror::Error, - wincode::{Deserialize as _, containers::Vec as WincodeVec}, + wincode::config::DefaultConfig, }; pub mod blockstore_purge; @@ -101,7 +101,7 @@ pub use { blockstore::error::{BlockstoreError, Result}, blockstore_db::{default_num_compaction_threads, default_num_flush_threads}, blockstore_meta::{OptimisticSlotMetaVersioned, SlotMeta}, - blockstore_metrics::BlockstoreInsertionMetrics, + blockstore_metrics::{BlockstoreInsertionMetrics, BlockstoreSwitchBankMetrics}, }, blockstore_purge::PurgeType, rocksdb::properties as RocksProperties, @@ -143,7 +143,7 @@ pub struct UpdateParentSignal { // Contiguous, sorted and non-empty ranges of shred indices: // completed_ranges[i].start < completed_ranges[i].end // completed_ranges[i].end == completed_ranges[i + 1].start -// The ranges represent data shred indices that can reconstruct a Vec. +// The ranges represent data shred indices that can reconstruct a block component. // In particular, the data shred at index // completed_ranges[i].end - 1 // has DATA_COMPLETE_SHRED flag. @@ -197,10 +197,11 @@ pub enum PossibleDuplicateShred { shred::Payload, // conflict ), // Merkle root chaining conflict with previous fec set - ChainedMerkleRootConflict( - Shred, // original - shred::Payload, // conflict - ), + // As part of SIMD-0340 + ChainedMerkleRootConflict(Slot), + // Secondary chained merkle root conflict with previous fec set + // As part of SIMD-0340 + FixedFECChainedMerkleRootConflict(Slot), } impl PossibleDuplicateShred { @@ -210,7 +211,8 @@ impl PossibleDuplicateShred { Self::LastIndexConflict(shred, _) => shred.slot(), Self::ErasureConflict(shred, _) => shred.slot(), Self::MerkleRootConflict(shred, _) => shred.slot(), - Self::ChainedMerkleRootConflict(shred, _) => shred.slot(), + Self::ChainedMerkleRootConflict(slot) + | Self::FixedFECChainedMerkleRootConflict(slot) => *slot, } } } @@ -240,6 +242,17 @@ pub struct InsertResults { duplicate_shreds: Vec, } +#[allow(clippy::large_enum_variant)] +pub enum ConfirmedBlockComponent { + EntryBatch(Vec), + BlockMarker(VersionedBlockMarker), +} + +pub struct VersionedConfirmedBlockWithComponents { + pub block: VersionedConfirmedBlock, + pub components: Vec, +} + /// A "complete data set" is a range of [`Shred`]s that combined in sequence carry a single /// serialized [`Vec`]. /// @@ -396,13 +409,6 @@ impl SlotMetaWorkingSetEntry { } } -pub(crate) fn hashes_per_tick_for_ledger(genesis_config: &GenesisConfig) -> u64 { - let Some(hashes_per_tick) = genesis_config.poh_config.hashes_per_tick else { - return 0; - }; - hashes_per_tick -} - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub(crate) struct ParentInfo { /// Parent slot to expose through SlotMeta and replay. @@ -495,6 +501,14 @@ impl ParentInfo { self.replay_fec_set_index > 0 } + fn validate_update_parent_slot(&self, slot: Slot) -> Result<()> { + if self.has_update_parent() && leader_slot_index(slot) != 0 { + return Err(BlockstoreError::UpdateParentNotFirstInLeaderWindow(slot)); + } + + Ok(()) + } + /// True when this metadata came from the slot's block header. fn populated_from_block_header(&self) -> bool { self.replay_fec_set_index == 0 @@ -657,7 +671,6 @@ impl Blockstore { manual_purge_request_sender: Mutex::default(), slots_stats: SlotsStats::default(), }; - blockstore.cleanup_old_entries()?; Ok(blockstore) } @@ -742,7 +755,7 @@ impl Blockstore { if let Some(last_shred) = shreds.last() { merkle_roots.insert(slot, last_shred.merkle_root().unwrap()); } - self.insert_shreds(shreds, None, false).unwrap(); + self.insert_shreds(shreds, false).unwrap(); } walk.forward(); } @@ -777,6 +790,15 @@ impl Blockstore { self.meta_cf.get(slot) } + /// Returns the [`SlotMetaRepair`] of the specified slot. + pub fn meta_repair(&self, slot: Slot) -> Result> { + self.meta_cf + .get_slice(slot)? + .as_deref() + .map(|bytes| Ok(wincode::deserialize::(bytes)?)) + .transpose() + } + /// Returns the SlotMeta of the specified slot from the specified location pub fn meta_from_location( &self, @@ -869,6 +891,33 @@ impl Blockstore { ) } + /// Attempts to find the previous consecutive erasure set for `erasure_set` + /// using the fact that FEC sets are fixed in size at DATA_SHREDS_PER_FEC_BLOCK + /// data shreds. + /// + /// Only for use in the `Original` column + fn previous_fec_set_shred_id( + &self, + erasure_set: &ErasureSetId, + merkle_root_metas: &HashMap<(BlockLocation, ErasureSetId), WorkingEntry>, + ) -> Option { + let prev_erasure_set = erasure_set.previous_fec_set()?; + let prev_merkle_root_meta = merkle_root_metas + .get(&(BlockLocation::Original, prev_erasure_set)) + .map(WorkingEntry::as_ref) + .map(Cow::Borrowed) + .or_else(|| { + self.merkle_root_meta(prev_erasure_set) + .unwrap() + .map(Cow::Owned) + })?; + Some(ShredId::new( + erasure_set.slot(), + prev_merkle_root_meta.first_received_shred_index(), + prev_merkle_root_meta.first_received_shred_type(), + )) + } + /// Attempts to find the previous consecutive erasure set for `erasure_set`. /// /// Checks the map `erasure_metas`, if not present scans blockstore. Returns None @@ -989,11 +1038,15 @@ impl Blockstore { return Ok(None); }; - // Get the doulbe merkle meta - the block must be full, so we can unwrap here - let dmm = self - .get_double_merkle_meta_maybe_populate_proofs(slot, location)? - .expect("block is full, double merkle meta must exist"); - Ok(Some((dmm, location))) + // Block was full above, get_block_location returned Some, but may have + // been purged by BlockstoreCleanupService in between, or swapped out. + if let Some(dmm) = self.get_double_merkle_meta_maybe_populate_proofs(slot, location)? + && dmm.double_merkle_root == block_id + { + Ok(Some((dmm, location))) + } else { + Ok(None) + } } /// Gets the double merkle meta for the given block. @@ -1149,6 +1202,11 @@ impl Blockstore { return Ok(()); }; + if let Err(err) = new_parent_info.validate_update_parent_slot(slot) { + self.mark_invalid_parent_info_dead(write_batch, new_parent_info, slot, location, &err)?; + return Err(err); + } + let max_root = self.max_root(); if let Err(err) = new_parent_info.validate_shred_parent(slot, shred_parent_slot, max_root) { self.mark_invalid_parent_info_dead(write_batch, new_parent_info, slot, location, &err)?; @@ -1333,12 +1391,12 @@ impl Blockstore { _ => return false, }; while let Some(slot) = next_slots.pop_front() { - if let Ok(Some(slot_meta)) = self.meta(slot) { - if slot_meta.is_full() { - match slot.cmp(&ending_slot) { - cmp::Ordering::Less => next_slots.extend(slot_meta.next_slots), - _ => return true, - } + if let Ok(Some(slot_meta)) = self.meta(slot) + && slot_meta.is_full() + { + match slot.cmp(&ending_slot) { + cmp::Ordering::Less => next_slots.extend(slot_meta.next_slots), + _ => return true, } } } @@ -1497,7 +1555,6 @@ impl Blockstore { IntoIter: ExactSizeIterator, >, is_trusted: bool, - leader_schedule: Option<&LeaderScheduleCache>, shred_insertion_tracker: &mut ShredInsertionTracker<'a>, metrics: &mut BlockstoreInsertionMetrics, ) { @@ -1518,7 +1575,6 @@ impl Blockstore { location, shred_insertion_tracker, is_trusted, - leader_schedule, shred_source, ) { Err(InsertDataShredError::Exists) => { @@ -1659,7 +1715,6 @@ impl Blockstore { /// Note: We only perform recovery for the Original shred column fn handle_shred_recovery( &self, - leader_schedule: Option<&LeaderScheduleCache>, shred_recovery_context: &mut ShredRecoveryContext, shred_insertion_tracker: &mut ShredInsertionTracker, is_trusted: bool, @@ -1682,7 +1737,6 @@ impl Blockstore { BlockLocation::Original, shred_insertion_tracker, is_trusted, - leader_schedule, ShredSource::Recovered, ) { Err(InsertDataShredError::Exists) => &mut metrics.num_recovered_exists, @@ -1717,9 +1771,6 @@ impl Blockstore { continue; } let (slot, _) = erasure_set.store_key(); - if self.has_duplicate_shreds_in_slot(slot) { - continue; - } // First coding shred from this erasure batch, check the forward merkle root chaining // Note: This check is not performed on Alternate columns, as we cannot repair coding shreds @@ -1737,13 +1788,20 @@ impl Blockstore { .get(&(BlockLocation::Original, shred_id)) .expect("Erasure meta was just created, initial shred must exist"); - self.check_forward_chained_merkle_root_consistency( - shred, - erasure_meta, - &shred_insertion_tracker.just_inserted_shreds, - &shred_insertion_tracker.merkle_root_metas, - &mut shred_insertion_tracker.duplicate_shreds, - ); + // Forward check for SIMD-0340 + if let Some(next_fec_set_index) = erasure_meta.next_fec_set_index() { + let next_erasure_set = ErasureSetId::new(slot, next_fec_set_index); + if !self.check_forward_chained_merkle_root_consistency( + shred, + next_erasure_set, + &shred_insertion_tracker.just_inserted_shreds, + &shred_insertion_tracker.merkle_root_metas, + ) { + shred_insertion_tracker + .duplicate_shreds + .push(PossibleDuplicateShred::ChainedMerkleRootConflict(slot)); + } + } } for ((location, erasure_set), working_merkle_root_meta) in @@ -1754,9 +1812,6 @@ impl Blockstore { continue; } let (slot, _) = erasure_set.store_key(); - if self.has_duplicate_shreds_in_slot(slot) { - continue; - } // First shred from this erasure batch, check the backwards merkle root chaining // Note: this check is not performed on Alternate columns as they are already validated @@ -1776,12 +1831,59 @@ impl Blockstore { .get(&(*location, shred_id)) .expect("Merkle root meta was just created, initial shred must exist"); - self.check_backwards_chained_merkle_root_consistency( - shred, - &shred_insertion_tracker.just_inserted_shreds, - &shred_insertion_tracker.erasure_metas, - &mut shred_insertion_tracker.duplicate_shreds, - ); + // Backward check for SIMD-0340 + if let Some((_prev_erasure_set, prev_erasure_meta)) = self + .previous_erasure_set(*erasure_set, &shred_insertion_tracker.erasure_metas) + .expect("Expect database operations to succeed") + { + let prev_shred_id = ShredId::new( + slot, + prev_erasure_meta + .first_received_coding_shred_index() + .expect("First received coding index must fit in u32"), + ShredType::Code, + ); + if !self.check_backwards_chained_merkle_root_consistency( + shred, + prev_shred_id, + &shred_insertion_tracker.just_inserted_shreds, + ) { + shred_insertion_tracker + .duplicate_shreds + .push(PossibleDuplicateShred::ChainedMerkleRootConflict(slot)); + continue; + } + } + + // Encompassing forward check for SIMD-0340 + if let Some(next_erasure_set) = erasure_set.next_fec_set() + && !self.check_forward_chained_merkle_root_consistency( + shred, + next_erasure_set, + &shred_insertion_tracker.just_inserted_shreds, + &shred_insertion_tracker.merkle_root_metas, + ) + { + shred_insertion_tracker.duplicate_shreds.push( + PossibleDuplicateShred::FixedFECChainedMerkleRootConflict(slot), + ); + continue; + } + + // Encompassing backward check for SIMD-0340 + if let Some(prev_shred_id) = self + .previous_fec_set_shred_id(erasure_set, &shred_insertion_tracker.merkle_root_metas) + && !self.check_backwards_chained_merkle_root_consistency( + shred, + prev_shred_id, + &shred_insertion_tracker.just_inserted_shreds, + ) + { + shred_insertion_tracker.duplicate_shreds.push( + PossibleDuplicateShred::FixedFECChainedMerkleRootConflict(slot), + ); + continue; + } } } @@ -1879,10 +1981,15 @@ impl Blockstore { .map_err(|_| shred::Error::InvalidMerkleRoot) .and_then(|mr| mr.ok_or(shred::Error::InvalidMerkleRoot)) }) - // Add parent info as the last leaf + // Add parent info as the last leaf. The `fec_set_count` is bound + // into this leaf so that an adversary cannot convince a verifier + // that the count is off-by-one when the number of FEC sets is + // even (which makes the total leaf count odd and causes the last + // leaf to be hashed with itself during tree construction). .chain(std::iter::once(Ok(hashv(&[ &parent_slot.to_le_bytes(), parent_block_id.as_ref(), + &fec_set_count.to_le_bytes(), ])))); MerkleTree::try_new_with_len(merkle_tree_leaves, fec_set_count as usize + 1) @@ -2065,7 +2172,6 @@ impl Blockstore { /// Arguments: /// - `shreds`: the shreds to be inserted, alongside their repaired flag and /// insertion location. - /// - `leader_schedule`: the leader schedule /// - `is_trusted`: whether the shreds come from a trusted source. If this /// is set to true, then the function will skip the shred duplication and /// integrity checks. @@ -2082,7 +2188,6 @@ impl Blockstore { Item = (Cow<'a, Shred>, /*is_repaired:*/ bool, BlockLocation), IntoIter: ExactSizeIterator, >, - leader_schedule: Option<&LeaderScheduleCache>, is_trusted: bool, // When inserting own shreds during leader slots, we shouldn't try to // recover shreds. If shreds are not to be recovered we don't need the @@ -2103,7 +2208,6 @@ impl Blockstore { let result = self.do_insert_shreds_locked( &lock, shreds, - leader_schedule, is_trusted, shred_recovery_context, metrics, @@ -2124,7 +2228,6 @@ impl Blockstore { Item = (Cow<'a, Shred>, /*is_repaired:*/ bool, BlockLocation), IntoIter: ExactSizeIterator, >, - leader_schedule: Option<&LeaderScheduleCache>, is_trusted: bool, shred_recovery_context: Option<&mut ShredRecoveryContext>, metrics: &mut BlockstoreInsertionMetrics, @@ -2133,16 +2236,9 @@ impl Blockstore { let mut shred_insertion_tracker = ShredInsertionTracker::new(shreds.len(), self.get_write_batch()?); - self.attempt_shred_insertion( - shreds, - is_trusted, - leader_schedule, - &mut shred_insertion_tracker, - metrics, - ); + self.attempt_shred_insertion(shreds, is_trusted, &mut shred_insertion_tracker, metrics); if let Some(shred_recovery_context) = shred_recovery_context { self.handle_shred_recovery( - leader_schedule, shred_recovery_context, &mut shred_insertion_tracker, is_trusted, @@ -2197,7 +2293,6 @@ impl Blockstore { Item = (Cow<'a, Shred>, /*is_repaired:*/ bool), IntoIter: ExactSizeIterator, >, - leader_schedule: Option<&LeaderScheduleCache>, is_trusted: bool, shred_recovery_context: &mut ShredRecoveryContext, handle_duplicate: &F, @@ -2210,7 +2305,6 @@ impl Blockstore { shreds .into_iter() .map(|(shred, is_repaired)| (shred, is_repaired, BlockLocation::Original)), - leader_schedule, is_trusted, shred_recovery_context, handle_duplicate, @@ -2229,7 +2323,6 @@ impl Blockstore { Item = (Cow<'a, Shred>, /*is_repaired:*/ bool, BlockLocation), IntoIter: ExactSizeIterator, >, - leader_schedule: Option<&LeaderScheduleCache>, is_trusted: bool, shred_recovery_context: &mut ShredRecoveryContext, handle_duplicate: &F, @@ -2241,13 +2334,7 @@ impl Blockstore { let InsertResults { completed_data_set_infos, duplicate_shreds, - } = self.do_insert_shreds( - shreds, - leader_schedule, - is_trusted, - Some(shred_recovery_context), - metrics, - )?; + } = self.do_insert_shreds(shreds, is_trusted, Some(shred_recovery_context), metrics)?; for shred in duplicate_shreds { handle_duplicate(shred); @@ -2334,7 +2421,6 @@ impl Blockstore { self.do_insert_shreds_locked( lock, shreds, - None, // leader_schedule true, // is_trusted None, // should_recover_shreds &mut BlockstoreInsertionMetrics::default(), @@ -2363,9 +2449,15 @@ impl Blockstore { "Cannot switch from Original location" ); - let lock = self.insert_shreds_lock.lock().unwrap(); + let mut metrics = BlockstoreSwitchBankMetrics::default(); + + let mut total_measure = Measure::start("switch_block_from_alternate_total"); + + let (lock, lock_time_us) = measure_us!(self.insert_shreds_lock.lock().unwrap()); + metrics.lock_elapsed_us = lock_time_us; // 1. Backup the original block if needed + let mut backup_measure = Measure::start("switch_block_from_alternate_backup"); if let Some(dmr) = self.get_double_merkle_root(slot, BlockLocation::Original)? { let backup_location = BlockLocation::Alternate { block_id: dmr }; if self @@ -2375,8 +2467,11 @@ impl Blockstore { self.copy_shreds_locked(&lock, slot, BlockLocation::Original, backup_location)?; } } + backup_measure.stop(); + metrics.backup_elapsed_us = backup_measure.as_us(); // 2. Purge the original column data, keeping alternate columns intact + let mut purge_measure = Measure::start("switch_block_from_alternate_purge"); self.purge_slot_cleanup_chaining_keep_alt(slot) .or_else(|err| { if matches!(err, BlockstoreError::SlotUnavailable) { @@ -2386,14 +2481,19 @@ impl Blockstore { Err(err) } })?; + purge_measure.stop(); + metrics.purge_elapsed_us = purge_measure.as_us(); // 3. Copy shreds from alternate location to original + let mut copy_measure = Measure::start("switch_block_from_alternate_copy"); let alt_meta = self .meta_from_location(slot, from_location)? .expect("Alternate slot must have SlotMeta"); debug_assert!(alt_meta.is_full(), "Alternate slot must be full"); self.copy_shreds_locked(&lock, slot, from_location, BlockLocation::Original)?; + copy_measure.stop(); + metrics.copy_elapsed_us = copy_measure.as_us(); // 4. Verify the switch was successful debug_assert!( @@ -2402,7 +2502,10 @@ impl Blockstore { .is_full(), "Slot must be full after switch" ); + total_measure.stop(); + metrics.total_elapsed_us = total_measure.as_us(); + metrics.report_metrics(slot, from_location); Ok(()) } @@ -2411,7 +2514,6 @@ impl Blockstore { pub fn insert_cow_shreds<'a>( &self, shreds: impl IntoIterator, IntoIter: ExactSizeIterator>, - leader_schedule: Option<&LeaderScheduleCache>, is_trusted: bool, ) -> Result> { let shreds = shreds @@ -2419,7 +2521,6 @@ impl Blockstore { .map(|shred| (shred, /*is_repaired:*/ false, BlockLocation::Original)); let insert_results = self.do_insert_shreds( shreds, - leader_schedule, is_trusted, None, // Skip recovery for locally produced shreds. &mut BlockstoreInsertionMetrics::default(), @@ -2431,19 +2532,14 @@ impl Blockstore { pub fn insert_shreds( &self, shreds: impl IntoIterator, - leader_schedule: Option<&LeaderScheduleCache>, is_trusted: bool, ) -> Result> { let shreds = shreds.into_iter().map(Cow::Owned); - self.insert_cow_shreds(shreds, leader_schedule, is_trusted) + self.insert_cow_shreds(shreds, is_trusted) } #[cfg(test)] - fn insert_shred_return_duplicate( - &self, - shred: Shred, - leader_schedule: &LeaderScheduleCache, - ) -> Vec { + fn insert_shred_return_duplicate(&self, shred: Shred) -> Vec { let insert_results = self .do_insert_shreds( [( @@ -2451,7 +2547,6 @@ impl Blockstore { /*is_repaired:*/ false, BlockLocation::Original, )], - Some(leader_schedule), false, None, // Skip recovery for this direct insertion path. &mut BlockstoreInsertionMetrics::default(), @@ -2514,10 +2609,9 @@ impl Blockstore { if let HashMapEntry::Vacant(entry) = merkle_root_metas.entry((BlockLocation::Original, erasure_set)) + && let Some(meta) = self.merkle_root_meta(erasure_set).unwrap() { - if let Some(meta) = self.merkle_root_meta(erasure_set).unwrap() { - entry.insert(WorkingEntry::Clean(meta)); - } + entry.insert(WorkingEntry::Clean(meta)); } // This gives the index of first coding shred in this FEC block @@ -2666,10 +2760,9 @@ impl Blockstore { } else if let Some(potential_shred) = { let key = ShredId::new(slot, u32::try_from(coding_index).unwrap(), ShredType::Code); just_received_shreds.get(&(BlockLocation::Original, key)) - } { - if shred.erasure_mismatch(potential_shred).unwrap() { - return Some(Cow::Borrowed(potential_shred.payload())); - } + } && shred.erasure_mismatch(potential_shred).unwrap() + { + return Some(Cow::Borrowed(potential_shred.payload())); } } None @@ -2699,18 +2792,13 @@ impl Blockstore { /// data. /// - `is_trusted`: if false, this function will check whether the /// input shred is duplicate. - /// - `handle_duplicate`: the function that handles duplication. - /// - `leader_schedule`: the leader schedule will be used to check - /// whether it is okay to insert the input shred. /// - `shred_source`: the source of the shred. - #[allow(clippy::too_many_arguments)] fn check_insert_data_shred<'a>( &self, shred: Cow<'a, Shred>, location: BlockLocation, shred_insertion_tracker: &mut ShredInsertionTracker<'a>, is_trusted: bool, - leader_schedule: Option<&LeaderScheduleCache>, shred_source: ShredSource, ) -> std::result::Result<(), InsertDataShredError> { let slot = shred.slot(); @@ -2739,13 +2827,12 @@ impl Blockstore { let slot_meta = &mut slot_meta_entry.new_slot_meta.borrow_mut(); let erasure_set = shred.erasure_set(); - if let HashMapEntry::Vacant(entry) = merkle_root_metas.entry((location, erasure_set)) { - if let Some(meta) = self + if let HashMapEntry::Vacant(entry) = merkle_root_metas.entry((location, erasure_set)) + && let Some(meta) = self .merkle_root_meta_from_location(erasure_set, location) .unwrap() - { - entry.insert(WorkingEntry::Clean(meta)); - } + { + entry.insert(WorkingEntry::Clean(meta)); } if !is_trusted { @@ -2777,7 +2864,6 @@ impl Blockstore { slot_meta, just_inserted_shreds, self.max_root(), - leader_schedule, shred_source, duplicate_shreds, ) { @@ -2833,17 +2919,22 @@ impl Blockstore { write_batch, shred_source, ); - newly_completed_data_sets.extend(completed_data_sets); + + if matches!(location, BlockLocation::Original) { + // We don't currently notify RPC when we complete data sets in alternate columns. This can be extended in the future + // if necessary. + newly_completed_data_sets.extend(completed_data_sets); + } merkle_root_metas .entry((location, erasure_set)) .or_insert(WorkingEntry::Dirty(MerkleRootMeta::from_shred(&shred))); just_inserted_shreds.insert((location, shred.id()), shred); index_meta_working_set_entry.did_insert_occur = true; slot_meta_entry.did_insert_occur = true; - if let BTreeMapEntry::Vacant(entry) = erasure_metas.entry(erasure_set) { - if let Some(meta) = self.erasure_meta(erasure_set).unwrap() { - entry.insert(WorkingEntry::Clean(meta)); - } + if let BTreeMapEntry::Vacant(entry) = erasure_metas.entry(erasure_set) + && let Some(meta) = self.erasure_meta(erasure_set).unwrap() + { + entry.insert(WorkingEntry::Clean(meta)); } Ok(()) } @@ -2995,21 +3086,14 @@ impl Blockstore { fn check_forward_chained_merkle_root_consistency( &self, shred: &Shred, - erasure_meta: &ErasureMeta, + next_erasure_set: ErasureSetId, just_inserted_shreds: &HashMap<(BlockLocation, ShredId), Cow<'_, Shred>>, merkle_root_metas: &HashMap<(BlockLocation, ErasureSetId), WorkingEntry>, - duplicate_shreds: &mut Vec, ) -> bool { - debug_assert!(erasure_meta.check_coding_shred(shred)); let slot = shred.slot(); let erasure_set = shred.erasure_set(); // If a shred from the next fec set has already been inserted, check the chaining - let Some(next_fec_set_index) = erasure_meta.next_fec_set_index() else { - error!("Invalid erasure meta, unable to compute next fec set index {erasure_meta:?}"); - return false; - }; - let next_erasure_set = ErasureSetId::new(slot, next_fec_set_index); let Some(next_merkle_root_meta) = merkle_root_metas .get(&(BlockLocation::Original, next_erasure_set)) .map(WorkingEntry::as_ref) @@ -3036,10 +3120,9 @@ impl Blockstore { ) .map(Cow::into_owned) else { error!( - "Shred {next_shred_id:?} indicated by merkle root meta {next_merkle_root_meta:?} \ - is missing from blockstore. This should only happen in extreme cases where \ - blockstore cleanup has caught up to the root. Skipping the forward chained \ - merkle root consistency check" + "Shred {next_shred_id:?} is missing from blockstore. This should only happen in \ + extreme cases where blockstore cleanup has caught up to the root. Skipping the \ + forward chained merkle root consistency check" ); return true; }; @@ -3056,12 +3139,6 @@ impl Blockstore { next_merkle_root_meta.first_received_shred_type(), ); - if !self.has_duplicate_shreds_in_slot(shred.slot()) { - duplicate_shreds.push(PossibleDuplicateShred::ChainedMerkleRootConflict( - shred.clone(), - next_shred, - )); - } return false; } @@ -3083,12 +3160,10 @@ impl Blockstore { fn check_backwards_chained_merkle_root_consistency( &self, shred: &Shred, + prev_shred_id: ShredId, just_inserted_shreds: &HashMap<(BlockLocation, ShredId), Cow<'_, Shred>>, - erasure_metas: &BTreeMap>, - duplicate_shreds: &mut Vec, ) -> bool { let slot = shred.slot(); - let erasure_set = shred.erasure_set(); let fec_set_index = shred.fec_set_index(); if fec_set_index == 0 { @@ -3099,26 +3174,6 @@ impl Blockstore { return true; } - // If a shred from the previous fec set has already been inserted, check the chaining. - // Since we cannot compute the previous fec set index, we check the in memory map, otherwise - // check the previous key from blockstore to see if it is consecutive with our current set. - let Some((prev_erasure_set, prev_erasure_meta)) = self - .previous_erasure_set(erasure_set, erasure_metas) - .expect("Expect database operations to succeed") - else { - // No shreds from the previous erasure batch have been received, - // so nothing to check. Once the previous erasure batch is received, - // we will verify this chain through the forward check above. - return true; - }; - - let prev_shred_id = ShredId::new( - slot, - prev_erasure_meta - .first_received_coding_shred_index() - .expect("First received coding index must fit in u32"), - ShredType::Code, - ); let Some(prev_shred) = Self::get_shred_from_just_inserted_or_db( self, just_inserted_shreds, @@ -3127,10 +3182,9 @@ impl Blockstore { ) .map(Cow::into_owned) else { warn!( - "Shred {prev_shred_id:?} indicated by the erasure meta {prev_erasure_meta:?} is \ - missing from blockstore. This can happen if you have recently upgraded from a \ - version < v1.18.13, or if blockstore cleanup has caught up to the root. Skipping \ - the backwards chained merkle root consistency check" + "Shred {prev_shred_id:?} is missing from blockstore. This can happen if \ + blockstore cleanup has caught up to the root. Skipping the backwards chained \ + merkle root consistency check" ); return true; }; @@ -3140,19 +3194,12 @@ impl Blockstore { if !self.check_chaining(merkle_root, chained_merkle_root) { warn!( "Received conflicting chained merkle roots for slot: {slot}, shred {:?} type {:?} \ - chains to merkle root {chained_merkle_root:?}, however previous fec set coding \ - shred {prev_erasure_set:?} has merkle root {merkle_root:?}. Reporting as \ - duplicate", + chains to merkle root {chained_merkle_root:?}, however previous fec set shred \ + {prev_shred_id:?} has merkle root {merkle_root:?}. Reporting as duplicate", shred.erasure_set(), shred.shred_type(), ); - if !self.has_duplicate_shreds_in_slot(shred.slot()) { - duplicate_shreds.push(PossibleDuplicateShred::ChainedMerkleRootConflict( - shred.clone(), - prev_shred, - )); - } return false; } @@ -3174,7 +3221,6 @@ impl Blockstore { slot_meta: &SlotMeta, just_inserted_shreds: &HashMap<(BlockLocation, ShredId), Cow<'_, Shred>>, max_root: Slot, - leader_schedule: Option<&LeaderScheduleCache>, shred_source: ShredSource, duplicate_shreds: &mut Vec, ) -> bool { @@ -3189,15 +3235,13 @@ impl Blockstore { debug_assert_matches!(shred.sanitize(), Ok(())); // Check that we do not receive shred_index >= than the last_index // for the slot - let last_index = slot_meta.last_index; - if last_index.map(|ix| shred_index >= ix).unwrap_or_default() { - let leader_pubkey = leader_schedule - .and_then(|leader_schedule| leader_schedule.slot_leader_at(slot, None)); - + if let Some(meta_last_index) = slot_meta.last_index + && shred_index >= meta_last_index + { if !self.has_duplicate_shreds_in_slot(slot) { let shred_id = ShredId::new( slot, - u32::try_from(last_index.unwrap()).unwrap(), + u32::try_from(meta_last_index).unwrap(), ShredType::Data, ); let Some(ending_shred) = self @@ -3230,8 +3274,9 @@ impl Blockstore { ( "error", format!( - "Leader {leader_pubkey:?}, slot {slot}: received index {shred_index} >= \ - slot.last_index {last_index:?}, shred_source: {shred_source:?}" + "Received shred from {shred_source:?} with index={shred_index} >= \ + meta.last_index={meta_last_index}; shred: {:?}", + shred.id() ), String ) @@ -3241,9 +3286,6 @@ impl Blockstore { // Check that we do not receive a shred with "last_index" true, but shred_index // less than our current received if last_in_slot && shred_index < slot_meta.received { - let leader_pubkey = leader_schedule - .and_then(|leader_schedule| leader_schedule.slot_leader_at(slot, None)); - if !self.has_duplicate_shreds_in_slot(slot) { let shred_id = ShredId::new( slot, @@ -3280,9 +3322,10 @@ impl Blockstore { ( "error", format!( - "Leader {:?}, slot {}: received shred_index {} < slot.received {}, \ - shred_source: {:?}", - leader_pubkey, slot, shred_index, slot_meta.received, shred_source + "Received shred from {shred_source:?} with LAST_SHRED_IN_SLOT flag and \ + index={shred_index} < meta.received={}; shred: {:?}", + slot_meta.received, + shred.id() ), String ) @@ -3314,25 +3357,19 @@ impl Blockstore { }; if expected_shred_parent != shred_parent { - let leader_pubkey = leader_schedule - .and_then(|leader_schedule| leader_schedule.slot_leader_at(slot, None)); - datapoint_error!( "blockstore_error", ( "error", format!( - "Leader {:?}, shred_id {:?}: received shred with parent {} but expected \ - shred parent {}{}", - leader_pubkey, - shred.id(), - shred_parent, - expected_shred_parent, + "Received shred from {shred_source:?} with parent={shred_parent} != \ + expected_shred_parent={expected_shred_parent}{}; shred: {:?}", if slot_meta.has_update_parent() { " after UpdateParent" } else { " before UpdateParent" - } + }, + shred.id(), ), String ) @@ -3761,7 +3798,7 @@ impl Blockstore { // `write_entries` is a test helper that synthesizes local shreds. // Insert as trusted to avoid dead-slot marking from untrusted-path // filters (for example slot-0 coding shred checks). - self.insert_shreds(all_shreds, None, true)?; + self.insert_shreds(all_shreds, true)?; Ok(num_data) } @@ -3769,6 +3806,14 @@ impl Blockstore { self.index_cf.get(slot) } + /// Get a DB pinned reference to the [`Index`] via [`IndexRef`]. + fn get_index_ref( + &self, + slot: Slot, + ) -> Result>>> { + self.index_cf.get_pinned_t(slot) + } + pub fn get_index_from_location( &self, slot: Slot, @@ -3815,87 +3860,56 @@ impl Blockstore { /// [`start_index`, `end_index`]. /// /// Arguments: - /// - `db_iterator`: Iterator to run search over. /// - `slot`: The slot to search for missing shreds for. /// - `start_index`: Begin search (inclusively) at this shred index. /// - `end_index`: Finish search (exclusively) at this shred index. /// - `max_missing`: Limit result to this many indices. - fn find_missing_indexes( - db_iterator: &mut DBRawIterator, + pub fn find_missing_data_indexes( + &self, slot: Slot, start_index: u64, end_index: u64, max_missing: usize, - ) -> Vec - where - C: Column, - { + ) -> Vec { if start_index >= end_index || max_missing == 0 { return vec![]; } - let mut missing_indexes = vec![]; - - // Seek to the first shred with index >= start_index - db_iterator.seek(C::key(&(slot, start_index))); + let max_missing = max_missing.min((end_index - start_index) as usize); - // The index of the first missing shred in the slot - let mut prev_index = start_index; - loop { - if !db_iterator.valid() { - let num_to_take = max_missing - missing_indexes.len(); - missing_indexes.extend((prev_index..end_index).take(num_to_take)); - break; + let index = match self.get_index_ref(slot) { + Ok(Some(index)) => index, + Ok(None) => return (start_index..end_index).take(max_missing).collect(), + Err(err) => { + warn!("failed to read index for slot {slot}: {err}"); + return vec![]; } - let (current_slot, index) = C::index(db_iterator.key().expect("Expect a valid key")); + }; + let index = index.data(); - let current_index = { - if current_slot > slot { - end_index - } else { - index - } - }; + if index.num_shreds() == 0 { + return (start_index..end_index).take(max_missing).collect(); + } - let upper_index = cmp::min(current_index, end_index); + let mut missing_indexes = Vec::with_capacity(max_missing); + let mut prev_index = start_index; + for current_index in index.range(start_index..end_index) { let num_to_take = max_missing - missing_indexes.len(); - missing_indexes.extend((prev_index..upper_index).take(num_to_take)); + missing_indexes.extend((prev_index..current_index).take(num_to_take)); - if missing_indexes.len() == max_missing - || current_slot > slot - || current_index >= end_index - { + if missing_indexes.len() == max_missing { break; } prev_index = current_index + 1; - db_iterator.next(); } - missing_indexes - } - - /// Find missing data shreds for the given `slot`. - /// - /// For more details on the arguments, see [`find_missing_indexes`]. - pub fn find_missing_data_indexes( - &self, - slot: Slot, - start_index: u64, - end_index: u64, - max_missing: usize, - ) -> Vec { - let Ok(mut db_iterator) = self.db.raw_iterator_cf(self.data_shred_cf.handle()) else { - return vec![]; - }; + if missing_indexes.len() < max_missing { + let num_to_take = max_missing - missing_indexes.len(); + missing_indexes.extend((prev_index..end_index).take(num_to_take)); + } - Self::find_missing_indexes::( - &mut db_iterator, - slot, - start_index, - end_index, - max_missing, - ) + missing_indexes } fn get_block_time(&self, slot: Slot) -> Result> { @@ -4091,11 +4105,15 @@ impl Blockstore { } let previous_blockhash = previous_blockhash.unwrap_or_else(Hash::default); - let (rewards, num_partitions) = self - .rewards_cf - .get_protobuf_or_wincode::(slot)? - .unwrap_or_default() - .into(); + let RewardsAndNumPartitions { + rewards, + num_partitions, + } = self + .read_rewards(slot)? + .unwrap_or_else(|| RewardsAndNumPartitions { + rewards: Vec::new(), + num_partitions: None, + }); // The Blocktime and BlockHeight column families are updated asynchronously; they // may not be written by the time the complete slot entries are available. In this @@ -4119,6 +4137,193 @@ impl Blockstore { Ok(VersionedConfirmedBlockWithEntries { block, entries }) } + #[cfg(feature = "dev-context-only-utils")] + pub fn get_complete_block_with_components( + &self, + slot: Slot, + require_previous_blockhash: bool, + populate_components: bool, + allow_dead_slots: bool, + ) -> Result { + self.do_get_complete_block_with_components( + slot, + require_previous_blockhash, + populate_components, + allow_dead_slots, + ) + } + + #[cfg(feature = "dev-context-only-utils")] + fn do_get_complete_block_with_components( + &self, + slot: Slot, + require_previous_blockhash: bool, + populate_components: bool, + allow_dead_slots: bool, + ) -> Result { + let Some(slot_meta) = self.meta_cf.get(slot)? else { + trace!("do_get_complete_block_with_components() failed for {slot} (missing SlotMeta)"); + return Err(BlockstoreError::SlotUnavailable); + }; + + if !slot_meta.is_full() { + trace!("do_get_complete_block_with_components() failed for {slot} (slot not full)"); + return Err(BlockstoreError::SlotUnavailable); + } + + let (slot_components, _, _) = self.get_slot_components_with_shred_info( + slot, + /*start_index:*/ 0, + allow_dead_slots, + )?; + + if slot_components.is_empty() { + trace!( + "do_get_complete_block_with_components() failed for {slot} (no components found)" + ); + return Err(BlockstoreError::SlotUnavailable); + } + + let blockhash = slot_components + .iter() + .rev() + .find_map(|component| match component { + BlockComponent::EntryBatch(entries) => entries.last().map(|entry| entry.hash), + BlockComponent::BlockMarker(_) => None, + }) + .unwrap_or_else(|| panic!("Rooted slot {slot:?} must have blockhash")); + + let mut starting_transaction_index = 0; + let mut components = if populate_components { + Vec::with_capacity(slot_components.len()) + } else { + Vec::new() + }; + + let slot_transaction_iterator = slot_components + .into_iter() + .filter_map(|component| match component { + BlockComponent::EntryBatch(entries) => { + if populate_components { + let entry_summaries = entries + .iter() + .map(|entry| { + let entry_summary = EntrySummary { + num_hashes: entry.num_hashes, + hash: entry.hash, + num_transactions: entry.transactions.len() as u64, + starting_transaction_index, + }; + starting_transaction_index += entry.transactions.len(); + entry_summary + }) + .collect(); + components.push(ConfirmedBlockComponent::EntryBatch(entry_summaries)); + } + Some(entries) + } + BlockComponent::BlockMarker(marker) => { + if populate_components { + components.push(ConfirmedBlockComponent::BlockMarker(marker)); + } + None + } + }) + .flatten() + .flat_map(|entry| entry.transactions) + .map(|transaction| { + if let Err(err) = transaction.sanitize() { + warn!( + "Blockstore::get_complete_block_with_components sanitize failed: {err:?}, \ + slot: {slot:?}, {transaction:?}", + ); + } + transaction + }); + + let block = self.build_versioned_confirmed_block( + slot, + require_previous_blockhash, + allow_dead_slots, + &slot_meta, + &blockhash, + slot_transaction_iterator, + )?; + + Ok(VersionedConfirmedBlockWithComponents { block, components }) + } + + // Helper to build VersionConfirmedBlock from blockhash and transactions + #[cfg(feature = "dev-context-only-utils")] + fn build_versioned_confirmed_block( + &self, + slot: Slot, + require_previous_blockhash: bool, + allow_dead_slots: bool, + slot_meta: &SlotMeta, + blockhash: &Hash, + slot_transaction_iterator: impl Iterator, + ) -> Result { + let previous_blockhash = slot_meta.parent_slot.and_then(|parent_slot| { + self.get_slot_components_with_shred_info( + parent_slot, + /*shred_start_index:*/ 0, + allow_dead_slots, + ) + .ok() + .and_then(|(components, _, is_full)| { + // The blockhash is specifically the final entry hash in a + // block so ensure the block is full + if is_full { + components + .iter() + .rev() + .find_map(|component| match component { + BlockComponent::EntryBatch(entries) => { + entries.last().map(|entry| entry.hash) + } + BlockComponent::BlockMarker(_) => None, + }) + } else { + None + } + }) + }); + if previous_blockhash.is_none() && require_previous_blockhash { + return Err(BlockstoreError::ParentEntriesUnavailable); + } + let previous_blockhash = previous_blockhash.unwrap_or_default(); + + let RewardsAndNumPartitions { + rewards, + num_partitions, + } = self + .read_rewards(slot)? + .unwrap_or_else(|| RewardsAndNumPartitions { + rewards: Vec::new(), + num_partitions: None, + }); + + // The Blocktime and BlockHeight column families are updated asynchronously; they + // may not be written by the time the complete slot entries are available. In this + // case, these fields will be `None`. + let block_time = self.blocktime_cf.get(slot)?; + let block_height = self.block_height_cf.get(slot)?; + + Ok(VersionedConfirmedBlock { + previous_blockhash: previous_blockhash.to_string(), + blockhash: blockhash.to_string(), + // If the slot is full it should have parent_slot populated + // from shreds received. + parent_slot: slot_meta.parent_slot.unwrap(), + transactions: self.map_transactions_to_statuses(slot, slot_transaction_iterator)?, + rewards, + num_partitions, + block_time, + block_height, + }) + } + pub fn map_transactions_to_statuses( &self, slot: Slot, @@ -4137,35 +4342,6 @@ impl Blockstore { .collect() } - fn cleanup_old_entries(&self) -> Result<()> { - if !self.is_primary_access() { - return Ok(()); - } - - // If present, delete dummy entries inserted by old software - // https://github.com/solana-labs/solana/blob/bc2b372/ledger/src/blockstore.rs#L2130-L2137 - let transaction_status_dummy_key = cf::TransactionStatus::as_index(2); - if self - .transaction_status_cf - .get_protobuf_or_wincode::(transaction_status_dummy_key)? - .is_some() - { - self.transaction_status_cf - .delete(transaction_status_dummy_key)?; - }; - let address_signatures_dummy_key = cf::AddressSignatures::as_index(2); - if self - .address_signatures_cf - .get(address_signatures_dummy_key)? - .is_some() - { - self.address_signatures_cf - .delete(address_signatures_dummy_key)?; - }; - - Ok(()) - } - pub fn read_transaction_status( &self, index: (Signature, Slot), @@ -4671,9 +4847,9 @@ impl Blockstore { }) } - pub fn read_rewards(&self, index: Slot) -> Result> { + fn read_rewards(&self, slot: Slot) -> Result> { self.rewards_cf - .get_protobuf_or_wincode::(index) + .get_protobuf(slot) .map(|result| result.map(|option| option.into())) } @@ -4911,10 +5087,10 @@ impl Blockstore { .all(|(a, b)| a.start < a.end && a.end == b.start && b.start < b.end) ); let maybe_panic = |index: u64| { - if let Some(slot_meta) = slot_meta { - if slot > self.lowest_cleanup_slot() { - panic!("Missing shred. slot: {slot}, index: {index}, slot meta: {slot_meta:?}"); - } + if let Some(slot_meta) = slot_meta + && slot > self.lowest_cleanup_slot() + { + panic!("Missing shred. slot: {slot}, index: {index}, slot meta: {slot_meta:?}"); } }; let Some((&Range { start, .. }, &Range { end, .. })) = @@ -4966,14 +5142,19 @@ impl Blockstore { wincode::deserialize(&payload) .map(|component| vec![component]) .map_err(|e| { - BlockstoreError::InvalidShredData(format!( - "could not reconstruct block component: {e}" - )) + if BlockComponent::infer_is_empty_entry_batch(&payload) { + BlockstoreError::BlockAborted(slot) + } else { + BlockstoreError::InvalidShredData(format!( + "could not reconstruct block component: {e}" + )) + } }) }) } /// Fetch the entries corresponding to all of the shred indices in `completed_ranges`. + /// Block markers are skipped because they do not contain entries. fn get_slot_entries_in_block( &self, slot: Slot, @@ -4981,9 +5162,20 @@ impl Blockstore { slot_meta: Option<&SlotMeta>, ) -> Result> { self.get_slot_data_in_block(slot, completed_ranges, slot_meta, |payload| { - >::deserialize(&payload).map_err(|e| { - BlockstoreError::InvalidShredData(format!("could not reconstruct entries: {e}")) - }) + wincode::deserialize(&payload) + .map(|component| match component { + BlockComponent::BlockMarker(_) => vec![], + BlockComponent::EntryBatch(entries) => entries, + }) + .map_err(|e| { + if BlockComponent::infer_is_empty_entry_batch(&payload) { + BlockstoreError::BlockAborted(slot) + } else { + BlockstoreError::InvalidShredData(format!( + "could not reconstruct block component: {e}" + )) + } + }) }) } @@ -5033,13 +5225,14 @@ impl Blockstore { } pub fn insert_bank_hash(&self, slot: Slot, frozen_hash: Hash, is_duplicate_confirmed: bool) { - if let Some(prev_value) = self.bank_hash_cf.get(slot).unwrap() { - if prev_value.frozen_hash() == frozen_hash && prev_value.is_duplicate_confirmed() { - // Don't overwrite is_duplicate_confirmed == true with is_duplicate_confirmed == false, - // which may happen on startup when processing from blockstore processor because the - // blocks may not reflect earlier observed gossip votes from before the restart. - return; - } + if let Some(prev_value) = self.bank_hash_cf.get(slot).unwrap() + && prev_value.frozen_hash() == frozen_hash + && prev_value.is_duplicate_confirmed() + { + // Don't overwrite is_duplicate_confirmed == true with is_duplicate_confirmed == false, + // which may happen on startup when processing from blockstore processor because the + // blocks may not reflect earlier observed gossip votes from before the restart. + return; } let data = FrozenHashVersioned::Current(FrozenHashStatus { frozen_hash, @@ -5196,10 +5389,10 @@ impl Blockstore { ShredType::Code => self.get_coding_shred(slot, u64::from(index)), } .expect("fetch from DuplicateSlots column family failed")?; - if let Ok(signature) = shred.retransmitter_signature() { - if let Err(err) = shred::layout::set_retransmitter_signature(&mut other, &signature) { - error!("set retransmitter signature failed: {err:?}"); - } + if let Ok(signature) = shred.retransmitter_signature() + && let Err(err) = shred::layout::set_retransmitter_signature(&mut other, &signature) + { + error!("set retransmitter signature failed: {err:?}"); } (other != **shred.payload()).then_some(other) } @@ -5913,7 +6106,7 @@ impl Blockstore { pub fn insert_shreds_for_bank(&self, bank: Arc) { let entries = create_ticks(bank.ticks_per_slot(), 1, Hash::new_unique()); let shreds = entries_to_test_shreds(&entries, bank.slot(), bank.parent_slot(), true, 0); - self.insert_shreds(shreds, None, false).unwrap(); + self.insert_shreds(shreds, false).unwrap(); } } @@ -6118,9 +6311,9 @@ pub fn create_new_ledger( }, )?; let ticks_per_slot = genesis_config.ticks_per_slot; - // Slot-0 tick entries are created before a Bank exists, so derive the - // effective hashes-per-tick directly from genesis feature state here. - let hashes_per_tick = hashes_per_tick_for_ledger(genesis_config); + // Slot-0 tick entries are created before a Bank exists, so use the + // genesis PoH config directly. + let hashes_per_tick = genesis_config.poh_config.hashes_per_tick.unwrap_or(0); let entries = create_ticks(ticks_per_slot, hashes_per_tick, genesis_config.hash()); let last_hash = entries.last().unwrap().hash; let version = solana_shred_version::version_from_hash(&last_hash); @@ -6141,7 +6334,7 @@ pub fn create_new_ledger( ); assert!(shreds.last().unwrap().last_in_slot()); - blockstore.insert_shreds(shreds, None, false)?; + blockstore.insert_shreds(shreds, false)?; blockstore.set_roots(std::iter::once(&0))?; // Explicitly close the blockstore before we create the archived genesis file drop(blockstore); @@ -6390,95 +6583,6 @@ pub fn make_many_slot_entries( (shreds, entries) } -#[cfg(feature = "dev-context-only-utils")] -pub fn test_all_empty_or_min(blockstore: &Blockstore, min_slot: Slot) { - let condition_met = blockstore - .meta_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .roots_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .data_shred_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((slot, _), _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .code_shred_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((slot, _), _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .dead_slots_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .duplicate_slots_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .erasure_meta_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((slot, _), _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .orphans_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .index_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true) - & blockstore - .transaction_status_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((_, slot), _)| slot >= min_slot || slot == 0) - .unwrap_or(true) - & blockstore - .address_signatures_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|((_, slot, _, _), _)| slot >= min_slot || slot == 0) - .unwrap_or(true) - & blockstore - .rewards_cf - .iter(IteratorMode::Start) - .unwrap() - .next() - .map(|(slot, _)| slot >= min_slot) - .unwrap_or(true); - assert!(condition_met); -} - // Create shreds for slots that have a parent-child relationship defined by the input `chain` // used for tests only #[cfg(feature = "dev-context-only-utils")] diff --git a/ledger/src/blockstore/blockstore_purge.rs b/ledger/src/blockstore/blockstore_purge.rs index e4e7584b61a..8fac8943e32 100644 --- a/ledger/src/blockstore/blockstore_purge.rs +++ b/ledger/src/blockstore/blockstore_purge.rs @@ -121,16 +121,6 @@ impl Blockstore { } } - #[cfg(test)] - pub(crate) fn run_purge( - &self, - from_slot: Slot, - to_slot: Slot, - purge_type: PurgeType, - ) -> Result<()> { - self.run_purge_with_stats(from_slot, to_slot, purge_type, &mut PurgeStats::default()) - } - /// Purges all columns relating to `slot`. /// /// Additionally, we cleanup the parent of `slot` by clearing `slot` from @@ -404,8 +394,11 @@ impl Blockstore { } for slot in from_slot..=to_slot { - let (slot_entries, _, _) = - self.get_slot_entries_with_shred_info(slot, 0, true /* allow_dead_slots */)?; + let Ok((slot_entries, _, _)) = + self.get_slot_entries_with_shred_info(slot, 0, /*allow_dead_slots:*/ true) + else { + continue; + }; let transactions = slot_entries .into_iter() .flat_map(|entry| entry.transactions); @@ -445,9 +438,9 @@ impl Blockstore { // Deleting data newer than the latest root is likely to interfere // with replay so save any callers from themself let max_root = self.max_root(); - if max_slot_to_delete > max_root { + if max_slot_to_delete >= max_root { return Err(BlockstoreError::ManualPurge( - BlockstoreManualPurgeError::SlotNewerThanRoot { + BlockstoreManualPurgeError::SlotGreaterThanOrEqualToRoot { request_slot: max_slot_to_delete, max_root, }, @@ -476,32 +469,249 @@ pub mod tests { crate::{ blockstore::tests::make_slot_entries_with_transactions, get_tmp_ledger_path_auto_delete, }, - bincode::serialize, solana_entry::entry::next_entry_mut, solana_hash::Hash, solana_message::Message, solana_sha256_hasher::hash, solana_transaction::Transaction, test_case::test_case, + wincode::serialize, }; + fn all_columns_empty_or_greater_than_slot(blockstore: &Blockstore, min_slot: Slot) { + assert!( + blockstore + .data_shred_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .code_shred_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .index_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .erasure_meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .merkle_root_meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .double_merkle_meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .orphans_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .duplicate_slots_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + + assert!( + blockstore + .alt_data_shred_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .alt_meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .alt_index_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .alt_merkle_root_meta_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|((slot, _, _), _)| slot >= min_slot) + .unwrap_or(true) + ); + + assert!( + blockstore + .bank_hash_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .optimistic_slots_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .roots_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .dead_slots_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + + assert!( + blockstore + .block_height_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .blocktime_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + assert!( + blockstore + .rewards_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + // The slot is not stored in the leading bytes for keys in the + // `TransactionStatus`, `TransactionMemos`, and `AddressSignatures` + // columns so the entire column must be checked + assert!( + blockstore + .transaction_status_cf + .iter(IteratorMode::Start) + .unwrap() + .all(|((_, slot), _)| slot >= min_slot) + ); + assert!( + blockstore + .transaction_memos_cf + .iter(IteratorMode::Start) + .unwrap() + .all(|((_, slot), _)| slot >= min_slot) + ); + assert!( + blockstore + .address_signatures_cf + .iter(IteratorMode::Start) + .unwrap() + .all(|((_, slot, _, _), _)| slot >= min_slot) + ); + assert!( + blockstore + .perf_samples_cf + .iter(IteratorMode::Start) + .unwrap() + .next() + .map(|(slot, _)| slot >= min_slot) + .unwrap_or(true) + ); + } + #[test] fn test_purge_slots() { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()).unwrap(); let (shreds, _) = make_many_slot_entries(0, 50, 5); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); blockstore.purge_slots(0, 5, PurgeType::Exact).unwrap(); - - test_all_empty_or_min(&blockstore, 6); + all_columns_empty_or_greater_than_slot(&blockstore, 6); blockstore.purge_slots(0, 50, PurgeType::Exact).unwrap(); - // min slot shouldn't matter, blockstore should be empty - test_all_empty_or_min(&blockstore, 100); - test_all_empty_or_min(&blockstore, 0); + all_columns_empty_or_greater_than_slot(&blockstore, 100); + all_columns_empty_or_greater_than_slot(&blockstore, 0); assert_eq!(blockstore.slot_meta_iterator(0).unwrap().next(), None); } @@ -530,7 +740,7 @@ pub mod tests { } // Purging range outside of TransactionStatus max slots should not affect TransactionStatus data - blockstore.run_purge(10, 20, PurgeType::Exact).unwrap(); + blockstore.purge_slots(10, 20, PurgeType::Exact).unwrap(); let status_entries: Vec<_> = blockstore .transaction_status_cf @@ -541,7 +751,9 @@ pub mod tests { } fn clear_and_repopulate_transaction_statuses_for_test(blockstore: &Blockstore, max_slot: u64) { - blockstore.run_purge(0, max_slot, PurgeType::Exact).unwrap(); + blockstore + .purge_slots(0, max_slot, PurgeType::Exact) + .unwrap(); let mut iter = blockstore .transaction_status_cf .iter(IteratorMode::Start) @@ -565,7 +777,7 @@ pub mod tests { true, // is_full_slot 0, // version ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let signature = entries .iter() .filter(|entry| !entry.is_tick()) @@ -609,7 +821,7 @@ pub mod tests { true, // is_full_slot 0, // version ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); for transaction in entries.into_iter().flat_map(|entry| entry.transactions) { assert_eq!(transaction.signatures.len(), 1); @@ -632,12 +844,14 @@ pub mod tests { // Partially purge and ensure special columns are non-empty blockstore - .run_purge(0, max_slot - 5, PurgeType::Exact) + .purge_slots(0, max_slot - 5, PurgeType::Exact) .unwrap(); assert!(!blockstore.special_columns_empty().unwrap()); // Purge the rest and ensure the special columns are empty once again - blockstore.run_purge(0, max_slot, PurgeType::Exact).unwrap(); + blockstore + .purge_slots(0, max_slot, PurgeType::Exact) + .unwrap(); assert!(blockstore.special_columns_empty().unwrap()); } @@ -651,7 +865,7 @@ pub mod tests { // Test purge outside bounds clear_and_repopulate_transaction_statuses_for_test(&blockstore, max_slot); - blockstore.run_purge(10, 12, PurgeType::Exact).unwrap(); + blockstore.purge_slots(10, 12, PurgeType::Exact).unwrap(); let mut status_entry_iterator = blockstore .transaction_status_cf @@ -666,7 +880,7 @@ pub mod tests { // Test purge inside written range clear_and_repopulate_transaction_statuses_for_test(&blockstore, max_slot); - blockstore.run_purge(2, 4, PurgeType::Exact).unwrap(); + blockstore.purge_slots(2, 4, PurgeType::Exact).unwrap(); let mut status_entry_iterator = blockstore .transaction_status_cf @@ -683,7 +897,7 @@ pub mod tests { // Purge up to but not including max_slot clear_and_repopulate_transaction_statuses_for_test(&blockstore, max_slot); blockstore - .run_purge(0, max_slot - 1, PurgeType::Exact) + .purge_slots(0, max_slot - 1, PurgeType::Exact) .unwrap(); let mut status_entry_iterator = blockstore @@ -697,7 +911,7 @@ pub mod tests { // Test purge all clear_and_repopulate_transaction_statuses_for_test(&blockstore, max_slot); - blockstore.run_purge(0, 22, PurgeType::Exact).unwrap(); + blockstore.purge_slots(0, 22, PurgeType::Exact).unwrap(); let mut status_entry_iterator = blockstore .transaction_status_cf @@ -708,7 +922,7 @@ pub mod tests { fn purge_exact(blockstore: &Blockstore, oldest_slot: Slot) { blockstore - .run_purge(0, oldest_slot - 1, PurgeType::Exact) + .purge_slots(0, oldest_slot - 1, PurgeType::Exact) .unwrap(); } @@ -778,7 +992,7 @@ pub mod tests { true, // is_full_slot 0, // version ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let mut write_batch = blockstore.get_write_batch().unwrap(); blockstore @@ -846,7 +1060,7 @@ pub mod tests { let blockstore = Blockstore::open(ledger_path.path()).unwrap(); let (shreds, _) = make_many_slot_entries(0, 10, 5); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert!(matches!( blockstore.purge_slot_cleanup_chaining(11).unwrap_err(), @@ -860,11 +1074,11 @@ pub mod tests { let blockstore = Blockstore::open(ledger_path.path()).unwrap(); let (shreds, _) = make_many_slot_entries(0, 10, 5); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let (slot_11, _) = make_slot_entries(11, 4, 5); - blockstore.insert_shreds(slot_11, None, false).unwrap(); + blockstore.insert_shreds(slot_11, false).unwrap(); let (slot_12, _) = make_slot_entries(12, 5, 5); - blockstore.insert_shreds(slot_12, None, false).unwrap(); + blockstore.insert_shreds(slot_12, false).unwrap(); blockstore.purge_slot_cleanup_chaining(5).unwrap(); diff --git a/ledger/src/blockstore/error.rs b/ledger/src/blockstore/error.rs index bdae6108629..5b53df023d1 100644 --- a/ledger/src/blockstore/error.rs +++ b/ledger/src/blockstore/error.rs @@ -90,6 +90,8 @@ pub enum BlockstoreError { update_parent_slot: Slot, shred_parent_slot: Slot, }, + #[error("update parent is only valid in the first slot of a leader window for slot {0}")] + UpdateParentNotFirstInLeaderWindow(Slot), #[error("unexpected block component")] UnexpectedBlockComponent, #[error("multiple update parents for slot {0}")] @@ -111,6 +113,8 @@ pub enum BlockstoreError { block_header_parent_slot: Slot, shred_parent_slot: Slot, }, + #[error("Block in slot {0} was aborted as leader sent an empty entry batch")] + BlockAborted(Slot), } pub type Result = std::result::Result; @@ -119,8 +123,8 @@ pub enum BlockstoreManualPurgeError { #[error("purge request sender is unavailable")] SenderUnavailable, - #[error("purge request for slot {request_slot} is newer than the latest root {max_root}")] - SlotNewerThanRoot { request_slot: Slot, max_root: Slot }, + #[error("purge request for slot {request_slot} must be less than the latest root {max_root}")] + SlotGreaterThanOrEqualToRoot { request_slot: Slot, max_root: Slot }, #[error("purge request try send error")] TrySend, diff --git a/ledger/src/blockstore/tests.rs b/ledger/src/blockstore/tests.rs index 970abc63403..2f56d4e3d14 100644 --- a/ledger/src/blockstore/tests.rs +++ b/ledger/src/blockstore/tests.rs @@ -10,29 +10,23 @@ use { }, agave_feature_set::discard_unexpected_data_complete_shreds, assert_matches::assert_matches, - crossbeam_channel::unbounded, rand::{rng, seq::SliceRandom}, - solana_account_decoder::parse_token::UiTokenAmount, solana_entry::entry::next_entry_mut, solana_genesis_utils::{MAX_GENESIS_ARCHIVE_UNPACKED_SIZE, open_genesis_config}, solana_hash::Hash, - solana_leader_schedule::{FixedSchedule, LeaderSchedule, SlotLeader}, solana_message::{compiled_instruction::CompiledInstruction, v0::LoadedAddresses}, solana_packet::PACKET_DATA_SIZE, solana_pubkey::Pubkey, - solana_runtime::bank::{Bank, RewardType}, + solana_runtime::bank::Bank, solana_sha256_hasher::hash, solana_shred_version::version_from_hash, solana_signature::Signature, - solana_storage_proto::convert::generated, solana_streamer::evicting_sender::EvictingSender, solana_transaction::Transaction, solana_transaction_context::transaction::TransactionReturnData, solana_transaction_error::TransactionError, - solana_transaction_status::{ - InnerInstruction, InnerInstructions, Reward, Rewards, TransactionTokenBalance, - }, - std::{cmp::Ordering, num::NonZeroUsize, time::Duration}, + solana_transaction_status::{InnerInstruction, InnerInstructions}, + std::{cmp::Ordering, time::Duration}, test_case::{test_case, test_matrix}, }; @@ -48,7 +42,7 @@ pub(crate) fn make_slot_entries_with_transactions(num_entries: u64) -> Vec Vec { use solana_entry::block_component::UpdateParentV1; - let component = VersionedBlockMarker::new_update_parent(UpdateParentV1 { + let component = VersionedBlockMarker::from_update_parent(UpdateParentV1 { new_parent_slot: parent_slot, new_parent_block_id: parent_block_id, }); @@ -109,7 +103,7 @@ fn create_block_header_shreds_with_shred_parent( parent_block_id: Hash, ) -> Vec { use solana_entry::block_component::BlockHeaderV1; - let component = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let component = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot, parent_block_id, }); @@ -166,11 +160,11 @@ fn create_block_footer_shreds_with_last( bank_hash: Hash::new_unique(), block_producer_time_nanos: 0, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }; - let component = VersionedBlockMarker::new_block_footer(footer); + let component = VersionedBlockMarker::from_block_footer(footer); let component = BlockComponent::new_block_marker(component); Shredder::new(slot, parent_slot, 0, 0) @@ -192,15 +186,6 @@ fn data_shreds(shreds: Vec) -> Vec { shreds.into_iter().filter(Shred::is_data).collect() } -#[test] -fn test_hashes_per_tick_for_ledger() { - let mut genesis_config = GenesisConfig::default(); - assert_eq!(hashes_per_tick_for_ledger(&genesis_config), 0); - - genesis_config.poh_config.hashes_per_tick = Some(2); - assert_eq!(hashes_per_tick_for_ledger(&genesis_config), 2); -} - #[test] fn test_create_new_ledger() { agave_logger::setup(); @@ -250,7 +235,7 @@ fn test_insert_get_bytes() { let last_shred = shreds.pop().unwrap(); assert!(last_shred.index() > 0); blockstore - .insert_shreds(vec![last_shred.clone()], None, false) + .insert_shreds(vec![last_shred.clone()], false) .unwrap(); let serialized_shred = blockstore @@ -446,7 +431,7 @@ fn test_read_shred_bytes() { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let mut buf = [0; 4096]; let (_, bytes) = blockstore.get_data_shreds(slot, 0, 1, &mut buf).unwrap(); @@ -500,14 +485,14 @@ fn test_shred_cleanup_check() { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let mut buf = [0; 4096]; assert!(blockstore.get_data_shreds(slot, 0, 1, &mut buf).is_ok()); let max_purge_slot = 1; blockstore - .run_purge(0, max_purge_slot, PurgeType::Exact) + .purge_slots(0, max_purge_slot, PurgeType::Exact) .unwrap(); *blockstore.lowest_cleanup_slot.write().unwrap() = max_purge_slot; @@ -535,9 +520,7 @@ fn test_insert_data_shreds_basic() { // shreds starting from slot 0, index 0 should exist. assert!(shreds.len() > 1); let last_shred = shreds.pop().unwrap(); - blockstore - .insert_shreds(vec![last_shred], None, false) - .unwrap(); + blockstore.insert_shreds(vec![last_shred], false).unwrap(); assert!(blockstore.get_slot_entries(0, 0).unwrap().is_empty()); let meta = blockstore @@ -547,7 +530,7 @@ fn test_insert_data_shreds_basic() { assert!(meta.consumed == 0 && meta.received == num_shreds); // Insert the other shreds, check for consecutive returned entries - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let result = blockstore.get_slot_entries(0, 0).unwrap(); assert_eq!(result, entries); @@ -581,7 +564,7 @@ fn test_insert_data_shreds_reverse() { // Insert shreds in reverse, check for consecutive returned shreds for i in (0..num_shreds).rev() { let shred = shreds.pop().unwrap(); - blockstore.insert_shreds(vec![shred], None, false).unwrap(); + blockstore.insert_shreds(vec![shred], false).unwrap(); let result = blockstore.get_slot_entries(0, 0).unwrap(); let meta = blockstore @@ -613,7 +596,7 @@ fn test_get_slot_entries1() { let entries = create_ticks(8, 0, Hash::default()); let shreds = entries_to_test_shreds(&entries[0..4], 1, 0, false, 0); blockstore - .insert_shreds(shreds, None, false) + .insert_shreds(shreds, false) .expect("Expected successful write of shreds"); assert_eq!( @@ -640,7 +623,7 @@ fn test_get_slot_entries3() { let shreds = entries_to_test_shreds(&entries, slot, slot.saturating_sub(1), false, 0); assert!(shreds.len() as u64 >= shreds_per_slot); blockstore - .insert_shreds(shreds, None, false) + .insert_shreds(shreds, false) .expect("Expected successful write of shreds"); assert_eq!(blockstore.get_slot_entries(slot, 0).unwrap(), entries); } @@ -672,7 +655,7 @@ fn test_insert_data_shreds_consecutive() { } } - blockstore.insert_shreds(odd_shreds, None, false).unwrap(); + blockstore.insert_shreds(odd_shreds, false).unwrap(); assert_eq!(blockstore.get_slot_entries(slot, 0).unwrap(), vec![]); @@ -690,7 +673,7 @@ fn test_insert_data_shreds_consecutive() { assert_eq!(meta.last_index, None); } - blockstore.insert_shreds(even_shreds, None, false).unwrap(); + blockstore.insert_shreds(even_shreds, false).unwrap(); assert_eq!( blockstore.get_slot_entries(slot, 0).unwrap(), @@ -720,13 +703,13 @@ fn test_data_set_completed_on_insert() { assert!(num_shreds > 1); assert!( blockstore - .insert_shreds(shreds[1..].to_vec(), None, false) + .insert_shreds(shreds[1..].to_vec(), false) .unwrap() .is_empty() ); assert_eq!( blockstore - .insert_shreds(vec![shreds[0].clone()], None, false) + .insert_shreds(vec![shreds[0].clone()], false) .unwrap(), vec![CompletedDataSetInfo { slot, @@ -734,12 +717,7 @@ fn test_data_set_completed_on_insert() { }] ); // Inserting shreds again doesn't trigger notification - assert!( - blockstore - .insert_shreds(shreds, None, false) - .unwrap() - .is_empty() - ); + assert!(blockstore.insert_shreds(shreds, false).unwrap().is_empty()); } #[test] @@ -764,19 +742,19 @@ fn test_new_shreds_signal() { // Insert second shred, but we're missing the first shred, so no consecutive // shreds starting from slot 0, index 0 should exist. blockstore - .insert_shreds(vec![shreds.remove(1)], None, false) + .insert_shreds(vec![shreds.remove(1)], false) .unwrap(); let timer = Duration::from_secs(1); assert!(recvr.recv_timeout(timer).is_err()); // Insert first shred, now we've made a consecutive block blockstore - .insert_shreds(vec![shreds.remove(0)], None, false) + .insert_shreds(vec![shreds.remove(0)], false) .unwrap(); // Wait to get notified of update, should only be one update assert!(recvr.recv_timeout(timer).is_ok()); assert!(recvr.try_recv().is_err()); // Insert the rest of the ticks - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // Wait to get notified of update, should only be one update assert!(recvr.recv_timeout(timer).is_ok()); assert!(recvr.try_recv().is_err()); @@ -799,7 +777,7 @@ fn test_new_shreds_signal() { } // Should be no updates, since no new chains from block 0 were formed - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert!(recvr.recv_timeout(timer).is_err()); // For slots 1..num_slots/2, fill in the holes in one batch insertion, @@ -807,17 +785,13 @@ fn test_new_shreds_signal() { let missing_shreds2 = missing_shreds .drain((num_slots / 2) as usize..) .collect_vec(); - blockstore - .insert_shreds(missing_shreds, None, false) - .unwrap(); + blockstore.insert_shreds(missing_shreds, false).unwrap(); assert!(recvr.recv_timeout(timer).is_ok()); assert!(recvr.try_recv().is_err()); // Fill in the holes for each of the remaining slots, // we should get a single update - blockstore - .insert_shreds(missing_shreds2, None, false) - .unwrap(); + blockstore.insert_shreds(missing_shreds2, false).unwrap(); assert!(recvr.recv_timeout(timer).is_ok()); assert!(recvr.try_recv().is_err()); @@ -840,11 +814,11 @@ fn test_completed_shreds_signal() { let shred0 = shreds.remove(0); // Insert all but the first shred in the slot, should not be considered complete - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert!(recvr.try_recv().is_err()); // Insert first shred, slot should now be considered complete - blockstore.insert_shreds(vec![shred0], None, false).unwrap(); + blockstore.insert_shreds(vec![shred0], false).unwrap(); assert_eq!(recvr.try_recv().unwrap(), vec![0]); } @@ -870,25 +844,23 @@ fn test_completed_shreds_signal_orphans() { // Insert all but the first shred in the slot, should not be considered complete let orphan_child0 = orphan_child.remove(0); - blockstore.insert_shreds(orphan_child, None, false).unwrap(); + blockstore.insert_shreds(orphan_child, false).unwrap(); assert!(recvr.try_recv().is_err()); // Insert first shred, slot should now be considered complete blockstore - .insert_shreds(vec![orphan_child0], None, false) + .insert_shreds(vec![orphan_child0], false) .unwrap(); assert_eq!(recvr.try_recv().unwrap(), vec![slots[2]]); // Insert the shreds for the orphan_slot let orphan_shred0 = orphan_shreds.remove(0); - blockstore - .insert_shreds(orphan_shreds, None, false) - .unwrap(); + blockstore.insert_shreds(orphan_shreds, false).unwrap(); assert!(recvr.try_recv().is_err()); // Insert first shred, slot should now be considered complete blockstore - .insert_shreds(vec![orphan_shred0], None, false) + .insert_shreds(vec![orphan_shred0], false) .unwrap(); assert_eq!(recvr.try_recv().unwrap(), vec![slots[1]]); } @@ -923,7 +895,7 @@ fn test_completed_shreds_signal_many() { .collect(); all_shreds.shuffle(&mut rng()); - blockstore.insert_shreds(all_shreds, None, false).unwrap(); + blockstore.insert_shreds(all_shreds, false).unwrap(); let mut result = recvr.try_recv().unwrap(); result.sort_unstable(); slots.push(disconnected_slot); @@ -947,7 +919,7 @@ fn test_handle_chaining_basic() { let shreds1 = shreds .drain(shreds_per_slot..2 * shreds_per_slot) .collect_vec(); - blockstore.insert_shreds(shreds1, None, false).unwrap(); + blockstore.insert_shreds(shreds1, false).unwrap(); let meta1 = blockstore.meta(1).unwrap().unwrap(); assert!(meta1.next_slots.is_empty()); // Slot 1 is not connected because slot 0 hasn't been inserted yet @@ -959,7 +931,7 @@ fn test_handle_chaining_basic() { let shreds2 = shreds .drain(shreds_per_slot..2 * shreds_per_slot) .collect_vec(); - blockstore.insert_shreds(shreds2, None, false).unwrap(); + blockstore.insert_shreds(shreds2, false).unwrap(); let meta2 = blockstore.meta(2).unwrap().unwrap(); assert!(meta2.next_slots.is_empty()); // Slot 2 is not connected because slot 0 hasn't been inserted yet @@ -977,7 +949,7 @@ fn test_handle_chaining_basic() { // 3) Write to the zeroth slot, check that every slot // is now part of the trunk - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); for slot in 0..3 { let meta = blockstore.meta(slot).unwrap().unwrap(); // The last slot will not chain to any other slots @@ -1009,7 +981,7 @@ fn test_handle_chaining_missing_slots() { shreds.into_iter().partition(|shred| shred.slot() % 2 == 0); // Write the odd slot shreds - blockstore.insert_shreds(odd_slots, None, false).unwrap(); + blockstore.insert_shreds(odd_slots, false).unwrap(); for slot in 0..num_slots { // The slots that were inserted (the odds) will ... @@ -1035,7 +1007,7 @@ fn test_handle_chaining_missing_slots() { } // Write the even slot shreds that we did not earlier - blockstore.insert_shreds(even_slots, None, false).unwrap(); + blockstore.insert_shreds(even_slots, false).unwrap(); for slot in 0..num_slots { let meta = blockstore.meta(slot).unwrap().unwrap(); @@ -1082,9 +1054,7 @@ pub fn test_forward_chaining_is_connected() { let shred0 = shreds_for_slot.remove(0); missing_shreds.push(shred0); } - blockstore - .insert_shreds(shreds_for_slot, None, false) - .unwrap(); + blockstore.insert_shreds(shreds_for_slot, false).unwrap(); } // Check metadata @@ -1115,7 +1085,7 @@ pub fn test_forward_chaining_is_connected() { for slot_index in 0..num_slots { if slot_index % 3 == 0 { let shred = missing_shreds.remove(0); - blockstore.insert_shreds(vec![shred], None, false).unwrap(); + blockstore.insert_shreds(vec![shred], false).unwrap(); for slot in 0..num_slots { let meta = blockstore.meta(slot).unwrap().unwrap(); @@ -1171,7 +1141,7 @@ fn test_scan_and_fix_roots() { shreds.into_iter() }) .collect(); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // Start slot must be a root let (start, end) = (Some(16), None); @@ -1256,7 +1226,7 @@ fn test_set_and_chain_connected_on_root_and_next_slots() { // Insert some new slots and ensure they connect to the root correctly start_slot += 1; let (shreds, _) = make_many_slot_entries(start_slot, num_slots, entries_per_slot); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); for slot in start_slot..start_slot + num_slots { info!("Evaluating slot {slot}"); let meta = blockstore.meta(slot).unwrap().unwrap(); @@ -1284,7 +1254,7 @@ fn test_set_and_chain_connected_on_root_and_next_slots() { let (shreds, missing_shreds): (Vec<_>, Vec<_>) = shreds .into_iter() .partition(|shred| shred.slot() != non_full_slot || shred.index() == 0); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // Chain method hasn't been called yet, so none of these connected yet for slot in start_slot..start_slot + num_slots { let meta = blockstore.meta(slot).unwrap().unwrap(); @@ -1317,9 +1287,7 @@ fn test_set_and_chain_connected_on_root_and_next_slots() { } // Insert the missing shreds and ensure all slots connected now - blockstore - .insert_shreds(missing_shreds, None, false) - .unwrap(); + blockstore.insert_shreds(missing_shreds, false).unwrap(); for slot in start_slot..start_slot + num_slots { let meta = blockstore.meta(slot).unwrap().unwrap(); assert!(meta.is_parent_connected()); @@ -1418,9 +1386,7 @@ fn test_orphans() { // Write slot 2, which chains to slot 1. We're missing slot 0, // so slot 1 is the orphan let shreds_for_slot = shreds.drain((shreds_per_slot * 2)..).collect_vec(); - blockstore - .insert_shreds(shreds_for_slot, None, false) - .unwrap(); + blockstore.insert_shreds(shreds_for_slot, false).unwrap(); let meta = blockstore .meta(1) .expect("Expect database get to succeed") @@ -1434,9 +1400,7 @@ fn test_orphans() { // Write slot 1 which chains to slot 0, so now slot 0 is the // orphan, and slot 1 is no longer the orphan. let shreds_for_slot = shreds.drain(shreds_per_slot..).collect_vec(); - blockstore - .insert_shreds(shreds_for_slot, None, false) - .unwrap(); + blockstore.insert_shreds(shreds_for_slot, false).unwrap(); let meta = blockstore .meta(1) .expect("Expect database get to succeed") @@ -1456,15 +1420,15 @@ fn test_orphans() { // nothing should change let (shred4, _) = make_slot_entries(4, 0, 1); let (shred5, _) = make_slot_entries(5, 1, 1); - blockstore.insert_shreds(shred4, None, false).unwrap(); - blockstore.insert_shreds(shred5, None, false).unwrap(); + blockstore.insert_shreds(shred4, false).unwrap(); + blockstore.insert_shreds(shred5, false).unwrap(); assert_eq!( blockstore.orphans_iterator(0).unwrap().collect::>(), vec![0] ); // Write zeroth slot, no more orphans - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); for i in 0..3 { let meta = blockstore .meta(i) @@ -1494,11 +1458,11 @@ fn test_insert_data_shreds_slots(should_bulk_write: bool) { let num_shreds = shreds.len(); // Write shreds to the database if should_bulk_write { - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); } else { for _ in 0..num_shreds { let shred = shreds.remove(0); - blockstore.insert_shreds(vec![shred], None, false).unwrap(); + blockstore.insert_shreds(vec![shred], false).unwrap(); } } @@ -1531,7 +1495,7 @@ fn test_find_missing_data_indexes() { shreds.retain(|s| (s.index() % gap as u32) == 0); let num_shreds = 2; shreds.truncate(num_shreds); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // Index of the first shred is 0 // Index of the second shred is "gap" @@ -1694,7 +1658,7 @@ fn test_find_missing_data_indexes_sanity() { let shreds = vec![shreds.remove(OTHER as usize), shreds.remove(ONE as usize)]; // Insert one shred at index = first_index - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); const STARTS: u64 = OTHER * 2; const END: u64 = OTHER * 3; @@ -1726,7 +1690,7 @@ fn test_no_missing_shred_indexes() { let shreds = entries_to_test_shreds(&entries, slot, 0, true, 0); let num_shreds = shreds.len(); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let empty: Vec = vec![]; for i in 0..num_shreds as u64 { @@ -1788,7 +1752,7 @@ fn test_should_insert_data_shred() { // Insert the first 5 shreds, we don't have a "is_last" shred yet blockstore - .insert_shreds(shreds[0..5].to_vec(), None, false) + .insert_shreds(shreds[0..5].to_vec(), false) .unwrap(); let slot_meta = blockstore.meta(0).unwrap().unwrap(); @@ -1815,7 +1779,6 @@ fn test_should_insert_data_shred() { &slot_meta, &HashMap::new(), max_root, - None, ShredSource::Repaired, &mut Vec::new(), )); @@ -1825,7 +1788,6 @@ fn test_should_insert_data_shred() { blockstore .insert_shreds( shreds[term_last_idx + 2..term_last_idx + 3].iter().cloned(), - None, false, ) .unwrap(); @@ -1840,7 +1802,6 @@ fn test_should_insert_data_shred() { &slot_meta, &HashMap::new(), max_root, - None, ShredSource::Repaired, &mut duplicate_shreds, ), @@ -1855,7 +1816,7 @@ fn test_should_insert_data_shred() { assert_eq!(duplicate_shreds[0].slot(), 0); let last_idx = shreds.last().unwrap().index(); // Insert all pending shreds - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let slot_meta = blockstore.meta(0).unwrap().unwrap(); let past_tail_shreds = shredder @@ -1882,7 +1843,6 @@ fn test_should_insert_data_shred() { &slot_meta, &HashMap::new(), max_root, - None, ShredSource::Repaired, &mut duplicate_shreds, ), @@ -1906,7 +1866,7 @@ fn test_is_data_shred_present() { let index_cf = &blockstore.index_cf; blockstore - .insert_shreds(shreds[0..5].to_vec(), None, false) + .insert_shreds(shreds[0..5].to_vec(), false) .unwrap(); // Insert a shred less than `slot_meta.consumed`, check that // it already exists @@ -1921,7 +1881,7 @@ fn test_is_data_shred_present() { // Insert a shred, check that it already exists blockstore - .insert_shreds(shreds[6..7].to_vec(), None, false) + .insert_shreds(shreds[6..7].to_vec(), false) .unwrap(); let slot_meta = blockstore.meta(0).unwrap().unwrap(); let index = index_cf.get(0).unwrap().unwrap(); @@ -1929,7 +1889,7 @@ fn test_is_data_shred_present() { &shreds[6], &slot_meta, index.data() - ),); + )); } #[test] @@ -1940,7 +1900,7 @@ fn test_merkle_root_metas_coding() { let parent_slot = 0; let slot = 1; let index = 0; - let (_, coding_shreds, _) = setup_erasure_shreds(slot, parent_slot, 10); + let (_, coding_shreds) = setup_erasure_shreds(slot, parent_slot, 10); let coding_shred = coding_shreds[index as usize].clone(); let mut shred_insertion_tracker = @@ -1995,7 +1955,7 @@ fn test_merkle_root_metas_coding() { blockstore.write_batch(write_batch).unwrap(); // Add a shred with different merkle root and index - let (_, coding_shreds, _) = setup_erasure_shreds(slot, parent_slot, 10); + let (_, coding_shreds) = setup_erasure_shreds(slot, parent_slot, 10); let new_coding_shred = coding_shreds[(index + 1) as usize].clone(); let mut shred_insertion_tracker = @@ -2063,7 +2023,7 @@ fn test_merkle_root_metas_coding() { // Add a shred from different fec set let new_index = index + 31; - let (_, coding_shreds, _) = setup_erasure_shreds_with_index(slot, parent_slot, 10, new_index); + let (_, coding_shreds) = setup_erasure_shreds_with_index(slot, parent_slot, 10, new_index); let new_coding_shred = coding_shreds[0].clone(); blockstore @@ -2126,7 +2086,7 @@ fn test_merkle_root_metas_data() { let slot = 1; let index = 11; let fec_set_index = 11; - let (data_shreds, _, _) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); + let (data_shreds, _) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); let data_shred = data_shreds[0].clone(); let mut shred_insertion_tracker = @@ -2137,7 +2097,6 @@ fn test_merkle_root_metas_data() { BlockLocation::Original, &mut shred_insertion_tracker, false, - None, ShredSource::Turbine, ) .unwrap(); @@ -2182,7 +2141,7 @@ fn test_merkle_root_metas_data() { blockstore.write_batch(write_batch).unwrap(); // Add a shred with different merkle root and index - let (data_shreds, _, _) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); + let (data_shreds, _) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); let new_data_shred = data_shreds[1].clone(); let mut shred_insertion_tracker = @@ -2193,7 +2152,6 @@ fn test_merkle_root_metas_data() { BlockLocation::Original, &mut shred_insertion_tracker, false, - None, ShredSource::Turbine, ); assert_matches!( @@ -2288,7 +2246,6 @@ fn test_merkle_root_metas_data() { BlockLocation::Original, &mut shred_insertion_tracker, false, - None, ShredSource::Turbine, ) .unwrap(); @@ -2343,7 +2300,7 @@ fn test_check_insert_coding_shred() { let blockstore = Blockstore::open(ledger_path.path()).unwrap(); let slot = 1; - let (_data_shreds, code_shreds, _) = + let (_data_shreds, code_shreds) = setup_erasure_shreds_with_index_and_chained_merkle_and_last_in_slot( slot, 0, @@ -2388,7 +2345,7 @@ fn test_should_insert_coding_shred() { let max_root = 0; let slot = 1; - let (_data_shreds, code_shreds, _) = + let (_data_shreds, code_shreds) = setup_erasure_shreds_with_index_and_chained_merkle_and_last_in_slot( slot, 0, @@ -2405,7 +2362,7 @@ fn test_should_insert_coding_shred() { ); blockstore - .insert_shreds(vec![coding_shred.clone()], None, false) + .insert_shreds(vec![coding_shred.clone()], false) .expect("Insertion should succeed"); assert!( @@ -2433,7 +2390,7 @@ fn test_insert_multiple_is_last() { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let slot_meta = blockstore.meta(0).unwrap().unwrap(); assert_eq!(slot_meta.consumed, num_shreds); @@ -2443,7 +2400,7 @@ fn test_insert_multiple_is_last() { let (shreds, _) = make_slot_entries(0, 0, 600); assert!(shreds.len() > num_shreds as usize); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let slot_meta = blockstore.meta(0).unwrap().unwrap(); assert_eq!(slot_meta.consumed, num_shreds); @@ -2468,13 +2425,13 @@ fn test_mark_slot_dead_if_not_full() { let (mut shreds, _) = make_slot_entries(partial_slot, partial_slot - 1, 100); assert!(shreds.len() > 1); shreds.pop(); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert!(!blockstore.meta(partial_slot).unwrap().unwrap().is_full()); // Insert a full slot let full_slot = 10; let (shreds, _) = make_slot_entries(full_slot, full_slot - 1, 100); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert!(blockstore.meta(full_slot).unwrap().unwrap().is_full()); let mut shred_insertion_tracker = ShredInsertionTracker::new(1, blockstore.db.batch().unwrap()); @@ -2501,7 +2458,7 @@ fn test_slot_data_iterator() { let all_shreds = make_chaining_slot_entries(&slots, shreds_per_slot, 0); let slot_8_shreds = all_shreds[2].0.clone(); for (slot_shreds, _) in all_shreds { - blockstore.insert_shreds(slot_shreds, None, false).unwrap(); + blockstore.insert_shreds(slot_shreds, false).unwrap(); } // Slot doesn't exist, iterator should be empty @@ -2641,7 +2598,7 @@ fn test_get_slot_entries_with_shred_count_corruption() { let shreds = entries_to_test_shreds(&entries, slot, 0, false, 0); let next_shred_index = shreds.len(); blockstore - .insert_shreds(shreds, None, false) + .insert_shreds(shreds, false) .expect("Expected successful write of shreds"); assert_eq!( blockstore.get_slot_entries(slot, 0).unwrap().len() as u64, @@ -2672,7 +2629,7 @@ fn test_get_slot_entries_with_shred_count_corruption() { // With the corruption, nothing should be returned, even though an // earlier data block was valid blockstore - .insert_shreds(shreds, None, false) + .insert_shreds(shreds, false) .expect("Expected successful write of shreds"); assert!(blockstore.get_slot_entries(slot, 0).is_err()); } @@ -2687,7 +2644,7 @@ fn test_no_insert_but_modify_slot_meta() { // Insert the first 5 shreds, we don't have a "is_last" shred yet blockstore - .insert_shreds(shreds0[0..5].to_vec(), None, false) + .insert_shreds(shreds0[0..5].to_vec(), false) .unwrap(); // Insert a repetitive shred for slot 's', should get ignored, but also @@ -2697,10 +2654,10 @@ fn test_no_insert_but_modify_slot_meta() { let (mut shreds3, _) = make_slot_entries(3, 0, 200); shreds2.push(shreds0[1].clone()); shreds3.insert(0, shreds0[1].clone()); - blockstore.insert_shreds(shreds2, None, false).unwrap(); + blockstore.insert_shreds(shreds2, false).unwrap(); let slot_meta = blockstore.meta(0).unwrap().unwrap(); assert_eq!(slot_meta.next_slots, vec![2]); - blockstore.insert_shreds(shreds3, None, false).unwrap(); + blockstore.insert_shreds(shreds3, false).unwrap(); let slot_meta = blockstore.meta(0).unwrap().unwrap(); assert_eq!(slot_meta.next_slots, vec![2, 3]); } @@ -2718,13 +2675,13 @@ fn test_trusted_insert_shreds() { // Insert will fail, slot < root blockstore - .insert_shreds(shreds1[..].to_vec(), None, false) + .insert_shreds(shreds1[..].to_vec(), false) .unwrap(); assert!(blockstore.get_data_shred(1, 0).unwrap().is_none()); // Insert through trusted path will succeed blockstore - .insert_shreds(shreds1[..].to_vec(), None, true) + .insert_shreds(shreds1[..].to_vec(), true) .unwrap(); assert!(blockstore.get_data_shred(1, 0).unwrap().is_some()); } @@ -2747,7 +2704,7 @@ fn test_get_first_available_block() { true, // is_full_slot 0, // version ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); blockstore.set_roots([slot].iter()).unwrap(); } assert_eq!(blockstore.get_first_available_block().unwrap(), 0); @@ -2779,7 +2736,7 @@ fn test_get_rooted_block() { false, // is_full_slot 0, // version ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // Root the partially full slot and its parent blockstore.set_roots([slot - 1, slot].iter()).unwrap(); // An empty slot will return an error even if the slot is rooted @@ -2802,7 +2759,7 @@ fn test_get_rooted_block() { true, // is_full_slot 0, // version ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // Root both the full slot and its empty parent slot blockstore.set_roots([slot - 1, slot].iter()).unwrap(); // A full slot will return an error if the previous blockhash is @@ -2821,7 +2778,7 @@ fn test_get_rooted_block() { false, // is_full_slot 0, // version ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let shreds = entries_to_test_shreds( &entries, slot, @@ -2829,7 +2786,7 @@ fn test_get_rooted_block() { true, // is_full_slot 0, // version ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // Root both the full slot and its partially full parent slot blockstore.set_roots([slot - 1, slot].iter()).unwrap(); // A full root will return an error if the previous blockhash is @@ -2848,7 +2805,7 @@ fn test_get_rooted_block() { true, // is_full_slot 0, // version ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let shreds = entries_to_test_shreds( &entries, slot + 1, @@ -2856,7 +2813,7 @@ fn test_get_rooted_block() { true, // is_full_slot 0, // version ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let unrooted_shreds = entries_to_test_shreds( &entries, slot + 2, @@ -2864,9 +2821,7 @@ fn test_get_rooted_block() { true, // is_full_slot 0, // version ); - blockstore - .insert_shreds(unrooted_shreds, None, false) - .unwrap(); + blockstore.insert_shreds(unrooted_shreds, false).unwrap(); blockstore.set_roots([slot, slot + 1].iter()).unwrap(); let expected_transactions: Vec = entries .iter() @@ -3042,6 +2997,101 @@ fn test_get_rooted_block() { assert_eq!(complete_block, expected_complete_block); } +#[test] +fn test_get_complete_block_with_block_markers() { + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Blockstore::open(ledger_path.path()).unwrap(); + + let parent_slot = 20; + let slot = parent_slot + 1; + + let parent_entries = create_ticks(4, 0, Hash::new_unique()); + let parent_blockhash = parent_entries.last().unwrap().hash; + blockstore + .insert_shreds( + entries_to_test_shreds(&parent_entries, parent_slot, parent_slot - 1, true, 0), + false, + ) + .unwrap(); + + let block_header_shreds = data_shreds(create_block_header_shreds( + slot, + parent_slot, + parent_blockhash, + )); + let entry_start_index = block_header_shreds.last().unwrap().index() + 1; + + let entries = create_ticks(4, 0, parent_blockhash); + let entry_shreds = Shredder::new(slot, parent_slot, 0, 0) + .unwrap() + .make_merkle_shreds_from_entries( + &Keypair::new(), + &entries, + false, + Hash::new_unique(), + entry_start_index, + entry_start_index, + &ReedSolomonCache::default(), + &mut ProcessShredsStats::default(), + ) + .filter(Shred::is_data) + .collect_vec(); + let entry_end_index = entry_shreds.last().unwrap().index() + 1; + + let block_footer_shreds = data_shreds(create_block_footer_shreds( + slot, + parent_slot, + entry_end_index, + )); + let slot_end_index = block_footer_shreds.last().unwrap().index() + 1; + + let shreds: Vec<_> = block_header_shreds + .iter() + .chain(entry_shreds.iter()) + .chain(block_footer_shreds.iter()) + .cloned() + .collect(); + blockstore.insert_shreds(shreds, true).unwrap(); + + let (slot_entries, num_shreds, is_full) = blockstore + .get_slot_entries_with_shred_info(slot, 0, false) + .unwrap(); + assert_eq!(slot_entries, entries); + assert_eq!(num_shreds, u64::from(slot_end_index)); + assert!(is_full); + + assert!( + blockstore + .get_entries_in_data_block(slot, 0..entry_start_index, None) + .unwrap() + .is_empty() + ); + assert_eq!( + blockstore + .get_entries_in_data_block(slot, entry_start_index..entry_end_index, None) + .unwrap(), + entries + ); + assert!( + blockstore + .get_entries_in_data_block(slot, entry_end_index..slot_end_index, None) + .unwrap() + .is_empty() + ); + + let complete_block = blockstore.get_complete_block(slot, true).unwrap(); + assert_eq!(complete_block.parent_slot, parent_slot); + assert_eq!( + complete_block.previous_blockhash, + parent_blockhash.to_string() + ); + assert_eq!( + complete_block.blockhash, + entries.last().unwrap().hash.to_string() + ); + assert!(complete_block.transactions.is_empty()); +} + #[test] fn test_persist_transaction_status() { let ledger_path = get_tmp_ledger_path_auto_delete!(); @@ -3162,7 +3212,7 @@ fn test_persist_transaction_status() { .into(); assert!( transaction_status_cf - .put_protobuf((Signature::from([2u8; 64]), 9), &status,) + .put_protobuf((Signature::from([2u8; 64]), 9), &status) .is_ok() ); @@ -3660,7 +3710,7 @@ fn test_get_rooted_transaction() { ); let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); blockstore.set_roots([slot - 1, slot].iter()).unwrap(); let expected_transactions: Vec = entries @@ -3757,7 +3807,7 @@ fn test_get_rooted_transaction() { } blockstore - .run_purge(0, slot, PurgeType::CompactionFilter) + .purge_slots(0, slot, PurgeType::CompactionFilter) .unwrap(); *blockstore.lowest_cleanup_slot.write().unwrap() = slot; for VersionedTransactionWithStatusMeta { transaction, .. } in expected_transactions { @@ -3786,7 +3836,7 @@ fn test_get_complete_transaction() { true, // is_full_slot 0, // version ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let expected_transactions: Vec = entries .iter() @@ -3874,7 +3924,7 @@ fn test_get_complete_transaction() { } blockstore - .run_purge(0, slot, PurgeType::CompactionFilter) + .purge_slots(0, slot, PurgeType::CompactionFilter) .unwrap(); *blockstore.lowest_cleanup_slot.write().unwrap() = slot; for VersionedTransactionWithStatusMeta { transaction, .. } in expected_transactions { @@ -3885,7 +3935,7 @@ fn test_get_complete_transaction() { .unwrap(), None, ); - assert_eq!(blockstore.get_rooted_transaction(signature).unwrap(), None,); + assert_eq!(blockstore.get_rooted_transaction(signature).unwrap(), None); } } @@ -3998,7 +4048,7 @@ fn test_get_confirmed_signatures_for_address2() { let blockstore = Blockstore::open(ledger_path.path()).unwrap(); let (shreds, _) = make_slot_entries(1, 0, 4); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); fn make_slot_entries_with_transaction_addresses(addresses: &[Pubkey]) -> Vec { let mut entries: Vec = Vec::new(); @@ -4011,7 +4061,7 @@ fn test_get_confirmed_signatures_for_address2() { vec![CompiledInstruction::new(1, &(), vec![0])], ); entries.push(next_entry_mut(&mut Hash::default(), 0, vec![transaction])); - let mut tick = create_ticks(1, 0, hash(&bincode::serialize(address).unwrap())); + let mut tick = create_ticks(1, 0, hash(&wincode::serialize(address).unwrap())); entries.append(&mut tick); } entries @@ -4030,7 +4080,7 @@ fn test_get_confirmed_signatures_for_address2() { true, // is_full_slot 0, // version ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let mut counter = 0; for entry in entries.into_iter() { @@ -4059,7 +4109,7 @@ fn test_get_confirmed_signatures_for_address2() { let entries = make_slot_entries_with_transaction_addresses(&[address0, address1, address0, address1]); let shreds = entries_to_test_shreds(&entries, slot, 8, true, 0); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); let mut counter = 0; for entry in entries.into_iter() { @@ -4557,10 +4607,10 @@ fn test_lowest_slot() { for slot in 0..10 { let (shreds, _) = make_slot_entries(slot, 0, 1); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); } assert_eq!(blockstore.lowest_slot(), 1); - blockstore.run_purge(0, 5, PurgeType::Exact).unwrap(); + blockstore.purge_slots(0, 5, PurgeType::Exact).unwrap(); assert_eq!(blockstore.lowest_slot(), 6); } @@ -4573,13 +4623,13 @@ fn test_highest_slot() { for slot in 0..10 { let (shreds, _) = make_slot_entries(slot, 0, 1); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert_eq!(blockstore.highest_slot().unwrap(), Some(slot)); } - blockstore.run_purge(5, 10, PurgeType::Exact).unwrap(); + blockstore.purge_slots(5, 10, PurgeType::Exact).unwrap(); assert_eq!(blockstore.highest_slot().unwrap(), Some(4)); - blockstore.run_purge(0, 4, PurgeType::Exact).unwrap(); + blockstore.purge_slots(0, 4, PurgeType::Exact).unwrap(); assert_eq!(blockstore.highest_slot().unwrap(), None); } @@ -4589,7 +4639,7 @@ fn test_recovery() { let blockstore = Blockstore::open(ledger_path.path()).unwrap(); let slot = 1; - let (data_shreds, coding_shreds, leader_schedule_cache) = setup_erasure_shreds(slot, 0, 100); + let (data_shreds, coding_shreds) = setup_erasure_shreds(slot, 0, 100); let genesis_config = create_genesis_config(2).genesis_config; let root_bank = Arc::new(Bank::new_for_tests(&genesis_config)); @@ -4604,7 +4654,6 @@ fn test_recovery() { blockstore .do_insert_shreds( coding_shreds, - Some(&leader_schedule_cache), false, // is_trusted Some(&mut ShredRecoveryContext::new( ReedSolomonCache::default(), @@ -4637,7 +4686,7 @@ fn test_skip_alt_recovery() { let blockstore = Blockstore::open(ledger_path.path()).unwrap(); let slot = 1; - let (data_shreds, coding_shreds, leader_schedule_cache) = setup_erasure_shreds(slot, 0, 100); + let (data_shreds, coding_shreds) = setup_erasure_shreds(slot, 0, 100); let data_shred = data_shreds[0].clone(); let data_shred_index = u64::from(data_shred.index()); @@ -4652,7 +4701,6 @@ fn test_skip_alt_recovery() { /*is_repaired:*/ false, BlockLocation::Original, )), - Some(&leader_schedule_cache), false, // is_trusted None, &mut BlockstoreInsertionMetrics::default(), @@ -4674,7 +4722,6 @@ fn test_skip_alt_recovery() { /*is_repaired:*/ true, alternate_location, )), - Some(&leader_schedule_cache), false, // is_trusted Some(&mut ShredRecoveryContext::new( ReedSolomonCache::default(), @@ -4716,7 +4763,7 @@ fn test_recovery_discards_unexpected_data_complete_shreds() { let root_bank = Arc::new(root_bank); let slot = root_bank.get_slots_in_epoch(root_bank.epoch()); let reed_solomon_cache = ReedSolomonCache::default(); - let (data_shreds, coding_shreds, leader_keypair, leader_schedule_cache) = + let (data_shreds, coding_shreds, leader_keypair) = setup_erasure_shreds_with_index_and_chained_merkle_and_last_in_slot_and_keypair( slot, 0, @@ -4773,7 +4820,6 @@ fn test_recovery_discards_unexpected_data_complete_shreds() { blockstore .do_insert_shreds( shreds, - Some(&leader_schedule_cache), false, // is_trusted Some(&mut ShredRecoveryContext::new( reed_solomon_cache, @@ -4814,8 +4860,7 @@ fn test_recovery_discards_unexpected_data_complete_shreds() { fn test_index_integrity() { let slot = 1; let num_entries = 100; - let (data_shreds, coding_shreds, leader_schedule_cache) = - setup_erasure_shreds(slot, 0, num_entries); + let (data_shreds, coding_shreds) = setup_erasure_shreds(slot, 0, num_entries); assert!(data_shreds.len() > 3); assert!(coding_shreds.len() > 3); @@ -4828,26 +4873,20 @@ fn test_index_integrity() { .cloned() .chain(coding_shreds.iter().cloned()) .collect(); - blockstore - .insert_shreds(all_shreds, Some(&leader_schedule_cache), false) - .unwrap(); + blockstore.insert_shreds(all_shreds, false).unwrap(); verify_index_integrity(&blockstore, slot); blockstore.purge_slots(0, slot, PurgeType::Exact).unwrap(); // Test inserting just the codes, enough for recovery blockstore - .insert_shreds(coding_shreds.clone(), Some(&leader_schedule_cache), false) + .insert_shreds(coding_shreds.clone(), false) .unwrap(); verify_index_integrity(&blockstore, slot); blockstore.purge_slots(0, slot, PurgeType::Exact).unwrap(); // Test inserting some codes, but not enough for recovery blockstore - .insert_shreds( - coding_shreds[..coding_shreds.len() - 1].to_vec(), - Some(&leader_schedule_cache), - false, - ) + .insert_shreds(coding_shreds[..coding_shreds.len() - 1].to_vec(), false) .unwrap(); verify_index_integrity(&blockstore, slot); blockstore.purge_slots(0, slot, PurgeType::Exact).unwrap(); @@ -4858,9 +4897,7 @@ fn test_index_integrity() { .cloned() .chain(coding_shreds[..coding_shreds.len() - 1].iter().cloned()) .collect(); - blockstore - .insert_shreds(shreds, Some(&leader_schedule_cache), false) - .unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); verify_index_integrity(&blockstore, slot); blockstore.purge_slots(0, slot, PurgeType::Exact).unwrap(); @@ -4870,9 +4907,7 @@ fn test_index_integrity() { .cloned() .chain(coding_shreds[..coding_shreds.len() / 2 - 1].iter().cloned()) .collect(); - blockstore - .insert_shreds(shreds, Some(&leader_schedule_cache), false) - .unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); verify_index_integrity(&blockstore, slot); blockstore.purge_slots(0, slot, PurgeType::Exact).unwrap(); @@ -4887,12 +4922,8 @@ fn test_index_integrity() { .cloned() .chain(coding_shreds[coding_shreds.len() / 2 - 1..].iter().cloned()) .collect(); - blockstore - .insert_shreds(shreds1, Some(&leader_schedule_cache), false) - .unwrap(); - blockstore - .insert_shreds(shreds2, Some(&leader_schedule_cache), false) - .unwrap(); + blockstore.insert_shreds(shreds1, false).unwrap(); + blockstore.insert_shreds(shreds2, false).unwrap(); verify_index_integrity(&blockstore, slot); blockstore.purge_slots(0, slot, PurgeType::Exact).unwrap(); @@ -4912,12 +4943,8 @@ fn test_index_integrity() { .cloned(), ) .collect(); - blockstore - .insert_shreds(shreds1, Some(&leader_schedule_cache), false) - .unwrap(); - blockstore - .insert_shreds(shreds2, Some(&leader_schedule_cache), false) - .unwrap(); + blockstore.insert_shreds(shreds1, false).unwrap(); + blockstore.insert_shreds(shreds2, false).unwrap(); verify_index_integrity(&blockstore, slot); blockstore.purge_slots(0, slot, PurgeType::Exact).unwrap(); @@ -4937,21 +4964,13 @@ fn test_index_integrity() { .cloned(), ) .collect(); - blockstore - .insert_shreds(shreds1, Some(&leader_schedule_cache), false) - .unwrap(); - blockstore - .insert_shreds(shreds2, Some(&leader_schedule_cache), false) - .unwrap(); + blockstore.insert_shreds(shreds1, false).unwrap(); + blockstore.insert_shreds(shreds2, false).unwrap(); verify_index_integrity(&blockstore, slot); blockstore.purge_slots(0, slot, PurgeType::Exact).unwrap(); } -fn setup_erasure_shreds( - slot: u64, - parent_slot: u64, - num_entries: u64, -) -> (Vec, Vec, Arc) { +fn setup_erasure_shreds(slot: u64, parent_slot: u64, num_entries: u64) -> (Vec, Vec) { setup_erasure_shreds_with_index(slot, parent_slot, num_entries, 0) } @@ -4960,7 +4979,7 @@ fn setup_erasure_shreds_with_index( parent_slot: u64, num_entries: u64, fec_set_index: u32, -) -> (Vec, Vec, Arc) { +) -> (Vec, Vec) { setup_erasure_shreds_with_index_and_chained_merkle( slot, parent_slot, @@ -4976,7 +4995,7 @@ fn setup_erasure_shreds_with_index_and_chained_merkle( num_entries: u64, fec_set_index: u32, chained_merkle_root: Hash, -) -> (Vec, Vec, Arc) { +) -> (Vec, Vec) { setup_erasure_shreds_with_index_and_chained_merkle_and_last_in_slot( slot, parent_slot, @@ -4994,8 +5013,8 @@ fn setup_erasure_shreds_with_index_and_chained_merkle_and_last_in_slot( fec_set_index: u32, chained_merkle_root: Hash, is_last_in_slot: bool, -) -> (Vec, Vec, Arc) { - let (data_shreds, coding_shreds, _, leader_schedule_cache) = +) -> (Vec, Vec) { + let (data_shreds, coding_shreds, _) = setup_erasure_shreds_with_index_and_chained_merkle_and_last_in_slot_and_keypair( slot, parent_slot, @@ -5005,7 +5024,7 @@ fn setup_erasure_shreds_with_index_and_chained_merkle_and_last_in_slot( is_last_in_slot, ); - (data_shreds, coding_shreds, leader_schedule_cache) + (data_shreds, coding_shreds) } fn setup_erasure_shreds_with_index_and_chained_merkle_and_last_in_slot_and_keypair( @@ -5015,12 +5034,7 @@ fn setup_erasure_shreds_with_index_and_chained_merkle_and_last_in_slot_and_keypa fec_set_index: u32, chained_merkle_root: Hash, is_last_in_slot: bool, -) -> ( - Vec, - Vec, - Arc, - Arc, -) { +) -> (Vec, Vec, Arc) { let entries = make_slot_entries_with_transactions(num_entries); let leader_keypair = Arc::new(Keypair::new()); let shredder = Shredder::new(slot, parent_slot, 0, 0).unwrap(); @@ -5035,26 +5049,7 @@ fn setup_erasure_shreds_with_index_and_chained_merkle_and_last_in_slot_and_keypa &mut ProcessShredsStats::default(), ); - let genesis_config = create_genesis_config(2).genesis_config; - let bank = Arc::new(Bank::new_for_tests(&genesis_config)); - let mut leader_schedule_cache = LeaderScheduleCache::new_from_bank(&bank); - let fixed_schedule = FixedSchedule { - leader_schedule: Arc::new(LeaderSchedule::new_from_schedule( - vec![SlotLeader { - id: leader_keypair.pubkey(), - vote_address: Pubkey::new_unique(), - }], - NonZeroUsize::new(1).unwrap(), - )), - }; - leader_schedule_cache.set_fixed_leader_schedule(Some(fixed_schedule)); - - ( - data_shreds, - coding_shreds, - leader_keypair, - Arc::new(leader_schedule_cache), - ) + (data_shreds, coding_shreds, leader_keypair) } fn verify_index_integrity(blockstore: &Blockstore, slot: u64) { @@ -5126,7 +5121,7 @@ fn test_duplicate_slot() { let blockstore = Blockstore::open(ledger_path.path()).unwrap(); blockstore - .insert_shreds(vec![shred.clone()], None, false) + .insert_shreds(vec![shred.clone()], false) .unwrap(); // No duplicate shreds exist yet @@ -5175,7 +5170,7 @@ fn test_clear_unconfirmed_slot() { .into_iter() .flat_map(|x| x.0) .collect(); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // There are 32 data shreds in slot 9. for index in 0..32 { assert_matches!( @@ -5224,7 +5219,7 @@ fn test_clear_unconfirmed_slot_and_insert_again() { assert_eq!(unconfirmed_slot_shreds[0].slot(), unconfirmed_slot); // Insert into slot 9 - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // Purge the slot blockstore.clear_unconfirmed_slot(unconfirmed_slot); @@ -5239,7 +5234,7 @@ fn test_clear_unconfirmed_slot_and_insert_again() { // Re-add unconfirmed_slot and confirm that confirmed_slot only has // unconfirmed_slot in next_slots once blockstore - .insert_shreds(unconfirmed_slot_shreds, None, false) + .insert_shreds(unconfirmed_slot_shreds, false) .unwrap(); assert_eq!( blockstore.meta(confirmed_slot).unwrap().unwrap().next_slots, @@ -5304,133 +5299,6 @@ fn test_update_completed_data_indexes_out_of_order() { assert!(completed_data_indexes.clone().iter().eq([0, 1, 3])); } -#[test] -fn test_rewards_protobuf_backward_compatibility() { - let ledger_path = get_tmp_ledger_path_auto_delete!(); - let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - - let rewards: Rewards = (0..100) - .map(|i| Reward { - pubkey: solana_pubkey::new_rand().to_string(), - lamports: 42 + i, - post_balance: u64::MAX, - reward_type: Some(RewardType::Fee), - commission: None, - commission_bps: None, - }) - .collect(); - let protobuf_rewards: generated::Rewards = rewards.into(); - - let deprecated_rewards: StoredExtendedRewards = protobuf_rewards.clone().into(); - for slot in 0..2 { - let data = bincode::serialize(&deprecated_rewards).unwrap(); - blockstore.rewards_cf.put_bytes(slot, &data).unwrap(); - } - for slot in 2..4 { - blockstore - .rewards_cf - .put_protobuf(slot, &protobuf_rewards) - .unwrap(); - } - for slot in 0..4 { - assert_eq!( - blockstore - .rewards_cf - .get_protobuf_or_wincode::(slot) - .unwrap() - .unwrap(), - protobuf_rewards - ); - } -} - -// This test is probably superfluous, since it is highly unlikely that bincode-format -// TransactionStatus entries exist in any current ledger. They certainly exist in historical -// ledger archives, but typically those require contemporaraneous software for other reasons. -// However, we are persisting the test since the apis still exist in `blockstore_db`. -#[test] -fn test_transaction_status_protobuf_backward_compatibility() { - let ledger_path = get_tmp_ledger_path_auto_delete!(); - let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - - let status = TransactionStatusMeta { - status: Ok(()), - fee: 42, - pre_balances: vec![1, 2, 3], - post_balances: vec![1, 2, 3], - inner_instructions: Some(vec![]), - log_messages: Some(vec![]), - pre_token_balances: Some(vec![TransactionTokenBalance { - account_index: 0, - mint: Pubkey::new_unique().to_string(), - ui_token_amount: UiTokenAmount { - ui_amount: Some(1.1), - decimals: 1, - amount: "11".to_string(), - ui_amount_string: "1.1".to_string(), - }, - owner: Pubkey::new_unique().to_string(), - program_id: Pubkey::new_unique().to_string(), - }]), - post_token_balances: Some(vec![TransactionTokenBalance { - account_index: 0, - mint: Pubkey::new_unique().to_string(), - ui_token_amount: UiTokenAmount { - ui_amount: None, - decimals: 1, - amount: "11".to_string(), - ui_amount_string: "1.1".to_string(), - }, - owner: Pubkey::new_unique().to_string(), - program_id: Pubkey::new_unique().to_string(), - }]), - rewards: Some(vec![Reward { - pubkey: "My11111111111111111111111111111111111111111".to_string(), - lamports: -42, - post_balance: 42, - reward_type: Some(RewardType::Rent), - commission: None, - commission_bps: None, - }]), - loaded_addresses: LoadedAddresses::default(), - return_data: Some(TransactionReturnData { - program_id: Pubkey::new_unique(), - data: vec![1, 2, 3], - }), - compute_units_consumed: Some(23456), - cost_units: Some(5678), - }; - let deprecated_status: StoredTransactionStatusMeta = status.clone().try_into().unwrap(); - let protobuf_status: generated::TransactionStatusMeta = status.into(); - - for slot in 0..2 { - let data = bincode::serialize(&deprecated_status).unwrap(); - blockstore - .transaction_status_cf - .put_bytes((Signature::default(), slot), &data) - .unwrap(); - } - for slot in 2..4 { - blockstore - .transaction_status_cf - .put_protobuf((Signature::default(), slot), &protobuf_status) - .unwrap(); - } - for slot in 0..4 { - assert_eq!( - blockstore - .transaction_status_cf - .get_protobuf_or_wincode::(( - Signature::default(), - slot - )) - .unwrap() - .unwrap(), - protobuf_status - ); - } -} - fn make_large_tx_entry(num_txs: usize) -> Entry { let txs: Vec<_> = (0..num_txs) .map(|_| { @@ -5500,11 +5368,11 @@ fn erasure_multiple_config() { } // insert all but 2 data shreds from first set (so it is not yet recoverable) blockstore - .insert_shreds(data1[..data1.len() - 2].to_vec(), None, false) + .insert_shreds(data1[..data1.len() - 2].to_vec(), false) .unwrap(); // insert two coding shreds from conflicting FEC sets blockstore - .insert_shreds(vec![coding1[0].clone(), coding2[1].clone()], None, false) + .insert_shreds(vec![coding1[0].clone(), coding2[1].clone()], false) .unwrap(); assert!(blockstore.has_duplicate_shreds_in_slot(slot)); } @@ -5531,7 +5399,7 @@ fn test_insert_data_shreds_same_slot_last_index() { // times for _ in 0..10 { blockstore - .insert_shreds(original_shreds.clone(), None, false) + .insert_shreds(original_shreds.clone(), false) .unwrap(); let meta = blockstore.meta(0).unwrap().unwrap(); assert!(!blockstore.is_dead(0)); @@ -5544,9 +5412,7 @@ fn test_insert_data_shreds_same_slot_last_index() { } let num_shreds = duplicate_shreds.len() as u64; - blockstore - .insert_shreds(duplicate_shreds, None, false) - .unwrap(); + blockstore.insert_shreds(duplicate_shreds, false).unwrap(); assert_eq!(blockstore.get_slot_entries(0, 0).unwrap(), original_entries); @@ -5611,7 +5477,7 @@ fn test_duplicate_last_index() { let blockstore = Blockstore::open(ledger_path.path()).unwrap(); blockstore - .insert_shreds(vec![last_data1.clone(), last_data2.clone()], None, false) + .insert_shreds(vec![last_data1.clone(), last_data2.clone()], false) .unwrap(); assert!(blockstore.get_duplicate_slot(slot).is_some()); @@ -5632,9 +5498,7 @@ fn test_duplicate_last_index_mark_dead() { let get_expected_slot_meta_and_index_meta = |blockstore: &Blockstore, shreds: Vec| -> (SlotMeta, Index) { let slot = shreds[0].slot(); - blockstore - .insert_shreds(shreds.clone(), None, false) - .unwrap(); + blockstore.insert_shreds(shreds.clone(), false).unwrap(); let meta = blockstore.meta(slot).unwrap().unwrap(); assert_eq!(meta.consumed, shreds.len() as u64); let shreds_index = blockstore.get_index(slot).unwrap().unwrap(); @@ -5644,7 +5508,7 @@ fn test_duplicate_last_index_mark_dead() { // Cleanup the slot blockstore - .run_purge(slot, slot, PurgeType::Exact) + .purge_slots(slot, slot, PurgeType::Exact) .expect("Purge database operations failed"); assert!(blockstore.meta(slot).unwrap().is_none()); @@ -5663,9 +5527,7 @@ fn test_duplicate_last_index_mark_dead() { &blockstore, shreds[..=smaller_last_shred_index].to_vec(), ); - blockstore - .insert_shreds(shreds.clone(), None, false) - .unwrap(); + blockstore.insert_shreds(shreds.clone(), false).unwrap(); assert!(blockstore.get_duplicate_slot(slot).is_some()); // Block is already full not marked dead assert!(!blockstore.is_dead(slot)); @@ -5689,9 +5551,7 @@ fn test_duplicate_last_index_mark_dead() { slot += 1; let mut shreds = setup_test_shreds(slot); shreds.reverse(); - blockstore - .insert_shreds(shreds.clone(), None, false) - .unwrap(); + blockstore.insert_shreds(shreds.clone(), false).unwrap(); assert!(blockstore.is_dead(slot)); // All the shreds other than the two last index shreds because those two // are marked as last, but less than the first received index == 10. @@ -5727,7 +5587,7 @@ fn test_duplicate_last_index_mark_dead() { let mut shreds = setup_test_shreds(slot); shreds.reverse(); for shred in shreds.clone() { - blockstore.insert_shreds(vec![shred], None, false).unwrap(); + blockstore.insert_shreds(vec![shred], false).unwrap(); } assert!(blockstore.is_dead(slot)); // All the shreds will be inserted since dead slots can still be inserted into. @@ -5760,9 +5620,9 @@ fn test_get_slot_entries_dead_slot_race() { let ledger_path = get_tmp_ledger_path_auto_delete!(); { let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); - let (slot_sender, slot_receiver) = unbounded(); - let (shred_sender, shred_receiver) = unbounded::>(); - let (signal_sender, signal_receiver) = unbounded(); + let (slot_sender, slot_receiver) = bounded(1024); + let (shred_sender, shred_receiver) = bounded::>(1024); + let (signal_sender, signal_receiver) = bounded(1024); std::thread::scope(|scope| { scope.spawn(|| { @@ -5795,7 +5655,7 @@ fn test_get_slot_entries_dead_slot_race() { // Possible clippy bug, the lock is unused so clippy shouldn't care // about read vs. write lock let _lowest_cleanup_slot = blockstore.lowest_cleanup_slot.write().unwrap(); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert!(blockstore.get_duplicate_slot(slot).is_some()); assert!(blockstore.is_dead(slot)); signal_sender.send(Ok(())).unwrap(); @@ -5833,19 +5693,19 @@ fn test_previous_erasure_set() { let parent_slot = 0; let prev_slot = 1; let slot = 2; - let (data_shreds_0, coding_shreds_0, _) = + let (data_shreds_0, coding_shreds_0) = setup_erasure_shreds_with_index(slot, parent_slot, 10, 0); let erasure_set_0 = ErasureSetId::new(slot, 0); let erasure_meta_0 = ErasureMeta::from_coding_shred(coding_shreds_0.first().unwrap()).unwrap(); let prev_fec_set_index = data_shreds_0.len() as u32; - let (data_shreds_prev, coding_shreds_prev, _) = + let (data_shreds_prev, coding_shreds_prev) = setup_erasure_shreds_with_index(slot, parent_slot, 10, prev_fec_set_index); let erasure_set_prev = ErasureSetId::new(slot, prev_fec_set_index); let erasure_meta_prev = ErasureMeta::from_coding_shred(coding_shreds_prev.first().unwrap()).unwrap(); - let (_, coding_shreds_prev_slot, _) = + let (_, coding_shreds_prev_slot) = setup_erasure_shreds_with_index(prev_slot, parent_slot, 10, prev_fec_set_index); let erasure_set_prev_slot = ErasureSetId::new(prev_slot, prev_fec_set_index); let erasure_meta_prev_slot = @@ -5975,21 +5835,21 @@ fn test_chained_merkle_root_consistency_backwards() { let parent_slot = 0; let slot = 1; let fec_set_index = 0; - let (data_shreds, coding_shreds, leader_schedule) = + let (data_shreds, coding_shreds) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); let coding_shred = coding_shreds[0].clone(); let next_fec_set_index = fec_set_index + data_shreds.len() as u32; assert!( blockstore - .insert_shred_return_duplicate(coding_shred.clone(), &leader_schedule,) + .insert_shred_return_duplicate(coding_shred.clone()) .is_empty() ); let merkle_root = coding_shred.merkle_root().unwrap(); // Correctly chained merkle - let (data_shreds, coding_shreds, _) = setup_erasure_shreds_with_index_and_chained_merkle( + let (data_shreds, coding_shreds) = setup_erasure_shreds_with_index_and_chained_merkle( slot, parent_slot, 10, @@ -6000,12 +5860,12 @@ fn test_chained_merkle_root_consistency_backwards() { let coding_shred = coding_shreds[0].clone(); assert!( blockstore - .insert_shred_return_duplicate(coding_shred, &leader_schedule,) + .insert_shred_return_duplicate(coding_shred) .is_empty() ); assert!( blockstore - .insert_shred_return_duplicate(data_shred, &leader_schedule,) + .insert_shred_return_duplicate(data_shred) .is_empty() ); } @@ -6019,14 +5879,14 @@ fn test_chained_merkle_root_consistency_forwards() { let parent_slot = 0; let slot = 1; let fec_set_index = 0; - let (data_shreds, coding_shreds, leader_schedule) = + let (data_shreds, coding_shreds) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); let coding_shred = coding_shreds[0].clone(); let next_fec_set_index = fec_set_index + data_shreds.len() as u32; // Correctly chained merkle let merkle_root = coding_shred.merkle_root().unwrap(); - let (_, next_coding_shreds, _) = setup_erasure_shreds_with_index_and_chained_merkle( + let (_, next_coding_shreds) = setup_erasure_shreds_with_index_and_chained_merkle( slot, parent_slot, 10, @@ -6037,14 +5897,14 @@ fn test_chained_merkle_root_consistency_forwards() { assert!( blockstore - .insert_shred_return_duplicate(next_coding_shred, &leader_schedule,) + .insert_shred_return_duplicate(next_coding_shred) .is_empty() ); // Insert previous FEC set assert!( blockstore - .insert_shred_return_duplicate(coding_shred, &leader_schedule,) + .insert_shred_return_duplicate(coding_shred) .is_empty() ); } @@ -6057,20 +5917,19 @@ fn test_chained_merkle_root_across_slots_backwards() { let parent_slot = 0; let slot = 1; let fec_set_index = 0; - let (data_shreds, _, leader_schedule) = - setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); + let (data_shreds, _) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); let data_shred = data_shreds[0].clone(); assert!( blockstore - .insert_shred_return_duplicate(data_shred.clone(), &leader_schedule,) + .insert_shred_return_duplicate(data_shred.clone()) .is_empty() ); // Incorrectly chained merkle for next slot let merkle_root = Hash::new_unique(); assert!(merkle_root != data_shred.merkle_root().unwrap()); - let (next_slot_data_shreds, next_slot_coding_shreds, leader_schedule) = + let (next_slot_data_shreds, next_slot_coding_shreds) = setup_erasure_shreds_with_index_and_chained_merkle( slot + 1, slot, @@ -6082,12 +5941,12 @@ fn test_chained_merkle_root_across_slots_backwards() { let next_slot_coding_shred = next_slot_coding_shreds[0].clone(); assert!( blockstore - .insert_shred_return_duplicate(next_slot_coding_shred, &leader_schedule,) + .insert_shred_return_duplicate(next_slot_coding_shred) .is_empty() ); assert!( blockstore - .insert_shred_return_duplicate(next_slot_data_shred, &leader_schedule) + .insert_shred_return_duplicate(next_slot_data_shred) .is_empty() ); } @@ -6100,33 +5959,31 @@ fn test_chained_merkle_root_across_slots_forwards() { let parent_slot = 0; let slot = 1; let fec_set_index = 0; - let (_, coding_shreds, _) = - setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); + let (_, coding_shreds) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); let coding_shred = coding_shreds[0].clone(); // Incorrectly chained merkle for next slot let merkle_root = Hash::new_unique(); assert!(merkle_root != coding_shred.merkle_root().unwrap()); - let (next_slot_data_shreds, _, leader_schedule) = - setup_erasure_shreds_with_index_and_chained_merkle( - slot + 1, - slot, - 10, - fec_set_index, - merkle_root, - ); + let (next_slot_data_shreds, _) = setup_erasure_shreds_with_index_and_chained_merkle( + slot + 1, + slot, + 10, + fec_set_index, + merkle_root, + ); let next_slot_data_shred = next_slot_data_shreds[0].clone(); assert!( blockstore - .insert_shred_return_duplicate(next_slot_data_shred, &leader_schedule,) + .insert_shred_return_duplicate(next_slot_data_shred) .is_empty() ); // Insert for previous slot assert!( blockstore - .insert_shred_return_duplicate(coding_shred, &leader_schedule,) + .insert_shred_return_duplicate(coding_shred) .is_empty() ); } @@ -6140,45 +5997,40 @@ fn test_chained_merkle_root_inconsistency_backwards_insert_code() { let parent_slot = 0; let slot = 1; let fec_set_index = 0; - let (data_shreds, coding_shreds, leader_schedule) = + let (data_shreds, coding_shreds) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); let coding_shred_previous = coding_shreds[0].clone(); let next_fec_set_index = fec_set_index + data_shreds.len() as u32; assert!( blockstore - .insert_shred_return_duplicate(coding_shred_previous.clone(), &leader_schedule,) + .insert_shred_return_duplicate(coding_shred_previous.clone()) .is_empty() ); // Incorrectly chained merkle let merkle_root = Hash::new_unique(); assert!(merkle_root != coding_shred_previous.merkle_root().unwrap()); - let (data_shreds, coding_shreds, leader_schedule) = - setup_erasure_shreds_with_index_and_chained_merkle( - slot, - parent_slot, - 10, - next_fec_set_index, - merkle_root, - ); + let (data_shreds, coding_shreds) = setup_erasure_shreds_with_index_and_chained_merkle( + slot, + parent_slot, + 10, + next_fec_set_index, + merkle_root, + ); let data_shred = data_shreds[0].clone(); let coding_shred = coding_shreds[0].clone(); - let duplicate_shreds = - blockstore.insert_shred_return_duplicate(coding_shred.clone(), &leader_schedule); + let duplicate_shreds = blockstore.insert_shred_return_duplicate(coding_shred.clone()); assert_eq!(duplicate_shreds.len(), 1); assert_eq!( duplicate_shreds[0], - PossibleDuplicateShred::ChainedMerkleRootConflict( - coding_shred, - coding_shred_previous.into_payload() - ) + PossibleDuplicateShred::ChainedMerkleRootConflict(coding_shred.slot()) ); // Should not check again, even though this shred conflicts as well assert!( blockstore - .insert_shred_return_duplicate(data_shred, &leader_schedule,) + .insert_shred_return_duplicate(data_shred) .is_empty() ); } @@ -6192,45 +6044,40 @@ fn test_chained_merkle_root_inconsistency_backwards_insert_data() { let parent_slot = 0; let slot = 1; let fec_set_index = 0; - let (data_shreds, coding_shreds, leader_schedule) = + let (data_shreds, coding_shreds) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); let coding_shred_previous = coding_shreds[0].clone(); let next_fec_set_index = fec_set_index + data_shreds.len() as u32; assert!( blockstore - .insert_shred_return_duplicate(coding_shred_previous.clone(), &leader_schedule,) + .insert_shred_return_duplicate(coding_shred_previous.clone()) .is_empty() ); // Incorrectly chained merkle let merkle_root = Hash::new_unique(); assert!(merkle_root != coding_shred_previous.merkle_root().unwrap()); - let (data_shreds, coding_shreds, leader_schedule) = - setup_erasure_shreds_with_index_and_chained_merkle( - slot, - parent_slot, - 10, - next_fec_set_index, - merkle_root, - ); + let (data_shreds, coding_shreds) = setup_erasure_shreds_with_index_and_chained_merkle( + slot, + parent_slot, + 10, + next_fec_set_index, + merkle_root, + ); let data_shred = data_shreds[0].clone(); let coding_shred = coding_shreds[0].clone(); - let duplicate_shreds = - blockstore.insert_shred_return_duplicate(data_shred.clone(), &leader_schedule); + let duplicate_shreds = blockstore.insert_shred_return_duplicate(data_shred.clone()); assert_eq!(duplicate_shreds.len(), 1); assert_eq!( duplicate_shreds[0], - PossibleDuplicateShred::ChainedMerkleRootConflict( - data_shred, - coding_shred_previous.into_payload(), - ) + PossibleDuplicateShred::ChainedMerkleRootConflict(data_shred.slot()) ); // Should not check again, even though this shred conflicts as well assert!( blockstore - .insert_shred_return_duplicate(coding_shred, &leader_schedule,) + .insert_shred_return_duplicate(coding_shred) .is_empty() ); } @@ -6244,7 +6091,7 @@ fn test_chained_merkle_root_inconsistency_forwards() { let parent_slot = 0; let slot = 1; let fec_set_index = 0; - let (data_shreds, coding_shreds, leader_schedule) = + let (data_shreds, coding_shreds) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); let coding_shred = coding_shreds[0].clone(); let next_fec_set_index = fec_set_index + data_shreds.len() as u32; @@ -6252,33 +6099,74 @@ fn test_chained_merkle_root_inconsistency_forwards() { // Incorrectly chained merkle let merkle_root = Hash::new_unique(); assert!(merkle_root != coding_shred.merkle_root().unwrap()); - let (next_data_shreds, _, leader_schedule_next) = - setup_erasure_shreds_with_index_and_chained_merkle( - slot, - parent_slot, - 10, - next_fec_set_index, - merkle_root, - ); + let (next_data_shreds, _) = setup_erasure_shreds_with_index_and_chained_merkle( + slot, + parent_slot, + 10, + next_fec_set_index, + merkle_root, + ); let next_data_shred = next_data_shreds[0].clone(); assert!( blockstore - .insert_shred_return_duplicate(next_data_shred.clone(), &leader_schedule_next,) + .insert_shred_return_duplicate(next_data_shred.clone()) .is_empty() ); // Insert previous FEC set - let duplicate_shreds = - blockstore.insert_shred_return_duplicate(coding_shred.clone(), &leader_schedule); + let duplicate_shreds = blockstore.insert_shred_return_duplicate(coding_shred.clone()); + assert_eq!(duplicate_shreds.len(), 2); + assert!( + duplicate_shreds.contains(&PossibleDuplicateShred::ChainedMerkleRootConflict( + coding_shred.slot(), + )) + ); + assert!( + duplicate_shreds.contains(&PossibleDuplicateShred::FixedFECChainedMerkleRootConflict( + coding_shred.slot(), + )) + ); +} + +#[test] +fn test_chained_merkle_root_inconsistency_data_shreds_only() { + // Insert data shreds from consecutive FEC sets without any coding shreds. + // The ErasureMeta-based SIMD-0340 check cannot run, so the fixed-FEC + // MerkleRootMeta check must catch the inconsistent chain. + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Blockstore::open(ledger_path.path()).unwrap(); + + let parent_slot = 0; + let slot = 1; + let fec_set_index = 0; + let (data_shreds, _) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); + let data_shred_previous = data_shreds[0].clone(); + let next_fec_set_index = fec_set_index + data_shreds.len() as u32; + + assert!( + blockstore + .insert_shred_return_duplicate(data_shred_previous.clone()) + .is_empty() + ); + + let merkle_root = Hash::new_unique(); + assert!(merkle_root != data_shred_previous.merkle_root().unwrap()); + let (data_shreds, _) = setup_erasure_shreds_with_index_and_chained_merkle( + slot, + parent_slot, + 10, + next_fec_set_index, + merkle_root, + ); + let data_shred = data_shreds[0].clone(); + + let duplicate_shreds = blockstore.insert_shred_return_duplicate(data_shred.clone()); assert_eq!(duplicate_shreds.len(), 1); assert_eq!( duplicate_shreds[0], - PossibleDuplicateShred::ChainedMerkleRootConflict( - coding_shred, - next_data_shred.into_payload(), - ) + PossibleDuplicateShred::FixedFECChainedMerkleRootConflict(data_shred.slot()) ); } @@ -6293,7 +6181,7 @@ fn test_chained_merkle_root_inconsistency_both() { let parent_slot = 0; let slot = 1; let prev_fec_set_index = 0; - let (prev_data_shreds, prev_coding_shreds, leader_schedule_prev) = + let (prev_data_shreds, prev_coding_shreds) = setup_erasure_shreds_with_index(slot, parent_slot, 10, prev_fec_set_index); let prev_coding_shred = prev_coding_shreds[0].clone(); let fec_set_index = prev_fec_set_index + prev_data_shreds.len() as u32; @@ -6301,14 +6189,13 @@ fn test_chained_merkle_root_inconsistency_both() { // Incorrectly chained merkle let merkle_root = Hash::new_unique(); assert!(merkle_root != prev_coding_shred.merkle_root().unwrap()); - let (data_shreds, coding_shreds, leader_schedule) = - setup_erasure_shreds_with_index_and_chained_merkle( - slot, - parent_slot, - 10, - fec_set_index, - merkle_root, - ); + let (data_shreds, coding_shreds) = setup_erasure_shreds_with_index_and_chained_merkle( + slot, + parent_slot, + 10, + fec_set_index, + merkle_root, + ); let data_shred = data_shreds[0].clone(); let coding_shred = coding_shreds[0].clone(); let next_fec_set_index = fec_set_index + prev_data_shreds.len() as u32; @@ -6316,54 +6203,45 @@ fn test_chained_merkle_root_inconsistency_both() { // Incorrectly chained merkle let merkle_root = Hash::new_unique(); assert!(merkle_root != data_shred.merkle_root().unwrap()); - let (next_data_shreds, _, leader_schedule_next) = - setup_erasure_shreds_with_index_and_chained_merkle( - slot, - parent_slot, - 10, - next_fec_set_index, - merkle_root, - ); + let (next_data_shreds, _) = setup_erasure_shreds_with_index_and_chained_merkle( + slot, + parent_slot, + 10, + next_fec_set_index, + merkle_root, + ); let next_data_shred = next_data_shreds[0].clone(); assert!( blockstore - .insert_shred_return_duplicate(prev_coding_shred.clone(), &leader_schedule_prev,) + .insert_shred_return_duplicate(prev_coding_shred.clone()) .is_empty() ); assert!( blockstore - .insert_shred_return_duplicate(next_data_shred.clone(), &leader_schedule_next) + .insert_shred_return_duplicate(next_data_shred.clone()) .is_empty() ); // Insert data shred - let duplicate_shreds = - blockstore.insert_shred_return_duplicate(data_shred.clone(), &leader_schedule); + let duplicate_shreds = blockstore.insert_shred_return_duplicate(data_shred.clone()); // Only the backwards check will be performed assert_eq!(duplicate_shreds.len(), 1); assert_eq!( duplicate_shreds[0], - PossibleDuplicateShred::ChainedMerkleRootConflict( - data_shred, - prev_coding_shred.into_payload(), - ) + PossibleDuplicateShred::ChainedMerkleRootConflict(data_shred.slot()) ); // Insert coding shred - let duplicate_shreds = - blockstore.insert_shred_return_duplicate(coding_shred.clone(), &leader_schedule); + let duplicate_shreds = blockstore.insert_shred_return_duplicate(coding_shred.clone()); // Now the forwards check will be performed assert_eq!(duplicate_shreds.len(), 1); assert_eq!( duplicate_shreds[0], - PossibleDuplicateShred::ChainedMerkleRootConflict( - coding_shred, - next_data_shred.into_payload(), - ) + PossibleDuplicateShred::ChainedMerkleRootConflict(coding_shred.slot()) ); } @@ -6376,14 +6254,14 @@ fn test_chained_merkle_root_upgrade_inconsistency_backwards() { let parent_slot = 0; let slot = 1; let fec_set_index = 0; - let (data_shreds, coding_shreds, leader_schedule) = + let (data_shreds, coding_shreds) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); let coding_shred_previous = coding_shreds[1].clone(); let next_fec_set_index = fec_set_index + data_shreds.len() as u32; assert!( blockstore - .insert_shred_return_duplicate(coding_shred_previous.clone(), &leader_schedule,) + .insert_shred_return_duplicate(coding_shred_previous.clone()) .is_empty() ); @@ -6413,24 +6291,23 @@ fn test_chained_merkle_root_upgrade_inconsistency_backwards() { // we skip the duplicate check as the first received coding shred index shred is missing let merkle_root = Hash::new_unique(); assert!(merkle_root != coding_shred_previous.merkle_root().unwrap()); - let (data_shreds, coding_shreds, leader_schedule) = - setup_erasure_shreds_with_index_and_chained_merkle( - slot, - parent_slot, - 10, - next_fec_set_index, - merkle_root, - ); + let (data_shreds, coding_shreds) = setup_erasure_shreds_with_index_and_chained_merkle( + slot, + parent_slot, + 10, + next_fec_set_index, + merkle_root, + ); let data_shred = data_shreds[0].clone(); let coding_shred = coding_shreds[0].clone(); assert!( blockstore - .insert_shred_return_duplicate(coding_shred, &leader_schedule) + .insert_shred_return_duplicate(coding_shred) .is_empty() ); assert!( blockstore - .insert_shred_return_duplicate(data_shred, &leader_schedule,) + .insert_shred_return_duplicate(data_shred) .is_empty() ); } @@ -6444,7 +6321,7 @@ fn test_chained_merkle_root_upgrade_inconsistency_forwards() { let parent_slot = 0; let slot = 1; let fec_set_index = 0; - let (data_shreds, coding_shreds, leader_schedule) = + let (data_shreds, coding_shreds) = setup_erasure_shreds_with_index(slot, parent_slot, 10, fec_set_index); let coding_shred = coding_shreds[0].clone(); let next_fec_set_index = fec_set_index + data_shreds.len() as u32; @@ -6452,19 +6329,18 @@ fn test_chained_merkle_root_upgrade_inconsistency_forwards() { // Incorrectly chained merkle let merkle_root = Hash::new_unique(); assert!(merkle_root != coding_shred.merkle_root().unwrap()); - let (next_data_shreds, next_coding_shreds, leader_schedule_next) = - setup_erasure_shreds_with_index_and_chained_merkle( - slot, - parent_slot, - 10, - next_fec_set_index, - merkle_root, - ); + let (next_data_shreds, next_coding_shreds) = setup_erasure_shreds_with_index_and_chained_merkle( + slot, + parent_slot, + 10, + next_fec_set_index, + merkle_root, + ); let next_data_shred = next_data_shreds[0].clone(); assert!( blockstore - .insert_shred_return_duplicate(next_data_shred, &leader_schedule_next,) + .insert_shred_return_duplicate(next_data_shred) .is_empty() ); @@ -6486,7 +6362,7 @@ fn test_chained_merkle_root_upgrade_inconsistency_forwards() { // as the merkle root meta is missing. assert!( blockstore - .insert_shred_return_duplicate(coding_shred, &leader_schedule) + .insert_shred_return_duplicate(coding_shred) .is_empty() ); } @@ -6644,7 +6520,7 @@ fn test_get_double_merkle_root(use_alternate_location: bool) { let num_entries = 200; // Create a set of shreds for a complete block - let (data_shreds, _, leader_schedule) = setup_erasure_shreds(slot, parent_slot, num_entries); + let (data_shreds, _) = setup_erasure_shreds(slot, parent_slot, num_entries); // Collect FEC set merkle roots for verification let mut fec_set_roots = [Hash::default(); 3]; @@ -6655,7 +6531,12 @@ fn test_get_double_merkle_root(use_alternate_location: bool) { } } - let parent_info_hash = hashv(&[&parent_slot.to_le_bytes(), parent_block_id.as_ref()]); + let fec_set_count = u32::try_from(fec_set_roots.len()).unwrap(); + let parent_info_hash = hashv(&[ + &parent_slot.to_le_bytes(), + parent_block_id.as_ref(), + &fec_set_count.to_le_bytes(), + ]); let merkle_tree_leaves: Vec<_> = fec_set_roots .iter() .copied() @@ -6680,7 +6561,6 @@ fn test_get_double_merkle_root(use_alternate_location: bool) { let insert_results = blockstore .do_insert_shreds( shreds, - Some(&leader_schedule), false, None, &mut BlockstoreInsertionMetrics::default(), @@ -6749,15 +6629,14 @@ fn test_get_double_merkle_root(use_alternate_location: bool) { // Slot not full should return None let incomplete_slot = 1001; - let (partial_shreds, _, leader_schedule) = - setup_erasure_shreds_with_index_and_chained_merkle_and_last_in_slot( - incomplete_slot, - slot, // parent is 1000 - 5, - 0, - Hash::new_from_array(rand::random()), - false, // not last in slot - ); + let (partial_shreds, _) = setup_erasure_shreds_with_index_and_chained_merkle_and_last_in_slot( + incomplete_slot, + slot, // parent is 1000 + 5, + 0, + Hash::new_from_array(rand::random()), + false, // not last in slot + ); let shreds = partial_shreds .iter() @@ -6766,7 +6645,6 @@ fn test_get_double_merkle_root(use_alternate_location: bool) { let insert_results = blockstore .do_insert_shreds( shreds, - Some(&leader_schedule), false, None, &mut BlockstoreInsertionMetrics::default(), @@ -6804,8 +6682,7 @@ fn test_get_data_shreds_for_slot() { // Setup and insert shreds from 4 blocks into the columns for `slot` let data_shreds: [Vec; 4] = std::array::from_fn(|i| { - let (data_shreds, _coding_shreds, leader_schedule_cache) = - setup_erasure_shreds(slot, parent_slot, num_entries); + let (data_shreds, _coding_shreds) = setup_erasure_shreds(slot, parent_slot, num_entries); let location = locations[i]; let is_repaired = location != BlockLocation::Original; let shreds = data_shreds @@ -6814,7 +6691,6 @@ fn test_get_data_shreds_for_slot() { let insert_results = blockstore .do_insert_shreds( shreds, - Some(&leader_schedule_cache), false, None, &mut BlockstoreInsertionMetrics::default(), @@ -6890,7 +6766,7 @@ fn test_invalid_parent_info_marks_dead(block_header_first: bool, case: (u64, u64 .filter(|s| s.is_data()) .collect(); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); assert_eq!( blockstore.is_dead(slot), @@ -6921,7 +6797,7 @@ fn test_invalid_block_header_parent_info_marks_dead() { block_header_parent_slot, Hash::new_unique(), ); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert!( blockstore.is_dead(slot), @@ -6957,7 +6833,7 @@ fn test_invalid_update_parent_parent_info_marks_dead() { ); shreds.extend(create_block_footer_shreds(slot, shred_parent_slot, 0)); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert!( blockstore.is_dead(slot), @@ -6974,32 +6850,58 @@ fn test_invalid_update_parent_parent_info_marks_dead() { } } +#[test] +fn test_update_parent_non_first_leader_window_marks_dead() { + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Blockstore::open(ledger_path.path()).unwrap(); + + let slot = 10; + let shred_parent_slot = 5; + let update_parent_slot = 3; + let mut shreds = create_block_header_shreds(slot, shred_parent_slot, Hash::new_unique()); + shreds.extend(create_update_parent_shreds_with_shred_parent( + slot, + shred_parent_slot, + update_parent_slot, + Hash::new_unique(), + 32, + true, + )); + + blockstore.insert_shreds(shreds, true).unwrap(); + + let meta = blockstore.meta(slot).unwrap().unwrap(); + assert!(blockstore.is_dead(slot)); + assert_eq!(meta.parent_slot, Some(shred_parent_slot)); + assert!(!meta.has_update_parent()); +} + #[test] fn test_block_header_followed_by_update_parent() { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()).unwrap(); + let slot = 12; let parent_5_id = Hash::new_unique(); blockstore - .insert_shreds(create_block_header_shreds(10, 5, parent_5_id), None, true) + .insert_shreds(create_block_header_shreds(slot, 5, parent_5_id), true) .unwrap(); - assert_eq!(blockstore.meta(10).unwrap().unwrap().parent_slot, Some(5)); - verify_next_slots(&blockstore, 5, &[10]); + assert_eq!(blockstore.meta(slot).unwrap().unwrap().parent_slot, Some(5)); + verify_next_slots(&blockstore, 5, &[slot]); let parent_3_id = Hash::new_unique(); blockstore .insert_shreds( - create_update_parent_shreds_with_shred_parent(10, 5, 3, parent_3_id, 32, true), - None, + create_update_parent_shreds_with_shred_parent(slot, 5, 3, parent_3_id, 32, true), true, ) .unwrap(); - assert_eq!(blockstore.meta(10).unwrap().unwrap().parent_slot, Some(3)); + assert_eq!(blockstore.meta(slot).unwrap().unwrap().parent_slot, Some(3)); let parent_info = blockstore - .get_parent_info(10, BlockLocation::Original) + .get_parent_info(slot, BlockLocation::Original) .unwrap() .unwrap(); assert_eq!(parent_info.parent_slot, 3); @@ -7007,7 +6909,7 @@ fn test_block_header_followed_by_update_parent() { assert!(parent_info.has_update_parent()); verify_next_slots(&blockstore, 5, &[]); - verify_next_slots(&blockstore, 3, &[10]); + verify_next_slots(&blockstore, 3, &[slot]); } #[test] @@ -7015,7 +6917,7 @@ fn test_post_update_orig_after() { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - let slot = 90; + let slot = 92; let original_parent = 85; let update_parent = 80; blockstore @@ -7025,7 +6927,6 @@ fn test_post_update_orig_after() { original_parent, Hash::new_unique(), )), - None, false, ) .unwrap(); @@ -7041,7 +6942,6 @@ fn test_post_update_orig_after() { 32, false, )), - None, false, ) .unwrap(); @@ -7055,7 +6955,6 @@ fn test_post_update_orig_after() { blockstore .insert_shreds( data_shreds(create_block_footer_shreds(slot, original_parent, 64)), - None, false, ) .unwrap(); @@ -7072,7 +6971,7 @@ fn test_update_parent_shred_parent(update_parent_first: bool) { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - let slot = 90; + let slot = 92; let original_parent = 85; let update_parent = 80; let bad_shred_parent = 84; @@ -7093,7 +6992,7 @@ fn test_update_parent_shred_parent(update_parent_first: bool) { if update_parent_first { blockstore - .insert_shreds(update_parent_shreds, None, false) + .insert_shreds(update_parent_shreds, false) .unwrap(); blockstore .insert_shreds( @@ -7103,26 +7002,21 @@ fn test_update_parent_shred_parent(update_parent_first: bool) { 0, false, )), - None, false, ) .unwrap(); assert!(blockstore.meta(slot).unwrap().unwrap().has_update_parent()); - blockstore - .insert_shreds(bad_parent_shreds, None, false) - .unwrap(); + blockstore.insert_shreds(bad_parent_shreds, false).unwrap(); } else { - blockstore - .insert_shreds(bad_parent_shreds, None, false) - .unwrap(); + blockstore.insert_shreds(bad_parent_shreds, false).unwrap(); assert_eq!( blockstore.meta(slot).unwrap().unwrap().parent_slot, Some(bad_shred_parent) ); blockstore - .insert_shreds(update_parent_shreds, None, false) + .insert_shreds(update_parent_shreds, false) .unwrap(); } @@ -7134,7 +7028,7 @@ fn test_marker_boundary_ooo() { let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()).unwrap(); - let slot = 93; + let slot = 96; let original_parent = 88; let update_parent = 80; blockstore @@ -7144,7 +7038,6 @@ fn test_marker_boundary_ooo() { original_parent, Hash::new_unique(), )), - None, false, ) .unwrap(); @@ -7158,9 +7051,7 @@ fn test_marker_boundary_ooo() { false, ))); } - blockstore - .insert_shreds(post_update_shreds, None, false) - .unwrap(); + blockstore.insert_shreds(post_update_shreds, false).unwrap(); let meta = blockstore.meta(slot).unwrap().unwrap(); assert_eq!(meta.parent_slot, Some(original_parent)); @@ -7177,7 +7068,6 @@ fn test_marker_boundary_ooo() { 32, false, )), - None, false, ) .unwrap(); @@ -7205,7 +7095,7 @@ fn test_multiple_children_reparenting() { let parent_id = Hash::new_unique(); for slot in [44, 40, 48] { blockstore - .insert_shreds(create_block_header_shreds(slot, 35, parent_id), None, true) + .insert_shreds(create_block_header_shreds(slot, 35, parent_id), true) .unwrap(); } verify_next_slots(&blockstore, 35, &[40, 44, 48]); @@ -7213,7 +7103,6 @@ fn test_multiple_children_reparenting() { blockstore .insert_shreds( create_update_parent_shreds_with_shred_parent(44, 35, 32, Hash::new_unique(), 32, true), - None, true, ) .unwrap(); @@ -7223,7 +7112,6 @@ fn test_multiple_children_reparenting() { blockstore .insert_shreds( create_update_parent_shreds_with_shred_parent(40, 35, 33, Hash::new_unique(), 32, true), - None, true, ) .unwrap(); @@ -7237,11 +7125,7 @@ fn test_interleaved_shred_arrival() { let blockstore = Blockstore::open(ledger_path.path()).unwrap(); blockstore - .insert_shreds( - create_block_header_shreds(52, 48, Hash::new_unique()), - None, - true, - ) + .insert_shreds(create_block_header_shreds(52, 48, Hash::new_unique()), true) .unwrap(); assert_eq!(blockstore.meta(52).unwrap().unwrap().parent_slot, Some(48)); @@ -7251,8 +7135,8 @@ fn test_interleaved_shred_arrival() { let mid = update_shreds.len() / 2; let first_half: Vec<_> = update_shreds.drain(..mid).collect(); - blockstore.insert_shreds(first_half, None, true).unwrap(); - blockstore.insert_shreds(update_shreds, None, true).unwrap(); + blockstore.insert_shreds(first_half, true).unwrap(); + blockstore.insert_shreds(update_shreds, true).unwrap(); assert_eq!(blockstore.meta(52).unwrap().unwrap().parent_slot, Some(45)); verify_next_slots(&blockstore, 48, &[]); @@ -7276,7 +7160,7 @@ fn test_same_batch_block_header_then_update_parent() { true, )); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); // UpdateParent should take precedence assert_eq!(blockstore.meta(60).unwrap().unwrap().parent_slot, Some(52)); @@ -7295,7 +7179,7 @@ fn test_same_batch_update_parent_then_block_header() { create_update_parent_shreds_with_shred_parent(72, 68, 65, Hash::new_unique(), 32, true); shreds.extend(create_block_header_shreds(72, 68, Hash::new_unique())); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); // UpdateParent should take precedence regardless of insertion order assert_eq!(blockstore.meta(72).unwrap().unwrap().parent_slot, Some(65)); @@ -7312,7 +7196,6 @@ fn test_multiple_update_parents_out_of_order_marks_dead() { blockstore .insert_shreds( data_shreds(create_block_header_shreds(slot, 75, Hash::new_unique())), - None, true, ) .unwrap(); @@ -7333,7 +7216,7 @@ fn test_multiple_update_parents_out_of_order_marks_dead() { // Insert the tail of the earlier FEC set so the later UpdateParent can // be parsed before this UpdateParent marker arrives. blockstore - .insert_shreds(first_update_parent_shreds, None, true) + .insert_shreds(first_update_parent_shreds, true) .unwrap(); let second_update_parent_slot = 65; @@ -7347,7 +7230,6 @@ fn test_multiple_update_parents_out_of_order_marks_dead() { 64, false, )), - None, true, ) .unwrap(); @@ -7361,7 +7243,7 @@ fn test_multiple_update_parents_out_of_order_marks_dead() { assert!(!blockstore.is_dead(slot)); blockstore - .insert_shreds(vec![first_update_parent_marker], None, true) + .insert_shreds(vec![first_update_parent_marker], true) .unwrap(); assert!(blockstore.is_dead(slot)); } @@ -7373,16 +7255,12 @@ fn test_update_parent_propagates_connectivity() { // Slot 0 is connected when full let (shreds, _) = make_slot_entries(0, 0, 5); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); assert!(blockstore.meta(0).unwrap().unwrap().is_connected()); // Slot 8 with BlockHeader pointing to disconnected parent blockstore - .insert_shreds( - create_block_header_shreds(8, 5, Hash::new_unique()), - None, - true, - ) + .insert_shreds(create_block_header_shreds(8, 5, Hash::new_unique()), true) .unwrap(); assert!(!blockstore.meta(8).unwrap().unwrap().is_connected()); @@ -7390,7 +7268,6 @@ fn test_update_parent_propagates_connectivity() { blockstore .insert_shreds( create_update_parent_shreds_with_shred_parent(8, 5, 0, Hash::new_unique(), 32, true), - None, true, ) .unwrap(); @@ -7408,20 +7285,19 @@ fn test_update_parent_propagates_connectivity_to_descendants() { // Slot 0 is connected when full let (shreds, _) = make_slot_entries(0, 0, 5); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); // Slot 100 starts incomplete with BlockHeader pointing to disconnected parent blockstore .insert_shreds( create_block_header_shreds(100, 50, Hash::new_unique()), - None, true, ) .unwrap(); // Slots 200 and 300 are full, chained to 100 for (slot, parent) in [(200, 100), (300, 200)] { let (shreds, _) = make_slot_entries(slot, parent, 5); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); } for slot in [100, 200, 300] { assert!(!blockstore.meta(slot).unwrap().unwrap().is_connected()); @@ -7431,7 +7307,6 @@ fn test_update_parent_propagates_connectivity_to_descendants() { blockstore .insert_shreds( create_update_parent_shreds_with_shred_parent(100, 50, 0, Hash::new_unique(), 32, true), - None, true, ) .unwrap(); @@ -7451,31 +7326,26 @@ fn test_update_parent_clears_connectivity() { // Slot 0 and 5 are connected (full slots chained together) let (shreds, _) = make_slot_entries(0, 0, 5); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); let (shreds, _) = make_slot_entries(5, 0, 5); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); assert!(blockstore.meta(5).unwrap().unwrap().is_connected()); // Slot 8 with BlockHeader pointing to connected parent 5 blockstore - .insert_shreds( - create_block_header_shreds(8, 5, Hash::new_unique()), - None, - true, - ) + .insert_shreds(create_block_header_shreds(8, 5, Hash::new_unique()), true) .unwrap(); let meta = blockstore.meta(8).unwrap().unwrap(); assert!(meta.is_parent_connected()); // Slot 20 chains to slot 8 let (shreds, _) = make_slot_entries(20, 8, 5); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); // UpdateParent switches slot 8 to disconnected parent 3 (lower, doesn't exist) blockstore .insert_shreds( create_update_parent_shreds_with_shred_parent(8, 5, 3, Hash::new_unique(), 32, true), - None, true, ) .unwrap(); @@ -7495,23 +7365,19 @@ fn test_connectivity_does_not_propagate_through_incomplete_slot() { // Slot 0 is the connected root let (shreds, _) = make_slot_entries(0, 0, 5); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); assert!(blockstore.meta(0).unwrap().unwrap().is_connected()); // Slot 48 incomplete, pointing to disconnected parent 40 blockstore - .insert_shreds( - create_block_header_shreds(48, 40, Hash::new_unique()), - None, - true, - ) + .insert_shreds(create_block_header_shreds(48, 40, Hash::new_unique()), true) .unwrap(); // Full children chain from incomplete slot 48 let (shreds, _) = make_slot_entries(60, 48, 5); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); let (shreds, _) = make_slot_entries(70, 60, 5); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); for slot in [48, 60, 70] { assert!(!blockstore.meta(slot).unwrap().unwrap().is_connected()); @@ -7521,7 +7387,6 @@ fn test_connectivity_does_not_propagate_through_incomplete_slot() { blockstore .insert_shreds( create_update_parent_shreds_with_shred_parent(48, 40, 0, Hash::new_unique(), 32, false), - None, true, ) .unwrap(); diff --git a/ledger/src/blockstore_cleanup_service.rs b/ledger/src/blockstore_cleanup_service.rs index 1323671b7ff..74b0d3dce96 100644 --- a/ledger/src/blockstore_cleanup_service.rs +++ b/ledger/src/blockstore_cleanup_service.rs @@ -194,8 +194,10 @@ impl BlockstoreCleanupService { return; }; - // Ensure we don't cleanup anything past the last root we saw - let lowest_cleanup_slot = std::cmp::min(lowest_slot + num_slots_to_clean - 1, root); + // Use min() to ensure we do not purge the latest root or anything newer + // Purge is inclusive so subtract one from min() result + let lowest_cleanup_slot = + std::cmp::min(lowest_slot + num_slots_to_clean, root).saturating_sub(1); match cleanup_request_sender.try_send(lowest_cleanup_slot) { Ok(()) => {} @@ -298,7 +300,7 @@ mod tests { let total_num_shreds = shreds.len() as u64; let shreds_per_slot = (shreds.len() / num_slots as usize) as u64; assert!(shreds_per_slot > 1); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // Initiate a flush so inserted shreds found by find_slots_to_clean() let blockstore = Arc::new(flush_blockstore_contents_to_disk(blockstore)); @@ -313,7 +315,7 @@ mod tests { // Start with 1 as the latest root let mut latest_root = 1; blockstore.set_roots(std::iter::once(&latest_root)).unwrap(); - // Auto clean will select slot 1 (latest_root) as min clean slot + // Auto clean will select slot 0 (latest_root - 1) as min clean slot let max_ledger_shreds = Some(1); BlockstoreCleanupService::maybe_generate_automatic_cleanup_request( &blockstore, @@ -322,7 +324,7 @@ mod tests { &mut last_purge_slot, purge_interval, ); - assert_eq!(receiver.try_recv().unwrap(), latest_root); + assert_eq!(receiver.try_recv().unwrap(), latest_root - 1); // Reset last_purge_slot assert_eq!(last_purge_slot, 1); @@ -334,7 +336,7 @@ mod tests { &mut last_purge_slot, purge_interval, ); - assert_eq!(receiver.try_recv().unwrap(), latest_root); + assert_eq!(receiver.try_recv().unwrap(), latest_root - 1); // Reset last_purge_slot assert_eq!(last_purge_slot, 1); last_purge_slot = 0; @@ -385,13 +387,14 @@ mod tests { &mut last_purge_slot, purge_interval, ); - assert_eq!(receiver.try_recv().unwrap(), latest_root); + assert_eq!(receiver.try_recv().unwrap(), latest_root - 1); // Reset last_purge_slot assert_eq!(last_purge_slot, 1); last_purge_slot = 0; for slot in 1..=num_slots { - // Set last_root to make slots <= slot eligible for cleaning + // Update latest_root so that any slots < latest_root will become + // eligible to be cleaned latest_root = slot; blockstore.set_roots(std::iter::once(&latest_root)).unwrap(); // Set max_ledger_shreds to 0 so that all eligible slots are cleaned @@ -403,7 +406,7 @@ mod tests { &mut last_purge_slot, purge_interval, ); - assert_eq!(receiver.try_recv().unwrap(), latest_root); + assert_eq!(receiver.try_recv().unwrap(), latest_root - 1); } } @@ -415,7 +418,7 @@ mod tests { let (sender, receiver) = bounded(1); let (shreds, _) = make_many_slot_entries(0, 50, 5); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); // Initiate a flush so inserted shreds found by maybe_generate_automatic_cleanup_request() let blockstore = Arc::new(flush_blockstore_contents_to_disk(blockstore)); @@ -439,10 +442,10 @@ mod tests { // A request will be generated and consumed so channel should be empty assert!(receiver.is_empty()); - // Ensure that slots 0-40 are not present + // Ensure that slots 0-39 are not present; the root (40) is retained blockstore .slot_meta_iterator(0) .unwrap() - .for_each(|(slot, _)| assert!(slot > 40)); + .for_each(|(slot, _)| assert!(slot >= 40)); } } diff --git a/ledger/src/blockstore_db.rs b/ledger/src/blockstore_db.rs index d0fcd4830df..563e70c3e67 100644 --- a/ledger/src/blockstore_db.rs +++ b/ledger/src/blockstore_db.rs @@ -20,8 +20,8 @@ use { prost::Message, rocksdb::{ self, ColumnFamily, ColumnFamilyDescriptor, CompactionDecision, DB, DBCompressionType, - DBIterator, DBPinnableSlice, DBRawIterator, IteratorMode as RocksIteratorMode, LiveFile, - Options, WriteBatch as RWriteBatch, + DBIterator, DBPinnableSlice, IteratorMode as RocksIteratorMode, LiveFile, Options, + WriteBatch as RWriteBatch, compaction_filter::CompactionFilter, compaction_filter_factory::{CompactionFilterContext, CompactionFilterFactory}, properties as RocksProperties, @@ -39,7 +39,10 @@ use { atomic::{AtomicU64, Ordering}, }, }, - wincode::{SchemaReadOwned, deserialize}, + wincode::{ + SchemaRead, + config::{ConfigCore, DefaultConfig}, + }, }; const BLOCKSTORE_METRICS_ERROR: i64 = -1; @@ -396,7 +399,8 @@ impl Rocks { self.db.iterator_cf(cf, iterator_mode) } - pub(crate) fn raw_iterator_cf(&self, cf: &ColumnFamily) -> Result> { + #[cfg(test)] + pub(crate) fn raw_iterator_cf(&self, cf: &ColumnFamily) -> Result> { Ok(self.db.raw_iterator_cf(cf)) } @@ -566,6 +570,55 @@ impl WriteBatch { } } +/// A pinned deserialized value with the ability to hold references into a [`DBPinnableSlice`]. +/// +/// All references in the deserialized payload will be tied to the lifetime of the [`DBPinnableSlice`]. +/// +/// Use this for zero-copy access to a value stored in the blockstore -- no reason to keep +/// the [`DBPinnableSlice`] alive for owned values. +pub struct DBPinnedT<'a, C: ConfigCore, T: SchemaRead<'a, C>> { + val: T::Dst, + _slice: DBPinnableSlice<'a>, + _phantom: PhantomData, +} + +impl<'a, C, T> std::ops::Deref for DBPinnedT<'a, C, T> +where + C: ConfigCore, + T: SchemaRead<'a, C>, +{ + type Target = T::Dst; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.val + } +} + +impl<'de, T, C> DBPinnedT<'de, C, T> +where + C: ConfigCore, + T: SchemaRead<'de, C>, +{ + fn decode(slice: DBPinnableSlice<'de>) -> Result { + let bytes = slice.as_ref(); + // SAFETY: `DBPinnableSlice` provides a stable reference into the underlying storage and keeps + // the RocksDB value pointer valid until it is dropped. + // `DBPinnedT` stores `val` before `_slice`, so `val` is dropped first; + // any references decoded into `val` cannot outlive the pinned bytes. + let val = T::get(unsafe { std::slice::from_raw_parts(bytes.as_ptr(), bytes.len()) })?; + Ok(Self { + val, + _slice: slice, + _phantom: PhantomData, + }) + } + + #[inline] + pub fn get(&self) -> &T::Dst { + &self.val + } +} impl LedgerColumn where C: Column + ColumnName, @@ -610,6 +663,17 @@ where result } + #[inline] + pub fn get_pinned_t<'a, T>( + &'a self, + index: C::Index, + ) -> Result>> + where + T: SchemaRead<'a, DefaultConfig>, + { + self.get_slice(index)?.map(DBPinnedT::decode).transpose() + } + /// Create a key type suitable for use with multi_get_bytes() and /// multi_get(). Those functions return iterators, so the keys must be /// created with a separate function in order to live long enough @@ -888,47 +952,6 @@ impl LedgerColumn where C: ProtobufColumn + ColumnName, { - pub fn get_protobuf_or_wincode< - T: SchemaReadOwned + Into, - >( - &self, - index: C::Index, - ) -> Result> { - let key = ::key(&index); - self.get_raw_protobuf_or_wincode::(key) - } - - pub(crate) fn get_raw_protobuf_or_wincode< - T: SchemaReadOwned + Into, - >( - &self, - key: impl AsRef<[u8]>, - ) -> Result> { - let is_perf_enabled = maybe_enable_rocksdb_perf( - self.column_options.rocks_perf_sample_interval, - &self.read_perf_status, - ); - let result = self.backend.get_pinned_cf(self.handle(), key); - if let Some(op_start_instant) = is_perf_enabled { - report_rocksdb_read_perf( - C::NAME, - PERF_METRIC_OP_NAME_GET, - &op_start_instant.elapsed(), - &self.column_options, - ); - } - - if let Some(pinnable_slice) = result? { - let value = match C::Type::decode(pinnable_slice.as_ref()) { - Ok(value) => value, - Err(_) => deserialize::(pinnable_slice.as_ref())?.into(), - }; - Ok(Some(value)) - } else { - Ok(None) - } - } - pub fn get_protobuf(&self, index: C::Index) -> Result> { let is_perf_enabled = maybe_enable_rocksdb_perf( self.column_options.rocks_perf_sample_interval, diff --git a/ledger/src/blockstore_meta.rs b/ledger/src/blockstore_meta.rs index 2868667f0fc..493ed752e15 100644 --- a/ledger/src/blockstore_meta.rs +++ b/ledger/src/blockstore_meta.rs @@ -1,6 +1,6 @@ use { crate::{ - bit_vec::BitVec, + bit_vec::{BitVec, BitVecRef}, blockstore::ParentInfo, shred::{ self, DATA_SHREDS_PER_FEC_BLOCK, MAX_DATA_SHREDS_PER_SLOT, Shred, ShredType, @@ -185,6 +185,24 @@ pub struct SlotMetaV3 { pub type SlotMeta = SlotMetaV3; +/// Lighter-weight version of [`SlotMeta`] containing just the set +/// of fields needed for repair. +/// +/// wincode deserializes in field declaration order, so [`SlotMetaRepair`] +/// can always be deserialized from [`SlotMeta`]. +#[derive(Clone, Debug, Default, SchemaRead, Eq, PartialEq)] +pub struct SlotMetaRepair { + pub slot: Slot, + pub consumed: u64, + pub received: u64, + pub first_shred_timestamp: u64, + #[wincode(with = "wincode_compat::OptionCompat")] + pub last_index: Option, + #[wincode(with = "wincode_compat::OptionCompat")] + pub parent_slot: Option, + pub next_slots: Vec, +} + // Wincode implementation of serialize and deserialize for Option // where None is represented as u64::MAX; for backward compatibility. mod wincode_compat { @@ -277,6 +295,13 @@ pub struct Index { coding: ShredIndex, } +#[derive(Debug, SchemaRead, PartialEq, Eq)] +pub struct IndexRef<'a> { + pub slot: Slot, + data: ShredIndexRef<'a>, + coding: ShredIndexRef<'a>, +} + #[derive(Clone, Copy, Debug, SchemaRead, SchemaWrite, Eq, PartialEq)] /// Erasure coding information pub struct ErasureMeta { @@ -448,6 +473,12 @@ impl Index { } } +impl IndexRef<'_> { + pub fn data(&self) -> &ShredIndexRef<'_> { + &self.data + } +} + /// A bitvec (`Box<[u8]>`) of shred indices, where each u8 represents 8 shred indices. /// /// Bit vec implementation provides: @@ -517,32 +548,58 @@ impl FromIterator for ShredIndex { } } +/// A reference to a [`ShredIndex`]. +#[derive(Debug, SchemaRead, PartialEq, Eq)] +pub struct ShredIndexRef<'a> { + index: BitVecRef<'a, MAX_DATA_SHREDS_PER_SLOT>, + num_shreds: usize, +} + +impl ShredIndexRef<'_> { + pub fn num_shreds(&self) -> usize { + self.num_shreds + } + + pub(crate) fn range(&self, bounds: R) -> impl Iterator + '_ + where + R: RangeBounds, + { + let start = bounds.start_bound().map(|&b| b as usize); + let end = bounds.end_bound().map(|&b| b as usize); + self.index + .range((start, end)) + .iter_ones() + .map(|idx| idx as u64) + } +} + +fn slot_meta_is_full(last_index: Option, consumed: u64) -> bool { + // last_index is None when it has no information about how + // many shreds will fill this slot. + // Note: A full slot with zero shreds is not possible. + // Should never happen + if last_index.map(|ix| consumed > ix + 1).unwrap_or_default() { + datapoint_error!( + "blockstore_error", + ( + "error", + format!( + "Observed a slot meta with consumed: {} > meta.last_index + 1: {:?}", + consumed, + last_index.map(|ix| ix + 1), + ), + String + ) + ); + } + + Some(consumed) == last_index.map(|ix| ix + 1) +} + impl SlotMeta { + #[inline] pub fn is_full(&self) -> bool { - // last_index is None when it has no information about how - // many shreds will fill this slot. - // Note: A full slot with zero shreds is not possible. - // Should never happen - if self - .last_index - .map(|ix| self.consumed > ix + 1) - .unwrap_or_default() - { - datapoint_error!( - "blockstore_error", - ( - "error", - format!( - "Observed a slot meta with consumed: {} > meta.last_index + 1: {:?}", - self.consumed, - self.last_index.map(|ix| ix + 1), - ), - String - ) - ); - } - - Some(self.consumed) == self.last_index.map(|ix| ix + 1) + slot_meta_is_full(self.last_index, self.consumed) } /// Returns a boolean indicating whether this meta's parent slot is known. @@ -645,6 +702,13 @@ impl SlotMeta { } } +impl SlotMetaRepair { + #[inline] + pub fn is_full(&self) -> bool { + slot_meta_is_full(self.last_index, self.consumed) + } +} + impl ErasureMeta { pub(crate) fn from_coding_shred(shred: &Shred) -> Option { match shred.shred_type() { @@ -983,6 +1047,112 @@ mod test { } } + fn arb_slot_meta() -> impl Strategy { + ( + any::(), + any::(), + any::(), + any::(), + proptest::option::of(any::()), + proptest::option::of(any::()), + proptest::collection::vec(any::(), 0..32), + any::(), + proptest::collection::vec(0..MAX_DATA_SHREDS_PER_SLOT as u32, 0..64), + any::<[u8; HASH_BYTES]>(), + any::(), + ) + .prop_map( + |( + slot, + consumed, + received, + first_shred_timestamp, + last_index, + parent_slot, + next_slots, + connected_flags, + completed_data_indexes, + parent_block_id, + replay_fec_set_index, + )| SlotMeta { + slot, + consumed, + received, + first_shred_timestamp, + last_index, + parent_slot, + next_slots, + connected_flags: ConnectedFlags::from_bits_truncate(connected_flags), + completed_data_indexes: completed_data_indexes.into_iter().collect(), + parent_block_id: Hash::new_from_array(parent_block_id), + replay_fec_set_index, + }, + ) + } + + fn arb_index() -> impl Strategy { + ( + any::(), + proptest::collection::vec(0..MAX_DATA_SHREDS_PER_SLOT as u64, 0..128), + proptest::collection::vec(0..MAX_DATA_SHREDS_PER_SLOT as u64, 0..128), + ) + .prop_map(|(slot, data_indexes, coding_indexes)| { + let mut index = Index::new(slot); + for index_to_insert in data_indexes { + index.data_mut().insert(index_to_insert); + } + for index_to_insert in coding_indexes { + index.coding_mut().insert(index_to_insert); + } + index + }) + } + + proptest! { + // Property: `SlotMetaRepair` is always deserializable from serialized `SlotMeta`. + #[test] + fn test_slot_meta_repair_deserialization( + slot_meta in arb_slot_meta(), + ) { + let serialized = wincode::serialize(&slot_meta).unwrap(); + let deserialized: SlotMetaRepair = wincode::deserialize(&serialized).unwrap(); + + prop_assert_eq!(deserialized.slot, slot_meta.slot); + prop_assert_eq!(deserialized.consumed, slot_meta.consumed); + prop_assert_eq!(deserialized.received, slot_meta.received); + prop_assert_eq!( + deserialized.first_shred_timestamp, + slot_meta.first_shred_timestamp + ); + prop_assert_eq!(deserialized.last_index, slot_meta.last_index); + prop_assert_eq!(deserialized.parent_slot, slot_meta.parent_slot); + prop_assert_eq!(deserialized.next_slots, slot_meta.next_slots); + } + } + + proptest! { + // Property: `IndexRef` is always deserializable from `Index`. + #[test] + fn test_index_ref_deserialization( + index in arb_index(), + ) { + let serialized = wincode::serialize(&index).unwrap(); + let deserialized: IndexRef = wincode::deserialize(&serialized).unwrap(); + + prop_assert_eq!(deserialized.slot, index.slot); + prop_assert_eq!( + deserialized.data().range(..).collect::>(), + index.data().range(..).collect::>() + ); + prop_assert_eq!( + deserialized.coding.range(..).collect::>(), + index.coding().range(..).collect::>() + ); + prop_assert_eq!(deserialized.data().num_shreds(), index.data().num_shreds()); + prop_assert_eq!(deserialized.coding.num_shreds(), index.coding().num_shreds()); + } + } + #[test] fn test_shred_index_range_bounds() { let mut index = ShredIndex::default(); diff --git a/ledger/src/blockstore_metrics.rs b/ledger/src/blockstore_metrics.rs index c32555d39ff..1631eda4803 100644 --- a/ledger/src/blockstore_metrics.rs +++ b/ledger/src/blockstore_metrics.rs @@ -1,9 +1,10 @@ use { - crate::blockstore_options::LedgerColumnOptions, + crate::{blockstore_meta::BlockLocation, blockstore_options::LedgerColumnOptions}, rocksdb::{ PerfContext, perf::{PerfMetric, PerfStatsLevel, set_perf_stats}, }, + solana_clock::Slot, solana_metrics::datapoint_info, solana_time_utils::timestamp, std::{ @@ -43,6 +44,30 @@ pub struct BlockstoreInsertionMetrics { pub num_coding_shreds_inserted: usize, } +#[derive(Default)] +pub struct BlockstoreSwitchBankMetrics { + pub total_elapsed_us: u64, + pub lock_elapsed_us: u64, + pub backup_elapsed_us: u64, + pub purge_elapsed_us: u64, + pub copy_elapsed_us: u64, +} + +impl BlockstoreSwitchBankMetrics { + pub fn report_metrics(self, slot: Slot, from_location: BlockLocation) { + datapoint_info!( + "blockstore_switch_bank", + "from_location" => from_location.to_string(), + ("slot", slot as i64, i64), + ("total_elapsed_us", self.total_elapsed_us as i64, i64), + ("lock_elapsed_us", self.lock_elapsed_us as i64, i64), + ("backup_elapsed_us", self.backup_elapsed_us as i64, i64), + ("purge_elapsed_us", self.purge_elapsed_us as i64, i64), + ("copy_elapsed_us", self.copy_elapsed_us as i64, i64), + ); + } +} + impl BlockstoreInsertionMetrics { const NAME: &str = "blockstore-insert-shreds"; diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index d044c2c7f14..ffb81b536fa 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -19,10 +19,10 @@ use { solana_accounts_db::{ accounts_db::AccountsDbConfig, accounts_update_notifier_interface::AccountsUpdateNotifier, }, - solana_clock::Slot, + solana_clock::{BankId, Slot}, solana_cost_model::{cost_model::CostModel, transaction_cost::TransactionCost}, solana_entry::{ - block_component::{BlockComponent, VersionedBlockMarker}, + block_component::BlockComponent, entry::{self, Entry, EntrySlice, EntryType, create_ticks}, }, solana_genesis_config::GenesisConfig, @@ -39,6 +39,7 @@ use { commitment::VOTE_THRESHOLD_SIZE, dependency_tracker::DependencyTracker, installed_scheduler_pool::BankWithScheduler, + leader_schedule_utils::leader_slot_index, prioritization_fee_cache::PrioritizationFeeCache, runtime_config::RuntimeConfig, snapshot_controller::SnapshotController, @@ -48,6 +49,7 @@ use { solana_runtime_transaction::{ runtime_transaction::RuntimeTransaction, transaction_with_meta::TransactionWithMeta, }, + solana_shred_version::compute_shred_version, solana_signature::Signature, solana_svm::{ transaction_commit_result::{TransactionCommitResult, TransactionCommitResultExtensions}, @@ -872,6 +874,9 @@ pub type ProcessSlotCallback = Arc; pub struct ProcessOptions { /// Run PoH, transaction signature and other transaction verification on the entries. pub run_verification: bool, + /// For startup replay / ledger-tool skip checks that verify a block chains to its parent correctly + /// For Tower blocks this is validating the chained merkle root, for Alpenglow it is the double merkle root + pub skip_inter_slot_verification: bool, pub halt_at_slot: Option, pub slot_callback: Option, pub new_hard_forks: Option>, @@ -917,6 +922,7 @@ pub(crate) fn process_blockstore_for_bank_0( None, ); let bank0_slot = bank0.slot(); + let hard_forks = bank0.hard_forks(); let bank_forks = BankForks::new_rw_arc(bank0); info!("Processing ledger for slot 0..."); @@ -927,6 +933,7 @@ pub(crate) fn process_blockstore_for_bank_0( .unwrap() .get_with_scheduler(bank0_slot) .unwrap(), + compute_shred_version(&genesis_config.hash(), Some(&hard_forks)), blockstore, &replay_tx_thread_pool, opts, @@ -943,6 +950,7 @@ pub(crate) fn process_blockstore_for_bank_0( pub fn process_blockstore_from_root( blockstore: &Blockstore, bank_forks: &RwLock, + shred_version: u16, leader_schedule_cache: &LeaderScheduleCache, opts: &ProcessOptions, transaction_status_sender: Option<&TransactionStatusSender>, @@ -1000,6 +1008,7 @@ pub fn process_blockstore_from_root( let replay_tx_thread_pool = create_thread_pool(num_cpus::get()); load_frozen_forks( bank_forks, + shred_version, &start_slot_meta, blockstore, &replay_tx_thread_pool, @@ -1088,6 +1097,15 @@ fn verify_ticks( } if migration_status.should_have_alpenglow_ticks(bank.slot()) { + // When alpenglow is active, PoH MUST be in low power mode. + // We require that each block only has 1 tick at the very end + if entries.iter().any(|entry| entry.num_hashes != 1) { + warn!( + "Alpenglow entry with invalid num_hashes found in slot: {}", + bank.slot() + ); + return Err(BlockError::InvalidTickHashCount); + } return Ok(()); } @@ -1108,6 +1126,7 @@ fn verify_ticks( fn confirm_full_slot( blockstore: &Blockstore, bank: &BankWithScheduler, + shred_version: u16, replay_tx_thread_pool: &ThreadPool, opts: &ProcessOptions, progress: &mut ConfirmationProgress, @@ -1133,6 +1152,7 @@ fn confirm_full_slot( confirm_slot( blockstore, bank, + shred_version, replay_tx_thread_pool, &mut confirmation_timing, progress, @@ -1203,6 +1223,9 @@ pub struct ConfirmationTiming { /// `batch_execute()` measurements. pub batch_execute: BatchExecutionTiming, + + /// Number of times this slot was switched from an alternate location. + pub num_bank_switches: u64, } impl Default for ConfirmationTiming { @@ -1216,6 +1239,7 @@ impl Default for ConfirmationTiming { fetch_elapsed: 0, fetch_fail_elapsed: 0, batch_execute: BatchExecutionTiming::default(), + num_bank_switches: 0, } } } @@ -1378,6 +1402,7 @@ impl ReplaySlotStats { (confirmation_elapsed, self.confirmation_elapsed as i64, i64), (replay_elapsed, self.replay_elapsed as i64, i64), ("execute_batches_us", execute_batches_us, Option), + ("num_bank_switches", self.num_bank_switches as i64, i64), ( "replay_total_elapsed", self.started.elapsed().as_micros() as i64, @@ -1653,6 +1678,7 @@ impl AsyncVerificationProgress { pub fn confirm_slot( blockstore: &Blockstore, bank: &BankWithScheduler, + shred_version: u16, replay_tx_thread_pool: &ThreadPool, timing: &mut ConfirmationTiming, progress: &mut ConfirmationProgress, @@ -1660,110 +1686,7 @@ pub fn confirm_slot( transaction_status_sender: Option<&TransactionStatusSender>, entry_notification_sender: Option<&EntryNotifierSender>, replay_vote_sender: Option<&ReplayVoteSender>, - finalization_cert_sender: Option<&Sender>>, - allow_dead_slots: bool, - log_messages_bytes_limit: Option, - prioritization_fee_cache: Option<&PrioritizationFeeCache>, - migration_status: &MigrationStatus, -) -> result::Result<(), BlockstoreProcessorError> { - match bank - .feature_set - .is_active(&agave_feature_set::alpenglow::id()) - { - true => confirm_slot_with_components( - blockstore, - bank, - replay_tx_thread_pool, - timing, - progress, - skip_verification, - transaction_status_sender, - entry_notification_sender, - replay_vote_sender, - finalization_cert_sender, - allow_dead_slots, - log_messages_bytes_limit, - prioritization_fee_cache, - migration_status, - ), - false => confirm_slot_with_entries( - blockstore, - bank, - replay_tx_thread_pool, - timing, - progress, - skip_verification, - transaction_status_sender, - entry_notification_sender, - replay_vote_sender, - allow_dead_slots, - log_messages_bytes_limit, - prioritization_fee_cache, - migration_status, - ), - } -} - -#[allow(clippy::too_many_arguments)] -fn confirm_slot_with_entries( - blockstore: &Blockstore, - bank: &BankWithScheduler, - replay_tx_thread_pool: &ThreadPool, - timing: &mut ConfirmationTiming, - progress: &mut ConfirmationProgress, - skip_verification: bool, - transaction_status_sender: Option<&TransactionStatusSender>, - entry_notification_sender: Option<&EntryNotifierSender>, - replay_vote_sender: Option<&ReplayVoteSender>, - allow_dead_slots: bool, - log_messages_bytes_limit: Option, - prioritization_fee_cache: Option<&PrioritizationFeeCache>, - migration_status: &MigrationStatus, -) -> result::Result<(), BlockstoreProcessorError> { - let slot = bank.slot(); - - let slot_entries_load_result = { - let mut load_elapsed = Measure::start("load_elapsed"); - let load_result = blockstore - .get_slot_entries_with_shred_info(slot, progress.num_shreds, allow_dead_slots) - .map_err(BlockstoreProcessorError::FailedToLoadEntries); - load_elapsed.stop(); - if load_result.is_err() { - timing.fetch_fail_elapsed += load_elapsed.as_us(); - } else { - timing.fetch_elapsed += load_elapsed.as_us(); - } - load_result - }?; - - confirm_slot_entries( - bank, - replay_tx_thread_pool, - slot_entries_load_result, - timing, - progress, - skip_verification, - transaction_status_sender, - entry_notification_sender, - replay_vote_sender, - log_messages_bytes_limit, - prioritization_fee_cache, - migration_status, - ) -} - -#[allow(clippy::too_many_arguments)] -fn confirm_slot_with_components( - blockstore: &Blockstore, - bank: &BankWithScheduler, - replay_tx_thread_pool: &ThreadPool, - timing: &mut ConfirmationTiming, - progress: &mut ConfirmationProgress, - skip_verification: bool, - transaction_status_sender: Option<&TransactionStatusSender>, - entry_notification_sender: Option<&EntryNotifierSender>, - replay_vote_sender: Option<&ReplayVoteSender>, - finalization_cert_sender: Option<&Sender>>, + finalization_cert_sender: Option<&Sender>, allow_dead_slots: bool, log_messages_bytes_limit: Option, prioritization_fee_cache: Option<&PrioritizationFeeCache>, @@ -1802,7 +1725,9 @@ fn confirm_slot_with_components( // Only replay that starts at the persisted UpdateParent FEC set may accept // UpdateParent as its first parent marker. From-shred-zero replay still // requires a block header before UpdateParent. - let replay_starts_at_update_parent = migration_status.should_allow_fast_leader_handover(slot) + let replay_starts_at_update_parent = bank.feature_set.snapshot().alpenglow_fast_leader_handover + && migration_status.should_allow_block_markers(slot) + && leader_slot_index(slot) == 0 && blockstore .meta(slot) .expect("Blockstore operations must succeed") @@ -1856,17 +1781,22 @@ fn confirm_slot_with_components( )?; } BlockComponent::BlockMarker(marker) => { + if marker.is_footer() { + // The footer path mutates vote accounts directly to pay rewards. + // All prior transactions must finish first so vote account view is deterministic. + if let Some((result, execute_time)) = bank.wait_for_completed_scheduler() { + timing.batch_execute.totals.accumulate(&execute_time); + result?; + } + } if let Some(parent_bank) = bank.parent() { - let allow_initial_update_parent = replay_starts_at_update_parent - && matches!( - &marker, - VersionedBlockMarker::V1(marker) - if marker.as_update_parent().is_some() - ); + let allow_initial_update_parent = + replay_starts_at_update_parent && marker.is_update_parent(); processor .on_marker( bank.clone_without_scheduler(), parent_bank, + shred_version, marker, allow_initial_update_parent, finalization_cert_sender, @@ -1886,7 +1816,7 @@ fn confirm_slot_with_components( // Skip block component validation for genesis block. Slot 0 is handled specially, // since it won't have the required block markers. if is_final && slot != 0 { - processor.on_final(migration_status, slot)?; + processor.on_final(migration_status, slot, bank.parent_slot())?; } } @@ -2104,15 +2034,14 @@ fn confirm_slot_entries( } // If bank is in vote-only mode, validate that entries contain only vote transactions - if let EntryType::Transactions(ref transactions) = entry { - if transactions + if let EntryType::Transactions(ref transactions) = entry + && transactions .iter() .any(|tx| !is_valid_vote_only_transaction(tx)) - { - return Err(BlockstoreProcessorError::UserTransactionsInVoteOnlyBank( - bank.slot(), - )); - } + { + return Err(BlockstoreProcessorError::UserTransactionsInVoteOnlyBank( + bank.slot(), + )); } Ok(ReplayEntry { entry, @@ -2153,6 +2082,7 @@ fn confirm_slot_entries( #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] fn process_bank_0( bank0: &BankWithScheduler, + shred_version: u16, blockstore: &Blockstore, replay_tx_thread_pool: &ThreadPool, opts: &ProcessOptions, @@ -2165,6 +2095,7 @@ fn process_bank_0( confirm_full_slot( blockstore, bank0, + shred_version, replay_tx_thread_pool, opts, &mut progress, @@ -2195,9 +2126,24 @@ fn process_bank_0( Ok(()) } -/// Clean up a failed slot and restart processing from the given genesis slot +fn cleanup_outdated_tower_bft_startup_banks( + root_bank: &Bank, + blockstore: &Blockstore, + slots_to_cleanup: &[(Slot, BankId)], +) { + root_bank.remove_unrooted_slots(slots_to_cleanup); + + for &(slot, _) in slots_to_cleanup { + root_bank.clear_slot_signatures(slot); + root_bank.prune_program_cache_by_deployment_slot(slot); + reset_dead_if_primary_access(blockstore, slot); + } +} + +/// Clean up failed startup slots and restart processing from the given genesis slot /// -/// `first_alpenglow_bank` is removed from runtime caches, and its dead status is reset +/// `first_alpenglow_bank` and any current `pending_slots` banks are removed from runtime caches, +/// and their dead statuses are reset. /// `pending_slots` is the current child blocks left to be processed. We clear and update /// this with the children of `genesis_slot` instead. fn cleanup_and_populate_pending_from_alpenglow_genesis( @@ -2210,24 +2156,18 @@ fn cleanup_and_populate_pending_from_alpenglow_genesis( opts: &ProcessOptions, migration_status: &MigrationStatus, ) -> result::Result<(), BlockstoreProcessorError> { - // `first_alpenglow_bank` was processed as a TowerBFT bank. Reset it. - let first_alpenglow_slot = first_alpenglow_bank.slot(); - let root_bank = bank_forks.read().unwrap().root_bank(); - root_bank - .remove_unrooted_slots(&[(first_alpenglow_bank.slot(), first_alpenglow_bank.bank_id())]); - root_bank.clear_slot_signatures(first_alpenglow_bank.slot()); - root_bank.prune_program_cache_by_deployment_slot(first_alpenglow_bank.slot()); - - if blockstore.is_dead(first_alpenglow_slot) { - if blockstore.is_primary_access() { - blockstore.remove_dead_slot(first_alpenglow_slot).unwrap(); - } else { - info!( - "First alpenglow slot {first_alpenglow_slot} won't be cleared from dead due to \ - being read-only blockstore access" + // The frontier is now out of date, as all banks were created as TowerBFT banks. + // Cleanup all the banks in the frontier and recreate them as Alpenglow banks. + let slots_to_cleanup = + std::iter::once((first_alpenglow_bank.slot(), first_alpenglow_bank.bank_id())) + .chain( + pending_slots + .iter() + .map(|(_, bank, _)| (bank.slot(), bank.bank_id())), ) - } - } + .collect::>(); + let root_bank = bank_forks.read().unwrap().root_bank(); + cleanup_outdated_tower_bft_startup_banks(&root_bank, blockstore, &slots_to_cleanup); let genesis_slot_meta = blockstore .meta(genesis_slot) @@ -2296,19 +2236,21 @@ fn process_next_slots( // Only process full slots in blockstore_processor, replay_stage // handles any partials if next_meta.is_full() { - let parent_block_id = bank.block_id(); - if migration_status.should_allow_block_markers(*next_slot) - && bank.slot() != 0 - && Some(next_meta.parent_block_id) != parent_block_id - { - warn!( - "startup replay deferring slot {next_slot}: parent {} has block id {:?}, but \ - SlotMeta expects {:?}", - bank.slot(), - parent_block_id, - next_meta.parent_block_id, - ); - continue; + if !opts.skip_inter_slot_verification { + let parent_block_id = bank.block_id(); + if migration_status.should_allow_block_markers(*next_slot) + && bank.slot() != 0 + && Some(next_meta.parent_block_id) != parent_block_id + { + warn!( + "startup replay deferring slot {next_slot}: parent {} has block id {:?}, \ + but SlotMeta expects {:?}", + bank.slot(), + parent_block_id, + next_meta.parent_block_id, + ); + continue; + } } let next_bank = Bank::new_from_parent( @@ -2361,6 +2303,7 @@ pub fn set_alpenglow_ticks(bank: &Bank, migration_status: &MigrationStatus) { #[allow(clippy::too_many_arguments)] fn load_frozen_forks( bank_forks: &RwLock, + shred_version: u16, start_slot_meta: &SlotMeta, blockstore: &Blockstore, replay_tx_thread_pool: &ThreadPool, @@ -2447,7 +2390,10 @@ fn load_frozen_forks( // Live replay restarts UpdateParent slots from the marker's FEC set. // Startup replay must use the same offset or a restarted validator can // execute the obsolete optimistic-parent prefix. - if migration_status.should_allow_fast_leader_handover(slot) && meta.has_update_parent() + if bank.feature_set.snapshot().alpenglow_fast_leader_handover + && migration_status.should_allow_block_markers(slot) + && leader_slot_index(slot) == 0 + && meta.has_update_parent() { progress.num_shreds = u64::from(meta.replay_fec_set_index); } @@ -2456,6 +2402,7 @@ fn load_frozen_forks( if let Err(error) = process_single_slot( blockstore, &bank, + shred_version, replay_tx_thread_pool, opts, &mut progress, @@ -2567,11 +2514,7 @@ fn load_frozen_forks( root = new_root_bank.slot(); leader_schedule_cache.set_root(new_root_bank); - new_root_bank.prune_program_cache( - root, - new_root_bank.epoch(), - &bank_forks.read().unwrap(), - ); + new_root_bank.prune_program_cache(&bank_forks.read().unwrap()); let _ = bank_forks .write() .unwrap() @@ -2588,16 +2531,15 @@ fn load_frozen_forks( root_retain_us += m.as_us(); // If this root bank activated the feature flag, update migration status - if migration_status.is_pre_feature_activation() { - if let Some(slot) = bank_forks + if migration_status.is_pre_feature_activation() + && let Some(slot) = bank_forks .read() .unwrap() .root_bank() .feature_set .activated_slot(&agave_feature_set::alpenglow::id()) - { - migration_status.record_feature_activation(slot); - } + { + migration_status.record_feature_activation(slot); } } @@ -2684,9 +2626,9 @@ fn supermajority_root_from_vote_accounts( /// Validates the chained block ID for a child slot against its parent. /// /// Returns: -/// - `Inactive`: feature not active, no validation performed +/// - `Inactive`: feature not active, or alpenglow is active, no validation performed /// - `Pass`: chained block ID matches parent's block ID (or parent has no -/// block ID yet), or the slot replays from an UpdateParent FEC set +/// block ID yet) /// - `Mismatch`: definitive mismatch between child's chained merkle root /// and parent's block ID /// - `Unavailable`: data shred 0 not received yet, cannot validate @@ -2695,21 +2637,12 @@ pub fn check_chained_block_id( bank: &Bank, migration_status: &MigrationStatus, ) -> ChainedBlockIdCheck { - if !bank.feature_set.snapshot().validate_chained_block_id { - return ChainedBlockIdCheck::Inactive; - } - let slot = bank.slot(); - if migration_status.should_allow_fast_leader_handover(slot) - && blockstore - .meta(slot) - .expect("Blockstore operations must succeed") - .is_some_and(|meta| meta.has_update_parent()) + let feature_snapshot = bank.feature_set.snapshot(); + if !(feature_snapshot.validate_chained_block_id || feature_snapshot.validate_chained_block_id_2) + || migration_status.should_use_double_merkle_block_id(slot) { - // This block contains an `UpdateParent` and Alpenglow is active, so we - // rely on Double Merkle verification of parent chained block ID instead - // of CMR. - return ChainedBlockIdCheck::Pass; + return ChainedBlockIdCheck::Inactive; } let parent_slot = bank.parent_slot(); @@ -2744,12 +2677,37 @@ pub fn check_chained_block_id( } } +fn mark_dead_if_primary_access(blockstore: &Blockstore, slot: Slot) { + if blockstore.is_primary_access() { + blockstore + .set_dead_slot(slot) + .expect("Failed to mark slot as dead in blockstore"); + } else { + info!("Failed slot {slot} won't be marked dead due to being read-only blockstore access"); + } +} + +fn reset_dead_if_primary_access(blockstore: &Blockstore, slot: Slot) { + if !blockstore.is_dead(slot) { + return; + } + if blockstore.is_primary_access() { + blockstore.remove_dead_slot(slot).unwrap(); + } else { + info!("slot {slot} won't be cleared from dead due to being read-only blockstore access"); + } +} + // Processes and replays the contents of a single slot, returns Error -// if failed to play the slot +// if failed to play the slot. +// +// For use during startup replay, enforces any pre and post replay checks +// that occur in ReplayStage. #[allow(clippy::too_many_arguments)] pub fn process_single_slot( blockstore: &Blockstore, bank: &BankWithScheduler, + shred_version: u16, replay_tx_thread_pool: &ThreadPool, opts: &ProcessOptions, progress: &mut ConfirmationProgress, @@ -2760,28 +2718,21 @@ pub fn process_single_slot( migration_status: &MigrationStatus, ) -> result::Result<(), BlockstoreProcessorError> { let slot = bank.slot(); - match check_chained_block_id(blockstore, bank, migration_status) { - ChainedBlockIdCheck::Inactive | ChainedBlockIdCheck::Pass => (), - ChainedBlockIdCheck::Unavailable => { - // no shreds to replay - return Ok(()); - } - ChainedBlockIdCheck::Mismatch => { - // Mismatch, mark dead - if blockstore.is_primary_access() { - blockstore - .set_dead_slot(slot) - .expect("Failed to mark slot as dead in blockstore"); - } else { - info!( - "Failed slot {slot} won't be marked dead due to being read-only blockstore \ - access" - ); + if !opts.skip_inter_slot_verification { + match check_chained_block_id(blockstore, bank, migration_status) { + ChainedBlockIdCheck::Inactive | ChainedBlockIdCheck::Pass => (), + ChainedBlockIdCheck::Unavailable => { + // no shreds to replay + return Ok(()); + } + ChainedBlockIdCheck::Mismatch => { + // Mismatch, mark dead + mark_dead_if_primary_access(blockstore, slot); + return Err(BlockstoreProcessorError::ChainedBlockIdFailure( + slot, + bank.parent_slot(), + )); } - return Err(BlockstoreProcessorError::ChainedBlockIdFailure( - slot, - bank.parent_slot(), - )); } } @@ -2790,6 +2741,7 @@ pub fn process_single_slot( confirm_full_slot( blockstore, bank, + shred_version, replay_tx_thread_pool, opts, progress, @@ -2801,15 +2753,7 @@ pub fn process_single_slot( ) .map_err(|err| { warn!("slot {slot} failed to verify: {err}"); - if blockstore.is_primary_access() { - blockstore - .set_dead_slot(slot) - .expect("Failed to mark slot as dead in blockstore"); - } else { - info!( - "Failed slot {slot} won't be marked dead due to being read-only blockstore access" - ); - } + mark_dead_if_primary_access(blockstore, slot); err })?; @@ -2818,7 +2762,20 @@ pub fn process_single_slot( .expect("Blockstore operations must succeed") .expect("Full block must have block id"); bank.set_block_id(Some(block_id)); - bank.freeze(); // all banks handled by this routine are created from complete slots + let verify_result = bank.freeze_and_verify_bank_hash(); // all banks handled by this routine are created from complete slots + + if let Err((expected_hash, computed_hash)) = verify_result { + warn!( + "slot {slot} failed to freeze, bank hash mismatch expected {expected_hash} computed \ + {computed_hash}" + ); + mark_dead_if_primary_access(blockstore, slot); + return Err(BlockstoreProcessorError::BankHashMismatch( + slot, + expected_hash, + computed_hash, + )); + } if let Some(slot_callback) = &opts.slot_callback { slot_callback(bank); @@ -2940,20 +2897,23 @@ pub mod tests { use { super::*, crate::{ - blockstore::hashes_per_tick_for_ledger, blockstore_options::{AccessType, BlockstoreOptions}, genesis_utils::{ GenesisConfigInfo, create_genesis_config, create_genesis_config_with_leader, }, shred::{ProcessShredsStats, ReedSolomonCache, Shred, Shredder}, }, - agave_votor_messages::consensus_message::{Certificate, CertificateType}, + agave_votor_messages::{ + certificate::{CertSignature, GenesisCert}, + consensus_message::Block, + }, assert_matches::assert_matches, + crossbeam_channel::bounded, rand::{Rng, rng}, rayon::ThreadPoolBuilder, solana_account::{AccountSharedData, WritableAccount}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, - solana_cost_model::transaction_cost::TransactionCost, + solana_cost_model::cost_tracker::CostTrackerLimits, solana_entry::{ block_component::{BlockComponent, BlockFooterV1, BlockHeaderV1, VersionedBlockMarker}, entry::{create_ticks, next_entry, next_entry_mut}, @@ -3000,30 +2960,34 @@ pub mod tests { }; /// Generate a dummy alpenglow genesis certificate - fn genesis_certificate(slot: Slot, block_id: Hash) -> Arc { - Arc::new(Certificate { - cert_type: CertificateType::Genesis(slot, block_id), - signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), - bitmap: vec![], + fn genesis_certificate(block: Block) -> Arc { + Arc::new(GenesisCert { + block, + signature: CertSignature { + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }, }) } /// Generate a dummy `MigrationStatus` in the `ReadyToEnable` phase - fn ready_to_enable_migration_status(genesis_slot: Slot, block_id: Hash) -> MigrationStatus { + fn ready_to_enable_migration_status(genesis_block: Block) -> MigrationStatus { let migration_status = MigrationStatus::default(); let migration_slot = migration_status.record_feature_activation(0); - assert!(genesis_slot < migration_slot); - migration_status.set_genesis_block((genesis_slot, block_id)); - migration_status.set_genesis_certificate(genesis_certificate(genesis_slot, block_id)); + assert!(genesis_block.slot < migration_slot); + migration_status.set_genesis_block(genesis_block); + migration_status.set_genesis_certificate(genesis_certificate(genesis_block)); assert!(migration_status.is_ready_to_enable()); migration_status } #[test] fn test_startup_replay_enable_waits_for_poh_service_when_started() { - let genesis_slot = 1; - let block_id = Hash::new_from_array([7; solana_hash::HASH_BYTES]); - let migration_status = Arc::new(ready_to_enable_migration_status(genesis_slot, block_id)); + let genesis_block = Block { + slot: 1, + block_id: Hash::new_from_array([7; solana_hash::HASH_BYTES]), + }; + let migration_status = Arc::new(ready_to_enable_migration_status(genesis_block)); let poh_service = { let migration_status = Arc::clone(&migration_status); migration_status.set_poh_service_started(); @@ -3037,14 +3001,14 @@ pub mod tests { assert_eq!( migration_status.enable_alpenglow_during_startup(), - genesis_slot + genesis_block.slot ); poh_service.join().unwrap(); assert!(migration_status.is_alpenglow_enabled()); assert_eq!( migration_status.wait_for_migration_or_exit(&AtomicBool::new(false)), - Some((genesis_slot, block_id)) + Some(genesis_block) ); } @@ -3072,6 +3036,7 @@ pub mod tests { process_blockstore_from_root( blockstore, &bank_forks, + compute_shred_version(&genesis_config.hash(), None), &leader_schedule_cache, opts, None, @@ -3844,7 +3809,7 @@ pub mod tests { entries.push(entry); } - let hashes_per_tick = hashes_per_tick_for_ledger(&genesis_config); + let hashes_per_tick = genesis_config.poh_config.hashes_per_tick.unwrap_or(0); let remaining_hashes = hashes_per_tick - entries.len() as u64; let tick_entry = next_entry_mut(&mut last_entry_hash, remaining_hashes, vec![]); entries.push(tick_entry); @@ -4204,7 +4169,7 @@ pub mod tests { ); let tx = Transaction::new_signed_with_payer( - &[Instruction::new_with_bincode( + &[Instruction::new_with_wincode( mock_program_id, &10, Vec::new(), @@ -4248,7 +4213,7 @@ pub mod tests { ); let tx = Transaction::new_signed_with_payer( - &[Instruction::new_with_bincode( + &[Instruction::new_with_wincode( mock_program_id, &(err as u8), Vec::new(), @@ -4720,7 +4685,7 @@ pub mod tests { // Make sure fees-only transactions still update the signature cache let missing_program_id = Pubkey::new_unique(); let tx = Transaction::new_signed_with_payer( - &[Instruction::new_with_bincode( + &[Instruction::new_with_wincode( missing_program_id, &10, Vec::new(), @@ -4878,6 +4843,7 @@ pub mod tests { let replay_tx_thread_pool = create_thread_pool(1); process_bank_0( &bank0, + compute_shred_version(&genesis_config.hash(), None), &blockstore, &replay_tx_thread_pool, &opts, @@ -4893,6 +4859,7 @@ pub mod tests { confirm_full_slot( &blockstore, &bank1, + compute_shred_version(&genesis_config.hash(), None), &replay_tx_thread_pool, &opts, &mut ConfirmationProgress::new(bank0_last_blockhash), @@ -4911,6 +4878,7 @@ pub mod tests { process_blockstore_from_root( &blockstore, &bank_forks, + compute_shred_version(&genesis_config.hash(), None), &leader_schedule_cache, &opts, None, @@ -5124,11 +5092,7 @@ pub mod tests { fn test_replay_vote_sender() { let validator_keypairs: Vec<_> = (0..10).map(|_| ValidatorVoteKeypairs::new_rand()).collect(); - let GenesisConfigInfo { - genesis_config, - voting_keypair: _, - .. - } = create_genesis_config_with_vote_accounts( + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_vote_accounts( 1_000_000_000, &validator_keypairs, vec![100; validator_keypairs.len()], @@ -5191,7 +5155,7 @@ pub mod tests { }) .collect(); let entry = next_entry(&bank_1_blockhash, 1, vote_txs); - let (replay_vote_sender, replay_vote_receiver) = crossbeam_channel::unbounded(); + let (replay_vote_sender, replay_vote_receiver) = bounded(1024); let _ = process_entries_for_tests( &BankWithScheduler::new_without_scheduler(bank1), vec![entry], @@ -5564,8 +5528,7 @@ pub mod tests { bank.transfer(LAMPORTS_PER_SOL, &mint_keypair, &keypair2.pubkey()) .unwrap(); - let (transaction_status_sender, transaction_status_receiver) = - crossbeam_channel::unbounded(); + let (transaction_status_sender, transaction_status_receiver) = bounded(1024); let transaction_status_sender = TransactionStatusSender { sender: transaction_status_sender, dependency_tracker: None, @@ -5886,7 +5849,7 @@ pub mod tests { transaction_indexes: vec![], }; let mut timing = ExecuteTimings::default(); - let (sender, receiver) = crossbeam_channel::unbounded(); + let (sender, receiver) = bounded(1024); assert_eq!(bank.transaction_count(), 0); assert_eq!(bank.transaction_error_count(), 0); @@ -6087,17 +6050,24 @@ pub mod tests { let keypair = Arc::new(Keypair::new()); let reed_solomon_cache = ReedSolomonCache::default(); - let header = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let header = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), }); let header_component = BlockComponent::new_block_marker(header); - let footer = VersionedBlockMarker::new_block_footer(BlockFooterV1 { + let block_producer_time_nanos = u64::try_from( + genesis_config + .creation_time + .saturating_mul(1_000_000_000) + .saturating_add(1), + ) + .unwrap(); + let footer = VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), - block_producer_time_nanos: 1_000_000_000, + block_producer_time_nanos, block_user_agent: b"test".to_vec(), - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -6154,7 +6124,7 @@ pub mod tests { let mut all_shreds = header_shreds; all_shreds.extend(entry_shreds); all_shreds.extend(footer_shreds); - blockstore.insert_shreds(all_shreds, None, true).unwrap(); + blockstore.insert_shreds(all_shreds, true).unwrap(); let replay_tx_thread_pool = create_thread_pool(1); @@ -6181,25 +6151,24 @@ pub mod tests { ); let bank1 = bank_forks.write().unwrap().insert(bank1); - assert!( - confirm_slot( - &blockstore, - &bank1, - &replay_tx_thread_pool, - &mut ConfirmationTiming::default(), - &mut ConfirmationProgress::new(bank0.last_blockhash()), - false, - None, - None, - None, - None, - false, - None, - None, - &MigrationStatus::default(), - ) - .is_err() - ); + confirm_slot( + &blockstore, + &bank1, + compute_shred_version(&genesis_config.hash(), None), + &replay_tx_thread_pool, + &mut ConfirmationTiming::default(), + &mut ConfirmationProgress::new(bank0.last_blockhash()), + false, + None, + None, + None, + None, + false, + None, + None, + &MigrationStatus::default(), + ) + .unwrap_err(); } #[test] @@ -6218,25 +6187,24 @@ pub mod tests { ); let bank1 = bank_forks.write().unwrap().insert(bank1); - assert!( - confirm_slot( - &blockstore, - &bank1, - &replay_tx_thread_pool, - &mut ConfirmationTiming::default(), - &mut ConfirmationProgress::new(bank0.last_blockhash()), - true, - None, - None, - None, - None, - false, - None, - None, - &MigrationStatus::post_migration_status(), - ) - .is_ok() - ); + confirm_slot( + &blockstore, + &bank1, + compute_shred_version(&genesis_config.hash(), None), + &replay_tx_thread_pool, + &mut ConfirmationTiming::default(), + &mut ConfirmationProgress::new(bank0.last_blockhash()), + true, + None, + None, + None, + None, + false, + None, + None, + &MigrationStatus::post_migration_status(), + ) + .unwrap(); } #[test] @@ -6258,9 +6226,7 @@ pub mod tests { let mut tx_cost = CostModel::calculate_cost(&tx, &bank.feature_set); let actual_execution_cu = 1; let actual_loaded_accounts_data_size = 64 * 1024; - let TransactionCost::Transaction(ref mut usage_cost_details) = tx_cost else { - unreachable!("test tx is non-vote tx"); - }; + let usage_cost_details = tx_cost.usage_cost_details_mut(); usage_cost_details.programs_execution_cost = actual_execution_cu; usage_cost_details.loaded_accounts_data_size_cost = CostModel::calculate_loaded_accounts_data_size_cost( @@ -6271,7 +6237,7 @@ pub mod tests { let block_limit = tx_cost.sum(); bank.write_cost_tracker() .unwrap() - .set_limits(u64::MAX, block_limit, u64::MAX, u64::MAX); + .set_limits(CostTrackerLimits::new(u64::MAX, block_limit, u64::MAX)); let tx_costs = vec![None, Some(tx_cost), None]; // The transaction will fit when added the first time @@ -6314,7 +6280,7 @@ pub mod tests { ) .filter(Shred::is_data) .collect(); - blockstore.insert_shreds(shreds, None, true).unwrap(); + blockstore.insert_shreds(shreds, true).unwrap(); }; // Create a genesis bank (slot 0) with all features active. @@ -6355,26 +6321,46 @@ pub mod tests { ChainedBlockIdCheck::Mismatch )); + // Case 3a: With only the replacement feature active, replay should + // still run the existing inter-slot SIMD-0340 check. + insert_shreds_with_chained_merkle_root(14, 0, Hash::new_unique()); + let mut child_bank = Bank::new_from_parent(parent_bank.clone(), SlotLeader::default(), 14); + child_bank.deactivate_feature(&agave_feature_set::validate_chained_block_id::id()); + child_bank.activate_feature(&agave_feature_set::validate_chained_block_id_2::id()); + assert!(matches!( + check_chained_block_id(&blockstore, &child_bank, &MigrationStatus::default()), + ChainedBlockIdCheck::Mismatch + )); + + // Case 3b: With both feature gates inactive, replay should not run the + // chained block ID check. + let mut child_bank = Bank::new_from_parent(parent_bank.clone(), SlotLeader::default(), 14); + child_bank.deactivate_feature(&agave_feature_set::validate_chained_block_id::id()); + child_bank.deactivate_feature(&agave_feature_set::validate_chained_block_id_2::id()); + assert!(matches!( + check_chained_block_id(&blockstore, &child_bank, &MigrationStatus::default()), + ChainedBlockIdCheck::Inactive + )); + // Case 4: UpdateParent metadata does not bypass Tower validation. - insert_shreds_with_chained_merkle_root(13, 0, Hash::new_unique()); - let mut meta = blockstore.meta(13).unwrap().unwrap(); + insert_shreds_with_chained_merkle_root(16, 0, Hash::new_unique()); + let mut meta = blockstore.meta(16).unwrap().unwrap(); meta.replay_fec_set_index = 32; - blockstore.put_meta(13, &meta).unwrap(); - let child_bank = Bank::new_from_parent(parent_bank.clone(), SlotLeader::default(), 13); + blockstore.put_meta(16, &meta).unwrap(); + let child_bank = Bank::new_from_parent(parent_bank.clone(), SlotLeader::default(), 16); assert!(matches!( check_chained_block_id(&blockstore, &child_bank, &MigrationStatus::default()), ChainedBlockIdCheck::Mismatch )); - // Case 5: Alpenglow UpdateParent slots skip shred-0 chained block ID - // validation because replay starts at the UpdateParent FEC set. + // Case 5: When alpenglow is active, SIMD-0340 is skipped assert!(matches!( check_chained_block_id( &blockstore, &child_bank, &MigrationStatus::post_migration_status() ), - ChainedBlockIdCheck::Pass + ChainedBlockIdCheck::Inactive )); // Case 6: Parent has no shreds (get_block_merkle_root returns Err) — @@ -6392,6 +6378,86 @@ pub mod tests { )); } + #[test] + fn test_cleanup_alpenglow_genesis_cleans_pending_slots() { + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Blockstore::open(ledger_path.path()).unwrap(); + + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(10_000); + let bank_forks = BankForks::new_rw_arc(Bank::new_for_tests(&genesis_config)); + let bank0 = bank_forks.read().unwrap().get(0).unwrap(); + let leader_schedule_cache = LeaderScheduleCache::new_from_bank(&bank0); + + let mut genesis_meta = SlotMeta::new(0, None); + genesis_meta.next_slots = vec![1, 2]; + blockstore.put_meta(0, &genesis_meta).unwrap(); + + for slot in [1, 2] { + let mut meta = SlotMeta::new(slot, Some(0)); + meta.consumed = 1; + meta.received = 1; + meta.last_index = Some(0); + blockstore.put_meta(slot, &meta).unwrap(); + blockstore.set_dead_slot(slot).unwrap(); + } + + let owner = Pubkey::new_unique(); + let first_alpenglow_key = Pubkey::new_unique(); + let pending_key = Pubkey::new_unique(); + + let first_alpenglow_bank = Arc::new(Bank::new_from_parent( + bank0.clone(), + SlotLeader::default(), + 1, + )); + first_alpenglow_bank + .store_account(&first_alpenglow_key, &AccountSharedData::new(1, 0, &owner)); + assert!( + first_alpenglow_bank + .get_account(&first_alpenglow_key) + .is_some() + ); + let first_alpenglow_bank = + BankWithScheduler::new_without_scheduler(first_alpenglow_bank.clone()); + + let pending_bank = Bank::new_from_parent(bank0.clone(), SlotLeader::default(), 2); + pending_bank.store_account(&pending_key, &AccountSharedData::new(1, 0, &owner)); + assert!(pending_bank.get_account(&pending_key).is_some()); + + let pending_meta = blockstore.meta(2).unwrap().unwrap(); + let mut pending_slots = vec![(pending_meta, pending_bank, bank0.last_blockhash())]; + + cleanup_and_populate_pending_from_alpenglow_genesis( + &first_alpenglow_bank, + 0, + &bank_forks, + &blockstore, + &leader_schedule_cache, + &mut pending_slots, + &ProcessOptions::default(), + &MigrationStatus::post_migration_status(), + ) + .unwrap(); + + assert!(!blockstore.is_dead(1)); + assert!(!blockstore.is_dead(2)); + assert!( + first_alpenglow_bank + .get_account(&first_alpenglow_key) + .is_none() + ); + + let queued_slots: BTreeSet<_> = pending_slots + .iter() + .map(|(_, bank, _)| bank.slot()) + .collect(); + assert_eq!(queued_slots, BTreeSet::from([1, 2])); + for (_, bank, _) in &pending_slots { + assert!(bank.get_account(&first_alpenglow_key).is_none()); + assert!(bank.get_account(&pending_key).is_none()); + } + } + #[test] fn test_startup_parent_id_check() { let ledger_path = get_tmp_ledger_path_auto_delete!(); @@ -6432,5 +6498,28 @@ pub mod tests { assert_eq!(pending_slots.len(), 1); assert_eq!(pending_slots[0].1.slot(), 3); + + let mut pending_slots = Vec::new(); + process_next_slots( + &parent_bank, + &parent_meta, + &blockstore, + &leader_schedule_cache, + &mut pending_slots, + &ProcessOptions { + skip_inter_slot_verification: true, + ..ProcessOptions::default() + }, + &MigrationStatus::post_migration_status(), + ) + .unwrap(); + + assert_eq!( + pending_slots + .iter() + .map(|(_, bank, _)| bank.slot()) + .collect::>(), + BTreeSet::from([2, 3]) + ); } } diff --git a/ledger/src/leader_schedule_cache.rs b/ledger/src/leader_schedule_cache.rs index a33ea4e6661..e2193b95b49 100644 --- a/ledger/src/leader_schedule_cache.rs +++ b/ledger/src/leader_schedule_cache.rs @@ -243,13 +243,13 @@ mod tests { }, staking_utils::tests::setup_vote_and_stake_accounts, }, - crossbeam_channel::unbounded, - solana_clock::{DEFAULT_SLOTS_PER_EPOCH, NUM_CONSECUTIVE_LEADER_SLOTS}, + crossbeam_channel::bounded, + solana_clock::DEFAULT_SLOTS_PER_EPOCH, solana_epoch_schedule::{ DEFAULT_LEADER_SCHEDULE_SLOT_OFFSET, EpochSchedule, MINIMUM_SLOTS_PER_EPOCH, }, solana_keypair::Keypair, - solana_leader_schedule::{LeaderSchedule, SlotLeader}, + solana_leader_schedule::{LeaderSchedule, NUM_CONSECUTIVE_LEADER_SLOTS, SlotLeader}, solana_runtime::stake_utils, solana_signer::Signer, std::{sync::Arc, thread::Builder}, @@ -337,7 +337,7 @@ mod tests { .map(|_| { let cache = cache.clone(); let bank = bank.clone(); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); ( Builder::new() .name("test_thread_race_leader_schedule_cache".to_string()) @@ -457,7 +457,7 @@ mod tests { // Write a shred into slot 2 that chains to slot 1, // but slot 1 is empty so should not be skipped let (shreds, _) = make_slot_entries(2, 1, 1); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert_eq!( cache .next_leader_slot(&pubkey, 0, &bank, Some(&blockstore), u64::MAX) @@ -470,7 +470,7 @@ mod tests { let (shreds, _) = make_slot_entries(1, 0, 1); // Check that slot 1 and 2 are skipped - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); assert_eq!( cache .next_leader_slot(&pubkey, 0, &bank, Some(&blockstore), u64::MAX) @@ -571,7 +571,7 @@ mod tests { .unwrap(); assert_eq!(res.0, expected_slot); - assert!(res.1 >= expected_slot + NUM_CONSECUTIVE_LEADER_SLOTS - 1); + assert!(res.1 >= expected_slot + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot - 1); let res = cache .next_leader_slot( @@ -579,12 +579,15 @@ mod tests { 0, &bank, None, - NUM_CONSECUTIVE_LEADER_SLOTS - 1, + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot - 1, ) .unwrap(); assert_eq!(res.0, expected_slot); - assert_eq!(res.1, expected_slot + NUM_CONSECUTIVE_LEADER_SLOTS - 2); + assert_eq!( + res.1, + expected_slot + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot - 2 + ); } #[test] diff --git a/ledger/src/shred.rs b/ledger/src/shred.rs index 1b30cf25c67..3f3b0b29f72 100644 --- a/ledger/src/shred.rs +++ b/ledger/src/shred.rs @@ -352,6 +352,18 @@ impl ErasureSetId { self.1 } + pub(crate) fn previous_fec_set(&self) -> Option { + self.1 + .checked_sub(DATA_SHREDS_PER_FEC_BLOCK as u32) + .map(|fec_set_index| Self::new(self.0, fec_set_index)) + } + + pub(crate) fn next_fec_set(&self) -> Option { + self.1 + .checked_add(DATA_SHREDS_PER_FEC_BLOCK as u32) + .map(|fec_set_index| Self::new(self.0, fec_set_index)) + } + // Storage key for ErasureMeta and MerkleRootMeta in blockstore db. // Note: ErasureMeta column uses u64 so this will need to be typecast pub(crate) fn store_key(&self) -> (Slot, /*fec_set_index:*/ u32) { diff --git a/ledger/src/shred/filter.rs b/ledger/src/shred/filter.rs index 1ec10a07cf9..41d8993a091 100644 --- a/ledger/src/shred/filter.rs +++ b/ledger/src/shred/filter.rs @@ -3,9 +3,8 @@ use { super::{ - DATA_SHREDS_PER_FEC_BLOCK, Error, MAX_CODE_SHREDS_PER_SLOT, MAX_DATA_SHREDS_PER_SLOT, - Payload, ReedSolomonCache, Shred, ShredFetchStats, ShredFlags, ShredType, ShredVariant, - layout, merkle, + DATA_SHREDS_PER_FEC_BLOCK, Error, Payload, ReedSolomonCache, Shred, ShredFetchStats, + ShredFlags, ShredType, ShredVariant, layout, merkle, }, crate::blockstore, agave_feature_set::discard_unexpected_data_complete_shreds, @@ -25,6 +24,44 @@ use { }, }; +/// Per-slot shred index limits used by bank-aware shred filtering. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct ShredLimits { + /// Exclusive upper bound for data shred indexes in a slot. + max_data_shreds_per_slot: u32, + /// Exclusive upper bound for code shred indexes in a slot. + max_code_shreds_per_slot: u32, +} + +impl ShredLimits { + /// Slot-independent maximum cap used by tests. + #[cfg(test)] + const DEFAULT: Self = Self { + max_data_shreds_per_slot: super::MAX_DATA_SHREDS_PER_SLOT as u32, + max_code_shreds_per_slot: super::MAX_CODE_SHREDS_PER_SLOT as u32, + }; + + /// Creates shred limits with exclusive upper bounds for data and code indexes. + const fn new(max_data_shreds_per_slot: u32, max_code_shreds_per_slot: u32) -> Self { + Self { + max_data_shreds_per_slot, + max_code_shreds_per_slot, + } + } + + /// Returns true if `index` is below the data shred limit. + #[inline] + const fn is_data_index_in_bounds(self, index: u32) -> bool { + index < self.max_data_shreds_per_slot + } + + /// Returns true if `index` is below the code shred limit. + #[inline] + const fn is_code_index_in_bounds(self, index: u32) -> bool { + index < self.max_code_shreds_per_slot + } +} + /// Controls turbine and repair behavior for testing network partitions. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[repr(u8)] @@ -90,6 +127,36 @@ impl Default for TurbineMode { } } +/// Bank-backed shred limit lookup shared by fetch and recovery paths. +struct ShredLimitContext { + root_bank: Arc, + #[cfg(test)] + shred_limits_override: Option, +} + +impl ShredLimitContext { + fn new(root_bank: Arc) -> Self { + Self { + root_bank, + #[cfg(test)] + shred_limits_override: None, + } + } + + /// Returns the shred limits for `slot` as derived from the current root bank. + fn shred_limits(&self, slot: Slot) -> ShredLimits { + #[cfg(test)] + if let Some(shred_limits) = self.shred_limits_override { + return shred_limits; + } + + ShredLimits::new( + self.root_bank.max_data_shreds_per_slot_for_slot(slot), + self.root_bank.max_code_shreds_per_slot_for_slot(slot), + ) + } +} + pub struct ShredFilterContext { last_updated: Instant, @@ -111,60 +178,20 @@ pub struct ShredFilterContext { // but specify DATA_COMPLETE discard_unexpected_data_complete_shreds_feature_slot: Option, - // The shred limits per slot. At the moment these are hard coded - // In the future we could replace these with feature activation - // slots to case on what the limits should be - max_data_shreds_per_slot: u32, - max_code_shreds_per_slot: u32, + // The shred limits per slot, derived from the root bank for each shred slot. + shred_limit_context: ShredLimitContext, pub stats: ShredFetchStats, } impl ShredFilterContext { pub fn new(root_bank: Arc, shred_version: u16) -> Self { - Self::new_with_custom_shred_limits_and_turbine_mode( - root_bank, - shred_version, - MAX_DATA_SHREDS_PER_SLOT as u32, - MAX_CODE_SHREDS_PER_SLOT as u32, - None, - ) + Self::new_with_turbine_mode(root_bank, shred_version, None) } pub fn new_with_turbine_mode( root_bank: Arc, shred_version: u16, - turbine_mode: TurbineMode, - ) -> Self { - Self::new_with_custom_shred_limits_and_turbine_mode( - root_bank, - shred_version, - MAX_DATA_SHREDS_PER_SLOT as u32, - MAX_CODE_SHREDS_PER_SLOT as u32, - Some(turbine_mode), - ) - } - - pub fn new_with_custom_shred_limits( - root_bank: Arc, - shred_version: u16, - max_data_shreds_per_slot: u32, - max_code_shreds_per_slot: u32, - ) -> Self { - Self::new_with_custom_shred_limits_and_turbine_mode( - root_bank, - shred_version, - max_data_shreds_per_slot, - max_code_shreds_per_slot, - None, - ) - } - - fn new_with_custom_shred_limits_and_turbine_mode( - root_bank: Arc, - shred_version: u16, - max_data_shreds_per_slot: u32, - max_code_shreds_per_slot: u32, turbine_mode: Option, ) -> Self { let root = root_bank.slot(); @@ -186,8 +213,7 @@ impl ShredFilterContext { turbine_mode, cached_turbine_mode, discard_unexpected_data_complete_shreds_feature_slot, - max_data_shreds_per_slot, - max_code_shreds_per_slot, + shred_limit_context: ShredLimitContext::new(root_bank), stats: ShredFetchStats::default(), } } @@ -211,12 +237,19 @@ impl ShredFilterContext { self.discard_unexpected_data_complete_shreds_feature_slot = root_bank .feature_set .activated_slot(&discard_unexpected_data_complete_shreds::id()); - - // In the future as shred limit changes we can update self.max_*_shreds_per_slot based on root - // bank as well + self.shred_limit_context = ShredLimitContext::new(root_bank); } } + fn shred_limits(&self, slot: Slot) -> ShredLimits { + self.shred_limit_context.shred_limits(slot) + } + + #[cfg(test)] + fn set_shred_limits_for_tests(&mut self, shred_limits: ShredLimits) { + self.shred_limit_context.shred_limits_override = Some(shred_limits); + } + pub fn maybe_submit_stats(&mut self, metric_name: &'static str, cadence: Duration) -> bool { self.stats.maybe_submit(metric_name, cadence) } @@ -279,10 +312,11 @@ impl ShredFilterContext { self.stats.fec_set_index_bad_deserialize += 1; return true; }; + let shred_limits = self.shred_limits(slot); match ShredType::from(shred_variant) { ShredType::Code => { - if index >= self.max_code_shreds_per_slot { + if !shred_limits.is_code_index_in_bounds(index) { self.stats.index_out_of_bounds += 1; return true; } @@ -302,7 +336,7 @@ impl ShredFilterContext { } } ShredType::Data => { - if index >= self.max_data_shreds_per_slot { + if !shred_limits.is_data_index_in_bounds(index) { self.stats.index_out_of_bounds += 1; return true; } @@ -409,8 +443,8 @@ fn max_shred_slot(root: Slot, slots_per_epoch: Slot) -> Slot { // while being small enough to keep the overhead small on deduper, blockstore, // etc. const MAX_SHRED_DISTANCE_MINIMUM: Slot = 500; - // Allow shreds up to 2 epochs into the future to support catching up to the tip of the cluster. - root.saturating_add(MAX_SHRED_DISTANCE_MINIMUM.max(2 * slots_per_epoch)) + // Allow shreds up to half an epoch into the future to support catching up to the tip of the cluster. + root.saturating_add(MAX_SHRED_DISTANCE_MINIMUM.max(slots_per_epoch / 2)) } /// Returns true if `index` and `fec_set_index` are valid under the assumption that @@ -484,6 +518,7 @@ impl ShredRecoveryContext { ) -> Result<(), Error> { let shreds = shreds .into_iter() + .filter(|shred| !self.shred_filter_ctx.should_discard_shred(shred.payload())) .map(|shred| { debug_assert_matches!( shred.common_header().shred_variant, @@ -552,12 +587,18 @@ mod tests { super::{Shred, make_merkle_shreds_for_tests}, *, }, - crate::{genesis_utils::create_genesis_config, shred::tests::*}, + crate::{ + genesis_utils::create_genesis_config, + shred::{MAX_CODE_SHREDS_PER_SLOT, tests::*}, + }, assert_matches::assert_matches, itertools::Itertools, solana_leader_schedule::SlotLeader, solana_perf::packet::{Packet, PacketFlags}, - solana_runtime::bank::Bank, + solana_runtime::{ + bank::Bank, + slot_params::{slot_time_feature_gates, slot_time_feature_ids}, + }, std::{ io::{Cursor, Seek, SeekFrom, Write}, sync::Arc, @@ -582,6 +623,26 @@ mod tests { } } + fn deactivate_slot_time_features(bank: &mut Bank) { + for feature_id in slot_time_feature_ids() { + bank.deactivate_feature(&feature_id); + } + } + + fn shred_filter_for_tests( + feature_ids: impl IntoIterator, + ) -> (ShredFilterContext, Slot) { + let genesis_config = create_genesis_config(1).genesis_config; + let mut root_bank = Bank::new_for_tests(&genesis_config); + deactivate_slot_time_features(&mut root_bank); + for feature_id in feature_ids { + root_bank.activate_feature(&feature_id); + } + let first_epoch_slot = root_bank.epoch_schedule().get_first_slot_in_epoch(1); + let (root_bank, _) = root_bank.wrap_with_bank_forks_for_tests(); + (ShredFilterContext::new(root_bank, 0), first_epoch_slot) + } + #[test_case(true ; "last_in_slot")] #[test_case(false ; "not_last_in_slot")] fn test_should_discard_shred(is_last_in_slot: bool) { @@ -736,6 +797,61 @@ mod tests { } } + #[test] + fn test_recovery_shred_limits() { + agave_logger::setup(); + let mut rng = rand::rng(); + let slot = 200; + let shreds = make_merkle_shreds_for_tests( + &mut rng, + slot, + 1200 * 5, // data_size + false, // is_last_in_slot + ) + .unwrap(); + let shreds: Vec<_> = shreds.into_iter().map(Shred::from).collect(); + let coding_shreds: Vec<_> = shreds + .into_iter() + .filter(|shred| shred.shred_type() == ShredType::Code) + .collect(); + let shred_version = coding_shreds[0].common_header().version; + + // Feed recovery only coding shreds. Without the custom limit below, this + // is enough parity to recover the missing data shreds. + let max_code_shreds_per_slot = coding_shreds[0].index(); + let (dummy_retransmit_sender, _) = EvictingSender::new_bounded(0); + let mut shred_recovery_context = ShredRecoveryContext::new( + ReedSolomonCache::default(), + dummy_retransmit_sender, + new_test_bank(0, false), + shred_version, + ); + shred_recovery_context + .shred_filter_ctx + .set_shred_limits_for_tests(ShredLimits::new( + ShredLimits::DEFAULT.max_data_shreds_per_slot, + // Setting the limit to the first coding index makes every + // coding shred in this batch invalid. + max_code_shreds_per_slot, + )); + let mut recovered_shreds = Vec::new(); + let mut recovered_data_shreds = Vec::new(); + + // Recovery sees insufficient parity shreds and cannot recover the FEC. + assert_matches!( + shred_recovery_context.recover( + coding_shreds, + &mut recovered_shreds, + &mut recovered_data_shreds, + ), + Err(Error::Erasure( + reed_solomon_erasure::Error::TooFewParityShards + )) + ); + assert!(recovered_shreds.is_empty()); + assert!(recovered_data_shreds.is_empty()); + } + #[test] fn test_should_discard_packet_with_turbine_mode() { agave_logger::setup(); @@ -757,7 +873,7 @@ mod tests { let mut shred_filter_context = ShredFilterContext::new_with_turbine_mode( root_bank.clone(), shred_version, - turbine_mode.clone(), + Some(turbine_mode.clone()), ); assert!(shred_filter_context.should_discard_packet(&packet)); @@ -901,34 +1017,86 @@ mod tests { #[test] fn test_should_discard_shred_with_custom_shred_limits() { agave_logger::setup(); - let mut rng = rand::rng(); + let slot = 200; + let shreds = make_merkle_shreds_for_tests( + &mut rng, + slot, + 1200 * 5, // data_size + false, // is_last_in_slot + ) + .unwrap(); + let shreds: Vec<_> = shreds.into_iter().map(Shred::from).collect(); + let shred_version = shreds[0].common_header().version; let root_bank = new_test_bank(0, false); - let slot = 42; - let shreds = make_merkle_shreds_for_tests(&mut rng, slot, 10_000, false).unwrap(); - let shred = Shred::from(shreds[0].clone()); - let index = shred.common_header().index; - let shred_version = shred.common_header().version; - let mut packet = Packet::default(); - shred.copy_to_packet(&mut packet); + for shred_type in [ShredType::Data, ShredType::Code] { + let shred = shreds + .iter() + .find(|shred| shred.shred_type() == shred_type) + .unwrap(); + let index = shred.index(); + let mut shred_filter_context = + ShredFilterContext::new(root_bank.clone(), shred_version); + match shred_type { + ShredType::Data => shred_filter_context.set_shred_limits_for_tests( + ShredLimits::new(index + 1, ShredLimits::DEFAULT.max_code_shreds_per_slot), + ), + ShredType::Code => shred_filter_context.set_shred_limits_for_tests( + ShredLimits::new(ShredLimits::DEFAULT.max_data_shreds_per_slot, index + 1), + ), + } + assert!( + !shred_filter_context.should_discard_shred(shred.payload()), + "{shred_type:?} shred should be within the custom limit" + ); - let mut shred_filter_context = ShredFilterContext::new_with_custom_shred_limits( - root_bank.clone(), - shred_version, - index + 1, - index + 1, - ); - assert!(!shred_filter_context.should_discard_packet(&packet)); + let mut shred_filter_context = + ShredFilterContext::new(root_bank.clone(), shred_version); + match shred_type { + ShredType::Data => shred_filter_context.set_shred_limits_for_tests( + ShredLimits::new(index, ShredLimits::DEFAULT.max_code_shreds_per_slot), + ), + ShredType::Code => shred_filter_context.set_shred_limits_for_tests( + ShredLimits::new(ShredLimits::DEFAULT.max_data_shreds_per_slot, index), + ), + } + assert!( + shred_filter_context.should_discard_shred(shred.payload()), + "{shred_type:?} shred should be discarded at the custom limit" + ); + assert_eq!(shred_filter_context.stats.index_out_of_bounds, 1); + } + } - let mut shred_filter_context = ShredFilterContext::new_with_custom_shred_limits( - root_bank, - shred_version, - index, - index, + #[test] + fn test_shred_limit_for_slot_times() { + for (features, shred_limits) in std::iter::once((vec![], ShredLimits::DEFAULT)).chain( + slot_time_feature_gates().map(|(feature_id, params)| { + ( + vec![feature_id], + ShredLimits::new( + params.max_data_shreds_per_slot(), + params.max_code_shreds_per_slot(), + ), + ) + }), + ) { + let (shred_filter, effective_slot) = shred_filter_for_tests(features); + assert_eq!( + shred_filter.shred_limits(effective_slot.saturating_sub(1)), + ShredLimits::DEFAULT + ); + assert_eq!(shred_filter.shred_limits(effective_slot), shred_limits); + } + + let [reduce_to_350ms, _, _, reduce_to_200ms] = slot_time_feature_ids(); + let (shred_filter, effective_slot) = + shred_filter_for_tests([reduce_to_350ms, reduce_to_200ms]); + assert_eq!( + shred_filter.shred_limits(effective_slot), + ShredLimits::new(16_384, 16_384) ); - assert!(shred_filter_context.should_discard_packet(&packet)); - assert_eq!(shred_filter_context.stats.index_out_of_bounds, 1); } #[test] diff --git a/ledger/src/shredder.rs b/ledger/src/shredder.rs index bc50c54b6e6..97644cd77b8 100644 --- a/ledger/src/shredder.rs +++ b/ledger/src/shredder.rs @@ -226,10 +226,10 @@ impl Shredder { shred::layout::get_index(shred) .ok_or_else(|| Error::InvalidPayloadSize(shred.len()))?, ); - if let Some(prev) = prev { - if prev.checked_add(1) != index { - return Err(Error::from(TooFewDataShards)); - } + if let Some(prev) = prev + && prev.checked_add(1) != index + { + return Err(Error::from(TooFewDataShards)); } data.extend_from_slice(shred::layout::get_data(shred)?); let flags = shred::layout::get_flags(shred)?; diff --git a/ledger/src/slot_stats.rs b/ledger/src/slot_stats.rs index c8e375173e5..672931d432b 100644 --- a/ledger/src/slot_stats.rs +++ b/ledger/src/slot_stats.rs @@ -159,17 +159,17 @@ impl SlotsStats { } } let mut slot_full_reporting_info = None; - if let Some(meta) = slot_meta { - if meta.is_full() { - location_stats.last_index = meta.last_index.unwrap(); - if !location_stats.is_full { - location_stats.is_full = true; - slot_full_reporting_info = Some(( - location, - location_stats.num_repaired, - location_stats.num_recovered, - )); - } + if let Some(meta) = slot_meta + && meta.is_full() + { + location_stats.last_index = meta.last_index.unwrap(); + if !location_stats.is_full { + location_stats.is_full = true; + slot_full_reporting_info = Some(( + location, + location_stats.num_repaired, + location_stats.num_recovered, + )); } } (slot_full_reporting_info, evicted) diff --git a/ledger/src/staking_utils.rs b/ledger/src/staking_utils.rs index 07657ab383a..ef1295a26e4 100644 --- a/ledger/src/staking_utils.rs +++ b/ledger/src/staking_utils.rs @@ -108,7 +108,7 @@ pub(crate) mod tests { let account = AccountSharedData::create_from_existing_shared_data( 1, - Arc::new(bincode::serialize(&stake_account).unwrap()), + Arc::new(wincode::serialize(&stake_account).unwrap()), stake_program::id(), false, u64::MAX, diff --git a/ledger/src/transaction_address_lookup_table_scanner.rs b/ledger/src/transaction_address_lookup_table_scanner.rs index 8f6a626ac05..da2af1df1e0 100644 --- a/ledger/src/transaction_address_lookup_table_scanner.rs +++ b/ledger/src/transaction_address_lookup_table_scanner.rs @@ -1,12 +1,12 @@ use { agave_reserved_account_keys::ReservedAccountKeys, - bincode::deserialize, solana_address_lookup_table_interface::{ self as address_lookup_table, instruction::ProgramInstruction, }, solana_pubkey::Pubkey, solana_transaction::versioned::sanitized::SanitizedVersionedTransaction, std::collections::HashSet, + wincode::deserialize, }; static RESERVED_IDS_SET: std::sync::LazyLock> = diff --git a/ledger/tests/blockstore.rs b/ledger/tests/blockstore.rs deleted file mode 100644 index c6fba680839..00000000000 --- a/ledger/tests/blockstore.rs +++ /dev/null @@ -1,50 +0,0 @@ -use { - solana_entry::entry, - solana_hash::Hash, - solana_ledger::{ - blockstore::{self, Blockstore, PurgeType}, - get_tmp_ledger_path_auto_delete, - }, - std::{sync::Arc, thread::Builder}, -}; - -#[test] -fn test_multiple_threads_insert_shred() { - let ledger_path = get_tmp_ledger_path_auto_delete!(); - let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); - - for _ in 0..100 { - let num_threads = 10; - - // Create `num_threads` different ticks in slots 1..num_threads + 1, all - // with parent = slot 0 - let threads: Vec<_> = (0..num_threads) - .map(|i| { - let entries = entry::create_ticks(1, 0, Hash::default()); - let shreds = blockstore::entries_to_test_shreds(&entries, i + 1, 0, false, 0); - let blockstore_ = blockstore.clone(); - Builder::new() - .name("blockstore-writer".to_string()) - .spawn(move || { - blockstore_.insert_shreds(shreds, None, false).unwrap(); - }) - .unwrap() - }) - .collect(); - - for t in threads { - t.join().unwrap() - } - - // Check slot 0 has the correct children - let mut meta0 = blockstore.meta(0).unwrap().unwrap(); - meta0.next_slots.sort_unstable(); - let expected_next_slots: Vec<_> = (1..num_threads + 1).collect(); - assert_eq!(meta0.next_slots, expected_next_slots); - - // Delete slots for next iteration - blockstore - .purge_slots(0, num_threads + 1, PurgeType::Exact) - .unwrap(); - } -} diff --git a/ledger/tests/shred.rs b/ledger/tests/shred.rs index 9f795b06752..010fc2f8815 100644 --- a/ledger/tests/shred.rs +++ b/ledger/tests/shred.rs @@ -5,6 +5,7 @@ use { solana_epoch_schedule::{EpochSchedule, MINIMUM_SLOTS_PER_EPOCH}, solana_hash::Hash, solana_keypair::Keypair, + solana_leader_schedule::SlotLeader, solana_ledger::{ genesis_utils::create_genesis_config, shred::{ @@ -32,7 +33,16 @@ fn new_shred_recovery_context(shreds: &[Shred]) -> ShredRecoveryContext { let shred_slot = shreds.first().map(Shred::slot).unwrap_or_default(); let slots_per_epoch = shred_slot.max(MINIMUM_SLOTS_PER_EPOCH); genesis_config.epoch_schedule = EpochSchedule::custom(slots_per_epoch, slots_per_epoch, false); - let root_bank = Arc::new(Bank::new_for_tests(&genesis_config)); + let (genesis_bank, _bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + // Set the root bank close to the shred slot to avoid the recovery filter + // from discarding the shreds for being too far in the future. + let warp_slot = shred_slot.saturating_sub(10).max(1); + let root_bank = Arc::new(Bank::warp_from_parent( + genesis_bank, + SlotLeader::default(), + warp_slot, + )); let (dummy_retransmit_sender, _) = EvictingSender::new_bounded(0); ShredRecoveryContext::new( ReedSolomonCache::default(), @@ -46,7 +56,7 @@ fn new_shred_recovery_context(shreds: &[Shred]) -> ShredRecoveryContext { #[test_case(true)] fn test_multi_fec_block_coding(is_last_in_slot: bool) { let keypair = Arc::new(Keypair::new()); - let slot = 0x1234_5678_9abc_def0; + let slot = 10000; let shredder = Shredder::new(slot, slot - 5, 0, 0).unwrap(); let num_fec_sets = 100; let num_data_shreds = DATA_SHREDS_PER_FEC_BLOCK * num_fec_sets; @@ -145,7 +155,7 @@ fn test_multi_fec_block_coding(is_last_in_slot: bool) { #[test] fn test_multi_fec_block_different_size_coding() { - let slot = 0x1234_5678_9abc_def0; + let slot = 10000; let parent_slot = slot - 5; let keypair = Arc::new(Keypair::new()); let (fec_data, fec_coding, num_shreds_per_iter) = diff --git a/local-cluster/Cargo.toml b/local-cluster/Cargo.toml index d8d5f5a0a36..50139d429ae 100644 --- a/local-cluster/Cargo.toml +++ b/local-cluster/Cargo.toml @@ -11,13 +11,15 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] dev-context-only-utils = [ "solana-core/dev-context-only-utils", - "agave-feature-set/dev-context-only-utils", "agave-votor-messages/dev-context-only-utils", + "solana-tpu-client-next/dev-context-only-utils", ] [dependencies] @@ -53,6 +55,8 @@ solana-ledger = { workspace = true } solana-message = { workspace = true } solana-native-token = { workspace = true } solana-net-utils = { workspace = true } +solana-packet = { workspace = true } +solana-perf = { workspace = true } solana-poh-config = { workspace = true } solana-program-binaries = { workspace = true } solana-pubkey = { workspace = true } @@ -72,6 +76,7 @@ solana-system-interface = { workspace = true } solana-system-transaction = { workspace = true } solana-time-utils = { workspace = true } solana-tpu-client = { workspace = true } +solana-tpu-client-next = { workspace = true } solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } solana-turbine = { workspace = true } @@ -82,7 +87,9 @@ solana-vote-program = { workspace = true } static_assertions = { workspace = true } strum = { workspace = true, features = ["derive"] } tempfile = { workspace = true } +tokio = { workspace = true, features = ["full"] } tokio-util = { workspace = true } +wincode = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } @@ -95,3 +102,6 @@ solana-genesis-utils = { path = "../genesis-utils", features = ["agave-unstable- solana-ledger = { path = "../ledger", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-local-cluster = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/local-cluster/src/cluster_tests.rs b/local-cluster/src/cluster_tests.rs index 286e5357408..e9954760e92 100644 --- a/local-cluster/src/cluster_tests.rs +++ b/local-cluster/src/cluster_tests.rs @@ -5,12 +5,19 @@ use log::*; use { crate::{cluster::QuicTpuClient, local_cluster::LocalCluster}, - agave_votor_messages::consensus_message::ConsensusMessage, + agave_votor_messages::{ + consensus_message::VoteMessage, unverified_vote_message::DecodedWireConsensusMessage, + wire::VersionedWireConsensusMessage, + }, + crossbeam_channel::bounded, rand::{Rng, rng}, rayon::{ThreadPool, prelude::*}, solana_client::connection_cache::ConnectionCache, solana_clock::{self as clock, Slot}, solana_commitment_config::CommitmentConfig, + solana_core::consensus::tower_storage::{ + FileTowerStorage, SavedTower, SavedTowerVersions, TowerStorage, + }, solana_entry::entry::{self, Entry, EntrySlice}, solana_epoch_schedule::MINIMUM_SLOTS_PER_EPOCH, solana_gossip::{ @@ -25,7 +32,8 @@ use { solana_hash::Hash, solana_keypair::Keypair, solana_ledger::blockstore::Blockstore, - solana_net_utils::SocketAddrSpace, + solana_net_utils::{SocketAddrSpace, sockets::bind_to_localhost_unique}, + solana_perf::packet::{PacketRef, packet_config}, solana_poh_config::PohConfig, solana_pubkey::Pubkey, solana_rpc_client::rpc_client::RpcClient, @@ -38,6 +46,10 @@ use { solana_system_transaction as system_transaction, solana_time_utils::timestamp, solana_tpu_client::tpu_client::{TpuClient, TpuClientConfig, TpuSenderError}, + solana_tpu_client_next::{ + client_builder::{ClientBuilder, TransactionSender}, + leader_updater::create_pinned_leader_updater, + }, solana_transaction::Transaction, solana_transaction_error::TransportError, solana_validator_exit::Exit, @@ -46,7 +58,7 @@ use { std::{ collections::{HashMap, HashSet, VecDeque}, net::{SocketAddr, TcpListener, UdpSocket}, - path::Path, + path::{Path, PathBuf}, sync::{ Arc, RwLock, atomic::{AtomicBool, Ordering}, @@ -55,14 +67,139 @@ use { time::{Duration, Instant}, }, tokio_util::sync::CancellationToken, + wincode, }; -#[cfg(feature = "dev-context-only-utils")] -use { - solana_core::consensus::tower_storage::{ - FileTowerStorage, SavedTower, SavedTowerVersions, TowerStorage, - }, - std::path::PathBuf, -}; + +/// Packages a multi-threaded tokio runtime with a tpu-client-next sender, providing +/// a synchronous interface for sending transactions in local-cluster tests. +pub struct TpuSender { + runtime: Arc, +} + +impl TpuSender { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + runtime: Arc::new( + tokio::runtime::Builder::new_multi_thread() + .worker_threads(4) + .enable_all() + .build() + .expect("TpuSender: should create tokio runtime"), + ), + } + } + + /// Open a QUIC connection to `tpu_addr`, run `f` with the live sender, then shut down. + /// + /// Keeps the tpu-client-next scheduler alive for the duration of `f` so that any + /// transactions enqueued via [`send_wire_transaction`] are not dropped before wire + /// transmission. Shutdown happens synchronously after `f` returns. + pub fn with_connection(&self, tpu_addr: SocketAddr, f: F) -> R + where + F: FnOnce(&TransactionSender) -> R, + { + let bind_socket = bind_to_localhost_unique().expect("TpuSender: should bind UDP socket"); + let (sender, client) = self + .runtime + .block_on(async { + ClientBuilder::new(create_pinned_leader_updater(tpu_addr)) + .bind_socket(bind_socket) + .build() + }) + .expect("TpuSender: should build tpu-client-next client"); + let result = f(&sender); + self.runtime + .block_on(async { client.shutdown().await }) + .ok(); + result + } + + fn send_wire_transaction(&self, sender: &TransactionSender, wire_tx: Vec) { + self.runtime + .block_on(async { sender.send_transactions_in_batch(vec![wire_tx]).await }) + .expect("TpuSender: should send transactions in batch"); + } + + /// Send and confirm `transaction` with retries via `sender`, using `rpc_client` for + /// confirmation and blockhash refresh. + fn send_and_confirm_transaction( + &self, + sender: &TransactionSender, + rpc_client: &RpcClient, + signers: &T, + transaction: &mut Transaction, + max_attempts: usize, + ) -> Result<(), TransportError> { + for attempt in 1..=max_attempts { + let wire_tx = wincode::serialize(transaction) + .expect("send_and_confirm_transaction: should serialize transaction"); + self.send_wire_transaction(sender, wire_tx); + if LocalCluster::poll_for_successfully_processed_transaction(rpc_client, transaction)? + .is_some() + { + return Ok(()); + } + let (blockhash, _) = + rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::processed())?; + transaction.sign(signers, blockhash); + warn!("send_and_confirm_transaction: attempt {attempt} failed, retrying"); + } + Err(std::io::Error::other("failed to confirm transaction after max retries").into()) + } +} + +/// Resolve the current slot leader and its QUIC TPU address via RPC. +/// +/// Returns `None` when the leader cannot yet be mapped to a QUIC TPU address. +fn current_leader(rpc_client: &RpcClient) -> Option<(Pubkey, SocketAddr)> { + let leader_pubkey = rpc_client + .get_slot_leader() + .expect("current_leader: get_slot_leader"); + let tpu = rpc_client + .get_cluster_nodes() + .expect("current_leader: get_cluster_nodes") + .into_iter() + .find(|n| n.pubkey == leader_pubkey.to_string()) + .and_then(|n| n.tpu_quic)?; + Some((leader_pubkey, tpu)) +} + +/// Resolve the QUIC TPU address of the current slot leader via RPC. +/// +/// Returns `None` when the leader cannot yet be mapped to a QUIC TPU address +fn current_leader_tpu(rpc_client: &RpcClient) -> Option { + current_leader(rpc_client).map(|(_, tpu)| tpu) +} + +/// Poll until `transaction` is observed as processed by `rpc_client`, or `timeout` elapses. +/// +/// Returns `true` once the transaction is processed. Returns `false` if timeout expires. +fn poll_processed_until_timeout( + rpc_client: &RpcClient, + transaction: &Transaction, + timeout: Duration, +) -> bool { + let deadline = Instant::now() + timeout; + loop { + if let Ok(Some(status)) = rpc_client.get_signature_status_with_commitment( + &transaction.signatures[0], + CommitmentConfig::processed(), + ) { + // Surface a genuine execution error rather than silently retrying it as if the + // transaction had never arrived. + assert!( + status.is_ok(), + "poll_processed_within: transaction failed on-chain: {status:?}" + ); + return true; + } + if Instant::now() >= deadline { + return false; + } + sleep(Duration::from_millis(200)); + } +} /// Spend and verify from every node in the network pub fn spend_and_verify_all_nodes( @@ -71,7 +208,7 @@ pub fn spend_and_verify_all_nodes( nodes: usize, ignore_nodes: HashSet, socket_addr_space: SocketAddrSpace, - connection_cache: &Arc, + tpu_sender: &TpuSender, ) { let cluster_nodes = discover_validators( &entry_point_info.gossip().unwrap(), @@ -87,34 +224,38 @@ pub fn spend_and_verify_all_nodes( return; } let random_keypair = Keypair::new(); - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); - let bal = client - .rpc_client() + let rpc_client = RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())); + let bal = rpc_client .poll_get_balance_with_commitment( &funding_keypair.pubkey(), CommitmentConfig::processed(), ) .expect("balance in source"); assert!(bal > 0); - let (blockhash, _) = client - .rpc_client() + let (blockhash, _) = rpc_client .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed()) .unwrap(); let mut transaction = system_transaction::transfer(funding_keypair, &random_keypair.pubkey(), 1, blockhash); - LocalCluster::send_transaction_with_retries( - &client, - &[funding_keypair], - &mut transaction, - 10, - ) - .unwrap(); + // find target TPU address from RPC + let tpu_addr = current_leader_tpu(&rpc_client) + .expect("spend_and_verify_all_nodes: no current leader TPU"); + tpu_sender.with_connection(tpu_addr, |sender| { + tpu_sender + .send_and_confirm_transaction( + sender, + &rpc_client, + &[funding_keypair], + &mut transaction, + 10, + ) + .unwrap(); + }); for validator in &cluster_nodes { if ignore_nodes.contains(validator.pubkey()) { continue; } - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); - LocalCluster::poll_for_successfully_processed_transaction(&client, &transaction) + LocalCluster::poll_for_successfully_processed_transaction(&rpc_client, &transaction) .unwrap() .unwrap(); } @@ -124,12 +265,10 @@ pub fn spend_and_verify_all_nodes( pub fn verify_balances( expected_balances: HashMap, node: &ContactInfo, - connection_cache: Arc, ) { - let client = new_tpu_quic_client(node, connection_cache.clone()).unwrap(); + let rpc_client = RpcClient::new(format!("http://{}", node.rpc().unwrap())); for (pk, b) in expected_balances { - let bal = client - .rpc_client() + let bal = rpc_client .poll_get_balance_with_commitment(&pk, CommitmentConfig::processed()) .expect("balance in source"); assert_eq!(bal, b); @@ -139,47 +278,49 @@ pub fn verify_balances( pub fn send_many_transactions( node: &ContactInfo, funding_keypair: &Keypair, - connection_cache: &Arc, + tpu_sender: &TpuSender, max_tokens_per_transfer: u64, num_txs: u64, ) -> HashMap { - let client = new_tpu_quic_client(node, connection_cache.clone()).unwrap(); - let mut expected_balances = HashMap::new(); - for _ in 0..num_txs { - let random_keypair = Keypair::new(); - let bal = client - .rpc_client() - .poll_get_balance_with_commitment( - &funding_keypair.pubkey(), - CommitmentConfig::processed(), - ) - .expect("balance in source"); - assert!(bal > 0); - let (blockhash, _) = client - .rpc_client() - .get_latest_blockhash_with_commitment(CommitmentConfig::processed()) - .unwrap(); - let transfer_amount = rng().random_range(1..max_tokens_per_transfer); - - let mut transaction = system_transaction::transfer( - funding_keypair, - &random_keypair.pubkey(), - transfer_amount, - blockhash, - ); - - LocalCluster::send_transaction_with_retries( - &client, - &[funding_keypair], - &mut transaction, - 5, - ) - .unwrap(); - - expected_balances.insert(random_keypair.pubkey(), transfer_amount); - } - - expected_balances + let rpc_client = RpcClient::new(format!("http://{}", node.rpc().unwrap())); + // Ask RPC who the leader is + let tpu_addr = + current_leader_tpu(&rpc_client).expect("send_many_transactions: no current leader TPU"); + // One persistent QUIC connection for all transactions to avoid per-tx handshake overhead. + tpu_sender.with_connection(tpu_addr, |sender| { + let mut expected_balances = HashMap::new(); + for _ in 0..num_txs { + let random_keypair = Keypair::new(); + let bal = rpc_client + .poll_get_balance_with_commitment( + &funding_keypair.pubkey(), + CommitmentConfig::processed(), + ) + .expect("balance in source"); + assert!(bal > 0); + let (blockhash, _) = rpc_client + .get_latest_blockhash_with_commitment(CommitmentConfig::processed()) + .unwrap(); + let transfer_amount = rng().random_range(1..max_tokens_per_transfer); + let mut transaction = system_transaction::transfer( + funding_keypair, + &random_keypair.pubkey(), + transfer_amount, + blockhash, + ); + tpu_sender + .send_and_confirm_transaction( + sender, + &rpc_client, + &[funding_keypair], + &mut transaction, + 5, + ) + .unwrap(); + expected_balances.insert(random_keypair.pubkey(), transfer_amount); + } + expected_balances + }) } pub fn verify_ledger_ticks(ledger_path: &Path, ticks_per_slot: usize) { @@ -233,7 +374,7 @@ pub fn kill_entry_and_spend_and_verify_rest( entry_point_info: &ContactInfo, entry_point_validator_exit: &Arc>, funding_keypair: &Keypair, - connection_cache: &Arc, + tpu_sender: &TpuSender, nodes: usize, slot_millis: u64, socket_addr_space: SocketAddrSpace, @@ -249,10 +390,9 @@ pub fn kill_entry_and_spend_and_verify_rest( ) .unwrap(); assert!(cluster_nodes.len() >= nodes); - let client = new_tpu_quic_client(entry_point_info, connection_cache.clone()).unwrap(); + let ep_rpc = RpcClient::new(format!("http://{}", entry_point_info.rpc().unwrap())); for ingress_node in &cluster_nodes { - client - .rpc_client() + ep_rpc .poll_get_balance_with_commitment(ingress_node.pubkey(), CommitmentConfig::processed()) .unwrap_or_else(|err| panic!("Node {} has no balance: {}", ingress_node.pubkey(), err)); } @@ -273,9 +413,8 @@ pub fn kill_entry_and_spend_and_verify_rest( } // Ensure the current ingress node is still funded. - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); - let balance = client - .rpc_client() + let rpc_client = RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())); + let balance = rpc_client .poll_get_balance_with_commitment( &funding_keypair.pubkey(), CommitmentConfig::processed(), @@ -283,63 +422,76 @@ pub fn kill_entry_and_spend_and_verify_rest( .expect("balance in source"); assert_ne!(balance, 0); - let mut result = Ok(()); - let mut retries = 0; - - // Retry sending a transaction to the current ingress node until it is - // observed by the entire cluster or we exhaust all retries. - loop { - retries += 1; - if retries > 5 { - result.unwrap(); + // After the bootstrap leader is killed it stays in the (fixed) leader schedule for the + // rest of the epoch, so `get_slot_leader` points at the dead node for ~1/4 of slots. It + // accepts no QUIC connection, so targeting it wastes a whole poll window; skip it and wait + // one slot for rotation to a live leader instead. For live leaders, resend with a fresh + // blockhash each attempt and give the send a bounded window to land. + let killed_leader = *entry_point_info.pubkey(); + const MAX_SEND_ATTEMPTS: usize = 20; + // Generous enough for a live leader to land a transaction on a loaded CI runner, yet far + // below the blockhash lifetime so a wrong/stuck target is abandoned in seconds. + let poll_window = Duration::from_millis(slot_millis * 20).max(Duration::from_secs(8)); + let mut confirmed = false; + for attempt in 1..=MAX_SEND_ATTEMPTS { + let Some((leader_pubkey, tpu_addr)) = current_leader(&rpc_client) else { + // Leader not resolvable, back off a slot + sleep(Duration::from_millis(slot_millis)); + continue; + }; + if leader_pubkey == killed_leader { + // Scheduled leader is the dead bootstrap node; wait for rotation rather than + // burning a poll window on a node that will never accept the transaction. + sleep(Duration::from_millis(slot_millis)); + continue; } - - // Send a simple transfer transaction to the current ingress node. let random_keypair = Keypair::new(); - let (blockhash, _) = client - .rpc_client() + let (blockhash, _) = rpc_client .get_latest_blockhash_with_commitment(CommitmentConfig::processed()) .unwrap(); - let mut transaction = system_transaction::transfer( + let transaction = system_transaction::transfer( funding_keypair, &random_keypair.pubkey(), 1, blockhash, ); - - if let Err(err) = LocalCluster::send_transaction_with_retries( - &client, - &[funding_keypair], - &mut transaction, - 5, - ) { - result = Err(err); + let wire_tx = wincode::serialize(&transaction) + .expect("kill_entry_and_spend_and_verify_rest: should serialize transaction"); + // Poll *inside* the connection: tpu-client-next only enqueues the transaction, so the + // QUIC client must stay alive long enough to flush it onto the wire (and retransmit) + // before we tear it down. + let landed = tpu_sender.with_connection(tpu_addr, |sender| { + tpu_sender.send_wire_transaction(sender, wire_tx); + poll_processed_until_timeout(&rpc_client, &transaction, poll_window) + }); + if !landed { + warn!( + "kill_entry_and_spend_and_verify_rest: attempt {attempt} to {tpu_addr} did \ + not land within {poll_window:?}, re-resolving leader" + ); continue; } - // Ensure all non-entry point nodes are able to confirm the - // transaction. + // The transaction landed at the targeted node, ensure every other node + // observes it too. If it landed on a fork that loses, resend. info!("poll_all_nodes_for_signature()"); - match poll_all_nodes_for_signature( - entry_point_info, - &cluster_nodes, - connection_cache, - &transaction, - ) { - Err(e) => { - info!("poll_all_nodes_for_signature() failed {e:?}"); - result = Err(e); - } + match poll_all_nodes_for_signature(entry_point_info, &cluster_nodes, &transaction) { Ok(()) => { info!("poll_all_nodes_for_signature() succeeded, done."); + confirmed = true; break; } + Err(e) => info!("poll_all_nodes_for_signature() failed {e:?}, resending"), } } + assert!( + confirmed, + "kill_entry_and_spend_and_verify_rest: transaction never confirmed on all surviving \ + nodes after {MAX_SEND_ATTEMPTS} attempts" + ); } } -#[cfg(feature = "dev-context-only-utils")] pub fn apply_votes_to_tower(node_keypair: &Keypair, votes: Vec<(Slot, Hash)>, tower_path: PathBuf) { let tower_storage = FileTowerStorage::new(tower_path); let mut tower = tower_storage.load(&node_keypair.pubkey()).unwrap(); @@ -350,20 +502,14 @@ pub fn apply_votes_to_tower(node_keypair: &Keypair, votes: Vec<(Slot, Hash)>, to tower_storage.store(&saved_tower).unwrap(); } -pub fn check_min_slot_is_rooted( - min_slot: Slot, - contact_infos: &[ContactInfo], - connection_cache: &Arc, - test_name: &str, -) { +pub fn check_min_slot_is_rooted(min_slot: Slot, contact_infos: &[ContactInfo], test_name: &str) { let mut last_print = Instant::now(); let loop_start = Instant::now(); let loop_timeout = Duration::from_secs(180); for ingress_node in contact_infos.iter() { - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); + let rpc_client = RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())); loop { - let root_slot = client - .rpc_client() + let root_slot = rpc_client .get_slot_with_commitment(CommitmentConfig::finalized()) .unwrap_or(0); if root_slot >= min_slot || last_print.elapsed().as_secs() > 3 { @@ -388,7 +534,6 @@ pub fn check_min_slot_is_rooted( fn check_for_new_slots_with_commitment( num_new_slots: usize, contact_infos: &[ContactInfo], - connection_cache: &Arc, test_name: &str, commitment: CommitmentConfig, ) { @@ -402,11 +547,8 @@ fn check_for_new_slots_with_commitment( assert!(loop_start.elapsed() < loop_timeout); for (i, ingress_node) in contact_infos.iter().enumerate() { - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); - let slot = client - .rpc_client() - .get_slot_with_commitment(commitment) - .unwrap_or(0); + let rpc_client = RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())); + let slot = rpc_client.get_slot_with_commitment(commitment).unwrap_or(0); slots[i].insert(slot); num_slots_map.insert(*ingress_node.pubkey(), slots[i].len()); let num_slots = slots.iter().map(|r| r.len()).min().unwrap(); @@ -424,16 +566,10 @@ fn check_for_new_slots_with_commitment( } } -pub fn check_for_new_roots( - num_new_roots: usize, - contact_infos: &[ContactInfo], - connection_cache: &Arc, - test_name: &str, -) { +pub fn check_for_new_roots(num_new_roots: usize, contact_infos: &[ContactInfo], test_name: &str) { check_for_new_slots_with_commitment( num_new_roots, contact_infos, - connection_cache, test_name, CommitmentConfig::finalized(), ); @@ -442,13 +578,11 @@ pub fn check_for_new_roots( pub fn check_for_new_processed( num_new_processed: usize, contact_infos: &[ContactInfo], - connection_cache: &Arc, test_name: &str, ) { check_for_new_slots_with_commitment( num_new_processed, contact_infos, - connection_cache, test_name, CommitmentConfig::processed(), ); @@ -465,7 +599,7 @@ pub fn start_quic_streamer_to_listen_for_votes_and_certs( JoinHandle<()>, crossbeam_channel::Receiver, ) { - let (sender, receiver) = crossbeam_channel::unbounded(); + let (sender, receiver) = bounded(1024); let cancel = CancellationToken::new(); let stakes = validator_keys .iter() @@ -491,11 +625,30 @@ pub fn start_quic_streamer_to_listen_for_votes_and_certs( (cancel, result.thread, receiver) } +fn convert_packet_to_vote_message(packet: PacketRef, my_shred_version: u16) -> Option { + let Ok(msg) = wincode::config::deserialize_exact::( + packet.data(..).unwrap_or_default(), + packet_config(), + ) else { + return None; + }; + let DecodedWireConsensusMessage::Vote(vote_msg) = + DecodedWireConsensusMessage::try_new(msg, my_shred_version).unwrap() + else { + return None; + }; + Some(VoteMessage { + vote: vote_msg.vote, + signature: vote_msg.signature, + rank: vote_msg.rank, + }) +} + /// Check that all nodes in the cluster are producing notarized votes. pub fn check_for_new_notarized_votes( + my_shred_version: u16, num_new_votes: usize, contact_infos: &[ContactInfo], - connection_cache: &Arc, test_name: &str, vote_listener_socket: UdpSocket, validator_keys: &[Arc], @@ -507,10 +660,7 @@ pub fn check_for_new_notarized_votes( let Some(current_slot) = contact_infos .iter() .map(|ingress_node| { - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); - - client - .rpc_client() + RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())) .get_slot_with_commitment(CommitmentConfig::processed()) .unwrap_or(0) }) @@ -546,7 +696,8 @@ pub fn check_for_new_notarized_votes( continue; }; for packet in packet_batch.iter() { - let Ok(ConsensusMessage::Vote(vote_message)) = packet.deserialize_slice(..) + let Some(vote_message) = + convert_packet_to_vote_message(packet, my_shred_version) else { continue; }; @@ -593,7 +744,6 @@ pub fn check_for_new_notarized_votes( pub fn check_no_new_roots( num_slots_to_wait: usize, contact_infos: &[&ContactInfo], - connection_cache: &Arc, test_name: &str, ) { assert!(!contact_infos.is_empty()); @@ -602,14 +752,12 @@ pub fn check_no_new_roots( .iter() .enumerate() .map(|(i, ingress_node)| { - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); - let initial_root = client - .rpc_client() + let rpc_client = RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())); + let initial_root = rpc_client .get_slot() .unwrap_or_else(|_| panic!("get_slot for {} failed", ingress_node.pubkey())); roots[i] = initial_root; - client - .rpc_client() + rpc_client .get_slot_with_commitment(CommitmentConfig::processed()) .unwrap_or_else(|_| panic!("get_slot for {} failed", ingress_node.pubkey())) }) @@ -622,9 +770,8 @@ pub fn check_no_new_roots( let mut reached_end_slot = false; loop { for contact_info in contact_infos { - let client = new_tpu_quic_client(contact_info, connection_cache.clone()).unwrap(); - current_slot = client - .rpc_client() + let rpc_client = RpcClient::new(format!("http://{}", contact_info.rpc().unwrap())); + current_slot = rpc_client .get_slot_with_commitment(CommitmentConfig::processed()) .unwrap_or_else(|_| panic!("get_slot for {} failed", contact_infos[0].pubkey())); if current_slot > end_slot { @@ -648,10 +795,9 @@ pub fn check_no_new_roots( } for (i, ingress_node) in contact_infos.iter().enumerate() { - let client = new_tpu_quic_client(ingress_node, connection_cache.clone()).unwrap(); + let rpc_client = RpcClient::new(format!("http://{}", ingress_node.rpc().unwrap())); assert_eq!( - client - .rpc_client() + rpc_client .get_slot() .unwrap_or_else(|_| panic!("get_slot for {} failed", ingress_node.pubkey())), roots[i] @@ -662,15 +808,15 @@ pub fn check_no_new_roots( fn poll_all_nodes_for_signature( entry_point_info: &ContactInfo, cluster_nodes: &[ContactInfo], - connection_cache: &Arc, transaction: &Transaction, ) -> Result<(), TransportError> { for validator in cluster_nodes { if validator.pubkey() == entry_point_info.pubkey() { continue; } - let client = new_tpu_quic_client(validator, connection_cache.clone()).unwrap(); - LocalCluster::poll_for_successfully_processed_transaction(&client, transaction)?.unwrap(); + let rpc_client = RpcClient::new(format!("http://{}", validator.rpc().unwrap())); + LocalCluster::poll_for_successfully_processed_transaction(&rpc_client, transaction)? + .unwrap(); } Ok(()) diff --git a/local-cluster/src/integration_tests.rs b/local-cluster/src/integration_tests.rs index 4b12ec0a938..a718769f0f0 100644 --- a/local-cluster/src/integration_tests.rs +++ b/local-cluster/src/integration_tests.rs @@ -17,6 +17,7 @@ use { validator_configs::*, }, agave_snapshots::{SnapshotInterval, snapshot_config::SnapshotConfig}, + agave_votor::voting_service::{AlpenglowPortOverride, VotingServiceOverride}, log::*, solana_account::AccountSharedData, solana_accounts_db::utils::create_accounts_run_and_snapshot_dirs, @@ -38,15 +39,16 @@ use { shred::filter::{TurbineMode, TurbineModeKind}, }, solana_native_token::LAMPORTS_PER_SOL, - solana_net_utils::SocketAddrSpace, + solana_net_utils::{SocketAddrSpace, sockets::bind_to_localhost_unique}, solana_pubkey::Pubkey, solana_rpc_client::rpc_client::RpcClient, solana_signer::Signer, solana_turbine::broadcast_stage::BroadcastStageType, static_assertions, std::{ - collections::HashSet, + collections::{HashMap, HashSet}, fs, iter, + net::SocketAddr, num::{NonZeroU64, NonZeroUsize}, path::{Path, PathBuf}, sync::{Arc, atomic::AtomicBool}, @@ -74,7 +76,6 @@ pub struct ValidatorKeys { } impl ValidatorKeys { - #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { node_keypair: Arc::new(Keypair::new()), @@ -186,11 +187,10 @@ pub fn wait_for_duplicate_proof(ledger_path: &Path, dup_slot: Slot) -> Option u64 { (ticks_per_slot * DEFAULT_MS_PER_SLOT * num_blocks).div_ceil(DEFAULT_TICKS_PER_SLOT) } -// Test runner that performs the following steps: -// 1) Defines validator stake partitions based on input parameters -// 2) Defines leader schedule based on input parameters -// 3) Appends routine to kill specified validators on partition start -// 4) Runs cluster partition -pub fn run_kill_partition_switch_threshold( +/// Implements a test scenario that creates a network partition by killing validator nodes. +/// +/// # Arguments +/// * `stakes_to_kill` - Validators to remove from the network, where each tuple contains: +/// * First element (usize): The stake weight/size of the validator +/// * Second element (usize): The number of slots assigned to the validator +/// * `alive_stakes` - Validators to keep alive, where each tuple contains: +/// * First element (usize): The stake weight/size of the validator +/// * Second element (usize): The number of slots assigned to the validator +/// * `ticks_per_slot` - Optional override for the default ticks per slot +/// * `partition_context` - Test-specific context object that will be passed to callbacks +/// * `on_partition_start` - Callback executed when the partition begins +/// * `on_before_partition_resolved` - Callback executed right before the partition is resolved +/// * `on_partition_resolved` - Callback executed after the partition is resolved +/// +/// This function simulates a network partition by killing specified validator nodes, +/// waiting for a period, resolving the partition, and then verifying the network +/// can recover and reach consensus. The IS_ALPENGLOW parameter determines whether +/// to use Alpenglow-specific cluster initialization. +fn run_kill_partition_switch_threshold_impl( stakes_to_kill: &[(usize, usize)], alive_stakes: &[(usize, usize)], ticks_per_slot: Option, @@ -284,6 +298,47 @@ pub fn run_kill_partition_switch_threshold( ticks_per_slot, true, vec![], + IS_ALPENGLOW, + ) +} + +pub fn run_kill_partition_switch_threshold_alpenglow( + stakes_to_kill: &[(usize, usize)], + alive_stakes: &[(usize, usize)], + ticks_per_slot: Option, + partition_context: C, + on_partition_start: impl Fn(&mut LocalCluster, &[Pubkey], Vec, &mut C), + on_before_partition_resolved: impl Fn(&mut LocalCluster, &mut C), + on_partition_resolved: impl Fn(&mut LocalCluster, &mut C), +) { + run_kill_partition_switch_threshold_impl::( + stakes_to_kill, + alive_stakes, + ticks_per_slot, + partition_context, + on_partition_start, + on_before_partition_resolved, + on_partition_resolved, + ) +} + +pub fn run_kill_partition_switch_threshold( + stakes_to_kill: &[(usize, usize)], + alive_stakes: &[(usize, usize)], + ticks_per_slot: Option, + partition_context: C, + on_partition_start: impl Fn(&mut LocalCluster, &[Pubkey], Vec, &mut C), + on_before_partition_resolved: impl Fn(&mut LocalCluster, &mut C), + on_partition_resolved: impl Fn(&mut LocalCluster, &mut C), +) { + run_kill_partition_switch_threshold_impl::( + stakes_to_kill, + alive_stakes, + ticks_per_slot, + partition_context, + on_partition_start, + on_before_partition_resolved, + on_partition_resolved, ) } @@ -316,19 +371,36 @@ pub fn create_custom_leader_schedule_with_random_keys( (leader_schedule, validator_keys) } -/// This function runs a network, initiates a partition based on a -/// configuration, resolves the partition, then checks that the network -/// continues to achieve consensus. +/// Simulates a network partition test scenario by creating a cluster, triggering a partition, +/// allowing the partition to heal, and then verifying the network's ability to recover and +/// achieve consensus after the partition is resolved. +/// +/// This function: +/// 1. Creates a local cluster with nodes configured according to the provided stakes +/// 2. Induces a network partition by disabling communication between validators +/// 3. Runs the partition for a predetermined duration +/// 4. Resolves the partition by re-enabling communication +/// 5. Verifies the network can recover and continue to make progress /// -/// # Arguments: -/// * `partitions` - A slice of partition configurations, where each partition -/// configuration is a usize representing a node's relative stake -/// * `leader_schedule` - An option that specifies whether the cluster should -/// run with a fixed, predetermined leader schedule -/// * `no_wait_for_vote_to_start_leader` - provide option to only allow the -/// bootstrap to build blocks at first to minimize forking during cluster -/// startup. -#[allow(clippy::cognitive_complexity)] +/// # Arguments +/// * `partitions` - A slice of partition configurations, where each usize represents a validator's +/// stake weight. This determines the relative voting power of each node in the network. +/// * `leader_schedule` - An option that specifies whether the cluster should run with a fixed, +/// predetermined leader schedule. If provided, the partition will last for one complete +/// iteration of the leader schedule. +/// * `context` - A user-defined context object that is passed to the callback functions. +/// * `on_partition_start` - Callback function that runs when the partition begins. Can be used +/// to perform custom actions or checks at the start of the partition. +/// * `on_before_partition_resolved` - Callback function that runs just before the partition +/// is resolved. Can be used to verify partition state or prepare for resolution. +/// * `on_partition_resolved` - Callback function that runs after the partition is resolved and +/// the network has had time to recover. Can be used to verify recovery. +/// * `ticks_per_slot` - Optional override for the default ticks per slot. Controls the +/// rate at which slots advance in the cluster. +/// * `additional_accounts` - Additional accounts to be added to the genesis configuration. +/// * `is_alpenglow` - Boolean flag indicating whether to initialize the `LocalCluster` in Alpenglow +/// mode. +#[allow(clippy::cognitive_complexity, clippy::too_many_arguments)] pub fn run_cluster_partition( partitions: &[usize], leader_schedule: Option<(LeaderSchedule, Vec)>, @@ -339,8 +411,13 @@ pub fn run_cluster_partition( ticks_per_slot: Option, no_wait_for_vote_to_start_leader: bool, additional_accounts: Vec<(Pubkey, AccountSharedData)>, + is_alpenglow: bool, ) { - agave_logger::setup_with_default(RUST_LOG_FILTER); + if is_alpenglow { + agave_logger::setup_with_default(AG_DEBUG_LOG_FILTER); + } else { + agave_logger::setup_with_default(RUST_LOG_FILTER); + } info!("PARTITION_TEST!"); let num_nodes = partitions.len(); let node_stakes: Vec<_> = partitions @@ -402,6 +479,13 @@ pub fn run_cluster_partition( .unwrap() .no_wait_for_vote_to_start_leader = true; let slots_per_epoch = 2048; + let alpenglow_port_override = AlpenglowPortOverride::default(); + for config in &mut validator_configs { + config.voting_service_test_override = Some(VotingServiceOverride { + additional_listeners: vec![], + alpenglow_port_override: alpenglow_port_override.clone(), + }); + } let mut config = ClusterConfig { mint_lamports, node_stakes, @@ -425,7 +509,12 @@ pub fn run_cluster_partition( "PARTITION_TEST starting cluster with {:?} partitions slots_per_epoch: {}", partitions, config.slots_per_epoch, ); - let mut cluster = LocalCluster::new(&mut config, SocketAddrSpace::Unspecified); + + let mut cluster = if is_alpenglow { + LocalCluster::new_alpenglow(&mut config, SocketAddrSpace::Unspecified) + } else { + LocalCluster::new(&mut config, SocketAddrSpace::Unspecified) + }; info!("PARTITION_TEST spend_and_verify_all_nodes(), ensure all nodes are caught up"); cluster_tests::spend_and_verify_all_nodes( @@ -434,7 +523,7 @@ pub fn run_cluster_partition( num_nodes, HashSet::new(), SocketAddrSpace::Unspecified, - &cluster.connection_cache, + &cluster_tests::TpuSender::new(), ); let cluster_nodes = discover_validators( @@ -457,11 +546,23 @@ pub fn run_cluster_partition( on_partition_start(&mut cluster, &mut context); turbine_mode.set(TurbineModeKind::TurbineAndRepairDisabled); + // Make all to all votes/certs not able to reach each other by overriding the + // alpenglow port override to SocketAddr which no one is listening on. + let blackhole_socket = bind_to_localhost_unique().unwrap(); + let blackhole_addr: SocketAddr = blackhole_socket.local_addr().unwrap(); + let new_override = HashMap::from_iter( + cluster_nodes + .iter() + .map(|node| (*node.pubkey(), blackhole_addr)), + ); + alpenglow_port_override.update_override(new_override); sleep(partition_duration); on_before_partition_resolved(&mut cluster, &mut context); info!("PARTITION_TEST remove partition"); turbine_mode.set(TurbineModeKind::Enabled); + // Restore the alpenglow port override to the default, so that the nodes can communicate again. + alpenglow_port_override.clear(); // Give partitions time to propagate their blocks from during the partition // after the partition resolves diff --git a/local-cluster/src/local_cluster.rs b/local-cluster/src/local_cluster.rs index 96d722b7319..28ff425aecd 100644 --- a/local-cluster/src/local_cluster.rs +++ b/local-cluster/src/local_cluster.rs @@ -7,6 +7,7 @@ use { }, agave_feature_set::{FeatureSet, bls_pubkey_management_in_vote_account}, agave_snapshots::{paths::BANK_SNAPSHOTS_DIR, snapshot_config::SnapshotConfig}, + agave_votor::vote_history_storage::FileVoteHistoryStorage, itertools::izip, log::*, solana_account::{Account, AccountSharedData, ReadableAccount}, @@ -41,6 +42,7 @@ use { GenesisConfigInfo, ValidatorVoteKeypairs, create_genesis_config_with_vote_accounts_and_cluster_type, }, + solana_shred_version::compute_shred_version, solana_signer::{Signer, signers::Signers}, solana_stake_interface::{ instruction as stake_instruction, @@ -189,6 +191,8 @@ impl LocalCluster { ]; config.tower_storage = Arc::new(FileTowerStorage::new(ledger_path.to_path_buf())); config.accounts_db_config.bank_hash_details_dir = ledger_path.to_path_buf(); + config.vote_history_storage = + Arc::new(FileVoteHistoryStorage::new(ledger_path.to_path_buf())); let snapshot_config = &mut config.snapshot_config; let dummy: PathBuf = DUMMY_SNAPSHOT_CONFIG_PATH_MARKER.into(); @@ -205,6 +209,7 @@ impl LocalCluster { } pub fn new_alpenglow(config: &mut ClusterConfig, socket_addr_space: SocketAddrSpace) -> Self { + config.poh_config.hashes_per_tick = None; Self::init(config, socket_addr_space, AlpenglowMode::Enabled) } @@ -555,7 +560,7 @@ impl LocalCluster { pub fn close_preserve_ledgers(&mut self) { self.exit(); - for (_, node) in self.validators.iter_mut() { + for node in self.validators.values_mut() { if let Some(v) = node.validator.take() { v.join(); } @@ -873,12 +878,7 @@ impl LocalCluster { ) { let alive_node_contact_infos = self.discover_nodes(socket_addr_space, test_name); info!("{test_name} looking minimum root {min_root} on all nodes"); - cluster_tests::check_min_slot_is_rooted( - min_root, - &alive_node_contact_infos, - &self.connection_cache, - test_name, - ); + cluster_tests::check_min_slot_is_rooted(min_root, &alive_node_contact_infos, test_name); info!("{test_name} done waiting for roots"); } @@ -890,12 +890,7 @@ impl LocalCluster { ) { let alive_node_contact_infos = self.discover_nodes(socket_addr_space, test_name); info!("{test_name} looking for new roots on all nodes"); - cluster_tests::check_for_new_roots( - num_new_roots, - &alive_node_contact_infos, - &self.connection_cache, - test_name, - ); + cluster_tests::check_for_new_roots(num_new_roots, &alive_node_contact_infos, test_name); info!("{test_name} done waiting for roots"); } @@ -910,7 +905,6 @@ impl LocalCluster { cluster_tests::check_for_new_processed( num_new_processed, &alive_node_contact_infos, - &self.connection_cache, test_name, ); info!("{test_name} done waiting for processed slots"); @@ -928,9 +922,9 @@ impl LocalCluster { let alive_node_contact_infos = self.discover_nodes(socket_addr_space, test_name); info!("{test_name} looking for new notarized votes on all nodes"); cluster_tests::check_for_new_notarized_votes( + compute_shred_version(&self.genesis_config.hash(), None), num_new_notarized_votes, &alive_node_contact_infos, - &self.connection_cache, test_name, vote_listener_addr, validator_keys, @@ -961,12 +955,7 @@ impl LocalCluster { .unwrap(); info!("{} discovered {} nodes", test_name, cluster_nodes.len()); info!("{test_name} making sure no new roots on any nodes"); - cluster_tests::check_no_new_roots( - num_slots_to_wait, - &alive_node_contact_infos, - &self.connection_cache, - test_name, - ); + cluster_tests::check_no_new_roots(num_slots_to_wait, &alive_node_contact_infos, test_name); info!("{test_name} done waiting for roots"); } @@ -976,14 +965,14 @@ impl LocalCluster { /// Ok(Some(())) if the transaction was processed successfully. Return /// Ok(None) if the transaction was not processed. pub fn poll_for_successfully_processed_transaction( - client: &QuicTpuClient, + rpc_client: &RpcClient, transaction: &Transaction, ) -> std::result::Result, TransportError> { loop { // Some local cluster tests create conditions where confirmation // is unable to be reached. So rather than checking for confirmation, // check for the transaction being processed. - let status = client.rpc_client().get_signature_status_with_commitment( + let status = rpc_client.get_signature_status_with_commitment( &transaction.signatures[0], CommitmentConfig::processed(), )?; @@ -997,7 +986,7 @@ impl LocalCluster { } } - if !client.rpc_client().is_blockhash_valid( + if !rpc_client.is_blockhash_valid( &transaction.message.recent_blockhash, CommitmentConfig::processed(), )? { @@ -1025,7 +1014,9 @@ impl LocalCluster { // in LocalCluster integration tests for attempt in 1..=attempts { client.send_transaction_to_upcoming_leaders(transaction)?; - if Self::poll_for_successfully_processed_transaction(client, transaction)?.is_some() { + if Self::poll_for_successfully_processed_transaction(client.rpc_client(), transaction)? + .is_some() + { return Ok(()); } diff --git a/local-cluster/src/local_cluster_snapshot_utils.rs b/local-cluster/src/local_cluster_snapshot_utils.rs index 04fe2de0324..bb87c96ddc3 100644 --- a/local-cluster/src/local_cluster_snapshot_utils.rs +++ b/local-cluster/src/local_cluster_snapshot_utils.rs @@ -106,13 +106,12 @@ impl LocalCluster { incremental_snapshot_archives_dir.as_ref().unwrap(), full_snapshot_archive_info.slot(), ) + && incremental_snapshot_archive_info.slot() > last_slot { - if incremental_snapshot_archive_info.slot() > last_slot { - break NextSnapshotResult::IncrementalAndFullSnapshot( - incremental_snapshot_archive_info, - full_snapshot_archive_info, - ); - } + break NextSnapshotResult::IncrementalAndFullSnapshot( + incremental_snapshot_archive_info, + full_snapshot_archive_info, + ); } } } diff --git a/local-cluster/src/validator_configs.rs b/local-cluster/src/validator_configs.rs index d2264d897bc..0c9eec39aef 100644 --- a/local-cluster/src/validator_configs.rs +++ b/local-cluster/src/validator_configs.rs @@ -34,6 +34,7 @@ pub fn safe_clone_config(config: &ValidatorConfig) -> ValidatorConfig { max_genesis_archive_unpacked_size: config.max_genesis_archive_unpacked_size, run_verification: config.run_verification, require_tower: config.require_tower, + require_vote_history: config.require_vote_history, tower_storage: config.tower_storage.clone(), vote_history_storage: config.vote_history_storage.clone(), debug_keys: config.debug_keys.clone(), @@ -44,6 +45,7 @@ pub fn safe_clone_config(config: &ValidatorConfig) -> ValidatorConfig { no_poh_speed_test: config.no_poh_speed_test, no_os_memory_stats_reporting: config.no_os_memory_stats_reporting, no_os_network_stats_reporting: config.no_os_network_stats_reporting, + xdp_network_config_report: config.xdp_network_config_report.clone(), no_os_cpu_stats_reporting: config.no_os_cpu_stats_reporting, no_os_disk_stats_reporting: config.no_os_disk_stats_reporting, enforce_ulimit_nofile: config.enforce_ulimit_nofile, diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index 90a692ae730..6f04fead6f7 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -7,7 +7,7 @@ use { agave_votor::voting_service::{AlpenglowPortOverride, VotingServiceOverride}, agave_votor_messages::migration::MIGRATION_SLOT_OFFSET, assert_matches::assert_matches, - crossbeam_channel::{Receiver, unbounded}, + crossbeam_channel::{Receiver, bounded}, gag::BufferRedirect, itertools::Itertools, log::*, @@ -15,10 +15,9 @@ use { serial_test::serial, solana_account::AccountSharedData, solana_accounts_db::utils::create_accounts_run_and_snapshot_dirs, + solana_client::rpc_client::RpcClient, solana_client_traits::AsyncClient, - solana_clock::{ - self as clock, DEFAULT_SLOTS_PER_EPOCH, DEFAULT_TICKS_PER_SLOT, MAX_PROCESSING_AGE, Slot, - }, + solana_clock::{DEFAULT_SLOTS_PER_EPOCH, DEFAULT_TICKS_PER_SLOT, MAX_PROCESSING_AGE, Slot}, solana_cluster_type::ClusterType, solana_commitment_config::CommitmentConfig, solana_core::{ @@ -74,7 +73,6 @@ use { solana_poh_config::PohConfig, solana_pubkey::Pubkey, solana_pubsub_client::pubsub_client::PubsubClient, - solana_rpc_client::rpc_client::RpcClient, solana_rpc_client_api::{ config::{ RpcBlockSubscribeConfig, RpcBlockSubscribeFilter, RpcProgramAccountsConfig, @@ -83,6 +81,7 @@ use { response::RpcSignatureResult, }, solana_runtime::{commitment::VOTE_THRESHOLD_SIZE, snapshot_bank_utils, snapshot_utils}, + solana_shred_version::compute_shred_version, solana_signer::Signer, solana_stake_interface as stake, solana_system_interface::program as system_program, @@ -92,7 +91,10 @@ use { BroadcastStageType, broadcast_duplicates_run::{BroadcastDuplicatesConfig, ClusterPartition}, }, - solana_vote::{vote_parser, vote_transaction}, + solana_vote::{ + vote_parser::{self}, + vote_transaction, + }, solana_vote_interface::state::TowerSync, solana_vote_program::vote_state::MAX_LOCKOUT_HISTORY, std::{ @@ -162,7 +164,7 @@ fn test_spend_and_verify_all_nodes() { num_nodes, HashSet::new(), SocketAddrSpace::Unspecified, - &local.connection_cache, + &cluster_tests::TpuSender::new(), ); } @@ -326,7 +328,7 @@ fn test_forwarding() { cluster_tests::send_many_transactions( validator_info, &cluster.funding_keypair, - &cluster.connection_cache, + &cluster_tests::TpuSender::new(), 10, 20, ); @@ -371,7 +373,7 @@ fn test_restart_node() { cluster_tests::send_many_transactions( &cluster.entry_point_info, &cluster.funding_keypair, - &cluster.connection_cache, + &cluster_tests::TpuSender::new(), 10, 1, ); @@ -1112,27 +1114,22 @@ fn test_incremental_snapshot_download_with_crossing_full_snapshot_interval_at_st validator_snapshot_test_config .full_snapshot_archives_dir .path(), - ) { - if full_snapshot_slot >= validator_next_full_snapshot_slot { - if let Some(incremental_snapshot_slot) = - snapshot_paths::get_highest_incremental_snapshot_archive_slot( - validator_snapshot_test_config - .incremental_snapshot_archives_dir - .path(), - full_snapshot_slot, - ) - { - if incremental_snapshot_slot >= validator_next_incremental_snapshot_slot { - // specific incremental snapshot is not important, just that one was created - info!( - "Validator made new snapshots, full snapshot slot: \ - {full_snapshot_slot}, incremental snapshot slot: \ - {incremental_snapshot_slot}", - ); - break; - } - } - } + ) && full_snapshot_slot >= validator_next_full_snapshot_slot + && let Some(incremental_snapshot_slot) = + snapshot_paths::get_highest_incremental_snapshot_archive_slot( + validator_snapshot_test_config + .incremental_snapshot_archives_dir + .path(), + full_snapshot_slot, + ) + && incremental_snapshot_slot >= validator_next_incremental_snapshot_slot + { + // specific incremental snapshot is not important, just that one was created + info!( + "Validator made new snapshots, full snapshot slot: {full_snapshot_slot}, \ + incremental snapshot slot: {incremental_snapshot_slot}", + ); + break; } assert!( @@ -1280,7 +1277,7 @@ fn test_snapshot_restart_tower() { 2, HashSet::new(), SocketAddrSpace::Unspecified, - &cluster.connection_cache, + &cluster_tests::TpuSender::new(), ); } @@ -1431,6 +1428,7 @@ fn test_snapshots_restart_validity() { let num_runs = 3; let mut expected_balances = HashMap::new(); let mut cluster = LocalCluster::new(&mut config, SocketAddrSpace::Unspecified); + let tpu_sender = cluster_tests::TpuSender::new(); for i in 1..num_runs { info!("run {i}"); // Push transactions to one of the nodes and confirm that transactions were @@ -1439,7 +1437,7 @@ fn test_snapshots_restart_validity() { let new_balances = cluster_tests::send_many_transactions( &cluster.entry_point_info, &cluster.funding_keypair, - &cluster.connection_cache, + &tpu_sender, 10, 10, ); @@ -1469,11 +1467,7 @@ fn test_snapshots_restart_validity() { // Verify account balances on validator trace!("Verifying balances"); - cluster_tests::verify_balances( - expected_balances.clone(), - &cluster.entry_point_info, - cluster.connection_cache.clone(), - ); + cluster_tests::verify_balances(expected_balances.clone(), &cluster.entry_point_info); // Check that we can still push transactions trace!("Spending and verifying"); @@ -1483,7 +1477,7 @@ fn test_snapshots_restart_validity() { 1, HashSet::new(), SocketAddrSpace::Unspecified, - &cluster.connection_cache, + &tpu_sender, ); } } @@ -1760,7 +1754,6 @@ fn test_optimistic_confirmation_violation_detection() { cluster_tests::check_for_new_roots( 16, &[cluster.get_contact_info(&node_to_restart).unwrap().clone()], - &cluster.connection_cache, "test_optimistic_confirmation_violation", ); } @@ -1964,14 +1957,16 @@ fn do_test_future_tower(cluster_mode: ClusterMode) { let mut cluster = LocalCluster::new(&mut config, SocketAddrSpace::Unspecified); let val_a_ledger_path = cluster.ledger_path(&validator_a_pubkey); + let root_before_restart; loop { sleep(Duration::from_millis(100)); - if let Some(root) = root_in_tower(&val_a_ledger_path, &validator_a_pubkey) { - if root >= 15 { - break; - } + if let Some(root) = root_in_tower(&val_a_ledger_path, &validator_a_pubkey) + && root >= 15 + { + root_before_restart = root; + break; } } let purged_slot_before_restart = 10; @@ -1993,15 +1988,15 @@ fn do_test_future_tower(cluster_mode: ClusterMode) { ); let mut newly_rooted = false; - let some_root_after_restart = purged_slot_before_restart + 25; // 25 is arbitrary; just wait a bit + let some_root_after_restart = root_before_restart + 1; for _ in 0..600 { sleep(Duration::from_millis(100)); - if let Some(root) = root_in_tower(&val_a_ledger_path, &validator_a_pubkey) { - if root >= some_root_after_restart { - newly_rooted = true; - break; - } + if let Some(root) = root_in_tower(&val_a_ledger_path, &validator_a_pubkey) + && root >= some_root_after_restart + { + newly_rooted = true; + break; } } let _validator_a_info = cluster.exit_node(&validator_a_pubkey); @@ -2139,10 +2134,10 @@ fn test_hard_fork_invalidates_tower() { loop { sleep(Duration::from_millis(100)); - if let Some(root) = root_in_tower(&val_a_ledger_path, &validator_a_pubkey) { - if root >= min_root { - break; - } + if let Some(root) = root_in_tower(&val_a_ledger_path, &validator_a_pubkey) + && root >= min_root + { + break; } } @@ -2251,6 +2246,7 @@ fn create_snapshot_to_hard_fork( blockstore_processor::process_blockstore_from_root( blockstore, &bank_forks, + compute_shred_version(&genesis_config.hash(), None), &leader_schedule_cache, &process_options, None, @@ -2327,12 +2323,11 @@ fn test_hard_fork_with_gap_in_roots() { loop { sleep(Duration::from_millis(100)); - if let Some((last_vote, _)) = last_vote_in_tower(&val_a_ledger_path, &validator_a_pubkey) { - if last_vote >= min_last_vote - && root_in_tower(&val_a_ledger_path, &validator_a_pubkey) > Some(min_root) - { - break; - } + if let Some((last_vote, _)) = last_vote_in_tower(&val_a_ledger_path, &validator_a_pubkey) + && last_vote >= min_last_vote + && root_in_tower(&val_a_ledger_path, &validator_a_pubkey) > Some(min_root) + { + break; } } @@ -2544,8 +2539,8 @@ fn test_run_test_load_program_accounts_partition_root() { num_slots_per_validator, ]); - let (update_client_sender, update_client_receiver) = unbounded(); - let (scan_client_sender, scan_client_receiver) = unbounded(); + let (update_client_sender, update_client_receiver) = bounded(1024); + let (scan_client_sender, scan_client_receiver) = bounded(1024); let exit = Arc::new(AtomicBool::new(false)); let (t_update, t_scan, additional_accounts) = setup_transfer_scan_threads( @@ -2590,6 +2585,7 @@ fn test_run_test_load_program_accounts_partition_root() { None, false, additional_accounts, + false, ); } @@ -2716,10 +2712,7 @@ fn test_oc_bad_signatures() { ); let (mut block_subscribe_client, receiver) = PubsubClient::block_subscribe( - format!( - "ws://{}", - &cluster.entry_point_info.rpc_pubsub().unwrap().to_string() - ), + format!("ws://{}", cluster.entry_point_info.rpc_pubsub().unwrap()), RpcBlockSubscribeFilter::All, Some(RpcBlockSubscribeConfig { commitment: Some(CommitmentConfig::confirmed()), @@ -2985,8 +2978,8 @@ fn run_test_load_program_accounts(scan_commitment: CommitmentConfig) { let num_starting_accounts = 100; let exit = Arc::new(AtomicBool::new(false)); - let (update_client_sender, update_client_receiver) = unbounded(); - let (scan_client_sender, scan_client_receiver) = unbounded(); + let (update_client_sender, update_client_receiver) = bounded(1024); + let (scan_client_sender, scan_client_receiver) = bounded(1024); // Setup the update/scan threads let (t_update, t_scan, starting_accounts) = setup_transfer_scan_threads( @@ -3182,10 +3175,10 @@ fn do_test_lockout_violation_with_or_without_tower(with_tower: bool) { // Step 1: Wait for validator A to vote so the tower file exists, and so we can determine the // `base_slot` and `next_slot_on_a` loop { - if let Some((last_vote, _)) = last_vote_in_tower(&val_a_ledger_path, &validator_a_pubkey) { - if last_vote >= 1 { - break; - } + if let Some((last_vote, _)) = last_vote_in_tower(&val_a_ledger_path, &validator_a_pubkey) + && last_vote >= 1 + { + break; } sleep(Duration::from_millis(100)); @@ -3626,7 +3619,17 @@ fn test_fork_choice_refresh_old_votes() { /// locked-out survivors still converge on a rooted fork. #[test] #[serial] -fn test_kill_heaviest_partition() { +fn test_kill_heaviest_partition_tower() { + test_kill_heaviest_partition(false); +} + +#[test] +#[serial] +fn test_kill_heaviest_partition_alpenglow() { + test_kill_heaviest_partition(true); +} + +fn test_kill_heaviest_partition(is_alpenglow: bool) { // This test: // 1) Spins up four partitions, the heaviest being the first with more stake // 2) Schedules the other validators for sufficient slots in the schedule @@ -3666,6 +3669,7 @@ fn test_kill_heaviest_partition() { None, true, vec![], + is_alpenglow, ) } @@ -3831,7 +3835,7 @@ fn run_duplicate_shreds_broadcast_leader(vote_on_duplicate: bool) { // for the partition. assert!(partition_node_stake < our_node_stake && partition_node_stake < good_node_stake); - let (duplicate_slot_sender, duplicate_slot_receiver) = unbounded(); + let (duplicate_slot_sender, duplicate_slot_receiver) = bounded(1024); // 1) Set up the cluster let (mut cluster, validator_keys) = test_faulty_node( @@ -4247,13 +4251,25 @@ fn find_latest_replayed_slot_from_ledger( /// Stricter fork-choice assertions are covered by separate partition tests. #[test] #[serial] -fn test_cluster_partition() { +fn test_cluster_partition_1_1_1() { + run_test_cluster_partition(3, false); +} + +/// Partition/recovery test for Alpenglow +#[test] +#[serial] +fn test_cluster_partition_1_1_1_alpenglow() { + run_test_cluster_partition(3, true); +} + +fn run_test_cluster_partition(num_partitions: usize, is_alpenglow: bool) { let empty = |_: &mut LocalCluster, _: &mut ()| {}; let on_partition_resolved = |cluster: &mut LocalCluster, _: &mut ()| { cluster.check_for_new_roots(16, "PARTITION_TEST", SocketAddrSpace::Unspecified); }; + let partition_sizes = vec![1; num_partitions]; run_cluster_partition( - &[1, 1, 1], + &partition_sizes, None, (), empty, @@ -4262,6 +4278,7 @@ fn test_cluster_partition() { None, false, vec![], + is_alpenglow, ) } @@ -4311,7 +4328,7 @@ fn test_leader_failure_4() { .config .validator_exit, &local.funding_keypair, - &local.connection_cache, + &cluster_tests::TpuSender::new(), num_nodes, config.ticks_per_slot * config.poh_config.target_tick_duration.as_millis() as u64, SocketAddrSpace::Unspecified, @@ -4391,10 +4408,10 @@ fn test_slot_hash_expiry() { // Let A run for a while until we get to the common ancestor info!("Letting A run until common_ancestor_slot"); loop { - if let Some((last_vote, _)) = last_vote_in_tower(&a_ledger_path, &a_pubkey) { - if last_vote >= common_ancestor_slot { - break; - } + if let Some((last_vote, _)) = last_vote_in_tower(&a_ledger_path, &a_pubkey) + && last_vote >= common_ancestor_slot + { + break; } sleep(Duration::from_millis(100)); } @@ -4472,14 +4489,14 @@ fn test_slot_hash_expiry() { let last_vote = wait_for_last_vote_in_tower_to_land_in_ledger(&b_ledger_path, &b_pubkey).unwrap(); let mut ancestors = AncestorIterator::new(last_vote, &blockstore); - if let Some(index) = ancestors.position(|x| x == common_ancestor_slot) { - if index > 7 { - info!( - "B has forked for enough lockout: {:?}", - AncestorIterator::new(last_vote, &blockstore).collect::>() - ); - break; - } + if let Some(index) = ancestors.position(|x| x == common_ancestor_slot) + && index > 7 + { + info!( + "B has forked for enough lockout: {:?}", + AncestorIterator::new(last_vote, &blockstore).collect::>() + ); + break; } sleep(Duration::from_millis(1000)); } @@ -4504,7 +4521,6 @@ fn test_slot_hash_expiry() { cluster_tests::check_for_new_roots( 16, &[cluster.get_contact_info(&a_pubkey).unwrap().clone()], - &cluster.connection_cache, "test_slot_hashes_expiry", ); } @@ -4755,7 +4771,7 @@ fn test_duplicate_with_pruned_ancestor() { ); let shreds = entries_to_test_shreds(&entries, last_majority_vote, last_minority_vote, true, 0); - our_blockstore.insert_shreds(shreds, None, false).unwrap(); + our_blockstore.insert_shreds(shreds, false).unwrap(); } // Actual test, `our_node` will replay the minority fork, then the majority fork which will @@ -4775,7 +4791,6 @@ fn test_duplicate_with_pruned_ancestor() { cluster_tests::check_for_new_roots( 16, &[cluster.get_contact_info(&our_node_pubkey).unwrap().clone()], - &cluster.connection_cache, "test_duplicate_with_pruned_ancestor", ); } @@ -4865,10 +4880,9 @@ fn test_boot_from_local_state() { let bank_snapshot = loop { if let Some(bank_snapshot) = snapshot_utils::get_highest_bank_snapshot(&validator2_config.bank_snapshots_dir) + && bank_snapshot.slot > incremental_snapshot_archive.slot() { - if bank_snapshot.slot > incremental_snapshot_archive.slot() { - break bank_snapshot; - } + break bank_snapshot; } assert!( timer.elapsed() < Duration::from_secs(30), @@ -5203,11 +5217,16 @@ fn test_duplicate_shreds_switch_failure() { // Ensure all the slots <= dup_slot are also full so we know we can replay up to dup_slot // on restart info!("Waiting to receive and replay entire duplicate fork with tip {dup_slot}"); + let start = Instant::now(); loop { let duplicate_fork_validator_blockstore = open_blockstore(ledger_path); if let Some(frozen_hash) = duplicate_fork_validator_blockstore.get_bank_hash(dup_slot) { return frozen_hash; } + assert!( + start.elapsed() < Duration::from_secs(60), + "Timed out waiting to receive and replay duplicate fork with tip {dup_slot}", + ); sleep(Duration::from_millis(1000)); } } @@ -5378,7 +5397,7 @@ fn test_duplicate_shreds_switch_failure() { let leader_schedule = create_custom_leader_schedule(validator_to_slots.into_iter()); // 1) Set up the cluster - let (duplicate_slot_sender, duplicate_slot_receiver) = unbounded(); + let (duplicate_slot_sender, duplicate_slot_receiver) = bounded(1024); let validator_configs = validator_keypairs .into_iter() .map(|(validator_keys, in_genesis)| { @@ -5543,16 +5562,16 @@ fn test_duplicate_shreds_switch_failure() { &target_switch_fork_validator_ledger_path, &target_switch_fork_validator_pubkey, ); - if let Some(latest_vote_slot) = last_vote { - if latest_vote_slot > dup_slot { - let blockstore = open_blockstore(&target_switch_fork_validator_ledger_path); - let ancestor_slots: HashSet = - AncestorIterator::new_inclusive(latest_vote_slot, &blockstore).collect(); - assert!(ancestor_slots.contains(&latest_vote_slot)); - assert!(ancestor_slots.contains(&0)); - assert!(!ancestor_slots.contains(&dup_slot)); - break; - } + if let Some(latest_vote_slot) = last_vote + && latest_vote_slot > dup_slot + { + let blockstore = open_blockstore(&target_switch_fork_validator_ledger_path); + let ancestor_slots: HashSet = + AncestorIterator::new_inclusive(latest_vote_slot, &blockstore).collect(); + assert!(ancestor_slots.contains(&latest_vote_slot)); + assert!(ancestor_slots.contains(&0)); + assert!(!ancestor_slots.contains(&dup_slot)); + break; } sleep(Duration::from_millis(1000)); } @@ -5639,7 +5658,7 @@ fn test_randomly_mixed_block_verification_methods_between_bootstrap_and_not() { num_nodes, HashSet::new(), SocketAddrSpace::Unspecified, - &local.connection_cache, + &cluster_tests::TpuSender::new(), ); } @@ -5673,7 +5692,7 @@ fn test_randomly_mixed_block_production_methods_between_bootstrap_and_not() { num_nodes, HashSet::new(), SocketAddrSpace::Unspecified, - &local.connection_cache, + &cluster_tests::TpuSender::new(), ); } @@ -5734,10 +5753,9 @@ fn test_invalid_forks_persisted_on_restart() { loop { if let Some(slot) = wait_for_last_vote_in_tower_to_land_in_ledger(&target_ledger_path, &target_pubkey) + && slot > dup_slot { - if slot > dup_slot { - break; - } + break; } assert!( @@ -5856,7 +5874,7 @@ fn test_alpenglow_nodes_basic(num_nodes: usize, num_offline_nodes: usize) { stakers_slot_offset: MINIMUM_SLOTS_PER_EPOCH * 2, poh_config: PohConfig { target_tick_duration: PohConfig::default().target_tick_duration, - hashes_per_tick: Some(clock::DEFAULT_HASHES_PER_TICK), + hashes_per_tick: None, target_tick_count: None, }, ..ClusterConfig::default() @@ -5871,7 +5889,7 @@ fn test_alpenglow_nodes_basic(num_nodes: usize, num_offline_nodes: usize) { num_nodes, HashSet::new(), SocketAddrSpace::Unspecified, - &cluster.connection_cache, + &cluster_tests::TpuSender::new(), ); if num_offline_nodes > 0 { @@ -5957,7 +5975,7 @@ fn test_restart_node_alpenglow() { cluster_tests::send_many_transactions( &cluster.entry_point_info, &cluster.funding_keypair, - &cluster.connection_cache, + &cluster_tests::TpuSender::new(), 10, 1, ); @@ -6197,8 +6215,7 @@ fn test_alpenglow_migration( let node_stakes = vec![DEFAULT_NODE_STAKE; num_nodes]; // We want the epochs to be as short as possible to reduce test time without being flaky. - // We start the migration at an offset of 32, so use 64 as the epoch length. - let slots_per_epoch = 2 * MINIMUM_SLOTS_PER_EPOCH; + let slots_per_epoch = 4 * MINIMUM_SLOTS_PER_EPOCH; assert!(slots_per_epoch > MIGRATION_SLOT_OFFSET); let mut cluster_config = ClusterConfig { validator_configs: make_identical_validator_configs(&validator_config, num_nodes), @@ -6210,6 +6227,15 @@ fn test_alpenglow_migration( skip_warmup_slots: false, ..ClusterConfig::default() }; + // Seed the feature account as pending activation. The runtime activates it at the next epoch + // boundary without needing the Alpenglow feature authority keypair. + cluster_config.additional_accounts.push(( + agave_feature_set::alpenglow::id(), + solana_feature_gate_interface::create_account( + &solana_feature_gate_interface::Feature::default(), + 1, + ), + )); // Create local cluster with alpenglow accounts but feature not activated let cluster = LocalCluster::new(&mut cluster_config, SocketAddrSpace::Unspecified); @@ -6220,47 +6246,21 @@ fn test_alpenglow_migration( .map(|v| v.info.keypair.clone()) .collect(); - // Send feature activation transaction - info!("Sending feature activation transaction"); let client = RpcClient::new_socket_with_commitment( cluster.entry_point_info.rpc().unwrap(), CommitmentConfig::processed(), ); - let faucet_keypair = &cluster.funding_keypair; - let feature_keypair = &*agave_feature_set::alpenglow::TEST_KEYPAIR; - let blockhash = client.get_latest_blockhash().unwrap(); - let lamports = client - .get_minimum_balance_for_rent_exemption(solana_feature_gate_interface::Feature::size_of()) - .unwrap(); - - let activation_message = solana_message::Message::new( - &solana_feature_gate_interface::activate_with_lamports( - &agave_feature_set::alpenglow::id(), - &faucet_keypair.pubkey(), - lamports, - ), - Some(&faucet_keypair.pubkey()), - ); - let activation_tx = solana_transaction::Transaction::new( - &[&feature_keypair, &faucet_keypair], - activation_message, - blockhash, - ); - - client.send_and_confirm_transaction(&activation_tx).unwrap(); - info!("Feature activation transaction confirmed"); // Monitor for feature activation let activation_slot; loop { - if let Ok(account) = client.get_account(&agave_feature_set::alpenglow::id()) { - if let Some(feature) = solana_feature_gate_interface::from_account(&account) { - if let Some(slot) = feature.activated_at { - activation_slot = slot; - info!("Feature activated at slot {slot}"); - break; - } - } + if let Ok(account) = client.get_account(&agave_feature_set::alpenglow::id()) + && let Some(feature) = solana_feature_gate_interface::from_account(&account) + && let Some(slot) = feature.activated_at + { + activation_slot = slot; + info!("Feature activated at slot {slot}"); + break; } std::thread::sleep(std::time::Duration::from_millis(100)); } diff --git a/logger/Cargo.toml b/logger/Cargo.toml index 946d8fffc08..56c6b3df98c 100644 --- a/logger/Cargo.toml +++ b/logger/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] name = "agave_logger" @@ -25,3 +27,6 @@ log = { workspace = true } [target."cfg(unix)".dependencies] libc = { workspace = true } signal-hook = { workspace = true } + +[lints] +workspace = true diff --git a/math-utils/Cargo.toml b/math-utils/Cargo.toml index a5666457841..e7575a60d9a 100644 --- a/math-utils/Cargo.toml +++ b/math-utils/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] diff --git a/measure/Cargo.toml b/measure/Cargo.toml index e933420e0db..706ba201313 100644 --- a/measure/Cargo.toml +++ b/measure/Cargo.toml @@ -11,6 +11,11 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] + +[lints] +workspace = true diff --git a/merkle-tree/Cargo.toml b/merkle-tree/Cargo.toml index 1b31febea98..e8cf2d39faa 100644 --- a/merkle-tree/Cargo.toml +++ b/merkle-tree/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -26,3 +28,6 @@ solana-sha256-hasher = { workspace = true } [dev-dependencies] hex = { workspace = true } + +[lints] +workspace = true diff --git a/metrics/Cargo.toml b/metrics/Cargo.toml index 7d313da1e7d..f48a661c904 100644 --- a/metrics/Cargo.toml +++ b/metrics/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] name = "solana_metrics" @@ -39,3 +41,6 @@ solana-sha256-hasher = { workspace = true, features = ["sha2"] } [[bench]] name = "metrics" harness = false + +[lints] +workspace = true diff --git a/metrics/scripts/grafana-provisioning/dashboards/cluster-monitor.json b/metrics/scripts/grafana-provisioning/dashboards/cluster-monitor.json index c33ece2f2eb..aa11ad38752 100644 --- a/metrics/scripts/grafana-provisioning/dashboards/cluster-monitor.json +++ b/metrics/scripts/grafana-provisioning/dashboards/cluster-monitor.json @@ -13015,376 +13015,6 @@ "alignLevel": null } }, - { - "collapsed": false, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 125 - }, - "id": 75, - "panels": [], - "title": "Bench TPS", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fill": 1, - "gridPos": { - "h": 5, - "w": 7, - "x": 0, - "y": 126 - }, - "id": 76, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": false, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "measurement": "cluster_info-vote-count", - "orderByTime": "ASC", - "policy": "autogen", - "query": "SELECT balance FROM \"$testnet\".\"autogen\".\"bench-tps-lamport_balance\" WHERE $timeFilter fill(null)\n\n\n\n", - "rawQuery": true, - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "count" - ], - "type": "field" - }, - { - "params": [], - "type": "sum" - } - ] - ], - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Bench TPS Token Balance", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": "0", - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": false - } - ], - "yaxis": { - "align": true, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fill": 1, - "gridPos": { - "h": 5, - "w": 7, - "x": 7, - "y": 126 - }, - "id": 77, - "legend": { - "alignAsTable": false, - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "measurement": "cluster_info-vote-count", - "orderByTime": "ASC", - "policy": "autogen", - "query": "SELECT mean(\"duration\") as \"Generation Time\" FROM \"$testnet\".\"autogen\".\"bench-tps-generate_txs\" WHERE $timeFilter GROUP BY time(1s)\n\n\n\n\n", - "rawQuery": true, - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "count" - ], - "type": "field" - }, - { - "params": [], - "type": "sum" - } - ] - ], - "tags": [] - }, - { - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "orderByTime": "ASC", - "policy": "default", - "query": "SELECT mean(\"duration\") as \"Transmit Time\" FROM \"$testnet\".\"autogen\".\"bench-tps-do_tx_transfers\" WHERE $timeFilter GROUP BY time(1s)", - "rawQuery": true, - "refId": "B", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "tags": [] - }, - { - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "orderByTime": "ASC", - "policy": "default", - "query": "SELECT mean(\"duration\") as \"Get Blockhash\" FROM \"$testnet\".\"autogen\".\"bench-tps-get_blockhash\" WHERE $timeFilter GROUP BY time(1s)", - "rawQuery": true, - "refId": "C", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "tags": [] - }, - { - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "orderByTime": "ASC", - "policy": "default", - "query": "SELECT mean(\"duration\") as \"Get Balance\" FROM \"$testnet\".\"autogen\".\"bench-tps-get_balance\" WHERE $timeFilter GROUP BY time(1s)", - "rawQuery": true, - "refId": "D", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Bench TPS Transaction Activity", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "µs", - "label": null, - "logBase": 1, - "max": null, - "min": "0", - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": false - } - ], - "yaxis": { - "align": true, - "alignLevel": null - } - }, { "columns": [], "datasource": "$datasource", diff --git a/metrics/src/metrics.rs b/metrics/src/metrics.rs index a67127238d7..6c986f8c571 100644 --- a/metrics/src/metrics.rs +++ b/metrics/src/metrics.rs @@ -90,7 +90,7 @@ impl InfluxDbMetricsWriter { let write_url = format!( "{}/write?db={}&u={}&p={}&precision=n", - &config.host, &config.db, &config.username, &config.password + config.host, config.db, config.username, config.password ); Ok(write_url) @@ -115,7 +115,7 @@ pub fn serialize_points(points: &Vec, host_id: &str) -> String { } let mut line = String::with_capacity(len); for point in points { - let _ = write!(line, "{},host_id={}", &point.name, host_id); + let _ = write!(line, "{},host_id={}", point.name, host_id); for (name, value) in point.tags.iter() { let _ = write!(line, ",{name}={value}"); } diff --git a/multinode-demo/bench-tps.sh b/multinode-demo/bench-tps.sh deleted file mode 100755 index 32f15810931..00000000000 --- a/multinode-demo/bench-tps.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -set -e - -here=$(dirname "$0") -# shellcheck source=multinode-demo/common.sh -source "$here"/common.sh - -usage() { - if [[ -n $1 ]]; then - echo "$*" - echo - fi - echo "usage: $0 [extra args]" - echo - echo " Run bench-tps " - echo - echo " extra args: additional arguments are passed along to solana-bench-tps" - echo - exit 1 -} - -args=("$@") -default_arg --url "http://127.0.0.1:8899" -default_arg --faucet "127.0.0.1:9900" -default_arg --duration 90 -default_arg --tx-count 50000 -default_arg --thread-batch-sleep-ms 0 -default_arg --bind-address "127.0.0.1" -default_arg --client-node-id "${SOLANA_CONFIG_DIR}/bootstrap-validator/identity.json" - -$solana_bench_tps "${args[@]}" diff --git a/multinode-demo/bootstrap-validator.sh b/multinode-demo/bootstrap-validator.sh index 6b231383e1d..fa75b51a88d 100755 --- a/multinode-demo/bootstrap-validator.sh +++ b/multinode-demo/bootstrap-validator.sh @@ -144,6 +144,7 @@ args+=( --no-wait-for-vote-to-start-leader --full-rpc-api --allow-private-addr + --no-xdp ) default_arg --gossip-port 8001 default_arg --log - diff --git a/multinode-demo/common.sh b/multinode-demo/common.sh index 67e44e3cf6c..5abc5700cb4 100644 --- a/multinode-demo/common.sh +++ b/multinode-demo/common.sh @@ -5,10 +5,12 @@ # The following directive disable complaints about unused variables in this # file: # shellcheck disable=2034 +# shellcheck shell=bash # here="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. || exit 1; pwd)" -# shellcheck source=net/common.sh +# shellcheck disable=SC1091 +# shellcheck source=../net/common.sh source "$here"/net/common.sh prebuild= @@ -34,7 +36,7 @@ else declare program="$1" declare crate="$program" declare manifest_path - if [[ $program == "bench-tps" || $program == "ledger-tool" ]]; then + if [[ $program == "ledger-tool" ]]; then manifest_path="--manifest-path $here/dev-bins/Cargo.toml" fi if [[ -z $program ]]; then @@ -63,7 +65,7 @@ else } fi -solana_bench_tps=$(solana_program bench-tps) +solana_transaction_bench=${SOLANA_TRANSACTION_BENCH:-solana-transaction-bench} solana_faucet=$(solana_program faucet) agave_validator=$(solana_program validator) solana_genesis=$(solana_program genesis) diff --git a/multinode-demo/txs-bench.sh b/multinode-demo/txs-bench.sh new file mode 100755 index 00000000000..95ea60a5067 --- /dev/null +++ b/multinode-demo/txs-bench.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +set -e + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=multinode-demo/common.sh +source "$here"/common.sh + +program="${solana_transaction_bench}" +default_staked_identity_file_count=1 + +usage() { + if [[ -n $1 ]]; then + echo "$*" + echo + fi + cat < + + Examples: + $0 --duration 30 --target-tps 5000 + $0 --num-payers 1024 --send-fanout 2 ws-leader-tracker + +EOF + exit 1 +} + +contains_arg() { + declare name=$1 + shift + + for arg in "$@"; do + if [[ $arg = "$name" || $arg = "$name="* ]]; then + return 0 + fi + done + + return 1 +} + +contains_url_arg() { + for arg in "$@"; do + if [[ $arg = --url || $arg = --url=* || $arg = -u || $arg = -u?* ]]; then + return 0 + fi + done + + return 1 +} + +add_default_arg() { + declare -n target_args=$1 + declare name=$2 + declare value=$3 + + if ! contains_arg "$name" "${target_args[@]}"; then + target_args+=("$name" "$value") + fi +} + +get_rpc_url() { + declare arg + declare -i expect_value=0 + + for arg in "${global_args[@]}"; do + if ((expect_value)); then + printf '%s' "$arg" + return 0 + fi + case "$arg" in + --url=*) + printf '%s' "${arg#--url=}" + return 0 + ;; + -u?*) + printf '%s' "${arg#-u}" + return 0 + ;; + --url | -u) + expect_value=1 + ;; + esac + done + + printf '%s' 'http://127.0.0.1:8899' +} + +# Query getClusterNodes over RPC and return host:port for the bootstrap validator's +# QUIC TPU (tpuQuic), falling back to UDP tpu. Requires curl and jq. +resolve_pinned_leader_target() { + declare rpc_url=$1 + declare identity="${SOLANA_CONFIG_DIR}/bootstrap-validator/identity.json" + declare identity_pubkey= + declare nodes_json tpu + + if [[ -f $identity ]]; then + identity_pubkey=$($solana_keygen pubkey "$identity" 2> /dev/null) || identity_pubkey= + fi + + if ! command -v jq > /dev/null 2>&1; then + echo "$0: jq is required to parse getClusterNodes (e.g. brew install jq)" >&2 + return 1 + fi + + if ! nodes_json=$(curl -sf -X POST -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"getClusterNodes"}' \ + "$rpc_url"); then + echo "$0: getClusterNodes request to $rpc_url failed (is the validator RPC up?)" >&2 + return 1 + fi + + if [[ -n $identity_pubkey ]]; then + tpu=$(echo "$nodes_json" | jq -r --arg pk "$identity_pubkey" ' + [.result[] | select(.pubkey == $pk) | .tpuQuic // .tpu // empty][0] // empty + ') + fi + + if [[ -z $tpu || $tpu = null ]]; then + tpu=$(echo "$nodes_json" | jq -r ' + [.result[] | .tpuQuic // .tpu // empty][0] // empty + ') + fi + + if [[ -z $tpu || $tpu = null ]]; then + echo "$0: getClusterNodes returned no tpuQuic/tpu address" >&2 + return 1 + fi + + printf '%s' "$tpu" +} + +global_args=() +run_args=() +leader_tracker_args=() + +while [[ -n $1 ]]; do + case "$1" in + -h | --help) + usage + ;; + --url | -u | --commitment-config | --authority) + [[ -n ${2:-} ]] || usage "Missing value for $1" + global_args+=("$1" "$2") + shift 2 + ;; + --url=* | --commitment-config=* | --authority=* | -u?*) + global_args+=("$1") + shift + ;; + --validate-accounts | --mock-rpc) + global_args+=("$1") + shift + ;; + run | read-accounts-run | write-accounts) + usage "Do not pass a solana-transaction-bench subcommand; this wrapper always uses run." + ;; + pinned-leader-tracker | legacy-leader-tracker | ws-leader-tracker | \ + yellowstone-leader-tracker | custom-leader-tracker) + leader_tracker_args=("$@") + break + ;; + *) + run_args+=("$1") + shift + ;; + esac +done + +if ! command -v "$program" > /dev/null 2>&1; then + echo "$program not found." + echo "Install it with: cargo install solana-transaction-bench" + echo "Or set SOLANA_TRANSACTION_BENCH=/path/to/solana-transaction-bench." + exit 1 +fi + +if ! contains_url_arg "${global_args[@]}"; then + global_args+=(--url "http://127.0.0.1:8899") +fi +add_default_arg global_args --authority "${SOLANA_CONFIG_DIR}/faucet.json" + +add_default_arg run_args --num-payers 1024 +add_default_arg run_args --payer-account-balance 10SOL +add_default_arg run_args --duration 90 +add_default_arg run_args --bind "127.0.0.1:0" +add_default_arg run_args --num-max-open-connections 8 +add_default_arg run_args --workers-pull-size 8 +add_default_arg run_args --lamports-to-transfer 65536 +add_default_arg run_args --num-send-instructions-per-tx 1 +add_default_arg run_args --send-fanout 1 + +for ((i = 0; i < default_staked_identity_file_count; i++)); do + run_args+=(--staked-identity-file "${SOLANA_CONFIG_DIR}/bootstrap-validator/identity.json") +done + +add_default_arg run_args --transfer-tx-cu-budget 400 + +if [[ ${#leader_tracker_args[@]} -eq 0 ]]; then + rpc_url=$(get_rpc_url) + pinned_leader_target=$(resolve_pinned_leader_target "$rpc_url") || exit 1 + leader_tracker_args=(pinned-leader-tracker "$pinned_leader_target") +fi + +"$program" "${global_args[@]}" run "${run_args[@]}" "${leader_tracker_args[@]}" diff --git a/multinode-demo/validator.sh b/multinode-demo/validator.sh index 5ac5443a633..07bd51a86fb 100755 --- a/multinode-demo/validator.sh +++ b/multinode-demo/validator.sh @@ -10,12 +10,15 @@ args=( --max-genesis-archive-unpacked-size 1073741824 --no-poh-speed-test --no-os-network-limits-test + --no-xdp ) airdrops_enabled=1 node_sol=500 # 500 SOL: number of SOL to airdrop the node for transaction fees and vote account rent exemption (ignored if airdrops_enabled=0) +stake_sol= # If set, create a stake account funded with this many SOL and delegate it to the validator's vote account label= identity= vote_account= +stake_account= no_restart=0 gossip_entrypoint= ledger_dir= @@ -29,7 +32,7 @@ usage() { usage: $0 [OPTIONS] [cluster entry point hostname] -Start a validator with no stake +Start a validator (optionally staked via --stake-sol) OPTIONS: --ledger PATH - store ledger under this PATH @@ -37,6 +40,8 @@ OPTIONS: --label LABEL - Append the given label to the configuration files, useful when running multiple validators in the same workspace --node-sol SOL - Number of SOL this node has been funded from the genesis config (default: $node_sol) + --stake-sol SOL - If set, create a stake account funded with this many SOL and delegate it to the validator's vote account + --stake-account PATH - Path to stake-account keypair file (default: /stake-account.json) --no-voting - start node without vote signer --rpc-port port - custom RPC port for this node --no-restart - do not restart the node if it exits @@ -61,6 +66,12 @@ while [[ -n $1 ]]; do elif [[ $1 = --node-sol ]]; then node_sol="$2" shift 2 + elif [[ $1 = --stake-sol ]]; then + stake_sol="$2" + shift 2 + elif [[ $1 = --stake-account ]]; then + stake_account=$2 + shift 2 elif [[ $1 = --no-airdrop ]]; then airdrops_enabled=0 shift @@ -242,6 +253,7 @@ faucet_address="${gossip_entrypoint%:*}":9900 : "${identity:=$ledger_dir/identity.json}" : "${vote_account:=$ledger_dir/vote-account.json}" +: "${stake_account:=$ledger_dir/stake-account.json}" : "${authorized_withdrawer:=$ledger_dir/authorized-withdrawer.json}" default_arg --entrypoint "$gossip_entrypoint" @@ -314,6 +326,24 @@ setup_validator_accounts() { echo "Creating validator vote account" wallet create-vote-account "$vote_account" "$identity" "$authorized_withdrawer" || return $? + + if [[ -n $stake_sol ]] && (( stake_sol > 0 )); then + if ((airdrops_enabled)); then + echo "Transferring $stake_sol SOL to fund stake account:" + ( + set -x + $solana_cli \ + --keypair "$SOLANA_CONFIG_DIR/faucet.json" --url "$rpc_url" \ + transfer --allow-unfunded-recipient "$identity" "$stake_sol" + ) || return $? + fi + echo "Creating stake account" + wallet create-stake-account "$stake_account" "$stake_sol" || return $? + echo "Delegating stake" + declare vote_pubkey + vote_pubkey=$($solana_keygen pubkey "$vote_account") || return $? + wallet delegate-stake "$stake_account" "$vote_pubkey" || return $? + fi fi echo "Validator vote account configured" @@ -329,6 +359,9 @@ rpc_url=$($solana_gossip --allow-private-addr rpc-url --timeout 180 --entrypoint [[ -r "$identity" ]] || $solana_keygen new --no-passphrase -so "$identity" [[ -r "$vote_account" ]] || $solana_keygen new --no-passphrase -so "$vote_account" [[ -r "$authorized_withdrawer" ]] || $solana_keygen new --no-passphrase -so "$authorized_withdrawer" +if [[ -n $stake_sol ]] && (( stake_sol > 0 )); then + [[ -r "$stake_account" ]] || $solana_keygen new --no-passphrase -so "$stake_account" +fi setup_validator_accounts "$node_sol" diff --git a/net-utils/Cargo.toml b/net-utils/Cargo.toml index c5517d6983e..ce5d90c2244 100644 --- a/net-utils/Cargo.toml +++ b/net-utils/Cargo.toml @@ -7,10 +7,12 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] name = "solana_net_utils" @@ -21,10 +23,12 @@ dev-context-only-utils = ["dep:anyhow", "dep:pcap-file", "dep:hxdmp"] shuttle-test = ["dep:shuttle", "solana-svm-type-overrides/shuttle-test"] [dependencies] +agave-xdp = { workspace = true } anyhow = { workspace = true, optional = true } bincode = { workspace = true } bytes = { workspace = true } cfg-if = { workspace = true } +crossbeam-channel = { workspace = true } dashmap = { workspace = true, features = ["raw-api"] } hxdmp = { version = "0.2.1", optional = true } itertools = { workspace = true } diff --git a/net-utils/src/ip_echo_client.rs b/net-utils/src/ip_echo_client.rs index 0b69224ecda..392c1726e86 100644 --- a/net-utils/src/ip_echo_client.rs +++ b/net-utils/src/ip_echo_client.rs @@ -178,10 +178,7 @@ pub(crate) async fn verify_all_reachable_tcp( .iter() .map(|l| l.local_addr().expect("Sockets should be bound").port()) .collect_vec(); - info!( - "Checking that tcp ports {:?} are reachable from {:?}", - &ports, ip_echo_server_addr - ); + info!("Checking that tcp ports {ports:?} are reachable from {ip_echo_server_addr:?}"); // make request to the echo server let _ = ip_echo_server_request_with_binding( diff --git a/net-utils/src/lib.rs b/net-utils/src/lib.rs index 321f9e51f4d..559e58361aa 100644 --- a/net-utils/src/lib.rs +++ b/net-utils/src/lib.rs @@ -5,6 +5,7 @@ pub mod banlist; mod ip_echo_client; mod ip_echo_server; pub mod multihomed_sockets; +pub mod pinned_xdp_sender; pub mod socket_addr_space; pub mod sockets; #[cfg(any(target_os = "android", target_os = "windows"))] @@ -22,6 +23,7 @@ pub use { ip_echo_server::{ DEFAULT_IP_ECHO_SERVER_THREADS, IpEchoServer, MAX_PORT_COUNT_PER_MESSAGE, ip_echo_server, }, + pinned_xdp_sender::PinnedXdpSender, socket_addr_space::SocketAddrSpace, }; use { @@ -196,16 +198,21 @@ pub fn parse_port_range(port_range: &str) -> Option { Some((start_port, end_port)) } -fn select_ipv4(host: &str, mut ips: impl Iterator) -> Result { - let Some(first_ip) = ips.next() else { +fn select_ipv4( + host: &str, + mut values: impl Iterator, + mut ip_addr: impl FnMut(&T) -> IpAddr, +) -> Result { + let Some(first_value) = values.next() else { return Err(format!("Unable to resolve host: {host}")); }; - if first_ip.is_ipv4() { - return Ok(first_ip); + if ip_addr(&first_value).is_ipv4() { + return Ok(first_value); } - ips.find(IpAddr::is_ipv4) + values + .find(|value| ip_addr(value).is_ipv4()) .ok_or_else(|| format!("IPv6 addresses are not supported: {host}")) } @@ -222,12 +229,12 @@ pub fn parse_host(host: &str) -> Result { } // Next, check to see if it resolves to an IPv4 address - let mut ips = (host, 0) + let ips = (host, 0) .to_socket_addrs() .map_err(|err| err.to_string())? .map(|socket_address| socket_address.ip()); - select_ipv4(host, &mut ips) + select_ipv4(host, ips, |ip| *ip) } pub fn is_host(string: String) -> Result<(), String> { @@ -235,15 +242,10 @@ pub fn is_host(string: String) -> Result<(), String> { } pub fn parse_host_port(host_port: &str) -> Result { - let addrs: Vec<_> = host_port + let addrs = host_port .to_socket_addrs() - .map_err(|err| format!("Unable to resolve host {host_port}: {err}"))? - .collect(); - if addrs.is_empty() { - Err(format!("Unable to resolve host: {host_port}")) - } else { - Ok(addrs[0]) - } + .map_err(|err| format!("Unable to resolve host {host_port}: {err}"))?; + select_ipv4(host_port, addrs, SocketAddr::ip) } pub fn is_host_port(string: String) -> Result<(), String> { @@ -396,7 +398,8 @@ mod tests { assert_eq!( select_ipv4( "ipv6-only.test", - [IpAddr::V6(std::net::Ipv6Addr::LOCALHOST)].into_iter() + [IpAddr::V6(std::net::Ipv6Addr::LOCALHOST)].into_iter(), + |ip| *ip, ) .unwrap_err(), "IPv6 addresses are not supported: ipv6-only.test", @@ -409,6 +412,7 @@ mod tests { IpAddr::V4(Ipv4Addr::LOCALHOST), ] .into_iter(), + |ip| *ip, ) .unwrap(), IpAddr::V4(Ipv4Addr::LOCALHOST), @@ -421,6 +425,10 @@ mod tests { parse_host_port("localhost").unwrap_err(); parse_host_port("127.0.0.0:1234").unwrap(); parse_host_port("127.0.0.0").unwrap_err(); + assert_eq!( + parse_host_port("[2001:db8:abcd:42::dead:beef]:1234").unwrap_err(), + "IPv6 addresses are not supported: [2001:db8:abcd:42::dead:beef]:1234", + ); } #[test] diff --git a/turbine/src/xdp_sender.rs b/net-utils/src/pinned_xdp_sender.rs similarity index 73% rename from turbine/src/xdp_sender.rs rename to net-utils/src/pinned_xdp_sender.rs index 1f6d734a731..54ee6251996 100644 --- a/turbine/src/xdp_sender.rs +++ b/net-utils/src/pinned_xdp_sender.rs @@ -1,20 +1,19 @@ -//! This module defines [`XdpSender`] which is a convenience wrapper around -//! [`agave_xdp::transmitter::XdpSender`] for the case when source address is fixed for all -//! items like it is in turbine. +//! This module defines [`PinnedXdpSender`] which is a convenience wrapper around +//! [`agave_xdp::transmitter::XdpSender`] for the case when source address is fixed. use { agave_xdp::transmitter as tx, bytes::Bytes, crossbeam_channel::TrySendError, std::net::SocketAddrV4, }; -/// [`XdpSender`] is a structure that simplifies sending packets over XDP with `XdpSender` +/// [`PinnedXdpSender`] simplifies sending packets over XDP /// when source address is fixed for all items. #[derive(Clone)] -pub struct XdpSender { +pub struct PinnedXdpSender { sender: tx::XdpSender, src_addr: SocketAddrV4, } -impl XdpSender { +impl PinnedXdpSender { pub fn new(sender: tx::XdpSender, src_addr: SocketAddrV4) -> Self { Self { sender, src_addr } } diff --git a/net-utils/src/tooling_for_tests.rs b/net-utils/src/tooling_for_tests.rs index 93f0b189bbb..a15cbf3e963 100644 --- a/net-utils/src/tooling_for_tests.rs +++ b/net-utils/src/tooling_for_tests.rs @@ -32,10 +32,7 @@ impl Iterator for PcapReader { fn next(&mut self) -> Option { loop { - let block = match self.reader.next_block() { - Some(block) => block.ok()?, - None => return None, - }; + let block = self.reader.next_block()?.ok()?; let data = match block { pcap_file::pcapng::Block::Packet(ref block) => { &block.data[0..block.original_len as usize] diff --git a/net/README.md b/net/README.md deleted file mode 120000 index 46a6d5baa9a..00000000000 --- a/net/README.md +++ /dev/null @@ -1 +0,0 @@ -../docs/src/clusters/testnet.md \ No newline at end of file diff --git a/net/README.md b/net/README.md new file mode 100644 index 00000000000..1c7419ee72b --- /dev/null +++ b/net/README.md @@ -0,0 +1,3 @@ +# Network Management + +See the [Test Network Management](https://docs.anza.xyz/clusters/testnet) guide. diff --git a/net/net.sh b/net/net.sh index 69d0cd4470b..0b3ae2d08f3 100755 --- a/net/net.sh +++ b/net/net.sh @@ -15,19 +15,20 @@ usage() { fi CLIENT_OPTIONS=$(cat << EOM -c clientType=numClients=extraArgs - Number of clientTypes to start. This options can be specified - more than once. Defaults to bench-tps for all clients if not - specified. + more than once. Defaults to transaction-bench for all clients + if not specified. Valid client types are: idle - bench-tps + transaction-bench User can optionally provide extraArgs that are transparently supplied to the client program as command line parameters. + extraArgs use solana-transaction-bench argument names. For example, - -c bench-tps=2="--tx_count 25000" - This will start 2 bench-tps clients, and supply "--tx_count 25000" - to the bench-tps client. + -c transaction-bench=2="--target-tps 5000 ws-leader-tracker" + This will start 2 solana-transaction-bench clients, and supply + "--target-tps 5000 ws-leader-tracker" to each of them. --use-unstaked-connection - Use unstaked connection. By default, staked connection with - bootstrap node credendials is used. + bootstrap node credentials is used. EOM ) cat < /dev/null } @@ -297,6 +295,7 @@ startBootstrapLeader() { declare ipAddress=$1 declare nodeIndex="$2" declare logFile="$3" + declare alpenglow="$4" echo "--- Starting bootstrap validator: $ipAddress" echo "start log: $logFile" @@ -322,7 +321,6 @@ startBootstrapLeader() { \"$internalNodesStakeLamports\" \ \"$internalNodesLamports\" \ $nodeIndex \ - ${#clientIpList[@]} \"$benchTpsExtraArgs\" \ \"$genesisOptions\" \ \"$maybeNoSnapshot $maybeSkipLedgerVerify $maybeLimitLedgerSize $maybeWaitForSupermajority $maybeAccountsDbSkipShrink $maybeSkipRequireTower\" \ \"$maybeWarpSlot\" \ @@ -332,6 +330,7 @@ startBootstrapLeader() { \"$TMPFS_ACCOUNTS\" \ \"$disableQuic\" \ \"$enableUdp\" \ + \"$alpenglow\" \ " ) >> "$logFile" 2>&1 || { @@ -345,6 +344,7 @@ startNode() { declare ipAddress=$1 declare nodeType=$2 declare nodeIndex="$3" + declare alpenglow="$4" initLogDir declare logFile="$netLogDir/validator-$ipAddress.log" @@ -395,7 +395,6 @@ startNode() { \"$internalNodesStakeLamports\" \ \"$internalNodesLamports\" \ $nodeIndex \ - ${#clientIpList[@]} \"$benchTpsExtraArgs\" \ \"$genesisOptions\" \ \"$maybeNoSnapshot $maybeSkipLedgerVerify $maybeLimitLedgerSize $maybeWaitForSupermajority $maybeAccountsDbSkipShrink $maybeSkipRequireTower\" \ \"$maybeWarpSlot\" \ @@ -405,6 +404,7 @@ startNode() { \"$TMPFS_ACCOUNTS\" \ \"$disableQuic\" \ \"$enableUdp\" \ + \"$alpenglow\" \ " ) >> "$logFile" 2>&1 & declare pid=$! @@ -415,7 +415,9 @@ startNode() { startClient() { declare ipAddress=$1 declare clientToRun="$2" - declare clientIndex="$3" + + # Forward the extra args that belong to the selected client program. + declare clientExtraArgs=$transactionBenchExtraArgs initLogDir declare logFile="$netLogDir/client-$clientToRun-$ipAddress.log" @@ -427,7 +429,7 @@ startClient() { startCommon "$ipAddress" ssh "${sshOptions[@]}" -f "$ipAddress" \ "./solana/net/remote/remote-client.sh $deployMethod $entrypointIp \ - $clientToRun \"$RUST_LOG\" \"$benchTpsExtraArgs\" $clientIndex $clientType \ + $clientToRun \"$RUST_LOG\" \"$clientExtraArgs\" \ $maybeUseUnstakedConnection" ) >> "$logFile" 2>&1 || { cat "$logFile" @@ -437,9 +439,9 @@ startClient() { } startClients() { - for ((i=0; i < "$numClients" && i < "$numClientsRequested"; i++)) do - if [[ $i -lt "$numBenchTpsClients" ]]; then - startClient "${clientIpList[$i]}" "solana-bench-tps" "$i" + for ((i=0; i < numClients && i < numClientsRequested; i++)) do + if [[ $i -lt $numTransactionBenchClients ]]; then + startClient "${clientIpList[$i]}" "solana-transaction-bench" else startClient "${clientIpList[$i]}" "idle" fi @@ -507,6 +509,52 @@ getNodeType() { exit 1 } +# solana-transaction-bench lives in the external anza-xyz/tpu-tools repository +# and is therefore not produced by the agave build. When transaction-bench +# clients are requested, stage the binary into the deploy artifacts so it ships +# to the nodes alongside the agave binaries (clients fetch ~/.cargo/bin/* from +# the entrypoint). +stageTransactionBenchBinary() { + [[ $numTransactionBenchClients -gt 0 ]] || return 0 + + declare destDir + case $deployMethod in + local) destDir="$SOLANA_ROOT"/farf/bin ;; + tar) destDir="$SOLANA_ROOT"/solana-release/bin ;; + *) return 0 ;; + esac + mkdir -p "$destDir" + + if [[ -x "$destDir"/solana-transaction-bench ]]; then + echo "solana-transaction-bench already staged in $destDir" + return 0 + fi + + declare src="${SOLANA_TRANSACTION_BENCH:-}" + if [[ -z $src ]]; then + src="$(command -v solana-transaction-bench || true)" + fi + if [[ -n $src && -x $src ]]; then + echo "Staging solana-transaction-bench from $src" + cp -f "$src" "$destDir"/solana-transaction-bench + return 0 + fi + + echo "solana-transaction-bench not found locally, installing it from crates.io" + declare -a cargoArgs=(install --root "$SOLANA_ROOT"/farf --locked) + [[ -z ${SOLANA_TRANSACTION_BENCH_VERSION:-} ]] || cargoArgs+=(--version "$SOLANA_TRANSACTION_BENCH_VERSION") + cargoArgs+=(solana-transaction-bench) + if cargo "${cargoArgs[@]}"; then + if [[ $destDir != "$SOLANA_ROOT"/farf/bin ]]; then + cp -f "$SOLANA_ROOT"/farf/bin/solana-transaction-bench "$destDir"/solana-transaction-bench + fi + else + echo "Warning: failed to stage solana-transaction-bench." + echo " Clients will try to install it themselves at start time." + echo " Alternatively set SOLANA_TRANSACTION_BENCH to a prebuilt binary path before deploying." + fi +} + prepareDeploy() { case $deployMethod in tar) @@ -543,6 +591,8 @@ prepareDeploy() { ;; esac + stageTransactionBenchBinary + if [[ -n $deployIfNewer ]]; then if [[ $deployMethod != tar ]]; then echo "Error: --deploy-if-newer only supported for tar deployments" @@ -585,7 +635,7 @@ deploy() { if $bootstrapLeader; then SECONDS=0 declare bootstrapNodeDeployTime= - startBootstrapLeader "$nodeAddress" "$nodeIndex" "$netLogDir/bootstrap-validator-$ipAddress.log" + startBootstrapLeader "$nodeAddress" "$nodeIndex" "$netLogDir/bootstrap-validator-$ipAddress.log" "$alpenglow" bootstrapNodeDeployTime=$SECONDS $metricsWriteDatapoint "testnet-deploy net-bootnode-leader-started=1" @@ -593,7 +643,7 @@ deploy() { SECONDS=0 pids=() else - startNode "$ipAddress" "$nodeType" "$nodeIndex" + startNode "$ipAddress" "$nodeType" "$nodeIndex" "$alpenglow" # Stagger additional node start time. If too many nodes start simultaneously # the bootstrap node gets more rsync requests from the additional nodes than @@ -758,8 +808,8 @@ sanityExtraArgs= skipSetup=false nodeAddress= numIdleClients=0 -numBenchTpsClients=0 -benchTpsExtraArgs= +numTransactionBenchClients=0 +transactionBenchExtraArgs= failOnValidatorBootupFailure=true genesisOptions= numValidatorsRequested= @@ -787,8 +837,8 @@ waitForNodeInit=true extraPrimordialStakes=0 disableQuic=false enableUdp=false -clientType=tpu-client maybeUseUnstakedConnection="" +alpenglow=false command=$1 [[ -n $command ]] || usage @@ -903,20 +953,12 @@ while [[ -n $1 ]]; do elif [[ $1 = --skip-require-tower ]]; then maybeSkipRequireTower="$1" shift 1 - elif [[ $1 = --client-type ]]; then - clientType=$2 - case "$clientType" in - tpu-client|rpc-client) - ;; - *) - echo "Unexpected client type: \"$clientType\"" - exit 1 - ;; - esac - shift 2 elif [[ $1 = --use-unstaked-connection ]]; then maybeUseUnstakedConnection="$1" shift 1 + elif [[ $1 = --alpenglow ]]; then + alpenglow=true + shift 1 else usage "Unknown long option: $1" fi @@ -984,9 +1026,9 @@ while getopts "h?T:t:o:f:rc:Fn:i:d" opt "${shortArgs[@]}"; do numIdleClients=$numClients # $extraArgs ignored for 'idle' ;; - bench-tps) - numBenchTpsClients=$numClients - benchTpsExtraArgs=$extraArgs + transaction-bench) + numTransactionBenchClients=$numClients + transactionBenchExtraArgs=$extraArgs ;; *) echo "Unknown client type: $clientType" @@ -1020,13 +1062,14 @@ if [[ -n $numValidatorsRequested ]]; then fi numClients=${#clientIpList[@]} -numClientsRequested=$((numBenchTpsClients + numIdleClients)) +numClientsRequested=$((numTransactionBenchClients + numIdleClients)) if [[ "$numClientsRequested" -eq 0 ]]; then - numBenchTpsClients=$numClients + # Default to solana-transaction-bench on every available client node. + numTransactionBenchClients=$numClients numClientsRequested=$numClients else if [[ "$numClientsRequested" -gt "$numClients" ]]; then - echo "Error: More clients requested ($numClientsRequested) then available ($numClients)" + echo "Error: More clients requested ($numClientsRequested) than available ($numClients)" exit 1 fi fi @@ -1099,7 +1142,7 @@ startnode) nodeType= nodeIndex= getNodeType - startNode "$nodeAddress" "$nodeType" "$nodeIndex" + startNode "$nodeAddress" "$nodeType" "$nodeIndex" "$alpenglow" ;; startclients) startClients diff --git a/net/remote/remote-client.sh b/net/remote/remote-client.sh index c66f3c78388..9783364f81d 100755 --- a/net/remote/remote-client.sh +++ b/net/remote/remote-client.sh @@ -9,10 +9,8 @@ clientToRun="$3" if [[ -n $4 ]]; then export RUST_LOG="$4" fi -benchTpsExtraArgs="$5" -clientIndex="$6" -clientType="${7:-tpu-client}" -maybeUseUnstakedConnection="$8" +clientExtraArgs="$5" +maybeUseUnstakedConnection="$6" missing() { echo "Error: $1 not specified" @@ -25,11 +23,6 @@ missing() { source net/common.sh loadConfigFile -threadCount=$(nproc) -if [[ $threadCount -gt 4 ]]; then - threadCount=4 -fi - case $deployMethod in local|tar) PATH="$HOME"/.cargo/bin:"$PATH" @@ -43,48 +36,139 @@ skip) exit 1 esac -RPC_CLIENT=false -case "$clientType" in - tpu-client) - RPC_CLIENT=false - ;; - rpc-client) - RPC_CLIENT=true - ;; - *) - echo "Unexpected clientType: \"$clientType\"" +# Does "$@" (beyond the first arg, which is the name to look for) contain $name, +# either as a bare flag or as --flag=value? +contains_arg() { + declare name=$1 + shift + declare arg + for arg in "$@"; do + if [[ $arg = "$name" || $arg = "$name="* ]]; then + return 0 + fi + done + return 1 +} + +contains_url_arg() { + declare arg + for arg in "$@"; do + case "$arg" in + --url | --url=* | -u | -u?*) + return 0 + ;; + esac + done + return 1 +} + +# Ensure the external solana-transaction-bench binary is available. Prefer a +# path provided via SOLANA_TRANSACTION_BENCH, then one already on PATH (e.g. +# rsynced from the entrypoint's ~/.cargo/bin), and finally fall back to +# installing it from crates.io. +ensureTransactionBench() { + transactionBench="${SOLANA_TRANSACTION_BENCH:-solana-transaction-bench}" + if command -v "$transactionBench" > /dev/null 2>&1; then + return 0 + fi + + echo "$transactionBench not found, installing solana-transaction-bench from crates.io" + declare -a cargoArgs=(install --locked) + [[ -z ${SOLANA_TRANSACTION_BENCH_VERSION:-} ]] || cargoArgs+=(--version "$SOLANA_TRANSACTION_BENCH_VERSION") + cargoArgs+=(solana-transaction-bench) + if ! cargo "${cargoArgs[@]}"; then + echo "Error: unable to obtain solana-transaction-bench." + echo "Stage it into the entrypoint's ~/.cargo/bin or set SOLANA_TRANSACTION_BENCH to a prebuilt binary path." exit 1 - ;; -esac + fi + transactionBench=solana-transaction-bench +} case $clientToRun in -solana-bench-tps) +solana-transaction-bench) + ensureTransactionBench + + # transaction-bench creates and funds ephemeral payer accounts from the + # faucet (the genesis mint) at runtime. We only need the faucet keypair as + # the funding authority and, for a staked QUIC connection, a staked validator + # identity. net/scripts/rsync-retry.sh -vPrc \ - "$entrypointIp":~/solana/config/bench-tps"$clientIndex".yml ./client-accounts.yml + "$entrypointIp":~/solana/config/faucet.json ./faucet.json net/scripts/rsync-retry.sh -vPrc \ "$entrypointIp":~/solana/config/validator-identity-1.json ./validator-identity.json - args=() + # solana-transaction-bench expects its arguments in positional buckets: + # run + # Split the operator-supplied extra args into those buckets so that, e.g., + # a --url override lands before "run" while --duration lands after it. + globalArgs=() + runArgs=() + leaderTrackerArgs=() + + # shellcheck disable=SC2206 # Intentional word-splitting of the extra args string + extraArgs=($clientExtraArgs) + argIndex=0 + while [[ $argIndex -lt ${#extraArgs[@]} ]]; do + arg="${extraArgs[$argIndex]}" + case "$arg" in + -u | --url | --authority | --commitment-config) + if [[ $((argIndex + 1)) -ge ${#extraArgs[@]} ]]; then + echo "Error: missing value for $arg in extra args" + exit 1 + fi + globalArgs+=("$arg" "${extraArgs[$((argIndex + 1))]}") + argIndex=$((argIndex + 2)) + ;; + --url=* | --authority=* | --commitment-config=* | -u?*) + globalArgs+=("$arg") + argIndex=$((argIndex + 1)) + ;; + --validate-accounts | --mock-rpc) + globalArgs+=("$arg") + argIndex=$((argIndex + 1)) + ;; + run | read-accounts-run | write-accounts) + echo "Error: do not pass a solana-transaction-bench subcommand in extra args; this wrapper always uses 'run'." + exit 1 + ;; + pinned-leader-tracker | legacy-leader-tracker | ws-leader-tracker | \ + yellowstone-leader-tracker | custom-leader-tracker) + leaderTrackerArgs=("${extraArgs[@]:$argIndex}") + break + ;; + *) + runArgs+=("$arg") + argIndex=$((argIndex + 1)) + ;; + esac + done - if ${RPC_CLIENT}; then - args+=(--use-rpc-client) - fi + contains_url_arg "${globalArgs[@]}" || globalArgs+=(--url "http://$entrypointIp:8899") + contains_arg --authority "${globalArgs[@]}" || globalArgs+=(--authority ./faucet.json) + + contains_arg --duration "${runArgs[@]}" || runArgs+=(--duration 7500) + contains_arg --num-payers "${runArgs[@]}" || runArgs+=(--num-payers 256) + contains_arg --payer-account-balance "${runArgs[@]}" || runArgs+=(--payer-account-balance 10SOL) + contains_arg --transfer-tx-cu-budget "${runArgs[@]}" || runArgs+=(--transfer-tx-cu-budget 600) if [[ -z "$maybeUseUnstakedConnection" ]]; then - args+=(--bind-address "$entrypointIp") - args+=(--client-node-id ./validator-identity.json) + contains_arg --staked-identity-file "${runArgs[@]}" || runArgs+=(--staked-identity-file ./validator-identity.json) + fi + + # In a multinode cluster the leader rotates, so follow the leader schedule + # over the RPC websocket by default. Operators can override by passing a + # leader-tracker subcommand in the extra args. + if [[ ${#leaderTrackerArgs[@]} -eq 0 ]]; then + leaderTrackerArgs=(ws-leader-tracker) fi clientCommand="\ - solana-bench-tps \ - --duration 7500 \ - --sustained \ - --threads $threadCount \ - $benchTpsExtraArgs \ - --read-client-keys ./client-accounts.yml \ - --url "http://$entrypointIp:8899" \ - ${args[*]} \ + $transactionBench \ + ${globalArgs[*]} \ + run \ + ${runArgs[*]} \ + ${leaderTrackerArgs[*]} \ " ;; idle) diff --git a/net/remote/remote-node.sh b/net/remote/remote-node.sh index 6b2de4a9f5b..6d95cbdf486 100755 --- a/net/remote/remote-node.sh +++ b/net/remote/remote-node.sh @@ -18,17 +18,16 @@ maybeDisableAirdrops="$9" internalNodesStakeLamports="${10}" internalNodesLamports="${11}" nodeIndex="${12}" -numBenchTpsClients="${13}" -benchTpsExtraArgs="${14}" -genesisOptions="${15}" -extraNodeArgs="${16}" -maybeWarpSlot="${17}" -maybeFullRpc="${18}" -waitForNodeInit="${19}" -extraPrimordialStakes="${20:=0}" -tmpfsAccounts="${21:false}" -disableQuic="${22}" -enableUdp="${23}" +genesisOptions="${13}" +extraNodeArgs="${14}" +maybeWarpSlot="${15}" +maybeFullRpc="${16}" +waitForNodeInit="${17}" +extraPrimordialStakes="${18:=0}" +tmpfsAccounts="${19:false}" +disableQuic="${20}" +enableUdp="${21}" +alpenglow="${22:-false}" set +x @@ -79,8 +78,6 @@ local|tar|skip) PATH="$HOME"/.cargo/bin:"$PATH" export USE_INSTALL=1 - ./fetch-perf-libs.sh - cat >> ~/solana/on-reboot <> config/client-accounts.yml - echo "" >> config/client-accounts.yml - done if [[ -f $externalPrimordialAccountsFile ]]; then cat "$externalPrimordialAccountsFile" >> config/validator-balances.yml fi if [[ -f config/validator-balances.yml ]]; then genesisOptions+=" --primordial-accounts-file config/validator-balances.yml" fi - if [[ -f config/client-accounts.yml ]]; then - genesisOptions+=" --primordial-accounts-file config/client-accounts.yml" - fi if [[ -n $internalNodesStakeLamports ]]; then args+=(--bootstrap-validator-stake-lamports "$internalNodesStakeLamports") @@ -205,9 +181,17 @@ EOF "$(solana-keygen pubkey "config/validator-vote-$i.json")" "$(solana-keygen pubkey "config/validator-stake-$i.json")" ) + args+=(--bootstrap-validator-bls-pubkey "$(solana-keygen bls_pubkey "config/validator-identity-$i.json")") done fi + if $alpenglow; then + echo "Consensus method: Alpenglow" + args+=(--alpenglow) + else + echo "Consensus method: TowerBFT" + fi + multinode-demo/setup.sh "${args[@]}" maybeWaitForSupermajority= @@ -354,7 +338,7 @@ EOF set -x # Add the faucet keypair to validators for convenient access from tools - # like bench-tps and add to blocktreamers to run a faucet + # and add it to blockstreamers to run a faucet. scp "$entrypointIp":~/solana/config/faucet.json "$SOLANA_CONFIG_DIR"/ if [[ $nodeType = blockstreamer ]]; then # Run another faucet with the same keypair on the blockstreamer node. diff --git a/notifier/Cargo.toml b/notifier/Cargo.toml index 72e249f617c..774c87b7d7c 100644 --- a/notifier/Cargo.toml +++ b/notifier/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] name = "solana_notifier" @@ -23,3 +25,6 @@ log = { workspace = true } reqwest = { workspace = true, features = ["blocking", "brotli", "deflate", "gzip", "rustls-tls", "json"] } serde_json = { workspace = true } solana-hash = { workspace = true, features = ["std", "decode"] } + +[lints] +workspace = true diff --git a/perf/Cargo.toml b/perf/Cargo.toml index 234cda447e2..c5cad572938 100644 --- a/perf/Cargo.toml +++ b/perf/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] name = "solana_perf" @@ -59,7 +61,8 @@ solana-keypair = { workspace = true, optional = true } solana-message = { workspace = true } solana-metrics = { workspace = true } solana-packet = { workspace = true, features = ["bincode"] } -solana-pubkey = { workspace = true, default-features = false } +solana-pubkey = { workspace = true, default-features = false, features = ["rand"] } +solana-runtime-transaction = { workspace = true } solana-sdk-ids = { workspace = true } solana-short-vec = { workspace = true } solana-signature = { workspace = true, features = ["verify"] } @@ -71,6 +74,7 @@ solana-transaction = { workspace = true, optional = true, features = ["serde"] } solana-transaction-context = { workspace = true } solana-vote = { workspace = true, optional = true } solana-vote-program = { workspace = true, optional = true } +wincode = { workspace = true } [target."cfg(target_os = \"linux\")".dependencies] caps = { workspace = true } @@ -109,3 +113,18 @@ harness = false [lints.rust.unexpected_cfgs] level = "warn" check-cfg = ['cfg(build_target_feature_avx)', 'cfg(build_target_feature_avx2)'] + +# Duplicated from the root [workspace.lints.clippy]. This crate defines its own +# [lints.rust.unexpected_cfgs] above, and cargo does not support combining +# `lints.workspace = true` with crate-specific lints in the same manifest +# (see https://github.com/rust-lang/cargo/issues/13157). Keep in sync with the workspace. +[lints.clippy] +# Denied lints +arithmetic_side_effects = "deny" +default_trait_access = "deny" +manual_let_else = "deny" +uninlined_format_args = "deny" +used_underscore_binding = "deny" + +# Allowed lints +new_without_default = "allow" diff --git a/perf/benches/sigverify.rs b/perf/benches/sigverify.rs index c255e1f359c..931dd10b228 100644 --- a/perf/benches/sigverify.rs +++ b/perf/benches/sigverify.rs @@ -132,7 +132,7 @@ fn bench_sigverify_uneven(b: &mut Bencher) { } else { tx = multi_tx.clone(); }; - let mut packet = BytesPacket::from_data(None, &tx).expect("serialize request"); + let mut packet = BytesPacket::from_data(&tx).expect("serialize request"); if rand::rng().random_ratio((num_packets - NUM) as u32, num_packets as u32) { packet.meta_mut().set_discard(true); } else { diff --git a/perf/src/lib.rs b/perf/src/lib.rs index 319fefbf797..9c8aec8f877 100644 --- a/perf/src/lib.rs +++ b/perf/src/lib.rs @@ -3,7 +3,6 @@ pub mod data_budget; pub mod deduper; pub mod packet; -pub mod perf_libs; pub mod recycled_vec; pub mod recycler; pub mod sigverify; diff --git a/perf/src/packet.rs b/perf/src/packet.rs index bec0a2ce949..6e39e698081 100644 --- a/perf/src/packet.rs +++ b/perf/src/packet.rs @@ -1,22 +1,25 @@ //! The `packet` module defines data structures and methods to pull data from the network. #[cfg(feature = "dev-context-only-utils")] -use bytes::{BufMut, BytesMut}; +use wincode::{ReadError, ReadResult, SchemaRead, config::DefaultConfig}; use { crate::{recycled_vec::RecycledVec, recycler::Recycler}, - bincode::config::Options, bytes::Bytes, rayon::{ iter::{IndexedParallelIterator, ParallelIterator}, prelude::{IntoParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator}, }, - serde::{Deserialize, Serialize, de::DeserializeOwned}, + serde::{Deserialize, Serialize}, std::{ borrow::Borrow, - io::Read, + io::Cursor, net::SocketAddr, ops::{Deref, DerefMut, Index, IndexMut}, slice::{Iter, SliceIndex}, }, + wincode::{ + SchemaWrite, WriteResult, + config::{Config, Configuration}, + }, }; pub use { bytes, @@ -28,6 +31,47 @@ pub const NUM_PACKETS: usize = 1024 * 8; pub const PACKETS_PER_BATCH: usize = 64; pub const NUM_RCVMMSGS: usize = 64; +pub type PacketConfig = Configuration; + +/// wincode configuration setup to use for deserializing a Packet. +/// - Zero-copy alignment check is enabled. +/// - Preallocation size limit is PACKET_DATA_SIZE. +#[inline] +const fn packet_config_inner() -> PacketConfig { + Configuration::default().with_preallocation_size_limit::<{ solana_packet::PACKET_DATA_SIZE }>() +} + +#[inline] +pub const fn packet_config() -> impl Config { + packet_config_inner() +} + +#[cfg(feature = "dev-context-only-utils")] +pub fn deserialize_slice_from_packet<'de, T, I>(packet: &'de Packet, index: I) -> ReadResult +where + T: SchemaRead<'de, PacketConfig, Dst = T>, + I: SliceIndex<[u8], Output = [u8]>, +{ + let data = packet + .data(index) + .ok_or(ReadError::Custom("packet discarded"))?; + wincode::config::deserialize(data, packet_config_inner()) +} + +pub fn packet_from_data(dest: Option<&SocketAddr>, data: T) -> WriteResult +where + T: SchemaWrite, +{ + let mut packet = Packet::default(); + let mut wr = Cursor::new(packet.buffer_mut()); + wincode::config::serialize_into(&mut wr, &data, packet_config_inner())?; + packet.meta_mut().size = wr.position() as usize; + if let Some(dest) = dest { + packet.meta_mut().set_socket_addr(dest); + } + Ok(packet) +} + /// Representation of a packet used in TPU. #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -62,22 +106,13 @@ impl BytesPacket { } #[cfg(feature = "dev-context-only-utils")] - pub fn from_data(dest: Option<&SocketAddr>, data: T) -> bincode::Result + pub fn from_data(data: T) -> WriteResult where - T: solana_packet::Encode, + T: SchemaWrite, { - let buffer = BytesMut::with_capacity(PACKET_DATA_SIZE); - let mut writer = buffer.writer(); - data.encode(&mut writer)?; - let buffer = writer.into_inner(); - let buffer = buffer.freeze(); - + let buffer = Bytes::from(wincode::serialize(&data)?); let mut meta = Meta::default(); meta.size = buffer.len(); - if let Some(dest) = dest { - meta.set_socket_addr(dest); - } - Ok(Self { buffer, meta }) } @@ -103,19 +138,6 @@ impl BytesPacket { &mut self.meta } - pub fn deserialize_slice(&self, index: I) -> bincode::Result - where - T: serde::de::DeserializeOwned, - I: SliceIndex<[u8], Output = [u8]>, - { - let bytes = self.data(index).ok_or(bincode::ErrorKind::SizeLimit)?; - bincode::options() - .with_limit(self.meta().size as u64) - .with_fixint_encoding() - .reject_trailing_bytes() - .deserialize(bytes) - } - #[cfg(feature = "dev-context-only-utils")] pub fn copy_from_slice(&mut self, slice: &[u8]) { self.buffer = Bytes::from(slice.to_vec()); @@ -359,17 +381,6 @@ impl<'a> PacketRef<'a> { } } - pub fn deserialize_slice(&self, index: I) -> bincode::Result - where - T: serde::de::DeserializeOwned, - I: SliceIndex<[u8], Output = [u8]>, - { - match self { - Self::Packet(packet) => packet.deserialize_slice(index), - Self::Bytes(packet) => packet.deserialize_slice(index), - } - } - pub fn to_bytes_packet(&self) -> BytesPacket { match self { // In case of the legacy `Packet` variant, we unfortunately need to @@ -442,17 +453,6 @@ impl PacketRefMut<'_> { } } - pub fn deserialize_slice(&self, index: I) -> bincode::Result - where - T: serde::de::DeserializeOwned, - I: SliceIndex<[u8], Output = [u8]>, - { - match self { - Self::Packet(packet) => packet.deserialize_slice(index), - Self::Bytes(packet) => packet.deserialize_slice(index), - } - } - #[cfg(feature = "dev-context-only-utils")] #[inline] pub fn copy_from_slice(&mut self, src: &[u8]) { @@ -883,20 +883,6 @@ impl<'a> IntoParallelIterator for &'a mut BytesPacketBatch { } } -pub fn deserialize_from_with_limit(reader: R) -> bincode::Result -where - R: Read, - T: DeserializeOwned, -{ - // with_limit causes pre-allocation size to be limited - // to prevent against memory exhaustion attacks. - bincode::options() - .with_limit(PACKET_DATA_SIZE as u64) - .with_fixint_encoding() - .allow_trailing_bytes() - .deserialize_from(reader) -} - #[cfg(test)] mod tests { use { diff --git a/perf/src/perf_libs.rs b/perf/src/perf_libs.rs deleted file mode 100644 index 31fb1d7b4e3..00000000000 --- a/perf/src/perf_libs.rs +++ /dev/null @@ -1,25 +0,0 @@ -use { - log::*, - std::{env, path::PathBuf}, -}; - -pub fn locate_perf_libs() -> Option { - let exe = env::current_exe().expect("Unable to get executable path"); - let perf_libs = exe.parent().unwrap().join("perf-libs"); - if perf_libs.is_dir() { - info!("perf-libs found at {perf_libs:?}"); - return Some(perf_libs); - } - warn!("{perf_libs:?} does not exist"); - None -} - -pub fn append_to_ld_library_path(mut ld_library_path: String) { - if let Ok(env_value) = env::var("LD_LIBRARY_PATH") { - ld_library_path.push(':'); - ld_library_path.push_str(&env_value); - } - info!("setting ld_library_path to: {ld_library_path:?}"); - // Safety: caller (init_poh) is executed before any spawned threads might access the environment - unsafe { env::set_var("LD_LIBRARY_PATH", ld_library_path) } -} diff --git a/perf/src/sigverify.rs b/perf/src/sigverify.rs index 388c3d5ff4b..f9a3b37e7da 100644 --- a/perf/src/sigverify.rs +++ b/perf/src/sigverify.rs @@ -8,6 +8,7 @@ use { transaction_view::SanitizedTransactionView, }, rayon::prelude::*, + solana_runtime_transaction::sanitize_config::sanitize_config, }; // Empirically derived to constrain max verify latency to ~8ms at lower packet counts @@ -27,7 +28,8 @@ fn verify_packet(packet: &mut PacketRefMut, reject_non_vote: bool, enable_tx_v1: }; let (is_simple_vote_tx, verified) = { - let Ok(view) = SanitizedTransactionView::try_new_sanitized(data, true) else { + let Ok(view) = SanitizedTransactionView::try_new_sanitized(data, &sanitize_config(true)) + else { return false; }; @@ -245,7 +247,7 @@ mod tests { }; let mut tx = Transaction::new_unsigned(message); tx.signatures = vec![Signature::default(); actual_num_sigs]; - BytesPacket::from_data(None, tx).unwrap() + BytesPacket::from_data(tx).unwrap() } #[test] @@ -287,7 +289,7 @@ mod tests { tx.signatures = vec![sig; NUM_SIG]; tx.message.account_keys = vec![]; tx.message.header.num_required_signatures = NUM_SIG as u8; - let mut packet = BytesPacket::from_data(None, tx).unwrap(); + let mut packet = BytesPacket::from_data(tx).unwrap(); assert!(!verify_packet(&mut packet.as_mut(), false, false)); @@ -318,7 +320,7 @@ mod tests { let sig = keypair1.try_sign_message(&tx.message_data()).unwrap(); tx.signatures = vec![sig; NUM_SIG]; - let mut packet = BytesPacket::from_data(None, tx).unwrap(); + let mut packet = BytesPacket::from_data(tx).unwrap(); assert!(!verify_packet(&mut packet.as_mut(), false, false)); @@ -395,7 +397,7 @@ mod tests { }; let mut tx = Transaction::new_unsigned(message); tx.signatures = vec![Signature::default()]; - let mut packet = BytesPacket::from_data(None, tx).unwrap(); + let mut packet = BytesPacket::from_data(tx).unwrap(); assert!(!sigverify::verify_packet( &mut packet.as_mut(), false, @@ -498,7 +500,7 @@ mod tests { let mut tx = test_tx(); // pretend malicious leader dropped a signature... tx.signatures.pop(); - let packet = BytesPacket::from_data(None, tx).unwrap(); + let packet = BytesPacket::from_data(tx).unwrap(); let mut batches = generate_packet_batches(&packet, 1, 1); @@ -601,10 +603,10 @@ mod tests { { let mut tx = test_tx(); tx.message.instructions[0].data = vec![1, 2, 3]; - let packet = BytesPacket::from_data(None, tx).unwrap(); + let packet = BytesPacket::from_data(tx).unwrap(); let view = SanitizedTransactionView::try_new_sanitized( packet.as_ref().data(..).unwrap(), - true, + &sanitize_config(true), ) .unwrap(); assert!(!is_simple_vote_transaction_view(&view)); @@ -614,10 +616,10 @@ mod tests { { let mut tx = new_test_vote_tx(&mut rng); tx.message.instructions[0].data = vec![1, 2, 3]; - let packet = BytesPacket::from_data(None, tx).unwrap(); + let packet = BytesPacket::from_data(tx).unwrap(); let view = SanitizedTransactionView::try_new_sanitized( packet.as_ref().data(..).unwrap(), - true, + &sanitize_config(true), ) .unwrap(); assert!(is_simple_vote_transaction_view(&view)); @@ -626,11 +628,11 @@ mod tests { // single versioned vote tx is not { let tx = new_test_vote_tx_v0(); - let packet = BytesPacket::from_data(None, tx).unwrap(); + let packet = BytesPacket::from_data(tx).unwrap(); let view = SanitizedTransactionView::try_new_sanitized( packet.as_ref().data(..).unwrap(), - true, + &sanitize_config(true), ) .unwrap(); assert!(!is_simple_vote_transaction_view(&view)); @@ -652,10 +654,10 @@ mod tests { CompiledInstruction::new(4, &(), vec![0, 2]), ], ); - let packet = BytesPacket::from_data(None, tx).unwrap(); + let packet = BytesPacket::from_data(tx).unwrap(); let view = SanitizedTransactionView::try_new_sanitized( packet.as_ref().data(..).unwrap(), - true, + &sanitize_config(true), ) .unwrap(); assert!(!is_simple_vote_transaction_view(&view)); @@ -667,10 +669,10 @@ mod tests { tx.signatures.push(Signature::default()); tx.message.header.num_required_signatures = 3; tx.message.instructions[0].data = vec![1, 2, 3]; - let packet = BytesPacket::from_data(None, tx).unwrap(); + let packet = BytesPacket::from_data(tx).unwrap(); let view = SanitizedTransactionView::try_new_sanitized( packet.as_ref().data(..).unwrap(), - true, + &sanitize_config(true), ) .unwrap(); assert!(!is_simple_vote_transaction_view(&view)); @@ -689,10 +691,10 @@ mod tests { let mut packet = if is_versioned_tx { let tx: VersionedTransaction = new_test_tx_with_number_of_ixs(number_of_ixs); - BytesPacket::from_data(None, tx.clone()).unwrap() + BytesPacket::from_data(tx.clone()).unwrap() } else { let tx: Transaction = new_test_tx_with_number_of_ixs(number_of_ixs); - BytesPacket::from_data(None, tx.clone()).unwrap() + BytesPacket::from_data(tx.clone()).unwrap() }; assert_eq!( diff --git a/poh-bench/Cargo.toml b/poh-bench/Cargo.toml index 77ba737d735..6ce95dddd56 100644 --- a/poh-bench/Cargo.toml +++ b/poh-bench/Cargo.toml @@ -24,3 +24,6 @@ solana-entry = { workspace = true } solana-measure = { workspace = true } solana-sha256-hasher = { workspace = true } solana-version = { workspace = true } + +[lints] +workspace = true diff --git a/poh-bench/src/main.rs b/poh-bench/src/main.rs index f1282a9b579..fdc9bda398b 100644 --- a/poh-bench/src/main.rs +++ b/poh-bench/src/main.rs @@ -1,10 +1,7 @@ #![allow(clippy::arithmetic_side_effects)] -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -use solana_entry::entry::{self, EntrySlice, create_ticks, init_poh}; -#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] -use solana_entry::entry::{EntrySlice, create_ticks, init_poh}; use { clap::{Arg, Command, crate_description, crate_name}, + solana_entry::entry::{EntrySlice, create_ticks}, solana_measure::measure::Measure, solana_sha256_hasher::hash, }; @@ -72,7 +69,6 @@ fn main() { .thread_name(|i| format!("solPohBench{i:02}")) .build() .expect("new rayon threadpool"); - init_poh(); while num_entries <= max_num_entries as usize { let mut time = Measure::start("time"); for _ in 0..iterations { @@ -89,46 +85,6 @@ fn main() { time.as_us() / iterations as u64 ); - // A target_arch check is required here since calling - // is_x86_feature_detected from a non-x86_64 arch results in a build - // error. - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - { - if is_x86_feature_detected!("avx2") && entry::api().is_some() { - let mut time = Measure::start("time"); - for _ in 0..iterations { - assert!(thread_pool.install(|| { - ticks[..num_entries] - .verify_cpu_x86_simd(&start_hash, 8) - .status() - })); - } - time.stop(); - println!( - "{},cpu_simd_avx2,{}", - num_entries, - time.as_us() / iterations as u64 - ); - } - - if is_x86_feature_detected!("avx512f") && entry::api().is_some() { - let mut time = Measure::start("time"); - for _ in 0..iterations { - assert!(thread_pool.install(|| { - ticks[..num_entries] - .verify_cpu_x86_simd(&start_hash, 16) - .status() - })); - } - time.stop(); - println!( - "{},cpu_simd_avx512,{}", - num_entries, - time.as_us() / iterations as u64 - ); - } - } - println!(); num_entries *= 2; } diff --git a/poh/Cargo.toml b/poh/Cargo.toml index 1df97861866..38a73a10f2a 100644 --- a/poh/Cargo.toml +++ b/poh/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -22,9 +24,9 @@ dev-context-only-utils = [] shuttle-test = ["dep:shuttle"] [dependencies] +agave-cpu-utils = { workspace = true } agave-votor-messages = { workspace = true } arc-swap = { workspace = true } -core_affinity = { workspace = true } crossbeam-channel = { workspace = true } log = { workspace = true } qualifier_attr = { workspace = true } @@ -66,3 +68,6 @@ name = "poh" [[bench]] name = "transaction_recorder" harness = false + +[lints] +workspace = true diff --git a/poh/src/poh_recorder.rs b/poh/src/poh_recorder.rs index da41baf3b87..80ed580447a 100644 --- a/poh/src/poh_recorder.rs +++ b/poh/src/poh_recorder.rs @@ -218,11 +218,6 @@ pub struct PohRecorder { // Allocation to hold PohEntrys recorded into PoHStream. entries: Vec, - - /// When alpenglow is enabled there will be no ticks apart from a final one - /// to complete the block. This tick will not be verified, and we use this - /// flag to unset hashes_per_tick - alpenglow_enabled: bool, } impl PohRecorder { @@ -305,7 +300,6 @@ impl PohRecorder { last_reported_slot_for_pending_fork: Arc::default(), is_exited, entries: Vec::with_capacity(64), - alpenglow_enabled: false, }, working_bank_receiver, ) @@ -479,18 +473,17 @@ impl PohRecorder { trace!("new working bank"); assert_eq!(working_bank.bank.ticks_per_slot(), self.ticks_per_slot()); let mut tick_height = self.tick_height(); - if let Some(hashes_per_tick) = *working_bank.bank.hashes_per_tick() { - if self.poh.lock().unwrap().hashes_per_tick() != hashes_per_tick { - // We must clear/reset poh when changing hashes per tick because it's - // possible there are ticks in the cache created with the old hashes per - // tick value that would get flushed later. This would corrupt the leader's - // block and it would be disregarded by the network. - info!( - "resetting poh due to hashes per tick change detected at {}", - working_bank.bank.slot() - ); - tick_height = self.reset_poh(working_bank.bank.clone(), false); - } + let hashes_per_tick = working_bank.bank.hashes_per_tick(); + if self.poh.lock().unwrap().hashes_per_tick_config() != hashes_per_tick { + // We must clear/reset poh when changing hashes per tick because it's + // possible there are ticks in the cache created with the old hashes per + // tick value that would get flushed later. This would corrupt the leader's + // block and it would be disregarded by the network. + info!( + "resetting poh due to hashes per tick change detected at {}", + working_bank.bank.slot() + ); + tick_height = self.reset_poh(working_bank.bank.clone(), false); } let leader_state = self.shared_leader_state.load(); @@ -568,11 +561,7 @@ impl PohRecorder { #[must_use] fn reset_poh(&mut self, reset_bank: Arc, reset_start_bank: bool) -> u64 { let blockhash = reset_bank.last_blockhash(); - let hashes_per_tick = if self.alpenglow_enabled { - None - } else { - *reset_bank.hashes_per_tick() - }; + let hashes_per_tick = reset_bank.hashes_per_tick(); let poh_hash = { let mut poh = self.poh.lock().unwrap(); poh.reset(blockhash, hashes_per_tick); @@ -612,10 +601,10 @@ impl PohRecorder { self.notify_replay_wakeup(); // Wait for the bank to be frozen with timeout - // TODO: change this to use DELTA_BLOCK from votor instead. let start = Instant::now(); + let delta_block = Duration::from_nanos_u128(working_bank.bank.ns_per_slot); while !working_bank.bank.is_frozen() && !self.is_exited.load(Ordering::Relaxed) { - if start.elapsed() > Duration::from_millis(400) { + if start.elapsed() > delta_block { break; } std::hint::spin_loop(); @@ -638,7 +627,7 @@ impl PohRecorder { // Send out the block footer - we now have the bank hash footer.bank_hash = working_bank.bank.hash(); - let footer = VersionedBlockMarker::new_block_footer(footer); + let footer = VersionedBlockMarker::from_block_footer(footer); let footer_entry_marker = ( EntryOrMarker::Marker(footer), working_bank.max_tick_height - 1, @@ -693,11 +682,11 @@ impl PohRecorder { for (entry, tick_height) in &self.tick_cache[..entry_count] { working_bank.bank.register_tick(&entry.hash); - if let Some(footer) = footer.clone() { - if let Err(err) = self.wait_for_freeze_and_send_footer(footer, working_bank) { - send_result = Err(err); - break; - } + if let Some(footer) = footer.clone() + && let Err(err) = self.wait_for_freeze_and_send_footer(footer, working_bank) + { + send_result = Err(err); + break; } let tick = (EntryOrMarker::from(entry.clone()), *tick_height); @@ -1083,7 +1072,6 @@ impl PohRecorder { pub fn enable_alpenglow(&mut self) { info!("Enabling Alpenglow, migrating poh to low power mode"); - self.alpenglow_enabled = true; self.tick_cache = vec![]; { let mut poh = self.poh.lock().unwrap(); @@ -1401,11 +1389,11 @@ mod tests { poh_recorder.set_bank_for_test(bank); drop(entry_receiver); - let marker = VersionedBlockMarker::new_block_footer(BlockFooterV1 { + let marker = VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: 0, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -1418,7 +1406,7 @@ mod tests { bank_hash: Hash::default(), block_producer_time_nanos: 0, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, } @@ -1499,6 +1487,64 @@ mod tests { assert!(!poh_recorder.tick_cache.is_empty()); } + #[test] + fn test_set_bank_resets_to_low_power_mode() { + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Arc::new( + Blockstore::open(ledger_path.path()).expect("Expected to be able to open ledger"), + ); + let GenesisConfigInfo { + mut genesis_config, .. + } = create_genesis_config(2); + genesis_config.poh_config.hashes_per_tick = Some(4); + let (parent, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + let child = Bank::new_from_parent_with_bank_forks( + bank_forks.as_ref(), + parent.clone(), + SlotLeader::default(), + parent.slot() + 1, + ); + child.set_hashes_per_tick(None); + child.set_tick_height(child.max_tick_height() - 1); + + let prev_hash = parent.last_blockhash(); + let (mut poh_recorder, _entry_receiver) = PohRecorder::new( + parent.tick_height(), + prev_hash, + parent.clone(), + Some((child.slot(), child.slot())), + parent.ticks_per_slot(), + blockstore, + &Arc::new(LeaderScheduleCache::new_from_bank(&parent)), + &genesis_config.poh_config, + Arc::new(AtomicBool::default()), + ); + assert_eq!( + poh_recorder.poh.lock().unwrap().hashes_per_tick_config(), + Some(4) + ); + + poh_recorder.set_bank_for_test(child.clone()); + assert_eq!( + poh_recorder.poh.lock().unwrap().hashes_per_tick_config(), + None + ); + + let bank_to_freeze = child.clone(); + let freezer = std::thread::spawn(move || { + while bank_to_freeze.tick_height() < bank_to_freeze.max_tick_height() { + std::hint::spin_loop(); + } + bank_to_freeze.freeze(); + }); + poh_recorder + .tick_alpenglow(child.max_tick_height(), test_footer()) + .unwrap(); + freezer.join().unwrap(); + assert!(!poh_recorder.has_bank()); + } + #[test] fn test_poh_recorder_tick_verifies() { // Setup diff --git a/poh/src/poh_service.rs b/poh/src/poh_service.rs index 5686afea25e..fc8b1871ec8 100644 --- a/poh/src/poh_service.rs +++ b/poh/src/poh_service.rs @@ -1,5 +1,7 @@ //! The `poh_service` module implements a service that records the passing of //! "ticks", a measure of time in the PoH stream +#[cfg(target_os = "linux")] +use agave_cpu_utils::{CpuId, set_cpu_affinity}; use { crate::{ poh_controller::{PohServiceMessage, PohServiceMessageGuard, PohServiceMessageReceiver}, @@ -37,7 +39,10 @@ const TARGET_HASH_BATCH_TIME_US: u64 = 50; pub const DEFAULT_HASHES_PER_BATCH: u64 = TARGET_HASH_BATCH_TIME_US * DEFAULT_HASHES_PER_SECOND / 1_000_000; -pub const DEFAULT_PINNED_CPU_CORE: usize = 0; +#[cfg(target_os = "linux")] +pub const DEFAULT_PINNED_CPU_CORE: Option = Some(0); +#[cfg(not(target_os = "linux"))] +pub const DEFAULT_PINNED_CPU_CORE: Option = None; const TARGET_SLOT_ADJUSTMENT_NS: u64 = 50_000_000; @@ -100,7 +105,7 @@ impl PohService { poh_config: &PohConfig, poh_exit: Arc, ticks_per_slot: u64, - pinned_cpu_core: usize, + pinned_cpu_core: Option, hashes_per_batch: u64, mut record_receiver: RecordReceiver, poh_service_receiver: PohServiceMessageReceiver, @@ -109,6 +114,8 @@ impl PohService { ) -> Self { migration_status.set_poh_service_started(); let poh_config = poh_config.clone(); + #[cfg(not(target_os = "linux"))] + let _ = pinned_cpu_core; let tick_producer = Builder::new() .name("solPohTickProd".to_string()) .spawn(move || { @@ -144,11 +151,18 @@ impl PohService { ) } } else { - // PoH service runs in a tight loop, generating hashes as fast as possible. - // Let's dedicate one of the CPU cores to this thread so that it can gain - // from cache performance. - if let Some(cores) = core_affinity::get_core_ids() { - core_affinity::set_for_current(cores[pinned_cpu_core]); + #[cfg(target_os = "linux")] + if let Some(pinned_cpu_core) = pinned_cpu_core { + // PoH service runs in a tight loop, generating hashes as fast as possible. + // Let's dedicate one of the CPU cores to this thread so that it can gain + // from cache performance. + let pinned_cpu = CpuId::new(pinned_cpu_core).unwrap(); + set_cpu_affinity(None, [pinned_cpu]).unwrap_or_else(|e| { + panic!( + "Failed to set CPU affinity for POH service to CPU \ + {pinned_cpu_core}: {e:?}. This is critical for performance." + ) + }); } Self::tick_producer( poh_recorder, @@ -603,14 +617,14 @@ impl PohService { } } - if let Some(service_message) = service_message { - if !should_exit { - Self::handle_service_message(&poh_recorder, service_message, record_receiver); - target_ns_per_tick = Self::target_tick_ns_adjusted( - ticks_per_slot, - Self::target_tick_ns_reconciled(&poh_recorder, poh_config), - ); - } + if let Some(service_message) = service_message + && !should_exit + { + Self::handle_service_message(&poh_recorder, service_message, record_receiver); + target_ns_per_tick = Self::target_tick_ns_adjusted( + ticks_per_slot, + Self::target_tick_ns_reconciled(&poh_recorder, poh_config), + ); } // If exit signal is set and there are no more records to process, exit. @@ -729,7 +743,7 @@ mod tests { PohConfig::default().target_tick_duration.as_micros() as u64; let target_tick_duration = Duration::from_micros(default_target_tick_duration); let poh_config = PohConfig { - hashes_per_tick: *bank.hashes_per_tick(), + hashes_per_tick: bank.hashes_per_tick(), target_tick_duration, target_tick_count: None, }; diff --git a/precompiles/Cargo.toml b/precompiles/Cargo.toml index ebe3b8028c3..d48e96adbdd 100644 --- a/precompiles/Cargo.toml +++ b/precompiles/Cargo.toml @@ -20,7 +20,7 @@ agave-unstable-api = [] [dependencies] agave-feature-set = { workspace = true } bincode = { workspace = true } -ed25519-dalek = { workspace = true } +ed25519-dalek = "=1.0.1" libsecp256k1 = { workspace = true, features = ["hmac"] } openssl = { workspace = true } solana-ed25519-program = { workspace = true } diff --git a/precompiles/src/lib.rs b/precompiles/src/lib.rs index b3fef2b8c45..98f2dab8b0f 100644 --- a/precompiles/src/lib.rs +++ b/precompiles/src/lib.rs @@ -1,5 +1,5 @@ #![cfg(feature = "agave-unstable-api")] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] use { agave_feature_set::FeatureSet, solana_message::compiled_instruction::CompiledInstruction, solana_precompile_error::PrecompileError, solana_pubkey::Pubkey, std::sync::LazyLock, diff --git a/program-binaries/Cargo.toml b/program-binaries/Cargo.toml index a80bdf4ef9d..ffcbfe3fa8e 100644 --- a/program-binaries/Cargo.toml +++ b/program-binaries/Cargo.toml @@ -8,6 +8,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] @@ -19,3 +24,6 @@ solana-pubkey = { workspace = true } solana-rent = { workspace = true } solana-sdk-ids = { workspace = true } spl-generic-token = { workspace = true } + +[lints] +workspace = true diff --git a/program-binaries/src/lib.rs b/program-binaries/src/lib.rs index f2693b550a9..bc3fdfe16fc 100644 --- a/program-binaries/src/lib.rs +++ b/program-binaries/src/lib.rs @@ -15,12 +15,15 @@ mod spl_memo_1_0 { mod spl_memo_3_0 { solana_pubkey::declare_id!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"); } +mod spl_memo_4_0 { + solana_pubkey::declare_id!("Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH"); +} static SPL_PROGRAMS: &[(Pubkey, Pubkey, &[u8])] = &[ ( spl_generic_token::token::ID, solana_sdk_ids::bpf_loader_upgradeable::ID, - include_bytes!("programs/spl_p_token-1.0.0-rc.1.so"), + include_bytes!("programs/spl_p_token-1.0.0.so"), ), ( spl_generic_token::token_2022::ID, @@ -37,6 +40,11 @@ static SPL_PROGRAMS: &[(Pubkey, Pubkey, &[u8])] = &[ solana_sdk_ids::bpf_loader::ID, include_bytes!("programs/spl_memo-3.0.0.so"), ), + ( + spl_memo_4_0::ID, + solana_sdk_ids::bpf_loader_upgradeable::ID, + include_bytes!("programs/spl_memo-4.0.0.so"), + ), ( spl_generic_token::associated_token_account::ID, solana_sdk_ids::bpf_loader::ID, @@ -67,7 +75,7 @@ static CORE_BPF_PROGRAMS: &[(Pubkey, Option, &[u8])] = &[ ( solana_sdk_ids::stake::ID, None, - include_bytes!("programs/core_bpf_stake-4.0.0.so"), + include_bytes!("programs/core_bpf_stake-5.1.0.so"), ), // Add more programs here post-migration... ]; diff --git a/program-binaries/src/programs/core_bpf_stake-4.0.0.so b/program-binaries/src/programs/core_bpf_stake-4.0.0.so deleted file mode 100644 index 22e5ff9cf8e..00000000000 Binary files a/program-binaries/src/programs/core_bpf_stake-4.0.0.so and /dev/null differ diff --git a/program-binaries/src/programs/core_bpf_stake-5.1.0.so b/program-binaries/src/programs/core_bpf_stake-5.1.0.so new file mode 100644 index 00000000000..3c66f8c8d98 Binary files /dev/null and b/program-binaries/src/programs/core_bpf_stake-5.1.0.so differ diff --git a/program-binaries/src/programs/spl_memo-4.0.0.so b/program-binaries/src/programs/spl_memo-4.0.0.so new file mode 100644 index 00000000000..04ba8f2e56e Binary files /dev/null and b/program-binaries/src/programs/spl_memo-4.0.0.so differ diff --git a/program-binaries/src/programs/spl_p_token-1.0.0-rc.1.so b/program-binaries/src/programs/spl_p_token-1.0.0.so similarity index 100% rename from program-binaries/src/programs/spl_p_token-1.0.0-rc.1.so rename to program-binaries/src/programs/spl_p_token-1.0.0.so diff --git a/program-runtime/Cargo.toml b/program-runtime/Cargo.toml index ce51107bcb1..eaf76175e06 100644 --- a/program-runtime/Cargo.toml +++ b/program-runtime/Cargo.toml @@ -7,10 +7,12 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -59,7 +61,7 @@ solana-pubkey = { workspace = true } solana-rent = { workspace = true } solana-sbpf = { workspace = true, features = ["jit"] } solana-sdk-ids = { workspace = true } -solana-slot-hashes = { workspace = true } +solana-slot-hashes = { workspace = true, features = ["wincode"] } solana-stable-layout = { workspace = true } solana-stake-interface = { workspace = true, features = ["bincode", "sysvar"] } solana-svm-callback = { workspace = true } @@ -74,6 +76,7 @@ solana-sysvar = { workspace = true, features = ["bincode"] } solana-sysvar-id = { workspace = true } solana-transaction-context = { workspace = true } thiserror = { workspace = true } +wincode = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } @@ -81,7 +84,7 @@ solana-account-info = { workspace = true } solana-instruction = { workspace = true, features = ["bincode"] } solana-instruction-error = { workspace = true, features = ["serde"] } solana-keypair = { workspace = true } -solana-program-runtime = { path = ".", features = ["dev-context-only-utils"] } +solana-program-runtime = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-pubkey = { workspace = true, features = ["rand"] } solana-signer = { workspace = true } solana-transaction = { workspace = true, features = ["dev-context-only-utils"] } diff --git a/program-runtime/src/cpi.rs b/program-runtime/src/cpi.rs index bbd3f1d8029..c661ec47d80 100644 --- a/program-runtime/src/cpi.rs +++ b/program-runtime/src/cpi.rs @@ -26,7 +26,7 @@ use { }; /// CPI-specific error types -#[derive(Debug, Error, PartialEq, Eq)] +#[derive(Clone, Debug, Error, PartialEq, Eq)] pub enum CpiError { #[error("Invalid pointer")] InvalidPointer, @@ -102,22 +102,6 @@ struct SolAccountInfo { pub executable: bool, } -/// Rust representation of C's SolSignerSeed -#[derive(Debug)] -#[repr(C)] -struct SolSignerSeedC { - pub addr: u64, - pub len: u64, -} - -/// Rust representation of C's SolSignerSeeds -#[derive(Debug)] -#[repr(C)] -struct SolSignerSeedsC { - pub addr: u64, - pub len: u64, -} - /// Maximum number of account info structs that can be used in a single CPI invocation const MAX_CPI_ACCOUNT_INFOS: usize = 255; @@ -549,12 +533,6 @@ pub trait SyscallInvokeSigned { account_infos_len: u64, invoke_context: &InvokeContext, ) -> Result>, Error>; - fn translate_signers( - program_id: &Pubkey, - signers_seeds_addr: u64, - signers_seeds_len: u64, - invoke_context: &InvokeContext, - ) -> Result, Error>; } pub fn translate_instruction_rust( @@ -650,7 +628,7 @@ pub fn translate_accounts_rust<'a>( )? } -pub fn translate_signers_rust( +pub fn translate_signers( program_id: &Pubkey, signers_seeds_addr: u64, signers_seeds_len: u64, @@ -658,7 +636,6 @@ pub fn translate_signers_rust( ) -> Result, Error> { let check_aligned = invoke_context.get_check_aligned(); let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; - let mut signers = Vec::new(); if signers_seeds_len > 0 { let signers_seeds = translate_slice::>>( memory_mapping, @@ -669,27 +646,28 @@ pub fn translate_signers_rust( if signers_seeds.len() > MAX_SIGNERS { return Err(Box::new(CpiError::TooManySigners)); } - for signer_seeds in signers_seeds.iter() { - let untranslated_seeds = translate_slice::>( - memory_mapping, - signer_seeds.ptr(), - signer_seeds.len(), - check_aligned, - )?; - if untranslated_seeds.len() > MAX_SEEDS { - return Err(Box::new(InstructionError::MaxSeedLengthExceeded)); - } - let seeds = untranslated_seeds - .iter() - .map(|untranslated_seed| { - translate_vm_slice(untranslated_seed, memory_mapping, check_aligned) - }) - .collect::, Error>>()?; - let signer = - Pubkey::create_program_address(&seeds, program_id).map_err(CpiError::BadSeeds)?; - signers.push(signer); - } - Ok(signers) + Ok(signers_seeds + .iter() + .map(|signer_seeds| { + let untranslated_seeds = translate_slice::>( + memory_mapping, + signer_seeds.ptr(), + signer_seeds.len(), + check_aligned, + )?; + if untranslated_seeds.len() > MAX_SEEDS { + return Err(Box::new(InstructionError::MaxSeedLengthExceeded) as Error); + } + let seeds_bytes = untranslated_seeds + .iter() + .map(|untranslated_seed| { + translate_vm_slice(untranslated_seed, memory_mapping, check_aligned) + }) + .collect::, Error>>()?; + Pubkey::create_program_address(&seeds_bytes, program_id) + .map_err(|err| Box::new(CpiError::BadSeeds(err)) as Error) + }) + .collect::, Error>>()?) } else { Ok(vec![]) } @@ -791,51 +769,6 @@ pub fn translate_accounts_c<'a>( )? } -pub fn translate_signers_c( - program_id: &Pubkey, - signers_seeds_addr: u64, - signers_seeds_len: u64, - invoke_context: &InvokeContext, -) -> Result, Error> { - let check_aligned = invoke_context.get_check_aligned(); - let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; - if signers_seeds_len > 0 { - let signers_seeds = translate_slice::( - memory_mapping, - signers_seeds_addr, - signers_seeds_len, - check_aligned, - )?; - if signers_seeds.len() > MAX_SIGNERS { - return Err(Box::new(CpiError::TooManySigners)); - } - Ok(signers_seeds - .iter() - .map(|signer_seeds| { - let seeds = translate_slice::( - memory_mapping, - signer_seeds.addr, - signer_seeds.len, - check_aligned, - )?; - if seeds.len() > MAX_SEEDS { - return Err(Box::new(InstructionError::MaxSeedLengthExceeded) as Error); - } - let seeds_bytes = seeds - .iter() - .map(|seed| { - translate_slice::(memory_mapping, seed.addr, seed.len, check_aligned) - }) - .collect::, Error>>()?; - Pubkey::create_program_address(&seeds_bytes, program_id) - .map_err(|err| Box::new(CpiError::BadSeeds(err)) as Error) - }) - .collect::, Error>>()?) - } else { - Ok(vec![]) - } -} - /// Call process instruction, common to both Rust and C pub fn cpi_common( invoke_context: &mut InvokeContext, @@ -865,7 +798,7 @@ pub fn cpi_common( .transaction_context .get_current_instruction_context()?; let caller_program_id = instruction_context.get_program_key()?; - let signers = S::translate_signers( + let signers = translate_signers( caller_program_id, signers_seeds_addr, signers_seeds_len, @@ -1935,7 +1868,7 @@ mod tests { )) .unwrap(); - let signers = translate_signers_rust(&program_id, vm_addr, 1, &invoke_context).unwrap(); + let signers = translate_signers(&program_id, vm_addr, 1, &invoke_context).unwrap(); assert_eq!(signers[0], derived_key); } @@ -2530,7 +2463,7 @@ mod tests { virtual_address_space_adjustments, account_data_direct_mapping, ), - Err(error) if error.downcast_ref::().unwrap() == &InstructionError::AccountDataSizeChanged + Err(error) if error.downcast_ref::().unwrap() == &InstructionError::ExternalAccountDataModified ); // truncating resize @@ -2548,7 +2481,7 @@ mod tests { virtual_address_space_adjustments, account_data_direct_mapping, ), - Err(error) if error.downcast_ref::().unwrap() == &InstructionError::AccountDataSizeChanged + Err(error) if error.downcast_ref::().unwrap() == &InstructionError::ExternalAccountDataModified ); } } diff --git a/program-runtime/src/invoke_context.rs b/program-runtime/src/invoke_context.rs index c81225cd53a..c4a998cec9c 100644 --- a/program-runtime/src/invoke_context.rs +++ b/program-runtime/src/invoke_context.rs @@ -296,8 +296,9 @@ impl<'a, 'ix_data> InvokeContext<'a, 'ix_data> { } } + self.transaction_context.push()?; self.memory_contexts.push_placeholder(); - self.transaction_context.push() + Ok(()) } /// Pop a stack frame from the invocation stack @@ -915,7 +916,7 @@ pub fn mock_compile_message( accounts: &[(Pubkey, A)], program_id: &Pubkey, loader_key: &Pubkey, -) -> Option<(SanitizedMessage, Vec<(Pubkey, AccountSharedData)>)> +) -> (SanitizedMessage, Vec<(Pubkey, AccountSharedData)>) where AccountSharedData: From, A: Clone, @@ -944,7 +945,7 @@ where let sanitized_message = SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())); - Some((sanitized_message, transaction_accounts)) + (sanitized_message, transaction_accounts) } #[cfg(feature = "dev-context-only-utils")] @@ -976,7 +977,7 @@ pub fn mock_process_instruction_with_feature_set< let instruction = Instruction::new_with_bytes(*program_id, instruction_data, instruction_account_metas); let (sanitized_message, transaction_accounts) = - mock_compile_message(&instruction, &accounts, program_id, &native_loader::id()).unwrap(); + mock_compile_message(&instruction, &accounts, program_id, &native_loader::id()); let program_owner = accounts .iter() @@ -1977,7 +1978,7 @@ mod tests { )]; let (message, tx_accounts) = - mock_compile_message(&instruction, &accounts, &program_id, &loader_key).unwrap(); + mock_compile_message(&instruction, &accounts, &program_id, &loader_key); assert_eq!(message.instructions().len(), 1); assert_eq!(tx_accounts.len(), 2); diff --git a/program-runtime/src/loaded_programs.rs b/program-runtime/src/loaded_programs.rs index 183f98cc5a4..93728ebbde5 100644 --- a/program-runtime/src/loaded_programs.rs +++ b/program-runtime/src/loaded_programs.rs @@ -2,7 +2,9 @@ use { crate::{ invoke_context::InvokeContext, loading_task::LoadingTaskWaiter, - program_cache_entry::{ProgramCacheEntry, ProgramCacheEntryType, retention_score}, + program_cache_entry::{ + ProgramCacheEntry, ProgramCacheEntryOwner, ProgramCacheEntryType, retention_score, + }, program_metrics::{EMA_SCALE, ProgramCacheStats}, }, log::error, @@ -183,6 +185,19 @@ impl EpochBoundaryPreparation { } } +/// Input of ProgramCache::extract() +#[derive(Clone, PartialEq, Debug)] +pub struct ProgramToLoad<'a> { + /// The program address + pub program_id: &'a Pubkey, + /// The program loader + pub loader: ProgramCacheEntryOwner, + /// Potentially filter out / ignore some entries during the start up / catch up phase + pub match_criteria: ProgramCacheMatchCriteria, + /// When the program account was last written to (might be after the deployment slot) + pub last_modification_slot: Slot, +} + #[derive(Debug)] pub(crate) enum IndexImplementation { /// Fork-graph aware index implementation @@ -341,6 +356,7 @@ impl ProgramCacheForTxBatch { } } +#[derive(Clone, PartialEq, Debug)] pub enum ProgramCacheMatchCriteria { DeployedOnOrAfterSlot(Slot), Tombstone, @@ -396,6 +412,7 @@ impl ProgramCache { at.effective_slot .cmp(&entry.effective_slot) .then(at.deployment_slot.cmp(&entry.deployment_slot)) + .then(at.account_owner.cmp(&entry.account_owner)) .then( // This `.then()` has no effect during normal operation. // Only during the cache preparation phase this does allow entries @@ -422,8 +439,6 @@ impl ProgramCache { ProgramCacheEntryType::Unloaded(_), ProgramCacheEntryType::Loaded(_), ) => {} - (ProgramCacheEntryType::Closed, ProgramCacheEntryType::Closed) - if existing.account_owner != entry.account_owner => {} _ => { // Something is wrong, I can feel it ... error!( @@ -477,7 +492,7 @@ impl ProgramCache { pub fn prune( &mut self, new_root_slot: Slot, - upcoming_environment: Option, + new_environment: Option, fork_graph: &FG, ) { match &mut self.index { @@ -524,8 +539,8 @@ impl ProgramCache { }) .filter(|entry| { // Remove outdated environment of previous feature set - if let Some(upcoming_environment) = upcoming_environment.as_ref() - && !Self::matches_environment(entry, upcoming_environment) + if let Some(new_environment) = new_environment.as_ref() + && !Self::matches_environment(entry, new_environment) { self.stats .prunes_environment @@ -572,7 +587,7 @@ impl ProgramCache { /// and returns which program accounts the accounts DB needs to load. pub fn extract( &self, - search_for: &mut Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)>, + search_for: &mut Vec, loaded_programs_for_tx_batch: &mut ProgramCacheForTxBatch, program_runtime_environment_for_execution: &ProgramRuntimeEnvironment, increment_usage_counter: bool, @@ -587,13 +602,15 @@ impl ProgramCache { entries, loading_entries, } => { - search_for.retain(|(key, match_criteria, _slot)| { - if let Some(second_level) = entries.get(key) { + search_for.retain(|program_to_load| { + if let Some(second_level) = entries.get(program_to_load.program_id) { let mut filter_by_deployment_slot = None; for entry in second_level.iter().rev() { let required_deployment_slot = filter_by_deployment_slot.unwrap_or(entry.deployment_slot); - if required_deployment_slot != entry.deployment_slot { + if required_deployment_slot != entry.deployment_slot + || program_to_load.loader != entry.account_owner + { continue; } let entry_in_same_branch = entry.deployment_slot @@ -625,7 +642,10 @@ impl ProgramCache { .or(Some(entry.deployment_slot)); continue; } - if !Self::matches_criteria(entry, match_criteria) { + if !Self::matches_criteria( + entry, + &program_to_load.match_criteria, + ) { break; } if let ProgramCacheEntryType::Unloaded(_environment) = @@ -656,20 +676,20 @@ impl ProgramCache { } loaded_programs_for_tx_batch .entries - .insert(*key, entry_to_return); + .insert(*program_to_load.program_id, entry_to_return); return false; } } } if cooperative_loading_task.is_none() { let mut loading_entries = loading_entries.lock().unwrap(); - let entry = loading_entries.entry(*key); + let entry = loading_entries.entry(*program_to_load.program_id); if let Entry::Vacant(entry) = entry { entry.insert(( loaded_programs_for_tx_batch.slot, thread::current().id(), )); - cooperative_loading_task = Some(*key); + cooperative_loading_task = Some(*program_to_load.program_id); } } true @@ -941,7 +961,7 @@ pub(crate) mod tests { crate::{ loaded_programs::{ BlockRelation, ForkGraph, ProgramCache, ProgramCacheForTxBatch, - ProgramCacheMatchCriteria, ProgramRuntimeEnvironment, + ProgramCacheMatchCriteria, ProgramRuntimeEnvironment, ProgramToLoad, get_mock_program_runtime_environment, }, program_cache_entry::{ @@ -955,15 +975,11 @@ pub(crate) mod tests { solana_clock::Slot, solana_pubkey::Pubkey, solana_sbpf::{elf::Executable, program::BuiltinProgram}, - std::{ - fs::File, - io::Read, - ops::ControlFlow, - sync::{ - Arc, RwLock, - atomic::{AtomicU64, Ordering}, - }, + solana_svm_type_overrides::sync::{ + Arc, RwLock, + atomic::{AtomicU64, Ordering}, }, + std::{fs::File, io::Read, ops::ControlFlow}, test_case::{test_case, test_matrix}, }; @@ -1793,11 +1809,11 @@ pub(crate) mod tests { } } - fn get_entries_to_load( + fn get_entries_to_load<'a>( cache: &ProgramCache, loading_slot: Slot, - keys: &[Pubkey], - ) -> Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)> { + keys: &'a [Pubkey], + ) -> Vec> { let fork_graph = cache.fork_graph.as_ref().unwrap().upgrade().unwrap(); let locked_fork_graph = fork_graph.read().unwrap(); let entries = cache.get_flattened_entries_for_tests(); @@ -1813,12 +1829,11 @@ pub(crate) mod tests { BlockRelation::Equal | BlockRelation::Ancestor, ) }) - .map(|(program_id, entry)| { - ( - *program_id, - ProgramCacheMatchCriteria::NoCriteria, - entry.deployment_slot, - ) + .map(|(_program_id, entry)| ProgramToLoad { + program_id: key, + loader: entry.account_owner, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: entry.deployment_slot, }) }) .collect() @@ -1839,11 +1854,11 @@ pub(crate) mod tests { } fn match_missing( - missing: &[(Pubkey, ProgramCacheMatchCriteria, Slot)], - program: &Pubkey, + missing: &[ProgramToLoad], + program_id: &Pubkey, expected_result: bool, ) -> bool { - missing.iter().any(|(key, _, _)| key == program) == expected_result + missing.iter().any(|entry| entry.program_id == program_id) == expected_result } #[test] @@ -1918,8 +1933,8 @@ pub(crate) mod tests { // 23 // Testing fork 0 - 10 - 20 - 22 with current slot at 22 - let mut missing = - get_entries_to_load(&cache, 22, &[program1, program2, program3, program4]); + let keys = &[program1, program2, program3, program4]; + let mut missing = get_entries_to_load(&cache, 22, keys); assert!(match_missing(&missing, &program2, false)); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(22); @@ -1928,8 +1943,7 @@ pub(crate) mod tests { assert!(match_slot(&extracted, &program4, 0, 22)); // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 15 - let mut missing = - get_entries_to_load(&cache, 15, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 15, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(15); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -1944,8 +1958,7 @@ pub(crate) mod tests { assert_eq!(tombstone.deployment_slot, 15); // Testing the same fork above, but current slot is now 18 (equal to effective slot of program4). - let mut missing = - get_entries_to_load(&cache, 18, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 18, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(18); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -1955,8 +1968,7 @@ pub(crate) mod tests { assert!(match_slot(&extracted, &program4, 15, 18)); // Testing the same fork above, but current slot is now 23 (future slot than effective slot of program4). - let mut missing = - get_entries_to_load(&cache, 23, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 23, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(23); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -1966,8 +1978,7 @@ pub(crate) mod tests { assert!(match_slot(&extracted, &program4, 15, 23)); // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 11 - let mut missing = - get_entries_to_load(&cache, 11, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 11, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(11); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -1998,8 +2009,7 @@ pub(crate) mod tests { // 23 // Testing fork 11 - 15 - 16- 19 - 22 with root at 5 and current slot at 22 - let mut missing = - get_entries_to_load(&cache, 21, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 21, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(21); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2009,8 +2019,7 @@ pub(crate) mod tests { assert!(match_slot(&extracted, &program4, 15, 21)); // Testing fork 0 - 5 - 11 - 25 - 27 with current slot at 27 - let mut missing = - get_entries_to_load(&cache, 27, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 27, keys); let mut extracted = ProgramCacheForTxBatch::new(27); cache.extract(&mut missing, &mut extracted, &env, true, true); assert!(match_slot(&extracted, &program1, 0, 27)); @@ -2036,8 +2045,7 @@ pub(crate) mod tests { // 23 // Testing fork 16, 19, 23, with root at 15, current slot at 23 - let mut missing = - get_entries_to_load(&cache, 23, &[program1, program2, program3, program4]); + let mut missing = get_entries_to_load(&cache, 23, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(23); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2086,7 +2094,8 @@ pub(crate) mod tests { cache.assign_program(&env, program3, 25, new_test_entry(25, 26)); // Testing fork 0 - 5 - 11 - 15 - 16 - 19 - 21 - 23 with current slot at 19 - let mut missing = get_entries_to_load(&cache, 12, &[program1, program2, program3]); + let keys = &[program1, program2, program3]; + let mut missing = get_entries_to_load(&cache, 12, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(12); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2094,9 +2103,11 @@ pub(crate) mod tests { assert!(match_slot(&extracted, &program2, 11, 12)); // Test the same fork, but request the program modified at a later slot than what's in the cache. - let mut missing = get_entries_to_load(&cache, 12, &[program1, program2, program3]); - missing.get_mut(0).unwrap().1 = ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5); - missing.get_mut(1).unwrap().1 = ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5); + let mut missing = get_entries_to_load(&cache, 12, keys); + missing.get_mut(0).unwrap().match_criteria = + ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5); + missing.get_mut(1).unwrap().match_criteria = + ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(12); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2159,7 +2170,8 @@ pub(crate) mod tests { ); // Testing fork 0 - 5 - 11 - 15 - 16 - 19 - 21 - 23 with current slot at 19 - let mut missing = get_entries_to_load(&cache, 19, &[program1, program2, program3]); + let keys = &[program1, program2, program3]; + let mut missing = get_entries_to_load(&cache, 19, keys); assert!(match_missing(&missing, &program3, false)); let mut extracted = ProgramCacheForTxBatch::new(19); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2167,7 +2179,7 @@ pub(crate) mod tests { assert!(match_slot(&extracted, &program2, 11, 19)); // Testing fork 0 - 5 - 11 - 25 - 27 with current slot at 27 - let mut missing = get_entries_to_load(&cache, 27, &[program1, program2, program3]); + let mut missing = get_entries_to_load(&cache, 27, keys); let mut extracted = ProgramCacheForTxBatch::new(27); cache.extract(&mut missing, &mut extracted, &env, true, true); assert!(match_slot(&extracted, &program1, 0, 27)); @@ -2175,7 +2187,7 @@ pub(crate) mod tests { assert!(match_missing(&missing, &program3, true)); // Testing fork 0 - 10 - 20 - 22 with current slot at 22 - let mut missing = get_entries_to_load(&cache, 22, &[program1, program2, program3]); + let mut missing = get_entries_to_load(&cache, 22, keys); assert!(match_missing(&missing, &program2, false)); let mut extracted = ProgramCacheForTxBatch::new(22); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2218,13 +2230,14 @@ pub(crate) mod tests { cache.assign_program(&env, program1, 20, new_test_entry(20, 21)); // Testing fork 0 - 10 - 20 - 22 with current slot at 22 - let mut missing = get_entries_to_load(&cache, 22, &[program1]); + let keys = &[program1]; + let mut missing = get_entries_to_load(&cache, 22, keys); let mut extracted = ProgramCacheForTxBatch::new(22); cache.extract(&mut missing, &mut extracted, &env, true, true); assert!(match_slot(&extracted, &program1, 20, 22)); // Looking for a different environment - let mut missing = get_entries_to_load(&cache, 22, &[program1]); + let mut missing = get_entries_to_load(&cache, 22, keys); let mut extracted = ProgramCacheForTxBatch::new(22); cache.extract(&mut missing, &mut extracted, &other_env, true, true); assert!(match_missing(&missing, &program1, true)); @@ -2239,7 +2252,12 @@ pub(crate) mod tests { cache.set_fork_graph(Arc::downgrade(&fork_graph)); let program1 = Pubkey::new_unique(); - let mut missing = vec![(program1, ProgramCacheMatchCriteria::NoCriteria, 0)]; + let mut missing = vec![ProgramToLoad { + program_id: &program1, + loader: ProgramCacheEntryOwner::LoaderV3, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }]; let mut extracted = ProgramCacheForTxBatch::new(0); cache.extract(&mut missing, &mut extracted, &env, true, true); assert!(match_missing(&missing, &program1, true)); @@ -2319,7 +2337,8 @@ pub(crate) mod tests { cache.prune(10, None, &fork_graph.read().unwrap()); - let mut missing = get_entries_to_load(&cache, 20, &[program1]); + let keys = &[program1]; + let mut missing = get_entries_to_load(&cache, 20, keys); let mut extracted = ProgramCacheForTxBatch::new(20); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2361,13 +2380,14 @@ pub(crate) mod tests { let program2 = Pubkey::new_unique(); cache.assign_program(&env, program2, 10, new_test_entry(10, 11)); - let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]); + let keys = &[program1, program2]; + let mut missing = get_entries_to_load(&cache, 20, keys); let mut extracted = ProgramCacheForTxBatch::new(20); cache.extract(&mut missing, &mut extracted, &env, true, true); assert!(match_slot(&extracted, &program1, 0, 20)); assert!(match_slot(&extracted, &program2, 10, 20)); - let mut missing = get_entries_to_load(&cache, 6, &[program1, program2]); + let mut missing = get_entries_to_load(&cache, 6, keys); assert!(match_missing(&missing, &program2, false)); let mut extracted = ProgramCacheForTxBatch::new(6); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2377,13 +2397,13 @@ pub(crate) mod tests { // On fork chaining from slot 5, the entry deployed at slot 0 will become visible. cache.prune_by_deployment_slot(5); - let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]); + let mut missing = get_entries_to_load(&cache, 20, keys); let mut extracted = ProgramCacheForTxBatch::new(20); cache.extract(&mut missing, &mut extracted, &env, true, true); assert!(match_slot(&extracted, &program1, 0, 20)); assert!(match_slot(&extracted, &program2, 10, 20)); - let mut missing = get_entries_to_load(&cache, 6, &[program1, program2]); + let mut missing = get_entries_to_load(&cache, 6, keys); assert!(match_missing(&missing, &program2, false)); let mut extracted = ProgramCacheForTxBatch::new(6); cache.extract(&mut missing, &mut extracted, &env, true, true); @@ -2393,7 +2413,7 @@ pub(crate) mod tests { // As there is no other entry for program2, extract() will return it as missing. cache.prune_by_deployment_slot(10); - let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]); + let mut missing = get_entries_to_load(&cache, 20, keys); assert!(match_missing(&missing, &program2, false)); let mut extracted = ProgramCacheForTxBatch::new(20); cache.extract(&mut missing, &mut extracted, &env, true, true); diff --git a/program-runtime/src/mem_pool.rs b/program-runtime/src/mem_pool.rs index 9907baad040..a0160e5747d 100644 --- a/program-runtime/src/mem_pool.rs +++ b/program-runtime/src/mem_pool.rs @@ -158,12 +158,6 @@ impl VmMemoryPool { } } -impl Default for VmMemoryPool { - fn default() -> Self { - Self::new() - } -} - #[cfg(test)] mod test { use super::*; diff --git a/program-runtime/src/program_cache_entry.rs b/program-runtime/src/program_cache_entry.rs index 35764a7bd99..815b093be4b 100644 --- a/program-runtime/src/program_cache_entry.rs +++ b/program-runtime/src/program_cache_entry.rs @@ -21,7 +21,7 @@ use { pub const DELAY_VISIBILITY_SLOT_OFFSET: Slot = 1; /// The owner of a programs accounts, thus the loader of a program -#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Debug)] pub enum ProgramCacheEntryOwner { #[default] NativeLoader, @@ -173,6 +173,7 @@ impl PartialEq for ProgramCacheEntry { fn eq(&self, other: &Self) -> bool { self.effective_slot == other.effective_slot && self.deployment_slot == other.deployment_slot + && self.account_owner == other.account_owner && self.is_tombstone() == other.is_tombstone() } } diff --git a/program-runtime/src/sysvar_cache.rs b/program-runtime/src/sysvar_cache.rs index 8d018cdce81..bef6ef4d260 100644 --- a/program-runtime/src/sysvar_cache.rs +++ b/program-runtime/src/sysvar_cache.rs @@ -91,7 +91,7 @@ impl SysvarCache { } sysvar::slot_hashes::ID => { let slot_hashes: SlotHashes = - bincode::deserialize(&data).expect("Failed to deserialize SlotHashes sysvar."); + wincode::deserialize(&data).expect("Failed to deserialize SlotHashes sysvar."); self.slot_hashes = Some(data); self.slot_hashes_obj = Some(Arc::new(slot_hashes)); } @@ -229,7 +229,7 @@ impl SysvarCache { if self.slot_hashes.is_none() { get_account_data(&SlotHashes::id(), &mut |data: &[u8]| { - if let Ok(obj) = bincode::deserialize::(data) { + if let Ok(obj) = wincode::deserialize::(data) { self.slot_hashes = Some(data.to_vec()); self.slot_hashes_obj = Some(Arc::new(obj)); } @@ -366,7 +366,10 @@ pub mod get_sysvar_with_account_check { #[cfg(test)] mod tests { - use {super::*, test_case::test_case}; + use { + super::*, solana_stake_interface::stake_history::SIZE as STAKE_HISTORY_ACCOUNT_SIZE, + test_case::test_case, + }; // sysvar cache provides the full account data of a sysvar // the setters MUST NOT be changed to serialize an object representation @@ -375,17 +378,17 @@ mod tests { // * account data is larger than struct sysvar // * vector sysvar has fewer than its maximum entries // if at any point the data is roundtripped through bincode, the vector will shrink - #[test_case(Clock::default(); "clock")] - #[test_case(EpochSchedule::default(); "epoch_schedule")] - #[test_case(EpochRewards::default(); "epoch_rewards")] - #[test_case(Rent::default(); "rent")] - #[test_case(SlotHashes::default(); "slot_hashes")] - #[test_case(StakeHistory::default(); "stake_history")] - #[test_case(LastRestartSlot::default(); "last_restart_slot")] - fn test_sysvar_cache_preserves_bytes(_: T) { + #[test_case(Clock::default(), 40; "clock")] + #[test_case(EpochSchedule::default(), 33; "epoch_schedule")] + #[test_case(EpochRewards::default(), 81; "epoch_rewards")] + #[test_case(Rent::default(), 17; "rent")] + #[test_case(SlotHashes::default(), 20_488; "slot_hashes")] + #[test_case(StakeHistory::default(), STAKE_HISTORY_ACCOUNT_SIZE; "stake_history")] + #[test_case(LastRestartSlot::default(), 8; "last_restart_slot")] + fn test_sysvar_cache_preserves_bytes(_: T, account_size: usize) { let id = T::id(); - let size = T::size_of().saturating_mul(2); - let in_buf = vec![0; size]; + let account_size = account_size.saturating_mul(2); + let in_buf = vec![0; account_size]; let mut sysvar_cache = SysvarCache::default(); sysvar_cache.fill_missing_entries(|pubkey, callback| { diff --git a/program-test/Cargo.toml b/program-test/Cargo.toml index a132dbc306a..35a3553cf44 100644 --- a/program-test/Cargo.toml +++ b/program-test/Cargo.toml @@ -8,6 +8,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] @@ -65,6 +70,7 @@ solana-transaction-context = { workspace = true } solana-transaction-error = { workspace = true } solana-vote-program = { workspace = true } spl-generic-token = { workspace = true } +spl-memo-interface = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } @@ -73,3 +79,6 @@ solana-cpi = { workspace = true } solana-program = { workspace = true } solana-program-test = { path = ".", features = ["agave-unstable-api"] } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/program-test/src/lib.rs b/program-test/src/lib.rs index d5310a5e8d5..d67703cea0b 100644 --- a/program-test/src/lib.rs +++ b/program-test/src/lib.rs @@ -186,21 +186,21 @@ pub fn invoke_builtin_function( // Commit AccountInfo changes back into KeyedAccounts for i in deduplicated_indices.into_iter() { let mut borrowed_account = instruction_context.try_borrow_instruction_account(i)?; - if borrowed_account.is_writable() { - if let Some(account_info) = account_info_map.get(borrowed_account.get_key()) { - if borrowed_account.get_lamports() != account_info.lamports() { - borrowed_account.set_lamports(account_info.lamports())?; - } + if borrowed_account.is_writable() + && let Some(account_info) = account_info_map.get(borrowed_account.get_key()) + { + if borrowed_account.get_lamports() != account_info.lamports() { + borrowed_account.set_lamports(account_info.lamports())?; + } - if borrowed_account - .can_data_be_resized(account_info.data_len()) - .is_ok() - { - borrowed_account.set_data_from_slice(&account_info.data.borrow())?; - } - if borrowed_account.get_owner() != account_info.owner { - borrowed_account.set_owner(account_info.owner.as_ref())?; - } + if borrowed_account + .can_data_be_resized(account_info.data_len()) + .is_ok() + { + borrowed_account.set_data_from_slice(&account_info.data.borrow())?; + } + if borrowed_account.get_owner() != account_info.owner { + borrowed_account.set_owner(account_info.owner.as_ref())?; } } } diff --git a/program-test/tests/core_bpf.rs b/program-test/tests/core_bpf.rs index 5af93d36a56..d2748d93a31 100644 --- a/program-test/tests/core_bpf.rs +++ b/program-test/tests/core_bpf.rs @@ -1,10 +1,7 @@ use { - solana_instruction::Instruction, solana_program_test::{ProgramTest, ProgramTestContext}, solana_pubkey::Pubkey, solana_sdk_ids::bpf_loader_upgradeable, - solana_signer::Signer, - solana_transaction::Transaction, }; async fn assert_bpf_program(context: &ProgramTestContext, program_id: &Pubkey) { @@ -28,33 +25,3 @@ async fn test_vended_core_bpf_programs() { assert_bpf_program(&context, &solana_sdk_ids::feature::id()).await; assert_bpf_program(&context, &solana_sdk_ids::stake::id()).await; } - -// Note this test has to use an entry from `solana_builtins::BUILTINS` with a -// `core_bpf_migration_config` set. -#[tokio::test] -async fn test_add_core_bpf_program_manually() { - // Core BPF program: Slashing. - let program_id = Pubkey::from_str_const("S1ashing11111111111111111111111111111111111"); - - let mut program_test = ProgramTest::default(); - program_test.add_upgradeable_program_to_genesis("noop_program", &program_id); - - let context = program_test.start_with_context().await; - - // Assert the program is a BPF Loader Upgradeable program. - assert_bpf_program(&context, &program_id).await; - - // Invoke the program. - let instruction = Instruction::new_with_bytes(program_id, &[], Vec::new()); - let transaction = Transaction::new_signed_with_payer( - &[instruction], - Some(&context.payer.pubkey()), - &[&context.payer], - context.last_blockhash, - ); - context - .banks_client - .process_transaction(transaction) - .await - .unwrap(); -} diff --git a/program-test/tests/spl.rs b/program-test/tests/spl.rs index 2c42020ba8b..22f993a2ccd 100644 --- a/program-test/tests/spl.rs +++ b/program-test/tests/spl.rs @@ -24,12 +24,18 @@ async fn programs_present() { let (token_2022_programdata_id, _) = Pubkey::find_program_address(&[token_2022_id.as_ref()], &bpf_loader_upgradeable::id()); + let memo_id = spl_memo_interface::v4::id(); + let (memo_programdata_id, _) = + Pubkey::find_program_address(&[memo_id.as_ref()], &bpf_loader_upgradeable::id()); + for (program_id, _) in spl_programs(&rent) { let program_account = banks_client.get_account(program_id).await.unwrap().unwrap(); if program_id == token_id || program_id == token_programdata_id || program_id == token_2022_id || program_id == token_2022_programdata_id + || program_id == memo_id + || program_id == memo_programdata_id { assert_eq!(program_account.owner, bpf_loader_upgradeable::id()); } else { diff --git a/program-test/tests/warp.rs b/program-test/tests/warp.rs index bcf32646a62..c541c426d53 100644 --- a/program-test/tests/warp.rs +++ b/program-test/tests/warp.rs @@ -194,10 +194,12 @@ async fn stake_rewards_from_warp() { let stake_history: StakeHistory = deserialize(&stake_history_account.data).unwrap(); let clock: Clock = deserialize(&clock_account.data).unwrap(); let stake = stake_state.stake().unwrap(); - assert_eq!( + let stake_activation_status = stake .delegation - .stake_activating_and_deactivating(clock.epoch, &stake_history, None), + .stake_activating_and_deactivating_v2(clock.epoch, &stake_history, None); + assert_eq!( + stake_activation_status, StakeActivationStatus::with_effective(stake.delegation.stake), ); } diff --git a/programs/bpf-loader-tests/Cargo.toml b/programs/bpf-loader-tests/Cargo.toml index 69455a4c1aa..abb4438778e 100644 --- a/programs/bpf-loader-tests/Cargo.toml +++ b/programs/bpf-loader-tests/Cargo.toml @@ -34,3 +34,6 @@ solana-signer = { workspace = true } solana-system-interface = { workspace = true } solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } + +[lints] +workspace = true diff --git a/programs/bpf_loader/Cargo.toml b/programs/bpf_loader/Cargo.toml index 2ce19efaf60..35cbe43c41c 100644 --- a/programs/bpf_loader/Cargo.toml +++ b/programs/bpf_loader/Cargo.toml @@ -7,10 +7,12 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -78,3 +80,6 @@ harness = false [[bench]] name = "bpf_loader_upgradeable" harness = false + +[lints] +workspace = true diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index 884a209cb5b..1e1c79de083 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -231,6 +231,14 @@ fn process_loader_upgradeable_instruction( // Verify Buffer account let buffer = instruction_context.try_borrow_instruction_account(3)?; + if !buffer.is_writable() { + ic_logger_msg!(log_collector, "Buffer account not writeable"); + return Err(InstructionError::InvalidArgument); + } + if buffer.get_owner() != program_id { + ic_logger_msg!(log_collector, "Buffer account not owned by loader"); + return Err(InstructionError::IncorrectProgramId); + } if let UpgradeableLoaderState::Buffer { authority_address } = buffer.get_state()? { if authority_address != authority_key { ic_logger_msg!(log_collector, "Buffer and upgrade authority don't match"); @@ -396,6 +404,14 @@ fn process_loader_upgradeable_instruction( // Verify Buffer account let buffer = instruction_context.try_borrow_instruction_account(2)?; + if !buffer.is_writable() { + ic_logger_msg!(log_collector, "Buffer account not writeable"); + return Err(InstructionError::InvalidArgument); + } + if buffer.get_owner() != program_id { + ic_logger_msg!(log_collector, "Buffer account not owned by loader"); + return Err(InstructionError::IncorrectProgramId); + } if let UpgradeableLoaderState::Buffer { authority_address } = buffer.get_state()? { if authority_address != authority_key { ic_logger_msg!(log_collector, "Buffer and upgrade authority don't match"); @@ -2081,14 +2097,33 @@ mod tests { Err(InstructionError::InvalidArgument), ); - // Case: Buffer account not owned by loader - // The program actually does not deliberately check for this. - // It's only when it attempts to mutate the account that it trips an - // error. + // Case: Buffer account not writable + for buffer_balance in [0, 1_000_000, 15 * 1_000_000_000] { + let (mut transaction_accounts, mut instruction_accounts) = get_accounts( + &buffer_address, + &upgrade_authority_address, + &upgrade_authority_address, + &elf_orig, + &elf_new, + ); + transaction_accounts + .get_mut(2) + .unwrap() + .1 + .set_lamports(buffer_balance); + instruction_accounts.get_mut(2).unwrap().is_writable = false; + process_instruction( + transaction_accounts, + instruction_accounts, + Err(InstructionError::InvalidArgument), + ); + } + + // Case: Buffer account not owned by loader: lamports scenario // - // For `Upgrade`, this occurs when attempting to close the buffer. - // The first thing it attempts to do is debit the lamports, which - // immediately throws `ExternalAccountLamportSpend`. + // In `Upgrade`, the buffer's lamports are used to fund the additional + // programdata rent directly, with the rest spilled to the spill + // account. Then, the buffer's data is set to `size_of_buffer(0)`. let (mut transaction_accounts, instruction_accounts) = get_accounts( &buffer_address, &upgrade_authority_address, @@ -2096,15 +2131,72 @@ mod tests { &elf_orig, &elf_new, ); - transaction_accounts - .get_mut(2) - .unwrap() - .1 - .set_owner(Pubkey::new_unique()); + { + // Let's make sure the programdata requires a top-up. + let required_rent = |elf_len| { + Rent::default() + .minimum_balance(UpgradeableLoaderState::size_of_programdata(elf_len)) + }; + let rent_orig = required_rent(elf_orig.len()); + let rent_new = required_rent(elf_new.len()); + let programdata = &mut transaction_accounts.first_mut().unwrap().1; + programdata.set_lamports(rent_orig); + let buffer = &mut transaction_accounts.get_mut(2).unwrap().1; + buffer.set_owner(Pubkey::new_unique()); + buffer.set_lamports(rent_new); + } process_instruction( transaction_accounts, instruction_accounts, - Err(InstructionError::ExternalAccountLamportSpend), + Err(InstructionError::IncorrectProgramId), + ); + + // Case: Buffer account not owned by loader: shrink scenario + // + // Same as the above case, but give the buffer a lamports balance of + // `0`, rendering its balance "unchanged" by the spill operation. + let (mut transaction_accounts, instruction_accounts) = get_accounts( + &buffer_address, + &upgrade_authority_address, + &upgrade_authority_address, + &elf_orig, + &elf_new, + ); + { + // Set the buffer's lamports to zero. + let buffer = &mut transaction_accounts.get_mut(2).unwrap().1; + buffer.set_owner(Pubkey::new_unique()); + buffer.set_lamports(0); + } + process_instruction( + transaction_accounts, + instruction_accounts, + Err(InstructionError::IncorrectProgramId), + ); + + // Case: Buffer account not owned by loader: no-op scenario + // + // Same as the above case, but also truncate the buffer's data to + // `size_of_buffer(0)` - just the buffer metadata, no ELF - rendering + // the closing resize "unchanged" as well. + let (mut transaction_accounts, instruction_accounts) = get_accounts( + &buffer_address, + &upgrade_authority_address, + &upgrade_authority_address, + &elf_orig, + &elf_new, + ); + { + // Empty the buffer (metadata only) and zero its lamports. + let buffer = &mut transaction_accounts.get_mut(2).unwrap().1; + buffer.set_owner(Pubkey::new_unique()); + buffer.set_lamports(0); + truncate_data(buffer, UpgradeableLoaderState::size_of_buffer(0)); + } + process_instruction( + transaction_accounts, + instruction_accounts, + Err(InstructionError::IncorrectProgramId), ); // Case: Buffer account too big @@ -2640,14 +2732,34 @@ mod tests { Err(InstructionError::InvalidArgument), ); - // Case: Buffer account not owned by loader - // The program actually does not deliberately check for this. - // It's only when it attempts to mutate the account that it trips an - // error. + // Case: Buffer account not writable + for buffer_balance in [0, 1_000_000, 15 * 1_000_000_000] { + let (mut transaction_accounts, mut instruction_accounts) = get_accounts( + &payer_address, + &buffer_address, + &upgrade_authority_address, + &upgrade_authority_address, + &elf, + ); + transaction_accounts + .get_mut(3) + .unwrap() + .1 + .set_lamports(buffer_balance); + instruction_accounts.get_mut(3).unwrap().is_writable = false; + process_instruction( + elf.len(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::InvalidArgument), + ); + } + + // Case: Buffer account not owned by loader: lamports scenario // - // For `DeployWithMaxDataLen`, this occurs when attempting to close the - // buffer. The first thing it attempts to do is debit the lamports, - // which immediately throws `ExternalAccountLamportSpend`. + // In `DeployWithMaxDataLen`, the buffer's lamports are drained to the + // payer before the payer is debited for the programdata's rent. Then, + // the buffer's data is set to `size_of_buffer(0)`. let (mut transaction_accounts, instruction_accounts) = get_accounts( &payer_address, &buffer_address, @@ -2655,16 +2767,71 @@ mod tests { &upgrade_authority_address, &elf, ); - transaction_accounts - .get_mut(3) - .unwrap() - .1 - .set_owner(Pubkey::new_unique()); + { + // Let's make sure the programdata requires a top-up. + let required_rent = Rent::default() + .minimum_balance(UpgradeableLoaderState::size_of_programdata(elf.len())); + let programdata = &transaction_accounts.get(1).unwrap().1; + assert!(programdata.lamports() < required_rent); + let buffer = &mut transaction_accounts.get_mut(3).unwrap().1; + buffer.set_owner(Pubkey::new_unique()); + buffer.set_lamports(required_rent); + } + process_instruction( + elf.len(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::IncorrectProgramId), + ); + + // Case: Buffer account not owned by loader: shrink scenario + // + // Same as the above case, but give the buffer a lamports balance of + // `0`, rendering its balance "unchanged" by the drain operation. + let (mut transaction_accounts, instruction_accounts) = get_accounts( + &payer_address, + &buffer_address, + &upgrade_authority_address, + &upgrade_authority_address, + &elf, + ); + { + // Set the buffer's lamports to zero. + let buffer = &mut transaction_accounts.get_mut(3).unwrap().1; + buffer.set_owner(Pubkey::new_unique()); + buffer.set_lamports(0); + } + process_instruction( + elf.len(), + transaction_accounts, + instruction_accounts, + Err(InstructionError::IncorrectProgramId), + ); + + // Case: Buffer account not owned by loader: no-op scenario + // + // Same as the above case, but also truncate the buffer's data to + // `size_of_buffer(0)` - just the buffer metadata, no ELF - rendering + // the closing resize "unchanged" as well. + let (mut transaction_accounts, instruction_accounts) = get_accounts( + &payer_address, + &buffer_address, + &upgrade_authority_address, + &upgrade_authority_address, + &elf, + ); + { + // Empty the buffer (metadata only) and zero its lamports. + let buffer = &mut transaction_accounts.get_mut(3).unwrap().1; + buffer.set_owner(Pubkey::new_unique()); + buffer.set_lamports(0); + truncate_data(buffer, UpgradeableLoaderState::size_of_buffer(0)); + } process_instruction( elf.len(), transaction_accounts, instruction_accounts, - Err(InstructionError::ExternalAccountLamportSpend), + Err(InstructionError::IncorrectProgramId), ); // Case: Max data length too small for Buffer data diff --git a/programs/compute-budget-bench/Cargo.toml b/programs/compute-budget-bench/Cargo.toml index 006f322e300..02db0a32310 100644 --- a/programs/compute-budget-bench/Cargo.toml +++ b/programs/compute-budget-bench/Cargo.toml @@ -27,3 +27,6 @@ solana-svm-transaction = { workspace = true } [[bench]] name = "compute_budget" harness = false + +[lints] +workspace = true diff --git a/programs/compute-budget/Cargo.toml b/programs/compute-budget/Cargo.toml index 722526b6b09..1213bdd4ba6 100644 --- a/programs/compute-budget/Cargo.toml +++ b/programs/compute-budget/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -21,3 +23,6 @@ agave-unstable-api = [] [dependencies] solana-program-runtime = { workspace = true } + +[lints] +workspace = true diff --git a/programs/ed25519-tests/Cargo.toml b/programs/ed25519-tests/Cargo.toml index 4fd9fc3d125..b676e59e43b 100644 --- a/programs/ed25519-tests/Cargo.toml +++ b/programs/ed25519-tests/Cargo.toml @@ -25,3 +25,6 @@ solana-program-test = { path = "../../program-test", features = ["agave-unstable solana-signer = { workspace = true } solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } + +[lints] +workspace = true diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index a436b6ecfaa..8325d112706 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -56,7 +56,7 @@ dependencies = [ [[package]] name = "agave-banking-stage-ingress-types" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "crossbeam-channel", "solana-perf", @@ -64,7 +64,7 @@ dependencies = [ [[package]] name = "agave-bls-cert-verify" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-votor-messages", "bitvec", @@ -76,13 +76,49 @@ dependencies = [ "wincode", ] +[[package]] +name = "agave-bls-sigverify" +version = "4.3.0-alpha.0" +dependencies = [ + "agave-bls-cert-verify", + "agave-math-utils", + "agave-votor-messages", + "bitvec", + "crossbeam-channel", + "log", + "parking_lot 0.12.2", + "qualifier_attr", + "rayon", + "solana-bls-signatures", + "solana-clock", + "solana-gossip", + "solana-ledger", + "solana-measure", + "solana-metrics", + "solana-packet 4.1.0", + "solana-perf", + "solana-pubkey 4.2.0", + "solana-runtime", + "solana-signer-store", + "solana-streamer", + "thiserror 2.0.18", + "wincode", +] + +[[package]] +name = "agave-cpu-utils" +version = "4.3.0-alpha.0" +dependencies = [ + "libc", +] + [[package]] name = "agave-feature-set" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "ahash 0.8.12", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-pubkey 4.2.0", "solana-sha256-hasher", @@ -91,7 +127,7 @@ dependencies = [ [[package]] name = "agave-fs" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-io-uring", "io-uring", @@ -105,11 +141,11 @@ dependencies = [ [[package]] name = "agave-geyser-plugin-interface" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "log", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-signature", "solana-transaction", @@ -119,7 +155,7 @@ dependencies = [ [[package]] name = "agave-io-uring" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "io-uring", "libc", @@ -130,7 +166,7 @@ dependencies = [ [[package]] name = "agave-logger" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "env_logger", "libc", @@ -140,14 +176,14 @@ dependencies = [ [[package]] name = "agave-math-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "num-traits", ] [[package]] name = "agave-precompiles" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "bincode", @@ -166,14 +202,14 @@ dependencies = [ [[package]] name = "agave-random" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "rand 0.9.4", ] [[package]] name = "agave-reserved-account-keys" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "solana-pubkey 4.2.0", @@ -182,11 +218,11 @@ dependencies = [ [[package]] name = "agave-scheduler-bindings" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "agave-scheduling-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-scheduler-bindings", @@ -199,13 +235,14 @@ dependencies = [ "slotmap", "solana-fee", "solana-pubkey 4.2.0", + "solana-runtime-transaction", "solana-transaction-error", "thiserror 2.0.18", ] [[package]] name = "agave-snapshots" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-fs", "bincode", @@ -219,7 +256,7 @@ dependencies = [ "solana-accounts-db", "solana-clock", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-lattice-hash", "solana-measure", "solana-metrics", @@ -234,34 +271,34 @@ dependencies = [ [[package]] name = "agave-transaction-view" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-packet 4.1.0", - "solana-program-runtime", "solana-pubkey 4.2.0", "solana-sdk-ids", "solana-short-vec", "solana-signature", "solana-svm-transaction", "solana-transaction", - "solana-transaction-context", ] [[package]] name = "agave-validator" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-cpu-utils", + "agave-feature-set", "agave-logger", "agave-snapshots", "agave-votor", "agave-xdp", + "bytesize", "caps", "chrono", "clap", "console 0.16.3", - "core_affinity", "crossbeam-channel", "fd-lock", "indicatif", @@ -279,7 +316,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-accounts-db", "solana-clap-utils", "solana-cli-config", @@ -294,7 +331,7 @@ dependencies = [ "solana-genesis-utils", "solana-geyser-plugin-manager", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-keypair", "solana-ledger", @@ -332,31 +369,29 @@ dependencies = [ [[package]] name = "agave-votor" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-bls-sigverify", "agave-logger", "agave-math-utils", "agave-votor-messages", - "bincode", "bitvec", "crossbeam-channel", "itertools 0.14.0", "lazy-lru", "log", "parking_lot 0.12.2", - "qualifier_attr", "serde", "serde_bytes", - "solana-accounts-db", - "solana-bloom", "solana-bls-signatures", "solana-client", "solana-clock", "solana-connection-cache", "solana-epoch-schedule", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-metrics", @@ -370,24 +405,23 @@ dependencies = [ "solana-time-utils", "solana-transaction", "solana-transaction-error", - "solana-vote", - "solana-vote-program", "thiserror 2.0.18", "wincode", ] [[package]] name = "agave-votor-messages" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", + "crossbeam-channel", "log", "serde", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-bls-signatures", "solana-clock", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-short-vec", "solana-signer-store", @@ -397,15 +431,15 @@ dependencies = [ [[package]] name = "agave-xdp" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-xdp-ebpf", "arc-swap", "arrayvec", "aya", "bytes", "caps", - "core_affinity", "crossbeam-channel", "libc", "log", @@ -414,7 +448,7 @@ dependencies = [ [[package]] name = "agave-xdp-ebpf" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "aya", "aya-ebpf", @@ -826,9 +860,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] name = "ascii" @@ -931,7 +965,7 @@ checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" dependencies = [ "base64 0.22.1", "flate2", - "http 1.4.0", + "http 1.4.2", "log", "native-tls", "serde", @@ -964,7 +998,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "itoa", @@ -988,7 +1022,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "mime", @@ -1000,19 +1034,20 @@ dependencies = [ [[package]] name = "aya" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18bc4e506fbb85ab7392ed993a7db4d1a452c71b75a246af4a80ab8c9d2dd50" +checksum = "66e644424fada9fff4fdc63848db1732fb69b626e8328202ef55c03df1f4d939" dependencies = [ "assert_matches", "aya-obj", - "bitflags 2.11.1", - "bytes", + "bitflags 2.13.0", + "hashbrown 0.17.0", "libc", "log", "object", "once_cell", - "thiserror 1.0.69", + "scopeguard", + "thiserror 2.0.18", ] [[package]] @@ -1056,16 +1091,14 @@ dependencies = [ [[package]] name = "aya-obj" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51b96c5a8ed8705b40d655273bc4212cbbf38d4e3be2788f36306f154523ec7" +checksum = "8c76b9c75d9cdc155ff8f6a06d61e873f67bf47be8cfa92a3b5aaea43f4b4077" dependencies = [ "bytes", - "core-error", - "hashbrown 0.15.1", "log", "object", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -1107,18 +1140,40 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", "syn 2.0.117", ] +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.6", + "rand_core 0.6.4", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +dependencies = [ + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.2.1" @@ -1127,9 +1182,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -1142,9 +1197,9 @@ checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" [[package]] name = "bitvec" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" dependencies = [ "funty", "radium", @@ -1225,9 +1280,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" dependencies = [ "borsh-derive", "bytes", @@ -1236,9 +1291,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" dependencies = [ "once_cell", "proc-macro-crate 3.1.0", @@ -1337,13 +1392,19 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" dependencies = [ "serde", ] +[[package]] +name = "bytesize" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e78e506b9d7633710dab98996f22f95f3d0f488e8f1aa162830556ed9fc14d" + [[package]] name = "bzip2" version = "0.6.1" @@ -1431,9 +1492,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -1580,15 +1641,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "core-error" -version = "0.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efcdb2972eb64230b4c50646d8498ff73f5128d196a90c7236eec4cbe8619b8f" -dependencies = [ - "version_check", -] - [[package]] name = "core-foundation" version = "0.9.3" @@ -1615,17 +1667,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core_affinity" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a034b3a7b624016c6e13f5df875747cc25f884156aad2abd12b6c46797971342" -dependencies = [ - "libc", - "num_cpus", - "winapi", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -1688,12 +1729,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" -dependencies = [ - "cfg-if 1.0.4", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -1803,38 +1841,14 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.117", + "darling_core", + "darling_macro", ] [[package]] @@ -1850,24 +1864,13 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.117", -] - [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.117", ] @@ -2066,29 +2069,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "downcast" version = "0.11.0" @@ -2749,7 +2729,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http 1.4.2", "indexmap", "slab", "tokio", @@ -2801,7 +2781,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" dependencies = [ "allocator-api2", - "equivalent", "foldhash 0.1.5", ] @@ -2834,6 +2813,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + [[package]] name = "histogram" version = "0.6.9" @@ -2883,9 +2871,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -2909,7 +2897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.2", ] [[package]] @@ -2920,7 +2908,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "pin-project-lite", ] @@ -2986,7 +2974,7 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "httparse", "httpdate", @@ -3005,7 +2993,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", - "http 1.4.0", + "http 1.4.2", "hyper 1.8.1", "hyper-util", "rustls", @@ -3055,7 +3043,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "hyper 1.8.1", "ipnet", @@ -3336,7 +3324,7 @@ version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "libc", ] @@ -3372,15 +3360,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -3805,9 +3784,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru" @@ -3881,9 +3860,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -4044,7 +4023,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "cfg_aliases", "libc", @@ -4162,6 +4141,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4198,12 +4178,12 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "crc32fast", - "hashbrown 0.15.1", + "hashbrown 0.17.0", "indexmap", "memchr", ] @@ -4237,11 +4217,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.80" +version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if 1.0.4", "foreign-types", "libc", @@ -4277,9 +4257,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.116" +version = "0.9.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" dependencies = [ "cc", "libc", @@ -4392,9 +4372,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pastey" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "pbkdf2" @@ -4405,16 +4385,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - [[package]] name = "pem" version = "1.1.1" @@ -4650,9 +4620,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -4660,9 +4630,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ "heck", "itertools 0.14.0", @@ -4681,9 +4651,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools 0.14.0", @@ -4694,9 +4664,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] @@ -4736,7 +4706,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "memchr", "unicase", ] @@ -4772,16 +4742,16 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "socket2 0.6.3", "thiserror 2.0.18", @@ -4792,9 +4762,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "bytes", "fastbloom", @@ -4802,7 +4772,7 @@ dependencies = [ "lru-slab", "rand 0.9.4", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -5030,9 +5000,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick 1.0.1", "memchr", @@ -5053,9 +5023,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -5068,7 +5038,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -5108,7 +5078,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -5142,7 +5112,7 @@ checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" dependencies = [ "anyhow", "async-trait", - "http 1.4.0", + "http 1.4.2", "reqwest 0.12.28", "serde", "thiserror 1.0.69", @@ -5194,9 +5164,9 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.5.2" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac5b223d9738ef56e0b98305410be40fa0941bf6036c56f1506751e43552d64" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" dependencies = [ "libc", "rtoolbox", @@ -5239,12 +5209,6 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -5275,7 +5239,7 @@ version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.4.14", @@ -5288,7 +5252,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.12.1", @@ -5297,9 +5261,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "log", "once_cell", @@ -5448,7 +5412,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -5531,9 +5495,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -5556,9 +5520,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "serde_core", "serde_with_macros", @@ -5566,11 +5530,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -5747,9 +5711,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" dependencies = [ "serde", ] @@ -5820,9 +5784,9 @@ dependencies = [ [[package]] name = "solana-account" -version = "4.3.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c1b95bd432efb92837bc38001365e5acb5c0175b199bc447b08a8c320ecf6c" +checksum = "385cad928d576683db3afef1b88d00a31a7126cc9f6f3f0c9f6b2d181437f53c" dependencies = [ "bincode", "qualifier_attr", @@ -5839,7 +5803,7 @@ dependencies = [ [[package]] name = "solana-account-decoder" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "Inflector", "base64 0.22.1", @@ -5848,7 +5812,7 @@ dependencies = [ "bv", "serde", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder-client-types", "solana-address-lookup-table-interface", "solana-clock", @@ -5868,24 +5832,26 @@ dependencies = [ "solana-stake-interface", "solana-sysvar", "solana-vote-interface", + "solana-zk-sdk-pod", "spl-generic-token", "spl-token-2022-interface", "spl-token-group-interface", "spl-token-interface", "spl-token-metadata-interface", "thiserror 2.0.18", + "wincode", "zstd", ] [[package]] name = "solana-account-decoder-client-types" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "bs58", "serde", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-pubkey 4.2.0", "zstd", ] @@ -5898,7 +5864,7 @@ checksum = "a9cf16495d9eb53e3d04e72366a33bb1c20c24e78c171d8b8f5978357b63ae95" dependencies = [ "bincode", "serde_core", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-program-error", "solana-program-memory", ] @@ -5909,13 +5875,13 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc141b940560430425ebaadb7645496c45f6a10fad9911d719bd03eab7f4d422" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-program-error", ] [[package]] name = "solana-accounts-db" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-fs", "ahash 0.8.12", @@ -5932,13 +5898,13 @@ dependencies = [ "seqlock", "serde", "smallvec", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-address-lookup-table-interface", "solana-bucket-map", "solana-clock", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-lattice-hash", "solana-measure", @@ -5971,14 +5937,14 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] name = "solana-address" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1384b52c435a750cc9c538760fc7bb472fd78e65a9900a2d07312c5bb335b72" +checksum = "39c93e262f671bf402e1040e4a7e40b05d81da5956c7681948c975a0997517bb" dependencies = [ "borsh", "bytemuck", @@ -6015,6 +5981,7 @@ dependencies = [ "solana-pubkey 4.2.0", "solana-sdk-ids", "solana-slot-hashes", + "wincode", ] [[package]] @@ -6028,15 +5995,15 @@ dependencies = [ [[package]] name = "solana-banks-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "borsh", "futures 0.3.32", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-banks-interface", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-program-pack", "solana-pubkey 4.2.0", @@ -6054,13 +6021,13 @@ dependencies = [ [[package]] name = "solana-banks-interface" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-pubkey 4.2.0", "solana-signature", @@ -6072,17 +6039,17 @@ dependencies = [ [[package]] name = "solana-banks-server" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "bincode", "crossbeam-channel", "futures 0.3.32", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-banks-interface", "solana-clock", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-net-utils", "solana-pubkey 4.2.0", @@ -6128,12 +6095,12 @@ checksum = "7116e1d942a2432ca3f514625104757ab8a56233787e95144c93950029e31176" dependencies = [ "blake3", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] name = "solana-bloom" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bv", "fnv", @@ -6141,6 +6108,7 @@ dependencies = [ "serde", "solana-sanitize", "solana-time-utils", + "wincode", ] [[package]] @@ -6210,11 +6178,11 @@ dependencies = [ [[package]] name = "solana-bpf-loader-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", "qualifier_attr", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bincode", "solana-clock", "solana-instruction", @@ -6235,13 +6203,13 @@ dependencies = [ [[package]] name = "solana-bucket-map" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "ahash 0.8.12", "bv", "bytemuck", "bytemuck_derive", - "memmap2 0.9.10", + "memmap2 0.9.11", "modular-bitfield", "num_enum", "rand 0.9.4", @@ -6253,12 +6221,12 @@ dependencies = [ [[package]] name = "solana-builtins" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "solana-bpf-loader-program", "solana-compute-budget-program", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-program-runtime", "solana-pubkey 4.2.0", "solana-sdk-ids", @@ -6270,7 +6238,7 @@ dependencies = [ [[package]] name = "solana-builtins-default-costs" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "ahash 0.8.12", @@ -6280,8 +6248,10 @@ dependencies = [ [[package]] name = "solana-clap-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "bip39", + "bs58", "chrono", "clap", "rpassword", @@ -6290,7 +6260,7 @@ dependencies = [ "solana-cluster-type", "solana-commitment-config", "solana-derivation-path", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-native-token", @@ -6301,14 +6271,13 @@ dependencies = [ "solana-signature", "solana-signer", "thiserror 2.0.18", - "tiny-bip39", "uriparse", "url 2.5.8", ] [[package]] name = "solana-cli-config" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "dirs-next", "serde", @@ -6320,11 +6289,13 @@ dependencies = [ [[package]] name = "solana-cli-output" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "Inflector", "agave-reserved-account-keys", + "agave-votor-messages", "base64 0.22.1", + "bitvec", "chrono", "clap", "console 0.16.3", @@ -6334,15 +6305,18 @@ dependencies = [ "semver", "serde", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-bincode", + "solana-bls-signatures", "solana-clap-utils", "solana-cli-config", "solana-clock", "solana-epoch-info", - "solana-hash 4.3.0", + "solana-epoch-schedule", + "solana-hash 4.4.0", "solana-message", + "solana-native-token", "solana-packet 4.1.0", "solana-pubkey 4.2.0", "solana-rpc-client-api", @@ -6359,7 +6333,7 @@ dependencies = [ [[package]] name = "solana-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-trait", "bincode", @@ -6369,7 +6343,7 @@ dependencies = [ "log", "solana-commitment-config", "solana-connection-cache", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-message", @@ -6398,10 +6372,10 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19635cc703678bcb26eb6faee4a363c71f4a0d622c445d9b70c048c42f4cbcd3" dependencies = [ - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-commitment-config", "solana-epoch-info", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -6415,12 +6389,13 @@ dependencies = [ [[package]] name = "solana-clock" -version = "3.0.1" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95cf11109c3b6115cc510f1e31f06fdd52f504271bc24ef5f1249fbbcae5f9f3" +checksum = "f0acdace90d96e2c9e70d681465b4fe888b6bcf27c354ae9774e9f8a3b72923d" dependencies = [ "serde", "serde_derive", + "solana-get-sysvar", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -6434,7 +6409,7 @@ checksum = "3a494cf8eda7d98d9f0144b288bb409c88308d2e86f15cc1045aa77b83304718" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -6449,7 +6424,7 @@ dependencies = [ [[package]] name = "solana-compute-budget" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-fee-structure", "solana-program-runtime", @@ -6457,7 +6432,7 @@ dependencies = [ [[package]] name = "solana-compute-budget-instruction" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "solana-borsh", @@ -6485,7 +6460,7 @@ dependencies = [ [[package]] name = "solana-compute-budget-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-program-runtime", ] @@ -6509,7 +6484,7 @@ dependencies = [ [[package]] name = "solana-connection-cache" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-trait", "crossbeam-channel", @@ -6526,10 +6501,11 @@ dependencies = [ [[package]] name = "solana-core" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-banking-stage-ingress-types", "agave-bls-cert-verify", + "agave-bls-sigverify", "agave-feature-set", "agave-fs", "agave-logger", @@ -6548,6 +6524,7 @@ dependencies = [ "assert_matches", "async-trait", "bincode", + "bitvec", "bytes", "chrono", "crossbeam-channel", @@ -6573,7 +6550,8 @@ dependencies = [ "shaq", "signal-hook", "slab", - "solana-account 4.3.0", + "smallvec", + "solana-account 4.3.1", "solana-accounts-db", "solana-address-lookup-table-interface", "solana-bincode", @@ -6594,7 +6572,7 @@ dependencies = [ "solana-geyser-plugin-manager", "solana-gossip", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-leader-schedule", @@ -6621,6 +6599,7 @@ dependencies = [ "solana-shred-version", "solana-signature", "solana-signer", + "solana-signer-store", "solana-slot-hashes", "solana-slot-history", "solana-streamer", @@ -6660,7 +6639,7 @@ dependencies = [ [[package]] name = "solana-cost-model" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "ahash 0.8.12", @@ -6697,23 +6676,9 @@ dependencies = [ [[package]] name = "solana-curve25519" -version = "2.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae4261b9a8613d10e77ac831a8fa60b6fa52b9b103df46d641deff9f9812a23" -dependencies = [ - "bytemuck", - "bytemuck_derive", - "curve25519-dalek 4.1.3", - "solana-define-syscall 2.3.0", - "subtle", - "thiserror 2.0.18", -] - -[[package]] -name = "solana-curve25519" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ee66a3e25ed8d20ed6fcd1ac71735415ff1edaf33ff1ec2118826eba927bcc" +checksum = "14b4d2a4bf0d0b0a86c22111917e86e8bd39a7b31420fb2c7d73eb83761fc7af" dependencies = [ "bytemuck", "bytemuck_derive", @@ -6723,12 +6688,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "solana-define-syscall" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2" - [[package]] name = "solana-define-syscall" version = "3.0.0" @@ -6760,7 +6719,7 @@ dependencies = [ [[package]] name = "solana-download-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-snapshots", "log", @@ -6784,20 +6743,19 @@ dependencies = [ [[package]] name = "solana-entry" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-votor-messages", "crossbeam-channel", - "dlopen2", "log", "num_cpus", "rayon", "smallvec", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-bls-signatures", "solana-clock", "solana-cost-model", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-measure", "solana-merkle-tree", "solana-message", @@ -6824,13 +6782,14 @@ dependencies = [ [[package]] name = "solana-epoch-rewards" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e7b0ba210593ba8ddd39d6d234d81795d1671cebf3026baa10d5dc23ac42f0" +checksum = "daf7eb4986b0b1d6f562b21f75a836f1a6df6e00c275efcef50aab5c144dc59e" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-get-sysvar", + "solana-hash 4.4.0", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -6843,18 +6802,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ee8beac9bff4db9225e57d532d169b0be5e447f1e6601a2f50f27a01bf5518f" dependencies = [ "siphasher 0.3.11", - "solana-address 2.6.0", - "solana-hash 4.3.0", + "solana-address 2.6.1", + "solana-hash 4.4.0", ] [[package]] name = "solana-epoch-schedule" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce264b7b42322325947c4136a09460bf5c73d9aa8262c9b0a2064be63ba8639" +checksum = "8116e6ffa6002237d5ab5edcbda17f9ba66b6742c45a89c9fb40a94dbacd4c1d" dependencies = [ "serde", "serde_derive", + "solana-get-sysvar", + "solana-program-error", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -6878,7 +6839,7 @@ checksum = "0eb265ff95e28eceda117e2e3d2d2a611ecbbfe911dfeeeecd1521814540ffab" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-nonce", "solana-pubkey 4.2.0", @@ -6889,12 +6850,12 @@ dependencies = [ [[package]] name = "solana-faucet" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "crossbeam-channel", "log", "solana-cli-output", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-message", @@ -6921,7 +6882,7 @@ dependencies = [ "bincode", "serde", "serde_derive", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-instruction", "solana-program-error", @@ -6933,7 +6894,7 @@ dependencies = [ [[package]] name = "solana-fee" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "solana-fee-structure", @@ -6942,9 +6903,9 @@ dependencies = [ [[package]] name = "solana-fee-calculator" -version = "3.2.0" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e8add96b5741573e9f7529c4bb7719cfcfa999c3847a68cdfaef0cb6adf567" +checksum = "ef67f01cc6a0c72e99a08d0d484683f995de4c80e9568728fa77d1537f9b7e09" dependencies = [ "log", "serde", @@ -6985,12 +6946,12 @@ dependencies = [ "memmap2 0.5.10", "serde", "serde_derive", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-clock", "solana-cluster-type", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-keypair", "solana-poh-config", @@ -7005,20 +6966,31 @@ dependencies = [ [[package]] name = "solana-genesis-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-snapshots", "log", "solana-download-utils", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-rpc-client", "thiserror 2.0.18", ] +[[package]] +name = "solana-get-sysvar" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef3bc859fc036ed490146793557386cbfae614ebba4adc704c37d94350824ed4" +dependencies = [ + "solana-address 2.6.1", + "solana-define-syscall 5.1.0", + "solana-program-error", +] + [[package]] name = "solana-geyser-plugin-manager" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-geyser-plugin-interface", "arc-swap", @@ -7030,16 +7002,15 @@ dependencies = [ "libloading", "log", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-accounts-db", "solana-clock", "solana-entry", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-ledger", "solana-measure", "solana-message", - "solana-metrics", "solana-pubkey 4.2.0", "solana-rpc", "solana-runtime", @@ -7052,16 +7023,16 @@ dependencies = [ [[package]] name = "solana-gossip" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-logger", "agave-random", "arc-swap", - "arrayvec", "assert_matches", - "bincode", "bv", + "bytes", "crossbeam-channel", + "ed25519-dalek 2.2.0", "flate2", "indexmap", "itertools 0.14.0", @@ -7074,13 +7045,12 @@ dependencies = [ "serde", "serde-big-array", "serde_bytes", - "siphasher 1.0.2", "solana-bloom", "solana-client", "solana-clock", "solana-entry", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-ledger", "solana-measure", @@ -7103,6 +7073,7 @@ dependencies = [ "solana-version", "solana-vote", "solana-vote-program", + "solana-wincode-varint", "static_assertions", "thiserror 2.0.18", "wincode", @@ -7110,9 +7081,9 @@ dependencies = [ [[package]] name = "solana-hard-forks" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52fd9cc610fd0782f09482527cb7b4f41ec22071303742718b7b57fc43bb236b" +checksum = "45406eccad36220e52988b024d8daa93e691e38d5d71ad5fec55410cc9cf427d" dependencies = [ "serde", "serde_derive", @@ -7124,14 +7095,14 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "337c246447142f660f778cf6cb582beba8e28deb05b3b24bfb9ffd7c562e5f41" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] name = "solana-hash" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b113239362cee7093bfb250467138f079a2a03673181dc15bff6ccd677912d" +checksum = "fe51db00ac3aa9f950d1e6201a126acfa26e6d81bc4a183ba64ec02effcad883" dependencies = [ "borsh", "bytemuck", @@ -7152,9 +7123,9 @@ checksum = "7ce934b02bab639341f2fd62c3e7e3c39dcceb47f0196b7630ed1f82ecb704bd" [[package]] name = "solana-inflation" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f762559c5f962727efdcb03c61f5cf6c5364645695978fb145d25c88bbacdada" +checksum = "ccf104167e42e747602b88e02b25cacfc5de699c3b7cbba60d3250437e6a22ed" dependencies = [ "serde", "serde_derive", @@ -7178,9 +7149,9 @@ dependencies = [ [[package]] name = "solana-instruction-error" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b188842592fdf6cb96f55263ae1bf11713ab5114401d1d5a881ed7cc41bef6" +checksum = "3b7d34343838343a3755b7dfb1e438d94c6db2263b519cfe3c2257af932b6e93" dependencies = [ "num-traits", "serde", @@ -7191,16 +7162,32 @@ dependencies = [ [[package]] name = "solana-instructions-sysvar" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddf67876c541aa1e21ee1acae35c95c6fbc61119814bfef70579317a5e26955" +checksum = "9e0732294560e88ecdb2bbc656e67383e9f88c78ec09469cef172f0d28cd1bcd" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", + "solana-account-info", + "solana-instruction", + "solana-instruction-error", + "solana-program-error", + "solana-sanitize", + "solana-sdk-ids", + "solana-serialize-utils", + "solana-sysvar-id", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e38363a181313d607f7a118df2b401bb27b477a0104b09283cbf504f5f0f00cc" +dependencies = [ + "bitflags 2.13.0", "solana-account-info", "solana-instruction", "solana-instruction-error", "solana-program-error", - "solana-pubkey 3.0.0", "solana-sanitize", "solana-sdk-ids", "solana-serialize-utils", @@ -7215,7 +7202,7 @@ checksum = "ed1c0d16d6fdeba12291a1f068cdf0d479d9bff1141bf44afd7aa9d485f65ef8" dependencies = [ "sha3", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -7229,7 +7216,7 @@ dependencies = [ "five8", "five8_core", "rand 0.9.4", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-derivation-path", "solana-seed-derivable", "solana-seed-phrase", @@ -7239,12 +7226,13 @@ dependencies = [ [[package]] name = "solana-last-restart-slot" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" +checksum = "c22474b83d3c7c318e1c3a725784fc2d1d03b728e36369e58ce48769a61ed85e" dependencies = [ "serde", "serde_derive", + "solana-get-sysvar", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -7252,7 +7240,7 @@ dependencies = [ [[package]] name = "solana-lattice-hash" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "blake3", @@ -7262,7 +7250,7 @@ dependencies = [ [[package]] name = "solana-leader-schedule" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-random", "itertools 0.14.0", @@ -7274,7 +7262,7 @@ dependencies = [ [[package]] name = "solana-ledger" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-reserved-account-keys", @@ -7282,8 +7270,7 @@ dependencies = [ "agave-votor-messages", "anyhow", "assert_matches", - "bincode", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "bzip2", "chrono", @@ -7309,7 +7296,7 @@ dependencies = [ "rocksdb", "scopeguard", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-accounts-db", "solana-address-lookup-table-interface", @@ -7319,7 +7306,7 @@ dependencies = [ "solana-epoch-schedule", "solana-genesis-config", "solana-genesis-utils", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-leader-schedule", @@ -7409,29 +7396,28 @@ dependencies = [ [[package]] name = "solana-measure" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "solana-merkle-tree" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "fast-math", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", ] [[package]] name = "solana-message" -version = "4.1.1" +version = "4.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a634d1db65a393d4e87ec49ef97c8dfb12201e2ba9e5faf87ff34e632f29e509" +checksum = "d2666e0ff38557ad1239d0fc42f4688fb9df0817fb12c041dbae90c2b2e3fa55" dependencies = [ "blake3", - "lazy_static", "serde", "serde_derive", - "solana-address 2.6.0", - "solana-hash 4.3.0", + "solana-address 2.6.1", + "solana-hash 4.4.0", "solana-instruction", "solana-sanitize", "solana-sdk-ids", @@ -7442,7 +7428,7 @@ dependencies = [ [[package]] name = "solana-metrics" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "crossbeam-channel", "gethostname", @@ -7471,11 +7457,13 @@ checksum = "ae8dd4c280dca9d046139eb5b7a5ac9ad10403fbd64964c7d7571214950d758f" [[package]] name = "solana-net-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-xdp", "bincode", "bytes", "cfg-if 1.0.4", + "crossbeam-channel", "dashmap", "itertools 0.14.0", "log", @@ -7505,7 +7493,7 @@ dependencies = [ "serde", "serde_derive", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-sha256-hasher", "wincode", @@ -7517,8 +7505,8 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda945f4ccb317791c26379ca8ec410c5938aa7a5eff211fe5fe0bb428c3a75f" dependencies = [ - "solana-account 4.3.0", - "solana-hash 4.3.0", + "solana-account 4.3.1", + "solana-hash 4.4.0", "solana-nonce", "solana-sdk-ids", "wincode", @@ -7526,10 +7514,11 @@ dependencies = [ [[package]] name = "solana-nullable" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90429a75d69fdcb31952c3dea79f5f3c8157cfe88221e066103c9c237876073" +checksum = "a0f95d3028ef0f682bb174b77379c19d5dae2904a649f4a103fe29be7a139980" dependencies = [ + "borsh", "bytemuck", ] @@ -7554,7 +7543,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edf2f25743c95229ac0fdc32f8f5893ef738dbf332c669e9861d33ddb0f469d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -7564,7 +7553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad62e1045c2347a0c0e219a6ceb0abfe904be622920996bfcac8d116fabe3c7" dependencies = [ "bincode", - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg_eval", "serde", "serde_derive", @@ -7574,7 +7563,7 @@ dependencies = [ [[package]] name = "solana-perf" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-transaction-view", "ahash 0.8.12", @@ -7589,31 +7578,41 @@ dependencies = [ "rand 0.9.4", "rayon", "serde", - "solana-hash 4.3.0", + "solana-clock", + "solana-hash 4.4.0", + "solana-keypair", "solana-message", "solana-metrics", "solana-packet 4.1.0", "solana-pubkey 4.2.0", + "solana-runtime-transaction", "solana-sdk-ids", "solana-short-vec", "solana-signature", + "solana-signer", + "solana-system-interface 3.2.0", + "solana-system-transaction", "solana-time-utils", + "solana-transaction", "solana-transaction-context", + "solana-vote", + "solana-vote-program", + "wincode", ] [[package]] name = "solana-poh" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-cpu-utils", "agave-votor-messages", "arc-swap", - "core_affinity", "crossbeam-channel", "log", "qualifier_attr", "solana-clock", "solana-entry", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-leader-schedule", "solana-ledger", "solana-measure", @@ -7688,10 +7687,10 @@ dependencies = [ "solana-epoch-stake", "solana-example-mocks", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", - "solana-instructions-sysvar", + "solana-instructions-sysvar 3.0.1", "solana-keccak-hasher", "solana-last-restart-slot", "solana-msg", @@ -7718,10 +7717,10 @@ dependencies = [ [[package]] name = "solana-program-binaries" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-loader-v3-interface", "solana-pubkey 4.2.0", "solana-rent", @@ -7778,7 +7777,7 @@ dependencies = [ [[package]] name = "solana-program-runtime" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "bincode", @@ -7788,13 +7787,13 @@ dependencies = [ "percentage", "rand 0.9.4", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-clock", "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-structure", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-last-restart-slot", "solana-loader-v3-interface", @@ -7819,11 +7818,12 @@ dependencies = [ "solana-sysvar-id", "solana-transaction-context", "thiserror 2.0.18", + "wincode", ] [[package]] name = "solana-program-test" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-logger", @@ -7834,10 +7834,10 @@ dependencies = [ "chrono-humanize", "log", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-accounts-db", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-banks-client", "solana-banks-interface", "solana-banks-server", @@ -7849,7 +7849,7 @@ dependencies = [ "solana-epoch-schedule", "solana-fee-calculator", "solana-genesis-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-loader-v3-interface", @@ -7878,6 +7878,7 @@ dependencies = [ "solana-transaction-error", "solana-vote-program", "spl-generic-token", + "spl-memo-interface", "thiserror 2.0.18", "tokio", ] @@ -7898,12 +7899,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7db719574990de7e8b0f55a8593ac92a5ccb42c8ce67b3e4bf05b139d5d9ee71" dependencies = [ "rand 0.9.4", - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] name = "solana-pubsub-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "crossbeam-channel", "futures-util", @@ -7925,7 +7926,7 @@ dependencies = [ [[package]] name = "solana-quic-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-lock", "async-trait", @@ -7949,7 +7950,7 @@ dependencies = [ [[package]] name = "solana-rayon-threadlimit" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "log", "num_cpus", @@ -7957,7 +7958,7 @@ dependencies = [ [[package]] name = "solana-remote-wallet" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "console 0.16.3", "dialoguer", @@ -7978,12 +7979,13 @@ dependencies = [ [[package]] name = "solana-rent" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9809b081e99bc142ce803bcd7ee18306759ce3b30a96a9da3f6f41c45e50ef0" +checksum = "39f0d780bf8e8a1fe8b5b5fce1acad6b209485b86dec246e7523d5e4a8b7c7fc" dependencies = [ "serde", "serde_derive", + "solana-get-sysvar", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -8002,10 +8004,11 @@ dependencies = [ [[package]] name = "solana-rpc" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-snapshots", + "agave-votor-messages", "base64 0.22.1", "bs58", "crossbeam-channel", @@ -8023,7 +8026,7 @@ dependencies = [ "serde", "serde_json", "soketto", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-accounts-db", "solana-cli-output", @@ -8037,8 +8040,9 @@ dependencies = [ "solana-faucet", "solana-genesis-config", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-message", @@ -8082,8 +8086,9 @@ dependencies = [ [[package]] name = "solana-rpc-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "agave-votor-messages", "async-trait", "base64 0.22.1", "bincode", @@ -8096,15 +8101,16 @@ dependencies = [ "semver", "serde", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-decoder", "solana-account-decoder-client-types", + "solana-bls-signatures", "solana-clock", "solana-commitment-config", "solana-epoch-info", "solana-epoch-schedule", "solana-feature-gate-interface", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-message", "solana-pubkey 4.2.0", @@ -8116,11 +8122,12 @@ dependencies = [ "solana-version", "solana-vote-interface", "tokio", + "wincode", ] [[package]] name = "solana-rpc-client-api" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "anyhow", "jsonrpc-core", @@ -8138,11 +8145,11 @@ dependencies = [ [[package]] name = "solana-rpc-client-nonce-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-commitment-config", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-nonce", "solana-pubkey 4.2.0", @@ -8153,7 +8160,7 @@ dependencies = [ [[package]] name = "solana-rpc-client-types" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "bs58", @@ -8161,7 +8168,7 @@ dependencies = [ "serde", "serde_json", "solana-account-decoder-client-types", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-clock", "solana-commitment-config", "solana-fee-calculator", @@ -8176,7 +8183,7 @@ dependencies = [ [[package]] name = "solana-runtime" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-bls-cert-verify", "agave-feature-set", @@ -8192,8 +8199,10 @@ dependencies = [ "assert_matches", "base64 0.22.1", "bincode", + "bitvec", "bytemuck", "crossbeam-channel", + "crossbeam-utils", "dashmap", "imbl", "itertools 0.14.0", @@ -8202,7 +8211,6 @@ dependencies = [ "mockall", "num-derive", "num-traits", - "num_cpus", "percentage", "qualifier_attr", "rand 0.9.4", @@ -8213,7 +8221,7 @@ dependencies = [ "serde_json", "serde_with", "smallvec", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-accounts-db", "solana-address-lookup-table-interface", @@ -8240,7 +8248,7 @@ dependencies = [ "solana-fee-structure", "solana-genesis-config", "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-inflation", "solana-instruction", "solana-keypair", @@ -8298,7 +8306,6 @@ dependencies = [ "static_assertions", "strum", "strum_macros", - "symlink", "tempfile", "thiserror 2.0.18", "wincode", @@ -8306,13 +8313,13 @@ dependencies = [ [[package]] name = "solana-runtime-transaction" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-transaction-view", "solana-compute-budget", "solana-compute-budget-instruction", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-program-entrypoint", "solana-pubkey 4.2.0", @@ -8333,7 +8340,7 @@ checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" [[package]] name = "solana-sbf-programs" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-logger", @@ -8341,7 +8348,7 @@ dependencies = [ "agave-validator", "bincode", "borsh", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-client-traits", "solana-clock", @@ -8352,7 +8359,7 @@ dependencies = [ "solana-fee", "solana-fee-calculator", "solana-fee-structure", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-loader-v3-interface", @@ -8370,12 +8377,12 @@ dependencies = [ "solana-stake-interface", "solana-svm", "solana-svm-feature-set", - "solana-svm-test-harness-instr", "solana-svm-timings", "solana-svm-transaction", "solana-svm-type-overrides", "solana-system-interface 3.2.0", "solana-sysvar", + "solana-test-validator", "solana-transaction", "solana-transaction-error", "solana-vote", @@ -8386,7 +8393,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-128bit" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-program-entrypoint", "solana-sbf-rust-128bit-dep", @@ -8394,11 +8401,11 @@ dependencies = [ [[package]] name = "solana-sbf-rust-128bit-dep" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "solana-sbf-rust-account-mem" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-program-entrypoint", @@ -8409,7 +8416,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-account-mem-deprecated" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-program", @@ -8420,7 +8427,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-alloc" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-msg", "solana-program", @@ -8429,7 +8436,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-alt-bn128" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-bn254", "solana-msg", @@ -8438,7 +8445,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-alt-bn128-compression" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-bn254", "solana-msg", @@ -8447,7 +8454,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-call-args" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "borsh", "solana-account-info", @@ -8459,7 +8466,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-call-depth" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-msg", "solana-program", @@ -8468,7 +8475,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-caller-access" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", @@ -8481,16 +8488,16 @@ dependencies = [ [[package]] name = "solana-sbf-rust-curve25519" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ - "solana-curve25519 4.0.0", + "solana-curve25519", "solana-msg", "solana-program-entrypoint", ] [[package]] name = "solana-sbf-rust-custom-heap" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-msg", @@ -8501,7 +8508,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-dep-crate" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "byteorder", "solana-program-entrypoint", @@ -8509,7 +8516,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-deprecated-loader" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", @@ -8524,11 +8531,11 @@ dependencies = [ [[package]] name = "solana-sbf-rust-direct-account-pointers" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-account-view", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-cpi", "solana-program-entrypoint", "solana-program-error", @@ -8536,7 +8543,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-divide-by-zero" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-program-entrypoint", @@ -8546,7 +8553,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-dup-accounts" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", @@ -8559,7 +8566,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-error-handling" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "num-derive", "num-traits", @@ -8573,7 +8580,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-external-spend" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-program-entrypoint", @@ -8583,7 +8590,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-inner_instruction_alignment_check" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", @@ -8596,11 +8603,11 @@ dependencies = [ [[package]] name = "solana-sbf-rust-instruction-introspection" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", - "solana-instructions-sysvar", + "solana-instructions-sysvar 4.0.0", "solana-msg", "solana-program", "solana-program-entrypoint", @@ -8610,7 +8617,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-invoke" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", @@ -8629,7 +8636,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-invoke-and-error" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", @@ -8641,7 +8648,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-invoke-and-ok" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", @@ -8653,7 +8660,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-invoke-and-return" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", @@ -8665,11 +8672,11 @@ dependencies = [ [[package]] name = "solana-sbf-rust-invoke-dep" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "solana-sbf-rust-invoked" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-msg", @@ -8684,7 +8691,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-invoked-dep" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-instruction", "solana-pubkey 4.2.0", @@ -8692,7 +8699,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-iter" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-program", "solana-program-entrypoint", @@ -8700,7 +8707,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-log-data" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-program", @@ -8711,7 +8718,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-many-args" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-msg", "solana-program", @@ -8721,7 +8728,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-many-args-dep" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-msg", "solana-program", @@ -8729,7 +8736,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-mem" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-program-entrypoint", @@ -8741,11 +8748,11 @@ dependencies = [ [[package]] name = "solana-sbf-rust-mem-dep" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "solana-sbf-rust-membuiltins" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-program-entrypoint", "solana-sbf-rust-mem-dep", @@ -8753,7 +8760,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-noop" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-program-entrypoint", @@ -8763,7 +8770,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-panic" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-msg", @@ -8774,7 +8781,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-param-passing" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-program", "solana-program-entrypoint", @@ -8783,11 +8790,11 @@ dependencies = [ [[package]] name = "solana-sbf-rust-param-passing-dep" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "solana-sbf-rust-poseidon" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-msg", "solana-poseidon", @@ -8796,7 +8803,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-r2-instruction-data-pointer" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-cpi", "solana-program-entrypoint", @@ -8804,7 +8811,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-rand" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-msg", @@ -8815,7 +8822,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-realloc" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-msg", @@ -8829,7 +8836,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-realloc-dep" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-instruction", "solana-pubkey 4.2.0", @@ -8837,7 +8844,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-realloc-invoke" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", @@ -8853,11 +8860,11 @@ dependencies = [ [[package]] name = "solana-sbf-rust-realloc-invoke-dep" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "solana-sbf-rust-remaining-compute-units" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-msg", @@ -8869,7 +8876,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-ro-account_modify" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", @@ -8882,7 +8889,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-ro-modify" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-msg", @@ -8895,7 +8902,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-sanity" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-msg", @@ -8908,10 +8915,10 @@ dependencies = [ [[package]] name = "solana-sbf-rust-secp256k1-recover" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "libsecp256k1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keccak-hasher", "solana-msg", "solana-program-entrypoint", @@ -8920,11 +8927,11 @@ dependencies = [ [[package]] name = "solana-sbf-rust-sha" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "blake3", "solana-blake3-hasher", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keccak-hasher", "solana-msg", "solana-program-entrypoint", @@ -8933,7 +8940,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-sibling-inner-instructions" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", @@ -8945,7 +8952,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-sibling-instructions" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", @@ -8958,7 +8965,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-simulation" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-clock", @@ -8971,7 +8978,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-spoof1" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-instruction", @@ -8985,7 +8992,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-spoof1-system" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-program-entrypoint", @@ -8995,13 +9002,13 @@ dependencies = [ [[package]] name = "solana-sbf-rust-sysvar" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", "solana-account-info", "solana-define-syscall 3.0.0", "solana-instruction", - "solana-instructions-sysvar", + "solana-instructions-sysvar 4.0.0", "solana-msg", "solana-program-entrypoint", "solana-program-error", @@ -9013,7 +9020,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-upgradeable" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-msg", @@ -9025,7 +9032,7 @@ dependencies = [ [[package]] name = "solana-sbf-rust-upgraded" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-msg", @@ -9037,7 +9044,7 @@ dependencies = [ [[package]] name = "solana-sbf-syscall-get-epoch-stake" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-account-info", "solana-msg", @@ -9049,9 +9056,9 @@ dependencies = [ [[package]] name = "solana-sbpf" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b120fcbbc0d8562997183bef32c76a28400ecae7d8c9fff279d33002783a29" +checksum = "d777d7a89267dd133e985113c7e7f820fb7cfd9123a4a350cf8b39ebae1920bc" dependencies = [ "byteorder", "combine 3.8.1", @@ -9070,7 +9077,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", ] [[package]] @@ -9138,19 +9145,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc905b200a95f2ea9146e43f2a7181e3aeb55de6bc12afb36462d00a3c7310de" dependencies = [ "hmac 0.12.1", - "pbkdf2 0.11.0", + "pbkdf2", "sha2 0.10.9", ] [[package]] name = "solana-send-transaction-service" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-trait", "crossbeam-channel", "itertools 0.14.0", "log", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-measure", "solana-metrics", @@ -9185,9 +9192,9 @@ dependencies = [ [[package]] name = "solana-serialize-utils" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cc401931d178472358e6b78dc72d031dc08f752d7410f0e8bd259dd6f02fa" +checksum = "761357b0853c9623bf12c1d2314b3d6160a85b087b84c45224fb85766d22616b" dependencies = [ "solana-instruction-error", "solana-pubkey 4.2.0", @@ -9202,7 +9209,7 @@ checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" dependencies = [ "sha2 0.10.9", "solana-define-syscall 4.0.1", - "solana-hash 4.3.0", + "solana-hash 4.4.0", ] [[package]] @@ -9218,9 +9225,9 @@ dependencies = [ [[package]] name = "solana-short-vec" -version = "3.2.1" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb8cc883fc7b8ce4a7814cb1441b48c06437049ec11847005cf63bcfa85c546" +checksum = "7d8250a4495aad49ad20556a607da53bdcb20de78da10b65afbf918b7f1de647" dependencies = [ "serde_core", "wincode", @@ -9233,15 +9240,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6c79722e299d957958bf33695f7cd1ef6724ff55563c60fd9e3e24487cccde2" dependencies = [ "solana-hard-forks", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-sha256-hasher", ] [[package]] name = "solana-signature" -version = "3.4.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a73c6e97cc2108be0adf6a6ea326434f8398df9d7eed81da2a4548b69e971c" +checksum = "b0364c7577c3c82a693ce28a1febc8d1b5d1b0a175fdc2114ae6186b69effe1e" dependencies = [ "ed25519-dalek 2.2.0", "five8", @@ -9254,11 +9261,11 @@ dependencies = [ [[package]] name = "solana-signer" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bfea97951fee8bae0d6038f39a5efcb6230ecdfe33425ac75196d1a1e3e3235" +checksum = "520bd6021163ee517f4bdc7ae03ded904f97e11320001ba0b3355f45eb14f558" dependencies = [ - "solana-pubkey 3.0.0", + "solana-pubkey 4.2.0", "solana-signature", "solana-transaction-error", ] @@ -9276,15 +9283,17 @@ dependencies = [ [[package]] name = "solana-slot-hashes" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2585f70191623887329dfb5078da3a00e15e3980ea67f42c2e10b07028419f43" +checksum = "5c7ce2b4b8911bf2db3de7b6266e67bfc21a6a9f8c566fb096d9782ca2ad16ee" dependencies = [ "serde", "serde_derive", - "solana-hash 4.3.0", + "solana-get-sysvar", + "solana-hash 4.4.0", "solana-sdk-ids", "solana-sysvar-id", + "wincode", ] [[package]] @@ -9298,6 +9307,7 @@ dependencies = [ "serde_derive", "solana-sdk-ids", "solana-sysvar-id", + "wincode", ] [[package]] @@ -9312,9 +9322,9 @@ dependencies = [ [[package]] name = "solana-stake-interface" -version = "3.1.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49eb5c77484214c3484921e2cdda79d185373118b5458c1b2df0f1a04c3bc30" +checksum = "252b4291eb25a5a356149ed4d4a940d5f655186210fa84b5d0d8d55c3dd42a81" dependencies = [ "num-traits", "serde", @@ -9327,11 +9337,12 @@ dependencies = [ "solana-system-interface 3.2.0", "solana-sysvar", "solana-sysvar-id", + "wincode", ] [[package]] name = "solana-storage-bigtable" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-reserved-account-keys", "base64 0.22.1", @@ -9340,7 +9351,7 @@ dependencies = [ "flate2", "futures 0.3.32", "goauth", - "http 1.4.0", + "http 1.4.2", "hyper-util", "log", "prost", @@ -9367,14 +9378,14 @@ dependencies = [ [[package]] name = "solana-storage-proto" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bs58", "prost", "protobuf-src", "serde", "solana-account-decoder", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-message", "solana-pubkey 4.2.0", @@ -9391,7 +9402,7 @@ dependencies = [ [[package]] name = "solana-streamer" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-xdp", "bytes", @@ -9405,12 +9416,12 @@ dependencies = [ "nix", "num_cpus", "pem", - "percentage", "quinn", "rand 0.9.4", "rustls", "smallvec", "solana-keypair", + "solana-measure", "solana-metrics", "solana-net-utils", "solana-packet 4.1.0", @@ -9427,24 +9438,29 @@ dependencies = [ [[package]] name = "solana-svm" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "ahash 0.8.12", "log", "percentage", "qualifier_attr", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", + "solana-bpf-loader-program", "solana-clock", + "solana-compute-budget", + "solana-compute-budget-program", "solana-fee-structure", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", - "solana-instructions-sysvar", + "solana-instruction-error", + "solana-instructions-sysvar 4.0.0", "solana-loader-v3-interface", "solana-loader-v4-interface", "solana-message", "solana-nonce", "solana-nonce-account", + "solana-precompile-error", "solana-program-entrypoint", "solana-program-pack", "solana-program-runtime", @@ -9458,7 +9474,9 @@ dependencies = [ "solana-svm-timings", "solana-svm-transaction", "solana-svm-type-overrides", + "solana-syscalls", "solana-system-interface 3.2.0", + "solana-system-program", "solana-sysvar-id", "solana-transaction-context", "solana-transaction-error", @@ -9468,9 +9486,9 @@ dependencies = [ [[package]] name = "solana-svm-callback" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-clock", "solana-precompile-error", "solana-pubkey 4.2.0", @@ -9478,65 +9496,22 @@ dependencies = [ [[package]] name = "solana-svm-feature-set" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" [[package]] name = "solana-svm-log-collector" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "log", ] [[package]] name = "solana-svm-measure" -version = "4.2.0-alpha.0" - -[[package]] -name = "solana-svm-test-harness-fixture" -version = "4.2.0-alpha.0" -dependencies = [ - "solana-account 4.3.0", - "solana-instruction", - "solana-instruction-error", - "solana-pubkey 4.2.0", - "solana-svm-feature-set", - "thiserror 2.0.18", -] - -[[package]] -name = "solana-svm-test-harness-instr" -version = "4.2.0-alpha.0" -dependencies = [ - "bincode", - "env_logger", - "solana-account 4.3.0", - "solana-bpf-loader-program", - "solana-clock", - "solana-compute-budget", - "solana-compute-budget-program", - "solana-instruction", - "solana-instruction-error", - "solana-precompile-error", - "solana-program-runtime", - "solana-pubkey 4.2.0", - "solana-rent", - "solana-sdk-ids", - "solana-svm", - "solana-svm-callback", - "solana-svm-feature-set", - "solana-svm-log-collector", - "solana-svm-test-harness-fixture", - "solana-svm-timings", - "solana-svm-transaction", - "solana-syscalls", - "solana-system-program", - "solana-sysvar-id", - "solana-transaction-context", -] +version = "4.3.0-alpha.0" [[package]] name = "solana-svm-timings" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "eager", "enum-iterator", @@ -9545,9 +9520,11 @@ dependencies = [ [[package]] name = "solana-svm-transaction" -version = "4.2.0-alpha.0" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736d2cec944c6f8f298a7bd8ada5eb318cca9ab859281b1300842fa0e9a25afa" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-message", "solana-pubkey 4.2.0", "solana-sdk-ids", @@ -9557,27 +9534,27 @@ dependencies = [ [[package]] name = "solana-svm-type-overrides" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "rand 0.9.4", ] [[package]] name = "solana-syscalls" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", "libsecp256k1", "num-traits", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-account-info", "solana-blake3-hasher", "solana-bls12-381-syscall", "solana-bn254", "solana-clock", "solana-cpi", - "solana-curve25519 4.0.0", - "solana-hash 4.3.0", + "solana-curve25519", + "solana-hash 4.4.0", "solana-hash-512", "solana-instruction", "solana-keccak-hasher", @@ -9599,6 +9576,7 @@ dependencies = [ "solana-sysvar-id", "solana-transaction-context", "thiserror 2.0.18", + "wincode", ] [[package]] @@ -9625,7 +9603,7 @@ dependencies = [ "num-traits", "serde", "serde_derive", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-instruction", "solana-msg", "solana-program-error", @@ -9634,11 +9612,11 @@ dependencies = [ [[package]] name = "solana-system-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", "log", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bincode", "solana-fee-calculator", "solana-instruction", @@ -9660,7 +9638,7 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a39522012be4127884bc611d65f58f1cf33a3afb7baee7e6774be90b48edc3c" dependencies = [ - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", "solana-message", "solana-pubkey 4.2.0", @@ -9688,7 +9666,7 @@ dependencies = [ "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-last-restart-slot", "solana-program-entrypoint", @@ -9709,24 +9687,26 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" dependencies = [ - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-sdk-ids", ] [[package]] name = "solana-test-validator" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-snapshots", + "agave-votor-messages", "arc-swap", "base64 0.22.1", "bincode", "crossbeam-channel", "log", "serde_json", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-accounts-db", + "solana-bls-signatures", "solana-cli-output", "solana-clock", "solana-cluster-type", @@ -9763,6 +9743,7 @@ dependencies = [ "solana-syscalls", "solana-transaction", "solana-validator-exit", + "solana-vote-interface", "tokio", ] @@ -9774,8 +9755,9 @@ checksum = "0ced92c60aa76ec4780a9d93f3bd64dfa916e1b998eacc6f1c110f3f444f02c9" [[package]] name = "solana-tls-utils" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ + "quinn", "rustls", "solana-keypair", "solana-pubkey 4.2.0", @@ -9785,7 +9767,7 @@ dependencies = [ [[package]] name = "solana-tpu-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "futures-util", "indicatif", @@ -9796,6 +9778,7 @@ dependencies = [ "solana-commitment-config", "solana-connection-cache", "solana-epoch-schedule", + "solana-leader-schedule", "solana-message", "solana-pubkey 4.2.0", "solana-pubsub-client", @@ -9812,7 +9795,7 @@ dependencies = [ [[package]] name = "solana-tpu-client-next" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-trait", "futures 0.3.32", @@ -9824,6 +9807,7 @@ dependencies = [ "solana-clock", "solana-commitment-config", "solana-keypair", + "solana-leader-schedule", "solana-measure", "solana-metrics", "solana-packet 4.1.0", @@ -9840,14 +9824,14 @@ dependencies = [ [[package]] name = "solana-transaction" -version = "4.1.1" +version = "4.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a98253cefd62dea714b67926d3d918c3b9f7d66993f1f2322ac7549ea991dc3e" +checksum = "2509e70bdce879db3e0f56cf97e40edd53742e8f0e6f34d64c46e7900071b53f" dependencies = [ "serde", "serde_derive", - "solana-address 2.6.0", - "solana-hash 4.3.0", + "solana-address 2.6.1", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-message", @@ -9862,13 +9846,13 @@ dependencies = [ [[package]] name = "solana-transaction-context" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-instruction", - "solana-instructions-sysvar", + "solana-instructions-sysvar 4.0.0", "solana-pubkey 4.2.0", "solana-rent", "solana-sbpf", @@ -9878,9 +9862,9 @@ dependencies = [ [[package]] name = "solana-transaction-error" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a2165ad25b694c654d5395fc7a049452a192376e4c96a7fad05580f6ba5ba1c" +checksum = "a949797bc1ac31836d0070e791083028a8c2e0d493fa4cf51c0bed7a04c65c22" dependencies = [ "serde", "serde_derive", @@ -9891,7 +9875,7 @@ dependencies = [ [[package]] name = "solana-transaction-status" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "Inflector", "agave-reserved-account-keys", @@ -9904,7 +9888,7 @@ dependencies = [ "solana-account-decoder", "solana-address-lookup-table-interface", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-loader-v2-interface", "solana-loader-v3-interface", @@ -9920,6 +9904,7 @@ dependencies = [ "solana-transaction-error", "solana-transaction-status-client-types", "solana-vote-interface", + "solana-zk-sdk-pod", "spl-associated-token-account-interface", "spl-memo-interface", "spl-token-2022-interface", @@ -9927,11 +9912,12 @@ dependencies = [ "spl-token-interface", "spl-token-metadata-interface", "thiserror 2.0.18", + "wincode", ] [[package]] name = "solana-transaction-status-client-types" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "base64 0.22.1", "bincode", @@ -9953,12 +9939,11 @@ dependencies = [ [[package]] name = "solana-turbine" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "agave-votor", "agave-votor-messages", - "agave-xdp", "bytes", "crossbeam-channel", "itertools 0.14.0", @@ -9973,8 +9958,9 @@ dependencies = [ "solana-cost-model", "solana-entry", "solana-gossip", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-keypair", + "solana-leader-schedule", "solana-ledger", "solana-measure", "solana-metrics", @@ -9999,7 +9985,7 @@ dependencies = [ [[package]] name = "solana-udp-client" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "async-trait", "solana-connection-cache", @@ -10011,11 +9997,11 @@ dependencies = [ [[package]] name = "solana-unified-scheduler-logic" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "assert_matches", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-pubkey 4.2.0", "solana-runtime-transaction", "solana-transaction", @@ -10025,7 +10011,7 @@ dependencies = [ [[package]] name = "solana-unified-scheduler-pool" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-banking-stage-ingress-types", "aquamarine", @@ -10061,7 +10047,7 @@ checksum = "c5d2face763df5afeaa9509b9019968860e69cc1531ec8b4a2e6c7b702204d5a" [[package]] name = "solana-version" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "rand 0.9.4", @@ -10069,22 +10055,24 @@ dependencies = [ "serde", "solana-sanitize", "solana-serde-varint", + "solana-wincode-varint", + "wincode", ] [[package]] name = "solana-vote" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bincode", "log", "qualifier_attr", "rand 0.9.4", "serde", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bincode", "solana-bls-signatures", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-keypair", "solana-packet 4.1.0", @@ -10101,9 +10089,9 @@ dependencies = [ [[package]] name = "solana-vote-interface" -version = "6.0.0" +version = "6.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d495cb001d3373229ba523625783e5168707f70a9c38ddc90fbc5b0470d600ea" +checksum = "61843d7be827cac5e025c3a16c1101a34fcdd8cf593f6a82eafdd253bd55a26b" dependencies = [ "bincode", "cfg_eval", @@ -10113,7 +10101,7 @@ dependencies = [ "serde_derive", "serde_with", "solana-clock", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-instruction-error", "solana-pubkey 4.2.0", @@ -10127,17 +10115,17 @@ dependencies = [ [[package]] name = "solana-vote-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "agave-feature-set", "bincode", "log", - "solana-account 4.3.0", + "solana-account 4.3.1", "solana-bincode", "solana-bls-signatures", "solana-clock", "solana-epoch-schedule", - "solana-hash 4.3.0", + "solana-hash 4.4.0", "solana-instruction", "solana-packet 4.1.0", "solana-program-runtime", @@ -10151,33 +10139,57 @@ dependencies = [ ] [[package]] -name = "solana-zero-copy" +name = "solana-wincode-varint" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f52dd8f733a13f6a18e55de83cf97c4c3f5fdf27ea3830bcff0b35313efcc2" +checksum = "6679e72a01dcfe7ff180f0e9d3a0d165e4e9664426930014f2702e7571c259c7" +dependencies = [ + "wincode", +] + +[[package]] +name = "solana-zero-copy" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea15126ebdc7e270c50d43884369af9f51d2308156d46a18e351522a164844d" dependencies = [ - "borsh", "bytemuck", "bytemuck_derive", ] +[[package]] +name = "solana-zk-elgamal-proof-interface" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8da7f01db2148a1dc16261ff1dc6f3930a1e255a33cece4f1b56658694f27f7" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "num-derive", + "num-traits", + "solana-address 2.6.1", + "solana-instruction", + "solana-sdk-ids", + "solana-zk-sdk-pod", +] + [[package]] name = "solana-zk-elgamal-proof-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "bytemuck", "solana-instruction", "solana-program-runtime", "solana-sdk-ids", "solana-svm-log-collector", - "solana-zk-sdk 5.0.1", + "solana-zk-sdk", ] [[package]] name = "solana-zk-sdk" -version = "4.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9602bcb1f7af15caef92b91132ec2347e1c51a72ecdbefdaefa3eac4b8711475" +checksum = "09670ff59f60e6ddc2209c2e4353992a9b9f01d4e244f3e9d67bd5146e33d388" dependencies = [ "aes-gcm-siv", "base64 0.22.1", @@ -10185,9 +10197,7 @@ dependencies = [ "bytemuck", "bytemuck_derive", "curve25519-dalek 4.1.3", - "getrandom 0.2.10", - "itertools 0.12.1", - "js-sys", + "itertools 0.14.0", "merlin", "num-derive", "num-traits", @@ -10196,9 +10206,9 @@ dependencies = [ "serde_derive", "serde_json", "sha3", + "solana-address 2.6.1", "solana-derivation-path", "solana-instruction", - "solana-pubkey 3.0.0", "solana-sdk-ids", "solana-seed-derivable", "solana-seed-phrase", @@ -10206,47 +10216,25 @@ dependencies = [ "solana-signer", "subtle", "thiserror 2.0.18", - "wasm-bindgen", "zeroize", ] [[package]] -name = "solana-zk-sdk" -version = "5.0.1" +name = "solana-zk-sdk-pod" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09670ff59f60e6ddc2209c2e4353992a9b9f01d4e244f3e9d67bd5146e33d388" +checksum = "a800583b7a4cea3e851686af162cc6e4712eef97fa91dfa9aca2459b95c84777" dependencies = [ - "aes-gcm-siv", "base64 0.22.1", - "bincode", "bytemuck", "bytemuck_derive", - "curve25519-dalek 4.1.3", - "itertools 0.14.0", - "merlin", - "num-derive", - "num-traits", - "rand 0.8.6", - "serde", - "serde_derive", - "serde_json", - "sha3", - "solana-address 2.6.0", - "solana-derivation-path", - "solana-instruction", - "solana-sdk-ids", - "solana-seed-derivable", - "solana-seed-phrase", - "solana-signature", - "solana-signer", - "subtle", + "solana-nullable", "thiserror 2.0.18", - "zeroize", ] [[package]] name = "solana-zk-token-proof-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" dependencies = [ "solana-program-runtime", ] @@ -10280,9 +10268,9 @@ dependencies = [ [[package]] name = "spl-discriminator" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48cc11459e265d5b501534144266620289720b4c44522a47bc6b63cd295d2f3" +checksum = "e597c5ff9ed7c74a54dbc47bae2f06e4db8c98f4356ad280200dc11878266db1" dependencies = [ "bytemuck", "solana-program-error", @@ -10326,56 +10314,38 @@ dependencies = [ [[package]] name = "spl-memo-interface" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4e2aedd58f858337fa609af5ad7100d4a243fdaf6a40d6eb4c28c5f19505d3" +checksum = "3745d384b0afee980d43d62b66c27bdcbbd03507732b8d3626d3413cb72084f2" dependencies = [ "solana-instruction", - "solana-pubkey 3.0.0", -] - -[[package]] -name = "spl-pod" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9c6e142cdf1e7e77f480053ec9f0ce989890768ddf91f619b50f39d1b456f5" -dependencies = [ - "borsh", - "bytemuck", - "bytemuck_derive", - "num-derive", - "num-traits", - "num_enum", - "solana-program-error", - "solana-program-option", - "solana-pubkey 3.0.0", - "solana-zero-copy", - "solana-zk-sdk 4.0.0", - "thiserror 2.0.18", + "solana-pubkey 4.2.0", ] [[package]] name = "spl-token-2022-interface" -version = "2.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fcd81188211f4b3c8a5eba7fd534c7142f9dd026123b3472492782cc72f4dc6" +checksum = "821d96d034ea31c4965d182c742153c491ae0abee531331b55771086c5030d86" dependencies = [ "arrayref", "bytemuck", + "getrandom 0.2.10", "num-derive", "num-traits", "num_enum", "solana-account-info", + "solana-address 2.6.1", "solana-instruction", + "solana-nullable", "solana-program-error", "solana-program-option", "solana-program-pack", - "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-zk-sdk 4.0.0", - "spl-pod", + "solana-zero-copy", + "solana-zk-elgamal-proof-interface", + "solana-zk-sdk-pod", "spl-token-confidential-transfer-proof-extraction", - "spl-token-confidential-transfer-proof-generation", "spl-token-group-interface", "spl-token-metadata-interface", "spl-type-length-value", @@ -10384,32 +10354,21 @@ dependencies = [ [[package]] name = "spl-token-confidential-transfer-proof-extraction" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a22217af69b7a61ca813f47c018afb0b00b02a74a4c70ff099cd4287740bc3d" +checksum = "0c1a845ec724e807643a04f1d43439ee3571dd913848112ac76aa90b850eaf7c" dependencies = [ "bytemuck", "solana-account-info", - "solana-curve25519 2.3.13", + "solana-address 2.6.1", + "solana-curve25519", "solana-instruction", - "solana-instructions-sysvar", + "solana-instructions-sysvar 3.0.1", "solana-msg", "solana-program-error", - "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-zk-sdk 4.0.0", - "spl-pod", - "thiserror 2.0.18", -] - -[[package]] -name = "spl-token-confidential-transfer-proof-generation" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a2b41095945dc15274b924b21ccae9b3ec9dc2fdd43dbc08de8c33bbcd915" -dependencies = [ - "curve25519-dalek 4.1.3", - "solana-zk-sdk 4.0.0", + "solana-zk-elgamal-proof-interface", + "solana-zk-sdk-pod", "thiserror 2.0.18", ] @@ -10423,7 +10382,7 @@ dependencies = [ "num-derive", "num-traits", "num_enum", - "solana-address 2.6.0", + "solana-address 2.6.1", "solana-instruction", "solana-nullable", "solana-program-error", @@ -10434,9 +10393,9 @@ dependencies = [ [[package]] name = "spl-token-interface" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c564ac05a7c8d8b12e988a37d82695b5ba4db376d07ea98bc4882c81f96c7f3" +checksum = "7143c4e676984047a400e2fbd7ac29a1659f2b1b52b897cfde19316b562e4589" dependencies = [ "arrayref", "bytemuck", @@ -10454,38 +10413,38 @@ dependencies = [ [[package]] name = "spl-token-metadata-interface" -version = "0.8.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c467c7c3bd056f8fe60119e7ec34ddd6f23052c2fa8f1f51999098063b72676" +checksum = "3d3d96f175e7022ff200464dfa75a3708a4e9b70c83c4ecd04fe52ee479f4fef" dependencies = [ "borsh", "num-derive", "num-traits", + "num_enum", + "solana-address 2.6.1", "solana-borsh", "solana-instruction", + "solana-nullable", "solana-program-error", - "solana-pubkey 3.0.0", "spl-discriminator", - "spl-pod", "spl-type-length-value", "thiserror 2.0.18", ] [[package]] name = "spl-type-length-value" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca20a1a19f4507a98ca4b28ff5ed54cac9b9d34ed27863e2bde50a3238f9a6ac" +checksum = "2504631748c48d2a937414d64a12dcac4588d34bd07d355d648619c189d29435" dependencies = [ "bytemuck", "num-derive", "num-traits", "num_enum", "solana-account-info", - "solana-msg", "solana-program-error", + "solana-zero-copy", "spl-discriminator", - "spl-pod", "thiserror 2.0.18", ] @@ -10615,7 +10574,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cca424247104946a59dacd27eaad296223b7feec3d168a6dd04585183091eb0b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "byteorder", "enum-as-inner", "libc", @@ -10857,23 +10816,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-bip39" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30fd743a02bf35236f6faf99adb03089bb77e91c998dac2c2ad76bb424f668c" -dependencies = [ - "once_cell", - "pbkdf2 0.12.2", - "rand 0.8.6", - "rustc-hash 1.1.0", - "sha2 0.10.9", - "thiserror 1.0.69", - "unicode-normalization", - "wasm-bindgen", - "zeroize", -] - [[package]] name = "tinystr" version = "0.7.6" @@ -11057,7 +10999,7 @@ dependencies = [ "base64 0.22.1", "bytes", "h2", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "hyper 1.8.1", @@ -11141,11 +11083,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "iri-string", @@ -11260,7 +11202,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http 1.4.2", "httparse", "log", "rand 0.9.4", @@ -11665,10 +11607,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "wincode" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37095eb18dd6254c66217edc61a29d83d51f8818de8a2ffe88e4584ad73fb5f9" +checksum = "66d967db7705dc29120bb6e8ce5b5a2e27734ed5976d1c904e95bd238d1c3c5a" dependencies = [ + "bv", "bytes", "pastey", "proc-macro2", @@ -11679,11 +11622,11 @@ dependencies = [ [[package]] name = "wincode-derive" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e262d55d1261f31e2cfe49cc6385a421d14d99faa0526bbe3cc1bda0d3005c62" +checksum = "15ab90b719560d0fda79c74550ad1c948d17b118765942838055ebaf34d67071" dependencies = [ - "darling 0.21.3", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -11997,7 +11940,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] diff --git a/programs/sbf/Cargo.toml b/programs/sbf/Cargo.toml index 5a03284e300..8bfe75f97d0 100644 --- a/programs/sbf/Cargo.toml +++ b/programs/sbf/Cargo.toml @@ -72,7 +72,7 @@ members = [ "rust/upgraded", ] [workspace.package] -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" description = "Solana SBF test program written in Rust" authors = ["Anza Maintainers "] repository = "https://github.com/anza-xyz/agave" @@ -88,11 +88,23 @@ check-cfg = [ 'cfg(target_feature, values("dynamic-frames"))', ] +# Keep in sync with the root [workspace.lints.clippy]. +[workspace.lints.clippy] +# Denied lints +arithmetic_side_effects = "deny" +default_trait_access = "deny" +manual_let_else = "deny" +uninlined_format_args = "deny" +used_underscore_binding = "deny" + +# Allowed lints +new_without_default = "allow" + [workspace.dependencies] -agave-feature-set = { path = "../../feature-set", version = "=4.2.0-alpha.0" } -agave-logger = { path = "../../logger", version = "=4.2.0-alpha.0" } -agave-reserved-account-keys = { path = "../../reserved-account-keys", version = "=4.2.0-alpha.0" } -agave-validator = { path = "../../validator", version = "=4.2.0-alpha.0" } +agave-feature-set = { path = "../../feature-set", version = "=4.3.0-alpha.0" } +agave-logger = { path = "../../logger", version = "=4.3.0-alpha.0" } +agave-reserved-account-keys = { path = "../../reserved-account-keys", version = "=4.3.0-alpha.0" } +agave-validator = { path = "../../validator", version = "=4.3.0-alpha.0" } array-bytes = "=1.4.1" bincode = { version = "1.1.4", default-features = false } blake3 = "1.0.0" @@ -102,22 +114,22 @@ libsecp256k1 = { version = "0.7.2", default-features = false } num-derive = "0.4.2" num-traits = "0.2" serde = { version = "1.0.112", features = ["derive"] } -serde_json = "1.0.149" +serde_json = "1.0.150" solana-account-info = "=3.1.1" solana-account-view = "=2.0.0" -solana-address = "=2.6.0" +solana-address = "=2.6.1" solana-blake3-hasher = { version = "=3.1.0", features = ["blake3"] } solana-bn254 = "=3.2.1" -solana-clock = { version = "=3.0.1", features = ["serde", "sysvar"] } -solana-compute-budget = { path = "../../compute-budget", version = "=4.2.0-alpha.0" } -solana-compute-budget-instruction = { path = "../../compute-budget-instruction", version = "=4.2.0-alpha.0" } +solana-clock = { version = "=3.1.1", features = ["serde", "sysvar"] } +solana-compute-budget = { path = "../../compute-budget", version = "=4.3.0-alpha.0" } +solana-compute-budget-instruction = { path = "../../compute-budget-instruction", version = "=4.3.0-alpha.0" } solana-cpi = "=3.1.0" -solana-curve25519 = "=4.0.0" +solana-curve25519 = "=4.0.1" solana-define-syscall = "=3.0.0" -solana-fee = { path = "../../fee", version = "=4.2.0-alpha.0" } -solana-hash = { version = "=4.3.0", features = ["bytemuck", "serde", "std"] } +solana-fee = { path = "../../fee", version = "=4.3.0-alpha.0" } +solana-hash = { version = "=4.4.0", features = ["bytemuck", "serde", "std"] } solana-instruction = "=3.4.0" -solana-instructions-sysvar = "=3.0.0" +solana-instructions-sysvar = "=4.0.0" solana-keccak-hasher = { version = "=3.1.0", features = ["sha3"] } solana-msg = "=3.1.0" solana-poseidon = "=4.0.0" @@ -125,33 +137,33 @@ solana-program = "=4.0.0" solana-program-entrypoint = "=3.1.1" solana-program-error = "=3.0.1" solana-program-memory = "=3.1.0" -solana-program-runtime = { path = "../../program-runtime", version = "=4.2.0-alpha.0" } +solana-program-runtime = { path = "../../program-runtime", version = "=4.3.0-alpha.0" } solana-pubkey = { version = "=4.2.0", default-features = false } -solana-runtime = { path = "../../runtime", version = "=4.2.0-alpha.0" } -solana-runtime-transaction = { path = "../../runtime-transaction", version = "=4.2.0-alpha.0" } -solana-sbf-rust-128bit-dep = { path = "rust/128bit_dep", version = "=4.2.0-alpha.0" } -solana-sbf-rust-invoke-dep = { path = "rust/invoke_dep", version = "=4.2.0-alpha.0" } -solana-sbf-rust-invoked-dep = { path = "rust/invoked_dep", version = "=4.2.0-alpha.0" } -solana-sbf-rust-many-args-dep = { path = "rust/many_args_dep", version = "=4.2.0-alpha.0" } -solana-sbf-rust-mem-dep = { path = "rust/mem_dep", version = "=4.2.0-alpha.0" } -solana-sbf-rust-param-passing-dep = { path = "rust/param_passing_dep", version = "=4.2.0-alpha.0" } -solana-sbf-rust-realloc-dep = { path = "rust/realloc_dep", version = "=4.2.0-alpha.0" } -solana-sbf-rust-realloc-invoke-dep = { path = "rust/realloc_invoke_dep", version = "=4.2.0-alpha.0" } +solana-runtime = { path = "../../runtime", version = "=4.3.0-alpha.0" } +solana-runtime-transaction = { path = "../../runtime-transaction", version = "=4.3.0-alpha.0" } +solana-sbf-rust-128bit-dep = { path = "rust/128bit_dep", version = "=4.3.0-alpha.0" } +solana-sbf-rust-invoke-dep = { path = "rust/invoke_dep", version = "=4.3.0-alpha.0" } +solana-sbf-rust-invoked-dep = { path = "rust/invoked_dep", version = "=4.3.0-alpha.0" } +solana-sbf-rust-many-args-dep = { path = "rust/many_args_dep", version = "=4.3.0-alpha.0" } +solana-sbf-rust-mem-dep = { path = "rust/mem_dep", version = "=4.3.0-alpha.0" } +solana-sbf-rust-param-passing-dep = { path = "rust/param_passing_dep", version = "=4.3.0-alpha.0" } +solana-sbf-rust-realloc-dep = { path = "rust/realloc_dep", version = "=4.3.0-alpha.0" } +solana-sbf-rust-realloc-invoke-dep = { path = "rust/realloc_invoke_dep", version = "=4.3.0-alpha.0" } solana-sdk-ids = "=3.1.0" solana-secp256k1-recover = "=3.1.1" solana-sha256-hasher = { version = "=3.1.0", features = ["sha2"] } -solana-stake-interface = { version = "=3.1.0", features = ["bincode"] } -solana-svm = { path = "../../svm", version = "=4.2.0-alpha.0" } -solana-svm-feature-set = { path = "../../svm-feature-set", version = "=4.2.0-alpha.0" } -solana-svm-test-harness-instr = { path = "../../svm-test-harness/instr", version = "=4.2.0-alpha.0" } -solana-svm-timings = { path = "../../svm-timings", version = "=4.2.0-alpha.0" } -solana-svm-transaction = { path = "../../svm-transaction", version = "=4.2.0-alpha.0" } -solana-svm-type-overrides = { path = "../../svm-type-overrides", version = "=4.2.0-alpha.0" } +solana-stake-interface = { version = "=4.3.0", features = ["bincode"] } +solana-svm = { path = "../../svm", version = "=4.3.0-alpha.0" } +solana-svm-feature-set = { path = "../../svm-feature-set", version = "=4.3.0-alpha.0" } +solana-svm-timings = { path = "../../svm-timings", version = "=4.3.0-alpha.0" } +solana-svm-transaction = "=4.2.0" +solana-svm-type-overrides = { path = "../../svm-type-overrides", version = "=4.3.0-alpha.0" } solana-system-interface = { version = "=3.2", features = ["bincode"] } solana-sysvar = "=4.0.0" -solana-vote = { path = "../../vote", version = "=4.2.0-alpha.0" } -solana-vote-interface = "6.0.0" -solana-vote-program = { path = "../../programs/vote", version = "=4.2.0-alpha.0" } +solana-test-validator = { path = "../../test-validator", version = "=4.3.0-alpha.0" } +solana-vote = { path = "../../vote", version = "=4.3.0-alpha.0" } +solana-vote-interface = "6.0.2" +solana-vote-program = { path = "../../programs/vote", version = "=4.3.0-alpha.0" } test-case = "3.3.1" thiserror = "2.0" @@ -201,17 +213,17 @@ solana-sbf-rust-realloc-dep = { workspace = true } solana-sbf-rust-realloc-invoke-dep = { workspace = true } solana-sdk-ids = "3.1.0" solana-signer = "3.0.0" -solana-stake-interface = "3.0.0" -solana-svm = { workspace = true } +solana-stake-interface = "4.3.0" +solana-svm = { workspace = true, features = ["dev-context-only-utils"] } solana-svm-feature-set = { workspace = true } -solana-svm-test-harness-instr = { workspace = true } solana-svm-timings = { workspace = true } solana-svm-transaction = { workspace = true } solana-svm-type-overrides = { workspace = true } solana-system-interface = { workspace = true } solana-sysvar = "4.0.0" +solana-test-validator = { workspace = true, features = ["agave-unstable-api", "dev-context-only-utils"] } solana-transaction = "4.1.0" -solana-transaction-error = "3.2.0" +solana-transaction-error = "3.3.0" solana-vote = { workspace = true } solana-vote-interface = { workspace = true } solana-vote-program = { workspace = true } @@ -233,3 +245,6 @@ opt-level = 1 # The test programs are build in release mode # Minimize their file size so that they fit into the account size limit strip = true + +[lints] +workspace = true diff --git a/programs/sbf/rust/128bit_dep/Cargo.toml b/programs/sbf/rust/128bit_dep/Cargo.toml index 6015395f67b..e11624f48f0 100644 --- a/programs/sbf/rust/128bit_dep/Cargo.toml +++ b/programs/sbf/rust/128bit_dep/Cargo.toml @@ -9,3 +9,6 @@ license = { workspace = true } edition = { workspace = true } [dependencies] + +[lints] +workspace = true diff --git a/programs/sbf/rust/dep_crate/Cargo.toml b/programs/sbf/rust/dep_crate/Cargo.toml index 6902b2fd5ee..707a73025bd 100644 --- a/programs/sbf/rust/dep_crate/Cargo.toml +++ b/programs/sbf/rust/dep_crate/Cargo.toml @@ -14,3 +14,6 @@ crate-type = ["cdylib"] [dependencies] byteorder = { workspace = true } solana-program-entrypoint = { workspace = true } + +[lints] +workspace = true diff --git a/programs/sbf/rust/divide_by_zero/src/lib.rs b/programs/sbf/rust/divide_by_zero/src/lib.rs index ad0f9f6e4f9..a29ba207d9d 100644 --- a/programs/sbf/rust/divide_by_zero/src/lib.rs +++ b/programs/sbf/rust/divide_by_zero/src/lib.rs @@ -1,6 +1,6 @@ //! Example/test program to trigger vm error by dividing by zero -#![feature(asm_experimental_arch)] +#![cfg_attr(target_os = "solana", feature(asm_experimental_arch))] use { solana_account_info::AccountInfo, solana_program_error::ProgramResult, solana_pubkey::Pubkey, diff --git a/programs/sbf/rust/invoke_dep/Cargo.toml b/programs/sbf/rust/invoke_dep/Cargo.toml index 4b6a403c3fe..da6d28a3cc9 100644 --- a/programs/sbf/rust/invoke_dep/Cargo.toml +++ b/programs/sbf/rust/invoke_dep/Cargo.toml @@ -10,3 +10,6 @@ edition = { workspace = true } [lib] crate-type = ["lib"] + +[lints] +workspace = true diff --git a/programs/sbf/rust/invoked_dep/Cargo.toml b/programs/sbf/rust/invoked_dep/Cargo.toml index a70b7d87bfd..6b01f3768d3 100644 --- a/programs/sbf/rust/invoked_dep/Cargo.toml +++ b/programs/sbf/rust/invoked_dep/Cargo.toml @@ -14,3 +14,6 @@ solana-pubkey = { workspace = true } [lib] crate-type = ["lib"] + +[lints] +workspace = true diff --git a/programs/sbf/rust/many_args_dep/Cargo.toml b/programs/sbf/rust/many_args_dep/Cargo.toml index 0dde7fc44cc..f3e73a810d8 100644 --- a/programs/sbf/rust/many_args_dep/Cargo.toml +++ b/programs/sbf/rust/many_args_dep/Cargo.toml @@ -11,3 +11,6 @@ edition = { workspace = true } [dependencies] solana-msg = { workspace = true } solana-program = { workspace = true } + +[lints] +workspace = true diff --git a/programs/sbf/rust/mem_dep/Cargo.toml b/programs/sbf/rust/mem_dep/Cargo.toml index 8b0ef1caed9..1d82d46af19 100644 --- a/programs/sbf/rust/mem_dep/Cargo.toml +++ b/programs/sbf/rust/mem_dep/Cargo.toml @@ -12,3 +12,6 @@ edition = { workspace = true } crate-type = ["lib"] [dependencies] + +[lints] +workspace = true diff --git a/programs/sbf/rust/param_passing_dep/Cargo.toml b/programs/sbf/rust/param_passing_dep/Cargo.toml index 7c85e0cf5cf..3e831df1127 100644 --- a/programs/sbf/rust/param_passing_dep/Cargo.toml +++ b/programs/sbf/rust/param_passing_dep/Cargo.toml @@ -9,3 +9,6 @@ license = { workspace = true } edition = { workspace = true } [dependencies] + +[lints] +workspace = true diff --git a/programs/sbf/rust/realloc_dep/Cargo.toml b/programs/sbf/rust/realloc_dep/Cargo.toml index c2b90bb6f4a..40ff02c3316 100644 --- a/programs/sbf/rust/realloc_dep/Cargo.toml +++ b/programs/sbf/rust/realloc_dep/Cargo.toml @@ -14,3 +14,6 @@ solana-pubkey = { workspace = true } [lib] crate-type = ["lib"] + +[lints] +workspace = true diff --git a/programs/sbf/rust/realloc_invoke_dep/Cargo.toml b/programs/sbf/rust/realloc_invoke_dep/Cargo.toml index 85629d589d9..3b2a59ed237 100644 --- a/programs/sbf/rust/realloc_invoke_dep/Cargo.toml +++ b/programs/sbf/rust/realloc_invoke_dep/Cargo.toml @@ -10,3 +10,6 @@ edition = { workspace = true } [lib] crate-type = ["lib"] + +[lints] +workspace = true diff --git a/programs/sbf/rust/sysvar/src/lib.rs b/programs/sbf/rust/sysvar/src/lib.rs index 9d5101995bb..8b5d208d898 100644 --- a/programs/sbf/rust/sysvar/src/lib.rs +++ b/programs/sbf/rust/sysvar/src/lib.rs @@ -185,7 +185,14 @@ pub fn process_instruction( { msg!("StakeHistory identifier:"); sysvar::stake_history::id().log(); - let _ = StakeHistory::from_account_info(&accounts[9]).unwrap(); + let _: StakeHistory = + if sysvar::stake_history::check_id(accounts[9].unsigned_key()) { + bincode::deserialize(&accounts[9].data.borrow()) + .map_err(|_| ProgramError::InvalidArgument) + } else { + Err(ProgramError::InvalidArgument) + } + .unwrap(); // Syscall `sol_get_sysvar`. let stake_history_sysvar = StakeHistorySysvar(1); assert!(stake_history_sysvar.get_entry(0).is_some()); diff --git a/programs/sbf/tests/programs.rs b/programs/sbf/tests/programs.rs index 4617c02fbb3..1b11cfe7b64 100644 --- a/programs/sbf/tests/programs.rs +++ b/programs/sbf/tests/programs.rs @@ -7,6 +7,8 @@ #![allow(clippy::unnecessary_cast)] #![allow(clippy::uninlined_format_args)] +#[cfg(not(feature = "sbf_sanity_list"))] +use solana_program_runtime::execution_budget::MAX_COMPUTE_UNIT_LIMIT; #[cfg(all(feature = "sbf_rust", feature = "sbpf-v3"))] use solana_runtime::loader_utils::{ load_upgradeable_program_and_advance_slot, set_upgrade_authority, upgrade_program, @@ -75,31 +77,46 @@ use { solana_account::Account, solana_program_runtime::sysvar_cache::SysvarCache, solana_sdk_ids::sysvar::rent, - solana_svm_test_harness_instr::{ - self as harness, fixture::instr_context::InstrContext, - keyed_account::keyed_account_for_system_program, + solana_svm::conformance::{ + instr::{context::InstrContext, harness::execute_instr}, + programs::{ + add_program_to_program_cache, keyed_account_for_system_program, + new_program_cache_with_builtins, + }, }, + std::{fs::File, io::Read, path::PathBuf}, }; +#[cfg(any(feature = "sbf_c", feature = "sbf_rust"))] +fn load_program_elf(program_name: &str) -> Vec { + let sbf_out_dir = + std::env::var("SBF_OUT_DIR").expect("SBF_OUT_DIR must be set to locate program ELFs"); + let path = PathBuf::from(sbf_out_dir).join(format!("{program_name}.so")); + let mut file = File::open(&path) + .unwrap_or_else(|e| panic!("Failed to open file {}: {}", path.display(), e)); + let mut data = Vec::new(); + file.read_to_end(&mut data) + .unwrap_or_else(|e| panic!("Failed to read file {}: {}", path.display(), e)); + data +} + #[cfg(feature = "sbf_rust")] fn default_program_cache() -> solana_program_runtime::loaded_programs::ProgramCacheForTxBatch { - harness::program_cache::new_with_builtins(/* slot */ 0) + new_program_cache_with_builtins(/* slot */ 0) } fn default_program_cache_with_program( program_id: &Pubkey, program_elf: &[u8], feature_set: &SVMFeatureSet, - compute_budget: &ComputeBudget, ) -> solana_program_runtime::loaded_programs::ProgramCacheForTxBatch { let mut program_cache = default_program_cache(); - harness::program_cache::add_program( + add_program_to_program_cache( &mut program_cache, program_id, &bpf_loader_upgradeable::id(), program_elf, feature_set, - compute_budget, ); program_cache } @@ -274,7 +291,7 @@ fn test_program_sbf_sanity() { for program in programs.iter() { println!("Test program: {:?}", program.0); - let program_elf = harness::file::load_program_elf(program.0); + let program_elf = load_program_elf(program.0); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); @@ -290,24 +307,18 @@ fn test_program_sbf_sanity() { let accounts = vec![(pubkey1, Account::default()), (pubkey2, Account::default())]; - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let context = InstrContext { feature_set, accounts, instruction, + cu_avail: MAX_COMPUTE_UNIT_LIMIT as u64, // Widen to 1.4 M }; - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); let result = match effects.result { Some(err) => Err(err), @@ -340,13 +351,11 @@ fn test_program_sbf_loader_deprecated() { for program in programs.iter() { println!("Test program: {:?}", program); - let program_elf = harness::file::load_program_elf(program); + let program_elf = load_program_elf(program); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); - let pubkey = Pubkey::new_unique(); let accounts = vec![ (program_id, Account::new(0, 0, &bpf_loader_deprecated::id())), @@ -354,28 +363,21 @@ fn test_program_sbf_loader_deprecated() { ]; let mut program_cache = default_program_cache(); - harness::program_cache::add_program( + add_program_to_program_cache( &mut program_cache, &program_id, &bpf_loader_deprecated::id(), &program_elf, &feature_set, - &compute_budget, ); let sysvar_cache = default_sysvar_cache(); let account_metas = vec![AccountMeta::new(pubkey, true)]; let instruction = Instruction::new_with_bytes(program_id, &[255], account_metas); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); } @@ -396,14 +398,13 @@ fn test_sol_alloc_free_no_longer_deployable_with_upgradeable_loader() { // In fact, `sol_alloc_free_` is called from sbf allocator, which is originated from // AccountInfo::realloc() in the program code. - let program_elf = harness::file::load_program_elf("solana_sbf_rust_deprecated_loader"); + let program_elf = load_program_elf("solana_sbf_rust_deprecated_loader"); let program_id = Pubkey::new_unique(); let authority_pubkey = Pubkey::new_unique(); let buffer_pubkey = Pubkey::new_unique(); let payer_pubkey = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let (programdata_address, _) = Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader_upgradeable::id()); @@ -485,20 +486,14 @@ fn test_sol_alloc_free_no_longer_deployable_with_upgradeable_loader() { .pop() .unwrap(); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); // Expect that deployment to fail. B/C during deployment, there is an elf // verification step, which uses the runtime to look up relocatable symbols // in elf inside syscall table. In this case, `sol_alloc_free_` can't be // found in syscall table. Hence, the verification fails and the deployment // fails. - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!(effects.result, Some(InstructionError::InvalidAccountData)); } @@ -521,16 +516,11 @@ fn test_program_sbf_duplicate_accounts() { for program in programs.iter() { println!("Test program: {:?}", program); - let program_elf = harness::file::load_program_elf(program); + let program_elf = load_program_elf(program); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let payer_pubkey = Pubkey::new_unique(); @@ -552,13 +542,8 @@ fn test_program_sbf_duplicate_accounts() { (pubkey, account.clone()), ]; let instruction = Instruction::new_with_bytes(program_id, data, account_metas.clone()); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap() + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); + execute_instr(&context, &mut program_cache, &sysvar_cache) }; let effects = execute(&[1]); @@ -605,14 +590,8 @@ fn test_program_sbf_duplicate_accounts() { (pubkey, Account::new(10, 1, &program_id)), ]; let instruction = Instruction::new_with_bytes(program_id, &[7], account_metas); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); } } @@ -635,7 +614,7 @@ fn test_program_sbf_error_handling() { for program in programs.iter() { println!("Test program: {:?}", program); - let program_elf = harness::file::load_program_elf(program); + let program_elf = load_program_elf(program); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); @@ -644,13 +623,8 @@ fn test_program_sbf_error_handling() { let accounts = vec![(pubkey1, Account::default())]; - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); // Helper to execute an instruction with the given data byte. @@ -658,14 +632,10 @@ fn test_program_sbf_error_handling() { let account_metas = vec![AccountMeta::new(pubkey1, true)]; let instruction = Instruction::new_with_bytes(program_id, data, account_metas); - let context = InstrContext { - feature_set, - accounts: accounts.clone(), - instruction, - }; + let context = + InstrContext::new_with_default_budget(feature_set, accounts.clone(), instruction); - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap() + execute_instr(&context, &mut program_cache, &sysvar_cache) }; let effects = execute(&[1]); @@ -730,36 +700,25 @@ fn test_return_data_and_log_data_syscall() { for program in programs.iter() { println!("Test program: {:?}", program); - let program_elf = harness::file::load_program_elf(program); + let program_elf = load_program_elf(program); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let pubkey = Pubkey::new_unique(); let accounts = vec![(pubkey, Account::default())]; - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let account_metas = vec![AccountMeta::new(pubkey, true)]; let instruction = Instruction::new_with_bytes(program_id, &[1, 2, 3, 0, 4, 5, 6], account_metas); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); @@ -1406,14 +1365,13 @@ fn test_program_sbf_invoke_sanity() { #[test] #[cfg(feature = "sbf_rust")] fn test_program_sbf_program_id_spoofing() { - let spoof1_elf = harness::file::load_program_elf("solana_sbf_rust_spoof1"); - let spoof1_system_elf = harness::file::load_program_elf("solana_sbf_rust_spoof1_system"); + let spoof1_elf = load_program_elf("solana_sbf_rust_spoof1"); + let spoof1_system_elf = load_program_elf("solana_sbf_rust_spoof1_system"); let malicious_swap_pubkey = Pubkey::new_unique(); let malicious_system_pubkey = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let from_pubkey = Pubkey::new_unique(); let to_pubkey = Pubkey::new_unique(); @@ -1428,22 +1386,20 @@ fn test_program_sbf_program_id_spoofing() { (to_pubkey, Account::new(0, 0, &system_program::id())), ]; - let mut program_cache = harness::program_cache::new_with_builtins(0); - harness::program_cache::add_program( + let mut program_cache = new_program_cache_with_builtins(0); + add_program_to_program_cache( &mut program_cache, &malicious_swap_pubkey, &bpf_loader_upgradeable::id(), &spoof1_elf, &feature_set, - &compute_budget, ); - harness::program_cache::add_program( + add_program_to_program_cache( &mut program_cache, &malicious_system_pubkey, &bpf_loader_upgradeable::id(), &spoof1_system_elf, &feature_set, - &compute_budget, ); let sysvar_cache = default_sysvar_cache(); @@ -1457,15 +1413,9 @@ fn test_program_sbf_program_id_spoofing() { let instruction = Instruction::new_with_bytes(malicious_swap_pubkey, &[], account_metas.clone()); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!( effects.result, @@ -1482,13 +1432,12 @@ fn test_program_sbf_program_id_spoofing() { #[test] #[cfg(feature = "sbf_rust")] fn test_program_sbf_caller_has_access_to_cpi_program() { - let caller_access_elf = harness::file::load_program_elf("solana_sbf_rust_caller_access"); + let caller_access_elf = load_program_elf("solana_sbf_rust_caller_access"); let caller_pubkey = Pubkey::new_unique(); let caller2_pubkey = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let accounts = vec![ ( @@ -1501,22 +1450,20 @@ fn test_program_sbf_caller_has_access_to_cpi_program() { ), ]; - let mut program_cache = harness::program_cache::new_with_builtins(0); - harness::program_cache::add_program( + let mut program_cache = new_program_cache_with_builtins(0); + add_program_to_program_cache( &mut program_cache, &caller_pubkey, &bpf_loader_upgradeable::id(), &caller_access_elf, &feature_set, - &compute_budget, ); - harness::program_cache::add_program( + add_program_to_program_cache( &mut program_cache, &caller2_pubkey, &bpf_loader_upgradeable::id(), &caller_access_elf, &feature_set, - &compute_budget, ); let sysvar_cache = default_sysvar_cache(); @@ -1526,15 +1473,9 @@ fn test_program_sbf_caller_has_access_to_cpi_program() { ]; let instruction = Instruction::new_with_bytes(caller_pubkey, &[1], account_metas); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!(effects.result, Some(InstructionError::MissingAccount)); } @@ -1544,11 +1485,10 @@ fn test_program_sbf_caller_has_access_to_cpi_program() { fn test_program_sbf_ro_modify() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("solana_sbf_rust_ro_modify"); + let program_elf = load_program_elf("solana_sbf_rust_ro_modify"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let test_pubkey = Pubkey::new_unique(); let accounts = vec![ @@ -1556,12 +1496,8 @@ fn test_program_sbf_ro_modify() { (test_pubkey, Account::new(10, 0, &system_program::id())), ]; - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let account_metas = vec![ @@ -1573,15 +1509,10 @@ fn test_program_sbf_ro_modify() { let instruction = Instruction::new_with_bytes(program_id, &[instr_data], account_metas.clone()); - let context = InstrContext { - feature_set, - accounts: accounts.clone(), - instruction, - }; + let context = + InstrContext::new_with_default_budget(feature_set, accounts.clone(), instruction); - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!( effects.result, @@ -1595,30 +1526,22 @@ fn test_program_sbf_ro_modify() { fn test_program_sbf_call_depth() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("solana_sbf_rust_call_depth"); + let program_elf = load_program_elf("solana_sbf_rust_call_depth"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let mut execute = |depth: usize| { let instruction = Instruction::new_with_bincode(program_id, &depth, vec![]); - let context = InstrContext { - feature_set, - accounts: vec![], - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, vec![], instruction); - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache).unwrap() + execute_instr(&context, &mut program_cache, &sysvar_cache) }; let effects = execute(compute_budget.max_call_depth - 1); @@ -1633,24 +1556,18 @@ fn test_program_sbf_call_depth() { fn test_program_sbf_compute_budget() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("solana_sbf_rust_noop"); + let program_elf = load_program_elf("solana_sbf_rust_noop"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let mut compute_budget = ComputeBudget::new_with_defaults(false); - compute_budget.compute_unit_limit = 0; let accounts = vec![( program_id, Account::new(0, 0, &bpf_loader_upgradeable::id()), )]; - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let instruction = Instruction::new_with_bincode(program_id, &0, vec![]); @@ -1659,11 +1576,10 @@ fn test_program_sbf_compute_budget() { feature_set, accounts, instruction, + cu_avail: 0, }; - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!( effects.result, @@ -1715,20 +1631,15 @@ fn assert_instruction_count() { } let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); let sysvar_cache = default_sysvar_cache(); println!("\n {:36} expected actual diff", "SBF program"); for (program_name, expected_consumption) in programs.iter() { - let program_elf = harness::file::load_program_elf(program_name); + let program_elf = load_program_elf(program_name); let program_id = Pubkey::new_unique(); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let account_pubkey = Pubkey::new_unique(); let accounts = vec![(account_pubkey, Account::new(0, 0, &program_id))]; @@ -1740,19 +1651,11 @@ fn assert_instruction_count() { }]; let instruction = Instruction::new_with_bytes(program_id, &[], instruction_accounts); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); - let consumption = compute_budget - .compute_unit_limit - .saturating_sub(effects.cu_avail); + let consumption = context.cu_avail.saturating_sub(effects.cu_avail); let diff: i64 = consumption as i64 - *expected_consumption as i64; println!( " {:36} {:8}{:6} {:+5} ({:+3.0}%)", @@ -1834,19 +1737,13 @@ fn test_program_sbf_instruction_introspection() { fn test_program_sbf_r2_instruction_data_pointer(num_accounts: usize, input_data_len: usize) { agave_logger::setup(); - let program_elf = - harness::file::load_program_elf("solana_sbf_rust_r2_instruction_data_pointer"); + let program_elf = load_program_elf("solana_sbf_rust_r2_instruction_data_pointer"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let mut accounts = Vec::new(); @@ -1871,15 +1768,9 @@ fn test_program_sbf_r2_instruction_data_pointer(num_accounts: usize, input_data_ let instruction = Instruction::new_with_bytes(program_id, &input_data, account_metas); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); assert_eq!(input_data, effects.return_data); @@ -2375,38 +2266,30 @@ fn test_program_sbf_disguised_as_sbf_loader() { } for program in programs.iter() { - let program_elf = harness::file::load_program_elf(program); + let program_elf = load_program_elf(program); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet { remove_bpf_loader_incorrect_program_id: false, ..SVMFeatureSet::all_enabled() }; - let compute_budget = ComputeBudget::new_with_defaults(false); let mut program_cache = default_program_cache(); - harness::program_cache::add_program( + add_program_to_program_cache( &mut program_cache, &program_id, &bpf_loader::id(), &program_elf, &feature_set, - &compute_budget, ); let sysvar_cache = default_sysvar_cache(); let account_metas = vec![AccountMeta::new_readonly(program_id, false)]; let instruction = Instruction::new_with_bytes(bpf_loader::id(), &[1], account_metas); - let context = InstrContext { - feature_set, - accounts: vec![], - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, vec![], instruction); - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!(effects.result, Some(InstructionError::UnsupportedProgramId)); } } @@ -2416,20 +2299,18 @@ fn test_program_sbf_disguised_as_sbf_loader() { fn test_program_reads_from_program_account() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("read_program"); + let program_elf = load_program_elf("read_program"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = harness::program_cache::new_with_builtins(0); - harness::program_cache::add_program( + let mut program_cache = new_program_cache_with_builtins(0); + add_program_to_program_cache( &mut program_cache, &program_id, &bpf_loader::id(), &program_elf, &feature_set, - &compute_budget, ); let sysvar_cache = default_sysvar_cache(); @@ -2446,15 +2327,13 @@ fn test_program_reads_from_program_account() { let account_metas = vec![AccountMeta::new_readonly(program_id, false)]; let instruction = Instruction::new_with_bytes(program_id, &program_elf[..48], account_metas); - let context = InstrContext { + let context = InstrContext::new_with_default_budget( feature_set, - accounts: vec![(program_id, program_account)], + vec![(program_id, program_account)], instruction, - }; + ); - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); } @@ -2463,19 +2342,17 @@ fn test_program_reads_from_program_account() { fn test_program_sbf_c_dup() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("ser"); + let program_elf = load_program_elf("ser"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = harness::program_cache::new_with_builtins(0); - harness::program_cache::add_program( + let mut program_cache = new_program_cache_with_builtins(0); + add_program_to_program_cache( &mut program_cache, &program_id, &bpf_loader_upgradeable::id(), &program_elf, &feature_set, - &compute_budget, ); let sysvar_cache = default_sysvar_cache(); @@ -2490,15 +2367,13 @@ fn test_program_sbf_c_dup() { ]; let instruction = Instruction::new_with_bytes(program_id, &[4, 5, 6, 7], account_metas); - let context = InstrContext { + let context = InstrContext::new_with_default_budget( feature_set, - accounts: vec![(account_address, account)], + vec![(account_address, account)], instruction, - }; + ); - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); } @@ -2673,18 +2548,13 @@ fn test_program_sbf_upgrade_via_cpi() { fn test_program_sbf_ro_account_modify() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("solana_sbf_rust_ro_account_modify"); + let program_elf = load_program_elf("solana_sbf_rust_ro_account_modify"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let argument_pubkey = Pubkey::new_unique(); @@ -2698,15 +2568,10 @@ fn test_program_sbf_ro_account_modify() { for case in [0, 1, 2] { let instruction = Instruction::new_with_bytes(program_id, &[case], account_metas.clone()); - let context = InstrContext { - feature_set, - accounts: accounts.clone(), - instruction, - }; + let context = + InstrContext::new_with_default_budget(feature_set, accounts.clone(), instruction); - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!(effects.result, Some(InstructionError::ReadonlyDataModified)); } @@ -3039,7 +2904,7 @@ fn test_program_sbf_realloc() { ) .unwrap_err() .unwrap(), - TransactionError::InstructionError(0, InstructionError::AccountDataSizeChanged) + TransactionError::InstructionError(0, InstructionError::ExternalAccountDataModified) ); // realloc and assign to self via cpi @@ -3066,7 +2931,7 @@ fn test_program_sbf_realloc() { ) .unwrap_err() .unwrap(), - TransactionError::InstructionError(0, InstructionError::AccountDataSizeChanged) + TransactionError::InstructionError(0, InstructionError::ExternalAccountDataModified) ); // Assign to self and realloc via cpi @@ -3344,7 +3209,7 @@ fn test_program_sbf_realloc_invoke() { ) .unwrap_err() .unwrap(), - TransactionError::InstructionError(0, InstructionError::AccountDataSizeChanged) + TransactionError::InstructionError(0, InstructionError::ExternalAccountDataModified) ); // realloc and assign to self via system program @@ -3375,7 +3240,7 @@ fn test_program_sbf_realloc_invoke() { ) .unwrap_err() .unwrap(), - TransactionError::InstructionError(0, InstructionError::AccountDataSizeChanged) + TransactionError::InstructionError(0, InstructionError::ExternalAccountDataModified) ); // Assign to self and realloc via system program @@ -4328,7 +4193,7 @@ fn test_cpi_account_data_updates() { if virtual_address_space_adjustments { InstructionError::InvalidRealloc } else { - InstructionError::AccountDataSizeChanged + InstructionError::ExternalAccountDataModified } ) ); @@ -4371,7 +4236,7 @@ fn test_cpi_account_data_updates() { if virtual_address_space_adjustments && deprecated_callee { InstructionError::InvalidRealloc } else { - InstructionError::AccountDataSizeChanged + InstructionError::ExternalAccountDataModified } ) ); @@ -4595,22 +4460,13 @@ fn test_deplete_cost_meter_with_access_violation() { fn test_program_sbf_deplete_cost_meter_with_divide_by_zero() { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("solana_sbf_rust_divide_by_zero"); + let program_elf = load_program_elf("solana_sbf_rust_divide_by_zero"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = { - let mut budget = ComputeBudget::new_with_defaults(false); - budget.compute_unit_limit = 10_000u64; - budget - }; - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let instruction = Instruction::new_with_bytes(program_id, &[], vec![]); @@ -4619,11 +4475,10 @@ fn test_program_sbf_deplete_cost_meter_with_divide_by_zero() { feature_set, accounts: vec![], instruction, + cu_avail: 10_000, }; - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert_eq!( effects.result, @@ -5661,18 +5516,13 @@ fn test_mem_syscalls_overlap_account_begin_or_end() { fn test_program_sbf_rust_direct_account_pointers(num_accounts: usize, input_data_len: usize) { agave_logger::setup(); - let program_elf = harness::file::load_program_elf("solana_sbf_rust_direct_account_pointers"); + let program_elf = load_program_elf("solana_sbf_rust_direct_account_pointers"); let program_id = Pubkey::new_unique(); let feature_set = SVMFeatureSet::all_enabled(); - let compute_budget = ComputeBudget::new_with_defaults(false); - let mut program_cache = default_program_cache_with_program( - &program_id, - &program_elf, - &feature_set, - &compute_budget, - ); + let mut program_cache = + default_program_cache_with_program(&program_id, &program_elf, &feature_set); let sysvar_cache = default_sysvar_cache(); let mut accounts = Vec::new(); @@ -5702,15 +5552,9 @@ fn test_program_sbf_rust_direct_account_pointers(num_accounts: usize, input_data let instruction = Instruction::new_with_bytes(program_id, &input_data, account_metas); - let context = InstrContext { - feature_set, - accounts, - instruction, - }; + let context = InstrContext::new_with_default_budget(feature_set, accounts, instruction); - let effects = - harness::execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .unwrap(); + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); assert!(effects.result.is_none()); // `num_accounts * 2` will be added as return data. diff --git a/programs/sbf/tests/simulation.rs b/programs/sbf/tests/simulation.rs index a2a874e5252..efd689c8d35 100644 --- a/programs/sbf/tests/simulation.rs +++ b/programs/sbf/tests/simulation.rs @@ -1,7 +1,6 @@ #![cfg(feature = "sbf_rust")] use { - agave_validator::test_validator::*, solana_instruction::{AccountMeta, Instruction}, solana_message::Message, solana_pubkey::Pubkey, @@ -15,6 +14,7 @@ use { solana_sdk_ids::bpf_loader_upgradeable, solana_signer::Signer, solana_sysvar::{clock, slot_history}, + solana_test_validator::TestValidatorGenesis, solana_transaction::Transaction, std::{thread::sleep, time::Duration}, }; @@ -61,7 +61,7 @@ fn test_no_panic_rpc_client() { agave_logger::setup(); let program_id = Pubkey::new_unique(); - let (test_validator, payer) = TestValidatorGenesis::default() + let (test_validator, payer) = TestValidatorGenesis::default_for_tests() .add_program("solana_sbf_rust_simulation", program_id) .start(); let rpc_client = test_validator.get_rpc_client(); diff --git a/programs/sbf/tests/sysvar.rs b/programs/sbf/tests/sysvar.rs index 2750d33a32a..eae08d55132 100644 --- a/programs/sbf/tests/sysvar.rs +++ b/programs/sbf/tests/sysvar.rs @@ -2,6 +2,7 @@ use { agave_feature_set::disable_fees_sysvar, + solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, solana_instruction::{AccountMeta, Instruction}, solana_message::Message, solana_pubkey::Pubkey, @@ -15,16 +16,39 @@ use { solana_sdk_ids::{ bpf_loader_upgradeable, sysvar::{ - clock, epoch_schedule, instructions, recent_blockhashes, rent, slot_hashes, + self, clock, epoch_schedule, instructions, recent_blockhashes, rent, slot_hashes, slot_history, stake_history, }, }, solana_signer::Signer, - solana_stake_interface::stake_history::{StakeHistory, StakeHistoryEntry}, + solana_stake_interface::stake_history::{ + SIZE as STAKE_HISTORY_ACCOUNT_SIZE, StakeHistory, StakeHistoryEntry, + }, solana_sysvar::epoch_rewards, solana_transaction::Transaction, }; +fn set_stake_history_sysvar_for_tests(bank: &Bank, stake_history: &StakeHistory) { + let stake_history_account_size = + STAKE_HISTORY_ACCOUNT_SIZE.max(bincode::serialized_size(stake_history).unwrap() as usize); + let old_stake_history_account = bank.get_account(&stake_history::id()).unwrap(); + let lamports = old_stake_history_account + .lamports() + .max(bank.get_minimum_balance_for_rent_exemption(stake_history_account_size)); + let mut stake_history_account = AccountSharedData::new_data_with_space( + lamports, + stake_history, + stake_history_account_size, + &sysvar::id(), + ) + .unwrap(); + stake_history_account.set_rent_epoch(old_stake_history_account.rent_epoch()); + bank.store_account(&stake_history::id(), &stake_history_account); + bank.get_transaction_processor().reset_sysvar_cache(); + bank.get_transaction_processor() + .fill_missing_sysvar_cache_entries(bank); +} + #[test] fn test_sysvar_syscalls() { agave_logger::setup(); @@ -59,7 +83,7 @@ fn test_sysvar_syscalls() { ); stake_history }; - bank.set_sysvar_for_tests(&stake_history); + set_stake_history_sysvar_for_tests(&bank, &stake_history); let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); let program_id = create_program( diff --git a/programs/system/Cargo.toml b/programs/system/Cargo.toml index b66ef9592c6..1a73af33781 100644 --- a/programs/system/Cargo.toml +++ b/programs/system/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -43,6 +45,7 @@ criterion = { workspace = true } solana-compute-budget = { path = "../../compute-budget", features = ["agave-unstable-api"] } solana-hash = { workspace = true } solana-program-runtime = { workspace = true, features = ["dev-context-only-utils"] } +solana-pubkey = { workspace = true, features = ["rand"] } solana-rent = { workspace = true } solana-sha256-hasher = { workspace = true } solana-svm-callback = { path = "../../svm-callback", features = ["agave-unstable-api"] } @@ -53,3 +56,6 @@ solana-transaction-context = { path = "../../transaction-context", features = [" [[bench]] name = "system" harness = false + +[lints] +workspace = true diff --git a/programs/system/src/system_processor.rs b/programs/system/src/system_processor.rs index 01a06e25ade..16aa060d44b 100644 --- a/programs/system/src/system_processor.rs +++ b/programs/system/src/system_processor.rs @@ -199,16 +199,16 @@ fn create_account_allow_prefund( let mut to = instruction_context.try_borrow_instruction_account(to_account_index)?; allocate_and_assign(&mut to, to_address, space, owner, signers, invoke_context)?; } - if let Some((from_account_index, lamports)) = from_and_lamports { - if lamports > 0 { - transfer( - from_account_index, - to_account_index, - lamports, - invoke_context, - instruction_context, - )?; - } + if let Some((from_account_index, lamports)) = from_and_lamports + && lamports > 0 + { + transfer( + from_account_index, + to_account_index, + lamports, + invoke_context, + instruction_context, + )?; } Ok(()) } diff --git a/programs/vote/Cargo.toml b/programs/vote/Cargo.toml index ca036c9e73e..c6691d0757a 100644 --- a/programs/vote/Cargo.toml +++ b/programs/vote/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] diff --git a/programs/vote/src/vote_state/handler.rs b/programs/vote/src/vote_state/handler.rs index ec482cf6597..aa859207e2b 100644 --- a/programs/vote/src/vote_state/handler.rs +++ b/programs/vote/src/vote_state/handler.rs @@ -163,7 +163,7 @@ impl VoteStateHandler { } } - pub(crate) fn node_pubkey(&self) -> &Pubkey { + pub fn node_pubkey(&self) -> &Pubkey { match &self.target_state { TargetVoteState::V4(v4) => &v4.node_pubkey, } @@ -277,19 +277,19 @@ impl VoteStateHandler { } } - pub(crate) fn epoch_credits_mut(&mut self) -> &mut Vec<(Epoch, u64, u64)> { + pub fn epoch_credits_mut(&mut self) -> &mut Vec<(Epoch, u64, u64)> { match &mut self.target_state { TargetVoteState::V4(v4) => &mut v4.epoch_credits, } } - pub(crate) fn last_timestamp(&self) -> &BlockTimestamp { + pub fn last_timestamp(&self) -> &BlockTimestamp { match &self.target_state { TargetVoteState::V4(v4) => &v4.last_timestamp, } } - pub(crate) fn set_last_timestamp(&mut self, timestamp: BlockTimestamp) { + pub fn set_last_timestamp(&mut self, timestamp: BlockTimestamp) { match &mut self.target_state { TargetVoteState::V4(v4) => v4.last_timestamp = timestamp, } diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index 7abe505425d..40c0390525a 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -71,10 +71,10 @@ fn check_and_filter_proposed_vote_state( .slot(); // If the proposed state is not new enough, return - if let Some(last_vote_slot) = vote_state.votes().back().map(|lockout| lockout.slot()) { - if last_proposed_slot <= last_vote_slot { - return Err(VoteError::VoteTooOld); - } + if let Some(last_vote_slot) = vote_state.votes().back().map(|lockout| lockout.slot()) + && last_proposed_slot <= last_vote_slot + { + return Err(VoteError::VoteTooOld); } if slot_hashes.is_empty() { @@ -461,17 +461,16 @@ pub fn process_new_vote_state( return Err(VoteError::ZeroConfirmations); } else if vote.confirmation_count() > MAX_LOCKOUT_HISTORY as u32 { return Err(VoteError::ConfirmationTooLarge); - } else if let Some(new_root) = new_root { - if vote.slot() <= new_root + } else if let Some(new_root) = new_root + && vote.slot() <= new_root && // This check is necessary because // https://github.com/ryoqun/solana/blob/df55bfb46af039cbc597cd60042d49b9d90b5961/core/src/consensus.rs#L120 // always sets a root for even empty towers, which is then hard unwrapped here // https://github.com/ryoqun/solana/blob/df55bfb46af039cbc597cd60042d49b9d90b5961/core/src/consensus.rs#L776 new_root != Slot::default() - { - return Err(VoteError::SlotSmallerThanRoot); - } + { + return Err(VoteError::SlotSmallerThanRoot); } if let Some(previous_vote) = previous_vote { diff --git a/programs/zk-elgamal-proof-tests/Cargo.toml b/programs/zk-elgamal-proof-tests/Cargo.toml index f86f3760f9b..badc620ad77 100644 --- a/programs/zk-elgamal-proof-tests/Cargo.toml +++ b/programs/zk-elgamal-proof-tests/Cargo.toml @@ -24,3 +24,6 @@ solana-system-interface = { workspace = true } solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } solana-zk-sdk = { workspace = true } + +[lints] +workspace = true diff --git a/programs/zk-elgamal-proof/Cargo.toml b/programs/zk-elgamal-proof/Cargo.toml index bbd0dc132eb..f3ecfb4606b 100644 --- a/programs/zk-elgamal-proof/Cargo.toml +++ b/programs/zk-elgamal-proof/Cargo.toml @@ -8,6 +8,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] @@ -26,3 +31,6 @@ curve25519-dalek = { workspace = true } [[bench]] name = "verify_proofs" harness = false + +[lints] +workspace = true diff --git a/programs/zk-token-proof/Cargo.toml b/programs/zk-token-proof/Cargo.toml index 7c34e2409f1..778b9af02e0 100644 --- a/programs/zk-token-proof/Cargo.toml +++ b/programs/zk-token-proof/Cargo.toml @@ -8,8 +8,16 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] [dependencies] solana-program-runtime = { workspace = true } + +[lints] +workspace = true diff --git a/pubsub-client/Cargo.toml b/pubsub-client/Cargo.toml index 486061a2f57..59920d2319c 100644 --- a/pubsub-client/Cargo.toml +++ b/pubsub-client/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -37,3 +39,6 @@ url = { workspace = true } anyhow = { workspace = true } solana-commitment-config = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } + +[lints] +workspace = true diff --git a/pubsub-client/src/nonblocking/pubsub_client.rs b/pubsub-client/src/nonblocking/pubsub_client.rs index ac3585c5703..e85295cc68b 100644 --- a/pubsub-client/src/nonblocking/pubsub_client.rs +++ b/pubsub-client/src/nonblocking/pubsub_client.rs @@ -283,28 +283,27 @@ async fn connect_with_retry( let result = connect_async(client_request.clone()) .await .map(|(socket, _)| socket); - if let Err(tungstenite::Error::Http(response)) = &result { - if response.status() == StatusCode::TOO_MANY_REQUESTS && connection_retries > 0 { - let mut duration = Duration::from_millis(500); - if let Some(retry_after) = response.headers().get(header::RETRY_AFTER) { - if let Ok(retry_after) = retry_after.to_str() { - if let Ok(retry_after) = retry_after.parse::() { - if retry_after < 120 { - duration = Duration::from_secs(retry_after); - } - } - } - } + if let Err(tungstenite::Error::Http(response)) = &result + && response.status() == StatusCode::TOO_MANY_REQUESTS + && connection_retries > 0 + { + let mut duration = Duration::from_millis(500); + if let Some(retry_after) = response.headers().get(header::RETRY_AFTER) + && let Ok(retry_after) = retry_after.to_str() + && let Ok(retry_after) = retry_after.parse::() + && retry_after < 120 + { + duration = Duration::from_secs(retry_after); + } - connection_retries -= 1; - debug!( - "Too many requests: server responded with {response:?}, {connection_retries} \ - retries left, pausing for {duration:?}" - ); + connection_retries -= 1; + debug!( + "Too many requests: server responded with {response:?}, {connection_retries} \ + retries left, pausing for {duration:?}" + ); - sleep(duration).await; - continue; - } + sleep(duration).await; + continue; } return result.map_err(Box::new); } @@ -577,7 +576,7 @@ impl PubsubClient { Some(msg) => msg.map_err(Box::new)?, None => break, }; - trace!("ws.next(): {:?}", &msg); + trace!("ws.next(): {msg:?}"); // Get text from the message let text = match msg { @@ -668,28 +667,28 @@ impl PubsubClient { // Notification, example: // `{"jsonrpc":"2.0","method":"logsNotification","params":{"result":{...},"subscription":3114862}}` - if let Some(Value::Object(params)) = json.get_mut("params") { - if let Some(sid) = params.get("subscription").and_then(Value::as_u64) { - let mut unsubscribe_required = false; - - if let Some(notifications_sender) = subscriptions.get(&sid) { - if let Some(result) = params.remove("result") { - if notifications_sender.send(result).is_err() { - unsubscribe_required = true; - } - } - } else { + if let Some(Value::Object(params)) = json.get_mut("params") + && let Some(sid) = params.get("subscription").and_then(Value::as_u64) + { + let mut unsubscribe_required = false; + + if let Some(notifications_sender) = subscriptions.get(&sid) { + if let Some(result) = params.remove("result") + && notifications_sender.send(result).is_err() + { unsubscribe_required = true; } + } else { + unsubscribe_required = true; + } - if unsubscribe_required { - if let Some(Value::String(method)) = json.remove("method") { - if let Some(operation) = method.strip_suffix("Notification") { - let (response_sender, _response_receiver) = oneshot::channel(); - let _ = unsubscribe_sender.send((operation.to_string(), sid, response_sender)); - } - } - } + if unsubscribe_required + && let Some(Value::String(method)) = json.remove("method") + && let Some(operation) = method.strip_suffix("Notification") + { + let (response_sender, _response_receiver) = oneshot::channel(); + let _ = unsubscribe_sender + .send((operation.to_string(), sid, response_sender)); } } } diff --git a/pubsub-client/src/pubsub_client.rs b/pubsub-client/src/pubsub_client.rs index 37bd276d7f6..499566894ac 100644 --- a/pubsub-client/src/pubsub_client.rs +++ b/pubsub-client/src/pubsub_client.rs @@ -44,7 +44,7 @@ //! This example subscribes to account events and then loops forever receiving //! them. //! -//! ``` +//! ```no_run //! use anyhow::Result; //! use solana_commitment_config::CommitmentConfig; //! use solana_pubkey::Pubkey; @@ -181,12 +181,11 @@ where fn extract_subscription_id(message: Message) -> Result { let message_text = &message.into_text().map_err(Box::new)?; - if let Ok(json_msg) = serde_json::from_str::>(message_text) { - if let Some(Number(x)) = json_msg.get("result") { - if let Some(x) = x.as_u64() { - return Ok(x); - } - } + if let Ok(json_msg) = serde_json::from_str::>(message_text) + && let Some(Number(x)) = json_msg.get("result") + && let Some(x) = x.as_u64() + { + return Ok(x); } Err(PubsubClientError::UnexpectedSubscriptionResponse(format!( @@ -226,14 +225,12 @@ where return Ok(None); } let message_text = &message.into_text().map_err(Box::new)?; - if let Ok(json_msg) = serde_json::from_str::>(message_text) { - if let Some(Object(params)) = json_msg.get("params") { - if let Some(result) = params.get("result") { - if let Ok(x) = T::deserialize(result) { - return Ok(Some(x)); - } - } - } + if let Ok(json_msg) = serde_json::from_str::>(message_text) + && let Some(Object(params)) = json_msg.get("params") + && let Some(result) = params.get("result") + && let Ok(x) = T::deserialize(result) + { + return Ok(Some(x)); } Err(PubsubClientError::UnexpectedMessageError(format!( @@ -315,28 +312,27 @@ fn connect_with_retry( let client_request = request.into_client_request().map_err(Box::new)?; loop { let result = connect(client_request.clone()).map(|(socket, _)| socket); - if let Err(tungstenite::Error::Http(response)) = &result { - if response.status() == StatusCode::TOO_MANY_REQUESTS && connection_retries > 0 { - let mut duration = Duration::from_millis(500); - if let Some(retry_after) = response.headers().get(header::RETRY_AFTER) { - if let Ok(retry_after) = retry_after.to_str() { - if let Ok(retry_after) = retry_after.parse::() { - if retry_after < 120 { - duration = Duration::from_secs(retry_after); - } - } - } - } + if let Err(tungstenite::Error::Http(response)) = &result + && response.status() == StatusCode::TOO_MANY_REQUESTS + && connection_retries > 0 + { + let mut duration = Duration::from_millis(500); + if let Some(retry_after) = response.headers().get(header::RETRY_AFTER) + && let Ok(retry_after) = retry_after.to_str() + && let Ok(retry_after) = retry_after.parse::() + && retry_after < 120 + { + duration = Duration::from_secs(retry_after); + } - connection_retries -= 1; - debug!( - "Too many requests: server responded with {response:?}, {connection_retries} \ - retries left, pausing for {duration:?}" - ); + connection_retries -= 1; + debug!( + "Too many requests: server responded with {response:?}, {connection_retries} \ + retries left, pausing for {duration:?}" + ); - sleep(duration); - continue; - } + sleep(duration); + continue; } return result.map_err(Box::new); } diff --git a/quic-client/Cargo.toml b/quic-client/Cargo.toml index 8a6ea2caea1..a27ada1d2bf 100644 --- a/quic-client/Cargo.toml +++ b/quic-client/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] @@ -41,3 +46,6 @@ solana-perf = { path = "../perf", features = ["agave-unstable-api"] } solana-quic-client = { path = ".", features = ["agave-unstable-api"] } solana-streamer = { path = "../streamer", features = ["agave-unstable-api", "dev-context-only-utils"] } tokio-util = { workspace = true } + +[lints] +workspace = true diff --git a/quic-client/src/quic_client.rs b/quic-client/src/quic_client.rs index 0af536966f1..0d9a991766b 100644 --- a/quic-client/src/quic_client.rs +++ b/quic-client/src/quic_client.rs @@ -184,5 +184,13 @@ impl ClientConnection for QuicClientConnection { pub(crate) fn close_quic_connection(connection: Arc) { // Close the connection and release resources trace!("Closing QUIC connection to {}", connection.server_addr()); - RUNTIME.block_on(connection.close()); + // Connection caches can be dropped by async callers, such as admin RPC set-identity; + // blocking on RUNTIME from a Tokio worker would panic. + if tokio::runtime::Handle::try_current().is_ok() { + let _handle = RUNTIME.spawn(async move { + connection.close().await; + }); + } else { + RUNTIME.block_on(connection.close()); + } } diff --git a/quic-client/tests/quic_client.rs b/quic-client/tests/quic_client.rs index 9fc64c0ccb5..eda5a1e8d8b 100644 --- a/quic-client/tests/quic_client.rs +++ b/quic-client/tests/quic_client.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { use { - crossbeam_channel::{Receiver, unbounded}, + crossbeam_channel::{Receiver, bounded}, log::*, solana_connection_cache::{ client_connection::ClientStats, connection_cache_stats::ConnectionCacheStats, @@ -60,6 +60,30 @@ mod tests { ) } + #[tokio::test] + async fn test_quic_connection_cache_update_key_from_tokio_runtime() { + use { + solana_connection_cache::connection_cache::{ConnectionCache, NewConnectionConfig}, + solana_quic_client::{QuicConfig, QuicConnectionManager}, + }; + + let keypair = Keypair::new(); + let mut config = QuicConfig::new().unwrap(); + config.update_client_certificate(&keypair, IpAddr::V4(Ipv4Addr::LOCALHOST)); + let connection_manager = QuicConnectionManager::new_with_connection_config(config); + let connection_cache = ConnectionCache::new( + "quic_connection_cache_update_key_test", + connection_manager, + 1, + ) + .unwrap(); + + let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 12345); + let _connection = connection_cache.get_nonblocking_connection(&server_addr); + + connection_cache.update_key(&Keypair::new()).unwrap(); + } + #[test] fn test_quic_client_multiple_writes() { use { @@ -67,7 +91,7 @@ mod tests { solana_quic_client::quic_client::QuicClientConnection, }; agave_logger::setup(); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let staked_nodes = Arc::new(RwLock::new(StakedNodes::default())); let (s, cancel, keypair) = server_args(); let SpawnServerResult { @@ -148,7 +172,7 @@ mod tests { solana_quic_client::nonblocking::quic_client::QuicClientConnection, }; agave_logger::setup(); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let staked_nodes = Arc::new(RwLock::new(StakedNodes::default())); let (s, cancel, keypair) = server_args(); let SpawnNonBlockingServerResult { @@ -207,7 +231,7 @@ mod tests { agave_logger::setup(); // Request Receiver - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let staked_nodes = Arc::new(RwLock::new(StakedNodes::default())); let (request_recv_socket, request_recv_cancel, keypair) = server_args(); let SpawnServerResult { @@ -230,7 +254,7 @@ mod tests { drop(request_recv_endpoints); // Response Receiver: let (response_recv_socket, response_recv_cancel, keypair2) = server_args(); - let (sender2, receiver2) = unbounded(); + let (sender2, receiver2) = bounded(1024); let addr = response_recv_socket.local_addr().unwrap().ip(); let port = response_recv_socket.local_addr().unwrap().port(); @@ -319,7 +343,7 @@ mod tests { #[tokio::test] async fn test_connection_close() { agave_logger::setup(); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let staked_nodes = Arc::new(RwLock::new(StakedNodes::default())); let (s, cancel, keypair) = server_args(); let solana_streamer::nonblocking::quic::SpawnNonBlockingServerResult { diff --git a/random/Cargo.toml b/random/Cargo.toml index 70b23088cbb..fe6b5991ecb 100644 --- a/random/Cargo.toml +++ b/random/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -28,3 +30,6 @@ rand_chacha = { workspace = true } solana-hash = { workspace = true, features = ["decode"] } solana-sha256-hasher = { workspace = true, features = ["sha2"] } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/rayon-threadlimit/Cargo.toml b/rayon-threadlimit/Cargo.toml index e0fa018d178..205a4e1c552 100644 --- a/rayon-threadlimit/Cargo.toml +++ b/rayon-threadlimit/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -21,3 +23,6 @@ num_cpus = { workspace = true } [dev-dependencies] solana-rayon-threadlimit = { path = ".", features = ["agave-unstable-api"] } + +[lints] +workspace = true diff --git a/rbpf-cli/Cargo.toml b/rbpf-cli/Cargo.toml index aae11450fcb..f2f380ca472 100644 --- a/rbpf-cli/Cargo.toml +++ b/rbpf-cli/Cargo.toml @@ -14,3 +14,6 @@ edition = { workspace = true } agave-unstable-api = [] [dependencies] + +[lints] +workspace = true diff --git a/remote-wallet/Cargo.toml b/remote-wallet/Cargo.toml index 02a1b06352c..0ec898dcc80 100644 --- a/remote-wallet/Cargo.toml +++ b/remote-wallet/Cargo.toml @@ -11,10 +11,22 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +# `all-features` can't be used here: hidapi's linux backends +# (linux-{shared,static}-{hidraw,libusb}) are mutually exclusive. +# Document the default backend plus the unstable API instead. +features = ["agave-unstable-api"] +rustdoc-args = ["--cfg=docsrs"] [features] default = ["linux-static-hidraw"] agave-unstable-api = [] +keystone = [ + "dep:hex", + "dep:rusb", + "dep:serde_json", + "dep:ur-parse-lib", + "dep:ur-registry", +] linux-shared-hidraw = ["hidapi/linux-shared-hidraw"] linux-shared-libusb = ["hidapi/linux-shared-libusb"] linux-static-hidraw = ["hidapi/linux-static-hidraw"] @@ -23,12 +35,15 @@ linux-static-libusb = ["hidapi/linux-static-libusb"] [dependencies] console = { workspace = true } dialoguer = { workspace = true } +hex = { workspace = true, optional = true } hidapi = { workspace = true, optional = true } log = { workspace = true } num-derive = { workspace = true } num-traits = { workspace = true } parking_lot = { workspace = true } +rusb = { workspace = true, optional = true } semver = { workspace = true } +serde_json = { workspace = true, optional = true } solana-derivation-path = { workspace = true } solana-offchain-message = { workspace = true } solana-pubkey = { workspace = true, features = ["std"] } @@ -36,9 +51,14 @@ solana-signature = { workspace = true, features = ["std"] } solana-signer = { workspace = true } thiserror = { workspace = true } trezor-client = { workspace = true } +ur-parse-lib = { workspace = true, optional = true } +ur-registry = { workspace = true, optional = true } uriparse = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } serial_test = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } + +[lints] +workspace = true diff --git a/remote-wallet/src/keystone.rs b/remote-wallet/src/keystone.rs new file mode 100644 index 00000000000..ad1ec90a096 --- /dev/null +++ b/remote-wallet/src/keystone.rs @@ -0,0 +1,806 @@ +use { + crate::{ + locator::Manufacturer, + remote_wallet::{RemoteWallet, RemoteWalletError, RemoteWalletInfo}, + }, + console::Emoji, + hex, + semver::Version as FirmwareVersion, + serde_json, + solana_derivation_path::DerivationPath, + solana_pubkey::Pubkey, + solana_signature::Signature, + std::{convert::TryFrom, fmt, time::Duration}, + ur_parse_lib::{keystone_ur_decoder::probe_decode, keystone_ur_encoder::probe_encode}, + ur_registry::{ + crypto_key_path::{CryptoKeyPath, PathComponent}, + extend::{ + crypto_multi_accounts::CryptoMultiAccounts, + key_derivation::KeyDerivationCall, + key_derivation_schema::{Curve, KeyDerivationSchema}, + qr_hardware_call::{CallParams, CallType, HardWareCallVersion, QRHardwareCall}, + }, + solana::{ + sol_sign_request::{SignType, SolSignRequest}, + sol_signature::SolSignature, + }, + traits::RegistryItem, + }, +}; + +static CHECK_MARK: Emoji = Emoji("✅ ", ""); + +const REQUEST_ID: u16 = 0x0000; + +/// Keystone vendor ID +const KEYSTONE_VID: u16 = 0x1209; +/// Keystone product ID +const KEYSTONE_PID: u16 = 0x3001; + +const HID_PACKET_SIZE: usize = 64; +const EAPDU_OFFSET_CLA: usize = 0; +const EAPDU_OFFSET_INS: usize = 1; +const EAPDU_OFFSET_P1: usize = 3; +const EAPDU_OFFSET_P2: usize = 5; +const EAPDU_OFFSET_LC: usize = 7; +const EAPDU_OFFSET_CDATA: usize = 9; +const EAPDU_RESPONSE_STATUS_LEN: usize = 2; +const EAPDU_MAX_REQ_DATA_PER_PACKET: usize = HID_PACKET_SIZE - EAPDU_OFFSET_CDATA; +const EAPDU_SUCCESS_STATUS: u16 = 0x0000; +const EAPDU_EXPORT_ADDRESS_PAGE_STATUS: u16 = 0x0006; +// USB operations are user-interactive and may wait on device screen approval. +// Keystone signing requires user approval on the device, so allow enough time +// for users to review and confirm requests before the USB read/write times out. +const USB_TIMEOUT: Duration = Duration::from_secs(60); +// Use a short timeout so stale packets are drained without delaying new requests. +const DRAIN_TIMEOUT: Duration = Duration::from_millis(20); +// Bound stale packet draining to at most one full-sized response window. +const MAX_DRAIN_PACKETS: usize = 64; +// Maximum UR fragment size; large enough to keep USB requests single-part. +const MAX_UR_FRAGMENT_LEN: usize = 0x0FFF_FFFF; + +// JSON response field names +const JSON_FIELD_PUBKEY: &str = "pubkey"; +const JSON_FIELD_PAYLOAD: &str = "payload"; +const JSON_FIELD_ERROR: &str = "error"; +const JSON_FIELD_FIRMWARE_VERSION: &str = "firmwareVersion"; +const JSON_FIELD_WALLET_MFP: &str = "walletMFP"; + +// Error messages +const ERROR_INVALID_JSON: &str = "Invalid JSON response"; +const ERROR_MISSING_FIELD: &str = "Missing required field"; +const ERROR_INVALID_HEX: &str = "Invalid hex data"; +const ERROR_SIGNATURE_SIZE: &str = "Signature packet size mismatch"; +const ERROR_KEY_SIZE: &str = "Key packet size mismatch"; +const ERROR_EXPORT_ADDRESS_PAGE: &str = "Export address is only allowed on specific pages"; + +// Keystone cached derivation-path ranges for USB pubkey export. +const CACHED_ACCOUNT_RANGE: u32 = 49; +const CACHED_FIXED_CHANGE: u32 = 0; +const SOLANA_COIN_TYPE: u32 = 501; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum CommandType { + CmdEchoTest = 0x01, + CmdResolveUR = 0x02, + CmdCheckLockStatus = 0x03, + CmdExportAddress = 0x04, + CmdGetDeviceInfo = 0x05, + CmdGetDeviceUSBPubkey = 0x06, +} + +#[derive(Clone, Copy)] +struct UsbIo { + interface_number: u8, + setting_number: u8, + endpoint_out: u8, + endpoint_in: u8, + transfer_type: rusb::TransferType, +} + +struct EndpointPair { + output: u8, + input: u8, +} + +struct EapduHeader { + command: u16, + total_packets: u16, + packet_sequence: u16, + request_id: u16, +} + +impl EapduHeader { + fn parse(packet: &[u8]) -> Result { + if packet.len() < EAPDU_OFFSET_CDATA + EAPDU_RESPONSE_STATUS_LEN { + return Err(RemoteWalletError::Protocol("Invalid EAPDU packet size")); + } + + let command = u16::from_be_bytes([packet[EAPDU_OFFSET_INS], packet[EAPDU_OFFSET_INS + 1]]); + let total_packets = + u16::from_be_bytes([packet[EAPDU_OFFSET_P1], packet[EAPDU_OFFSET_P1 + 1]]); + let packet_sequence = + u16::from_be_bytes([packet[EAPDU_OFFSET_P2], packet[EAPDU_OFFSET_P2 + 1]]); + let request_id = u16::from_be_bytes([packet[EAPDU_OFFSET_LC], packet[EAPDU_OFFSET_LC + 1]]); + + if !is_valid_command(command) || total_packets == 0 || packet_sequence >= total_packets { + return Err(RemoteWalletError::Protocol("Unable to parse packet header")); + } + + Ok(Self { + command, + total_packets, + packet_sequence, + request_id, + }) + } +} + +/// Keystone hardware wallet device +pub struct KeystoneWallet { + pub device: rusb::Device, + pub handle: rusb::DeviceHandle, + usb_io: UsbIo, + pub pretty_path: String, + pub version: Option, + pub mfp: Option<[u8; 4]>, +} + +impl fmt::Debug for KeystoneWallet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "KeystoneWallet") + } +} + +impl KeystoneWallet { + pub fn new( + device: rusb::Device, + handle: rusb::DeviceHandle, + ) -> Result { + let usb_io = Self::discover_usb_io(&device)?; + + // Best effort: detach kernel driver where supported. + #[cfg(any(target_os = "linux", target_os = "android"))] + { + if handle + .kernel_driver_active(usb_io.interface_number) + .unwrap_or(false) + { + let _ = handle.detach_kernel_driver(usb_io.interface_number); + } + } + + handle + .claim_interface(usb_io.interface_number) + .map_err(|e| { + RemoteWalletError::Hid(format!( + "Failed to claim USB interface {}: {e}", + usb_io.interface_number + )) + })?; + + Ok(Self { + device, + handle, + usb_io, + pretty_path: String::default(), + version: None, + mfp: None, + }) + } + + fn discover_usb_io(device: &rusb::Device) -> Result { + let config = device + .active_config_descriptor() + .or_else(|_| device.config_descriptor(0)) + .map_err(|e| { + RemoteWalletError::Hid(format!("Failed to read USB config descriptor: {e}")) + })?; + + // Match webusb-cli behavior first: interface 0, alternate setting 0, first IN/OUT endpoints. + for interface in config.interfaces() { + for descriptor in interface.descriptors() { + if descriptor.interface_number() != 0 || descriptor.setting_number() != 0 { + continue; + } + for transfer_type in [rusb::TransferType::Bulk, rusb::TransferType::Interrupt] { + if let Some(endpoints) = find_endpoint_pair(&descriptor, transfer_type) { + return Ok(UsbIo { + interface_number: descriptor.interface_number(), + setting_number: descriptor.setting_number(), + endpoint_out: endpoints.output, + endpoint_in: endpoints.input, + transfer_type, + }); + } + } + } + } + + // Fallback: scan all interfaces/settings, BULK first, then INTERRUPT. + for wanted_type in [rusb::TransferType::Bulk, rusb::TransferType::Interrupt] { + for interface in config.interfaces() { + for descriptor in interface.descriptors() { + if let Some(endpoints) = find_endpoint_pair(&descriptor, wanted_type) { + return Ok(UsbIo { + interface_number: descriptor.interface_number(), + setting_number: descriptor.setting_number(), + endpoint_out: endpoints.output, + endpoint_in: endpoints.input, + transfer_type: wanted_type, + }); + } + } + } + } + + Err(RemoteWalletError::Protocol( + "No suitable USB IN/OUT endpoints found", + )) + } + + /// Write data to device with Keystone USB transport framing + fn write(&self, command: CommandType, data: &[u8]) -> Result<(), RemoteWalletError> { + // Avoid carrying unread responses across requests; stale data can keep + // firmware IN endpoint busy and block subsequent sends. + self.drain_pending_input_packets(); + + let total_packets = std::cmp::max(1, data.len().div_ceil(EAPDU_MAX_REQ_DATA_PER_PACKET)); + + for packet_index in 0..total_packets { + let start = packet_index * EAPDU_MAX_REQ_DATA_PER_PACKET; + let end = std::cmp::min(start + EAPDU_MAX_REQ_DATA_PER_PACKET, data.len()); + let chunk = &data[start..end]; + + let mut eapdu_packet = [0u8; EAPDU_OFFSET_CDATA + EAPDU_MAX_REQ_DATA_PER_PACKET]; + eapdu_packet[EAPDU_OFFSET_CLA] = 0x00; + eapdu_packet[EAPDU_OFFSET_INS..EAPDU_OFFSET_INS + 2] + .copy_from_slice(&(command as u16).to_be_bytes()); + eapdu_packet[EAPDU_OFFSET_P1..EAPDU_OFFSET_P1 + 2] + .copy_from_slice(&(total_packets as u16).to_be_bytes()); + eapdu_packet[EAPDU_OFFSET_P2..EAPDU_OFFSET_P2 + 2] + .copy_from_slice(&(packet_index as u16).to_be_bytes()); + eapdu_packet[EAPDU_OFFSET_LC..EAPDU_OFFSET_LC + 2] + .copy_from_slice(&REQUEST_ID.to_be_bytes()); + eapdu_packet[EAPDU_OFFSET_CDATA..EAPDU_OFFSET_CDATA + chunk.len()] + .copy_from_slice(chunk); + + self.device_write(&eapdu_packet[..EAPDU_OFFSET_CDATA + chunk.len()])?; + } + + Ok(()) + } + + /// Read data from device with Keystone USB transport parsing + fn read(&self) -> Result, RemoteWalletError> { + let mut total_packets: Option = None; + let mut expected_req_id: Option = None; + let mut expected_command: Option = None; + let mut packet_chunks: Vec>> = Vec::new(); + let mut response_status: Option = None; + + loop { + let chunk = self.device_read()?; + if chunk.is_empty() || chunk.iter().all(|b| *b == 0) { + continue; + } + let packet = chunk.as_slice(); + let header = EapduHeader::parse(packet)?; + let packet_payload = &packet[EAPDU_OFFSET_CDATA..]; + if packet_payload.len() < EAPDU_RESPONSE_STATUS_LEN { + return Err(RemoteWalletError::Protocol("EAPDU payload too short")); + } + + let payload_len = packet_payload.len() - EAPDU_RESPONSE_STATUS_LEN; + let status = + u16::from_be_bytes([packet_payload[payload_len], packet_payload[payload_len + 1]]); + + if total_packets.is_none() { + total_packets = Some(header.total_packets); + expected_req_id = Some(header.request_id); + expected_command = Some(header.command); + packet_chunks = vec![None; header.total_packets as usize]; + } + + if total_packets != Some(header.total_packets) + || expected_req_id != Some(header.request_id) + || expected_command != Some(header.command) + { + return Err(RemoteWalletError::Protocol( + "Mismatched EAPDU packet header across fragments", + )); + } + + let idx = header.packet_sequence as usize; + if packet_chunks[idx].is_none() { + packet_chunks[idx] = Some(packet_payload[..payload_len].to_vec()); + } + + if let Some(prev_status) = response_status { + if prev_status != status { + return Err(RemoteWalletError::Protocol( + "Mismatched EAPDU status across fragments", + )); + } + } else { + response_status = Some(status); + } + + if packet_chunks.iter().all(|c| c.is_some()) { + break; + } + } + + let result_len = packet_chunks + .iter() + .map(|chunk| chunk.as_ref().map_or(0, Vec::len)) + .sum(); + let mut result_data = Vec::with_capacity(result_len); + for chunk in packet_chunks { + result_data.extend_from_slice(&chunk.unwrap()); + } + + match response_status { + Some(EAPDU_SUCCESS_STATUS) | None => {} + Some(_) => { + if let Some(payload) = keystone_response_payload(&result_data) { + return Err(RemoteWalletError::KeystoneError(payload)); + } + return Err(RemoteWalletError::Protocol( + "EAPDU returned non-success status", + )); + } + } + + Ok(result_data) + } + + /// Send APDU command and receive JSON response + fn send_apdu(&self, command: CommandType, data: &[u8]) -> Result { + self.write(command, data)?; + let message = self.read()?; + let message_str = String::from_utf8_lossy(&message); + + // Extract JSON from response + if let (Some(start), Some(end)) = (message_str.find('{'), message_str.rfind('}')) + && start < end + { + let json_str = &message_str[start..=end]; + return Ok(json_str.to_string()); + } + + Ok(message_str.to_string()) + } + + /// Get device firmware version and master fingerprint + fn get_device_info(&self) -> Result<(FirmwareVersion, Option<[u8; 4]>), RemoteWalletError> { + let json_str = self.send_apdu(CommandType::CmdGetDeviceInfo, &[])?; + let json = serde_json::from_str::(&json_str) + .map_err(|_| RemoteWalletError::Protocol(ERROR_INVALID_JSON))?; + + if let Some(device_error) = json.get(JSON_FIELD_ERROR).and_then(|v| v.as_str()) + && !device_error.trim().is_empty() + { + return Err(RemoteWalletError::KeystoneError(device_error.to_string())); + } + + // Parse firmware version + let version_str = json + .get(JSON_FIELD_FIRMWARE_VERSION) + .and_then(|v| v.as_str()) + .ok_or(RemoteWalletError::Protocol(ERROR_MISSING_FIELD))?; + + let version = FirmwareVersion::parse(version_str) + .map_err(|_| RemoteWalletError::Protocol("Invalid firmware version"))?; + + // Parse master fingerprint (MFP) + let mfp = json + .get(JSON_FIELD_WALLET_MFP) + .and_then(|v| v.as_str()) + .and_then(|hex_str| { + let bytes = hex::decode(hex_str).ok()?; + bytes.try_into().ok() + }); + + Ok((version, mfp)) + } + + /// Generate UR-encoded key derivation request for QR code display + fn generate_hardware_call( + &self, + derivation_path: &DerivationPath, + ) -> Result { + let key_path = parse_crypto_key_path(derivation_path, self.mfp); + let schema = KeyDerivationSchema::new(key_path, Some(Curve::Ed25519), None, None); + let schemas = vec![schema]; + let call = QRHardwareCall::new( + CallType::KeyDerivation, + CallParams::KeyDerivation(KeyDerivationCall::new(schemas)), + None, + HardWareCallVersion::V1, + ); + + let bytes: Vec = call + .try_into() + .map_err(|_| RemoteWalletError::Protocol("Failed to encode QR call"))?; + + let encoded = probe_encode( + &bytes, + MAX_UR_FRAGMENT_LEN, + QRHardwareCall::get_registry_type().get_type(), + ) + .map_err(|_| RemoteWalletError::Protocol("Failed to encode UR"))?; + + Ok(encoded.data) + } + + /// Generate UR-encoded sign request for transaction signing + fn generate_sol_sign_request( + &self, + derivation_path: &DerivationPath, + sign_data: &[u8], + ) -> Result { + let crypto_key_path = parse_crypto_key_path(derivation_path, self.mfp); + let request_id = [0u8; 16].to_vec(); + let sol_sign_request = SolSignRequest::new( + Some(request_id), + sign_data.to_vec(), + crypto_key_path, + None, + Some("solana cli".to_string()), + SignType::Transaction, + ); + + let bytes: Vec = sol_sign_request + .try_into() + .map_err(|_| RemoteWalletError::Protocol("Failed to encode sign request"))?; + + let encoded = probe_encode( + &bytes, + MAX_UR_FRAGMENT_LEN, + SolSignRequest::get_registry_type().get_type(), + ) + .map_err(|_| RemoteWalletError::Protocol("Failed to encode UR"))?; + + Ok(encoded.data) + } + + /// Low-level USB write to device + fn device_write(&self, data: &[u8]) -> Result<(), RemoteWalletError> { + match self.usb_io.transfer_type { + rusb::TransferType::Interrupt => self + .handle + .write_interrupt(self.usb_io.endpoint_out, data, USB_TIMEOUT) + .map_err(|e| RemoteWalletError::Hid(format!("USB write failed: {e}")))?, + rusb::TransferType::Bulk => self + .handle + .write_bulk(self.usb_io.endpoint_out, data, USB_TIMEOUT) + .map_err(|e| RemoteWalletError::Hid(format!("USB write failed: {e}")))?, + _ => { + return Err(RemoteWalletError::Protocol( + "Unsupported USB transfer type for write", + )); + } + }; + + Ok(()) + } + + fn drain_pending_input_packets(&self) { + for _ in 0..MAX_DRAIN_PACKETS { + match self.device_read_raw(DRAIN_TIMEOUT) { + Ok(chunk) => { + if chunk.is_empty() { + break; + } + } + Err(rusb::Error::Timeout) => break, + Err(rusb::Error::NotSupported) => break, + Err(_) => break, + } + } + } + + fn device_read_raw(&self, timeout: std::time::Duration) -> Result, rusb::Error> { + let mut buf = vec![0u8; HID_PACKET_SIZE]; + + let bytes_read = match self.usb_io.transfer_type { + rusb::TransferType::Interrupt => { + self.handle + .read_interrupt(self.usb_io.endpoint_in, &mut buf, timeout)? + } + rusb::TransferType::Bulk => { + self.handle + .read_bulk(self.usb_io.endpoint_in, &mut buf, timeout)? + } + _ => return Err(rusb::Error::NotSupported), + }; + + buf.truncate(bytes_read); + Ok(buf) + } + + /// Low-level USB read from device + fn device_read(&self) -> Result, RemoteWalletError> { + self.device_read_raw(USB_TIMEOUT).map_err(|e| match e { + rusb::Error::NotSupported => { + RemoteWalletError::Protocol("Unsupported USB transfer type for read") + } + _ => RemoteWalletError::Hid(format!("USB read failed: {e}")), + }) + } +} + +fn find_endpoint_pair( + descriptor: &rusb::InterfaceDescriptor<'_>, + transfer_type: rusb::TransferType, +) -> Option { + let mut endpoint_out = None; + let mut endpoint_in = None; + for ep in descriptor.endpoint_descriptors() { + if ep.transfer_type() != transfer_type { + continue; + } + match ep.direction() { + rusb::Direction::Out => { + endpoint_out.get_or_insert(ep.address()); + } + rusb::Direction::In => { + endpoint_in.get_or_insert(ep.address()); + } + } + } + match (endpoint_out, endpoint_in) { + (Some(output), Some(input)) => Some(EndpointPair { output, input }), + _ => None, + } +} + +fn is_valid_command(value: u16) -> bool { + matches!(value, 0x01..=0x06) +} + +fn keystone_response_payload(data: &[u8]) -> Option { + let message = String::from_utf8_lossy(data); + if let Ok(json) = serde_json::from_str::(&message) { + return json + .get(JSON_FIELD_PAYLOAD) + .and_then(|payload| payload.as_str()) + .map(str::to_string); + } + + (!message.trim().is_empty()).then(|| message.to_string()) +} + +fn parse_ur_pubkey(ur: &str) -> Result, RemoteWalletError> { + let result: ur_parse_lib::keystone_ur_decoder::URParseResult = + probe_decode(ur.to_lowercase()) + .map_err(|_| RemoteWalletError::Protocol("Failed to decode UR pubkey"))?; + + result + .data + .ok_or(RemoteWalletError::Protocol("No pubkey in response"))? + .get_keys() + .first() + .ok_or(RemoteWalletError::Protocol("Empty pubkey list")) + .map(|key| key.get_key()) +} + +fn parse_ur_signature(ur: &str) -> Result, RemoteWalletError> { + let result: ur_parse_lib::keystone_ur_decoder::URParseResult = + probe_decode(ur.to_lowercase()) + .map_err(|_| RemoteWalletError::Protocol("Failed to decode UR signature"))?; + + Ok(result + .data + .ok_or(RemoteWalletError::Protocol("No signature in response"))? + .get_signature() + .as_slice() + .to_vec()) +} + +/// Parse JSON field from response. +fn parse_json_field(json_str: &str, field_name: &str) -> Result { + let json = serde_json::from_str::(json_str) + .map_err(|_| RemoteWalletError::Protocol(ERROR_INVALID_JSON))?; + + if let Some(device_error) = json.get(JSON_FIELD_ERROR).and_then(|v| v.as_str()) + && !device_error.trim().is_empty() + { + return Err(RemoteWalletError::KeystoneError(device_error.to_string())); + } + + json.get(field_name) + .and_then(|v| v.as_str()) + .ok_or(RemoteWalletError::Protocol(ERROR_MISSING_FIELD)) + .map(String::from) +} + +impl RemoteWallet> for KeystoneWallet { + fn name(&self) -> &str { + "Keystone hardware wallet" + } + + fn read_device( + &mut self, + _dev_info: &rusb::Device, + ) -> Result { + // Get device info (firmware version and MFP) + let (version, mfp) = self.get_device_info()?; + self.version = Some(version); + self.mfp = mfp; + + // Get device descriptor for model and serial + let device_descriptor = self + .device + .device_descriptor() + .map_err(|e| RemoteWalletError::Hid(format!("Failed to get device descriptor: {e}")))?; + + let model = format!( + "Keystone {:04x}:{:04x}", + device_descriptor.vendor_id(), + device_descriptor.product_id() + ); + + let serial = self + .handle + .read_serial_number_string_ascii(&device_descriptor) + .unwrap_or_else(|_| "Unknown".to_string()); + + // Try to get default pubkey + let default_path = DerivationPath::new_bip44(Some(0), Some(0)); + let pubkey_result = self.get_pubkey(&default_path, false); + let (pubkey, error) = match pubkey_result { + Ok(pubkey) => (pubkey, None), + Err(err) => (Pubkey::default(), Some(err)), + }; + + let mut info = RemoteWalletInfo { + model, + manufacturer: Manufacturer::Keystone, + serial, + host_device_path: String::new(), + pubkey, + error, + }; + info.host_device_path = info.get_pretty_path(); + + Ok(info) + } + + fn get_pubkey( + &self, + derivation_path: &DerivationPath, + _confirm_key: bool, + ) -> Result { + let use_cached_usb_pubkey = is_path_in_cached_range(derivation_path); + + let pubkey_bytes = if use_cached_usb_pubkey { + let serialized_path = extend_and_serialize(derivation_path); + let json_response = + self.send_apdu(CommandType::CmdGetDeviceUSBPubkey, &serialized_path)?; + let pubkey_hex = parse_json_field(&json_response, JSON_FIELD_PUBKEY) + .or_else(|_| parse_json_field(&json_response, JSON_FIELD_PAYLOAD))?; + hex::decode(pubkey_hex).map_err(|_| RemoteWalletError::Protocol(ERROR_INVALID_HEX))? + } else { + let ur_request = self.generate_hardware_call(derivation_path)?; + let json_response = self.send_apdu(CommandType::CmdResolveUR, ur_request.as_bytes())?; + let pubkey_ur = parse_json_field(&json_response, JSON_FIELD_PAYLOAD)?; + if pubkey_ur.trim().is_empty() { + return Err(RemoteWalletError::Protocol( + "CmdResolveUR returned empty payload", + )); + } + parse_ur_pubkey(&pubkey_ur)? + }; + + Pubkey::try_from(pubkey_bytes).map_err(|_| RemoteWalletError::Protocol(ERROR_KEY_SIZE)) + } + + fn sign_message( + &self, + derivation_path: &DerivationPath, + data: &[u8], + ) -> Result { + let ur_request = self.generate_sol_sign_request(derivation_path, data)?; + + println!( + "Waiting for your approval on {} {}", + self.name(), + self.pretty_path + ); + + let json_response = self.send_apdu(CommandType::CmdResolveUR, ur_request.as_bytes())?; + + let signature_ur = parse_json_field(&json_response, JSON_FIELD_PAYLOAD)?; + let signature_bytes = parse_ur_signature(&signature_ur)?; + println!("{CHECK_MARK}Approved"); + + Signature::try_from(signature_bytes) + .map_err(|_| RemoteWalletError::Protocol(ERROR_SIGNATURE_SIZE)) + } + + fn sign_offchain_message( + &self, + derivation_path: &DerivationPath, + message: &[u8], + ) -> Result { + self.sign_message(derivation_path, message) + } +} + +/// Check if device is a Keystone +pub fn is_valid_keystone(vendor_id: u16, product_id: u16) -> bool { + vendor_id == KEYSTONE_VID && product_id == KEYSTONE_PID +} + +fn extend_and_serialize(derivation_path: &DerivationPath) -> Vec { + let path = derivation_path.path(); + // Firmware expects: [coin_type(4 bytes, BE)] [depth(1 byte)] [path components(4 bytes each, BE)] + let mut serialized = Vec::with_capacity(4 + 1 + path.len() * 4); + serialized.extend_from_slice(&SOLANA_COIN_TYPE.to_be_bytes()); + serialized.push(path.len() as u8); + for index in path { + serialized.extend_from_slice(&index.to_bits().to_be_bytes()); + } + serialized +} + +/// Check whether a derivation path can be served by Keystone's pre-cached usb pubkey range. +/// 44'/501' +/// 44'/501'/0' ... 44'/501'/49' +/// 44'/501'/0'/0 ... 44'/501'/49'/0 +fn is_path_in_cached_range(derivation_path: &DerivationPath) -> bool { + let path = derivation_path.path(); + if path.len() < 2 { + return false; + } + + let purpose = path[0].to_bits() & 0x7fff_ffff; + let coin = path[1].to_bits() & 0x7fff_ffff; + if purpose != 44 || coin != 501 { + return false; + } + + match path.len() { + 2 => true, // m/44'/501' + 3 => { + let account = path[2].to_bits() & 0x7fff_ffff; + account <= CACHED_ACCOUNT_RANGE + } + 4 => { + let account = path[2].to_bits() & 0x7fff_ffff; + let change = path[3].to_bits() & 0x7fff_ffff; + change == CACHED_FIXED_CHANGE && account <= CACHED_ACCOUNT_RANGE + } + _ => false, + } +} + +/// Parse derivation path into CryptoKeyPath for UR encoding +fn parse_crypto_key_path(derivation_path: &DerivationPath, mfp: Option<[u8; 4]>) -> CryptoKeyPath { + let mut path_components = Vec::new(); + + for index in derivation_path.path() { + let bits = index.to_bits(); + let hardened = (bits & 0x8000_0000) != 0; + let value = bits & 0x7fff_ffff; + if let Ok(component) = PathComponent::new(Some(value), hardened) { + path_components.push(component); + } + } + + if path_components.is_empty() { + if let Ok(component) = PathComponent::new(Some(44u32), true) { + path_components.push(component); + } + if let Ok(component) = PathComponent::new(Some(501u32), true) { + path_components.push(component); + } + if let Ok(component) = PathComponent::new(Some(0u32), true) { + path_components.push(component); + } + } + + CryptoKeyPath::new(path_components, mfp, None) +} diff --git a/remote-wallet/src/lib.rs b/remote-wallet/src/lib.rs index 300f619b57e..ae6088d5ec5 100644 --- a/remote-wallet/src/lib.rs +++ b/remote-wallet/src/lib.rs @@ -1,6 +1,8 @@ #![cfg(feature = "agave-unstable-api")] #![allow(clippy::arithmetic_side_effects)] #![allow(dead_code)] +#[cfg(feature = "keystone")] +pub mod keystone; pub mod ledger; pub mod ledger_error; pub mod locator; diff --git a/remote-wallet/src/locator.rs b/remote-wallet/src/locator.rs index aa299ea54ac..cfbb19eb0b0 100644 --- a/remote-wallet/src/locator.rs +++ b/remote-wallet/src/locator.rs @@ -14,11 +14,15 @@ pub enum Manufacturer { Unknown, Ledger, Trezor, + #[cfg(feature = "keystone")] + Keystone, } const MANUFACTURER_UNKNOWN: &str = "unknown"; const MANUFACTURER_LEDGER: &str = "ledger"; const MANUFACTURER_TREZOR: &str = "trezor"; +#[cfg(feature = "keystone")] +const MANUFACTURER_KEYSTONE: &str = "keystone"; #[derive(Clone, Debug, Error, PartialEq, Eq)] #[error("not a manufacturer")] @@ -37,6 +41,8 @@ impl FromStr for Manufacturer { match s.as_str() { MANUFACTURER_LEDGER => Ok(Self::Ledger), MANUFACTURER_TREZOR => Ok(Self::Trezor), + #[cfg(feature = "keystone")] + MANUFACTURER_KEYSTONE => Ok(Self::Keystone), _ => Err(ManufacturerError), } } @@ -55,6 +61,8 @@ impl AsRef for Manufacturer { Self::Unknown => MANUFACTURER_UNKNOWN, Self::Ledger => MANUFACTURER_LEDGER, Self::Trezor => MANUFACTURER_TREZOR, + #[cfg(feature = "keystone")] + Self::Keystone => MANUFACTURER_KEYSTONE, } } } diff --git a/remote-wallet/src/remote_keypair.rs b/remote-wallet/src/remote_keypair.rs index 5cce82980ff..9b8e1fb266c 100644 --- a/remote-wallet/src/remote_keypair.rs +++ b/remote-wallet/src/remote_keypair.rs @@ -3,8 +3,7 @@ use { ledger::get_wallet_from_info, locator::Locator, remote_wallet::{ - RemoteWallet, RemoteWalletError, RemoteWalletInfo, RemoteWalletManager, - RemoteWalletType, + RemoteWalletError, RemoteWalletInfo, RemoteWalletManager, RemoteWalletType, }, }, solana_derivation_path::DerivationPath, @@ -27,10 +26,7 @@ impl RemoteKeypair { confirm_key: bool, path: String, ) -> Result { - let pubkey = match &wallet_type { - RemoteWalletType::Ledger(wallet) => wallet.get_pubkey(&derivation_path, confirm_key)?, - RemoteWalletType::Trezor(wallet) => wallet.get_pubkey(&derivation_path, confirm_key)?, - }; + let pubkey = wallet_type.get_pubkey(&derivation_path, confirm_key)?; Ok(Self { wallet_type, @@ -47,14 +43,9 @@ impl Signer for RemoteKeypair { } fn try_sign_message(&self, message: &[u8]) -> Result { - match &self.wallet_type { - RemoteWalletType::Ledger(wallet) => wallet - .sign_message(&self.derivation_path, message) - .map_err(|e| e.into()), - RemoteWalletType::Trezor(wallet) => wallet - .sign_message(&self.derivation_path, message) - .map_err(|e| e.into()), - } + self.wallet_type + .sign_message(&self.derivation_path, message) + .map_err(|e| e.into()) } fn is_interactive(&self) -> bool { @@ -70,12 +61,16 @@ pub fn generate_remote_keypair( keypair_name: &str, ) -> Result { let remote_wallet_info = RemoteWalletInfo::parse_locator(locator); + let remote_wallet = get_wallet_from_info(remote_wallet_info, keypair_name, wallet_manager)?; let path = format!("{}{}", remote_wallet.path, derivation_path.get_query()); - RemoteKeypair::new( - remote_wallet.wallet_type, + let wallet_type = remote_wallet.wallet_type; + let pubkey = wallet_type.get_pubkey(&derivation_path, confirm_key)?; + + Ok(RemoteKeypair { + wallet_type, derivation_path, - confirm_key, + pubkey, path, - ) + }) } diff --git a/remote-wallet/src/remote_wallet.rs b/remote-wallet/src/remote_wallet.rs index 66f838134dc..6494bab72be 100644 --- a/remote-wallet/src/remote_wallet.rs +++ b/remote-wallet/src/remote_wallet.rs @@ -1,3 +1,7 @@ +#[cfg(feature = "keystone")] +use crate::keystone::KeystoneWallet; +#[cfg(all(feature = "hidapi", feature = "keystone"))] +use rusb::UsbContext; #[cfg(feature = "hidapi")] use {crate::ledger::is_valid_ledger, parking_lot::Mutex, std::sync::Arc}; use { @@ -60,6 +64,10 @@ pub enum RemoteWalletError { #[error("trezor error: {0}")] TrezorError(String), + #[cfg(feature = "keystone")] + #[error("keystone error: {0}")] + KeystoneError(String), + #[error("remote wallet operation rejected by the user")] UserCancel, @@ -83,6 +91,8 @@ impl From for SignerError { RemoteWalletError::InvalidInput(input) => SignerError::InvalidInput(input), RemoteWalletError::LedgerError(e) => SignerError::Protocol(e.to_string()), RemoteWalletError::TrezorError(e) => SignerError::Protocol(e), + #[cfg(feature = "keystone")] + RemoteWalletError::KeystoneError(e) => SignerError::Protocol(e), RemoteWalletError::NoDeviceFound => SignerError::NoDeviceFound, RemoteWalletError::Protocol(e) => SignerError::Protocol(e.to_string()), RemoteWalletError::UserCancel => { @@ -122,12 +132,31 @@ impl RemoteWalletManager { pub fn update_devices(&self) -> Result { let mut usb = self.usb.lock(); usb.refresh_devices()?; - let devices = usb.device_list(); let num_prev_devices = self.devices.read().len(); let mut detected_devices = vec![]; let mut errors = vec![]; - for device_info in devices.filter(|&device_info| { + Self::scan_ledger_devices(&usb, &mut detected_devices, &mut errors); + Self::scan_trezor_devices(&mut detected_devices, &mut errors); + #[cfg(feature = "keystone")] + Self::scan_keystone_devices(&mut detected_devices, &mut errors); + let num_curr_devices = detected_devices.len(); + *self.devices.write() = detected_devices; + + if num_curr_devices == 0 && !errors.is_empty() { + return Err(errors[0].clone()); + } + + Ok(num_curr_devices - num_prev_devices) + } + + #[cfg(feature = "hidapi")] + fn scan_ledger_devices( + usb: &hidapi::HidApi, + detected_devices: &mut Vec, + errors: &mut Vec, + ) { + for device_info in usb.device_list().filter(|&device_info| { #[cfg(not(any(feature = "linux-static-libusb", feature = "linux-shared-libusb")))] let is_valid_hid_device = is_valid_hid_device(device_info.usage_page(), device_info.interface_number()); @@ -160,7 +189,13 @@ impl RemoteWalletManager { Err(err) => error!("Error connecting to ledger device to read info: {err}"), } } + } + #[cfg(feature = "hidapi")] + fn scan_trezor_devices( + detected_devices: &mut Vec, + errors: &mut Vec, + ) { for device in trezor_client::find_devices(false) { let mut trezor = match device.connect() { Ok(t) => t, @@ -189,14 +224,66 @@ impl RemoteWalletManager { wallet_type: RemoteWalletType::Trezor(Rc::new(wallet)), }); } - let num_curr_devices = detected_devices.len(); - *self.devices.write() = detected_devices; + } - if num_curr_devices == 0 && !errors.is_empty() { - return Err(errors[0].clone()); - } + #[cfg(all(feature = "hidapi", feature = "keystone"))] + fn scan_keystone_devices( + detected_devices: &mut Vec, + errors: &mut Vec, + ) { + let Ok(context) = rusb::Context::new() else { + return; + }; + let Ok(device_list) = context.devices() else { + return; + }; - Ok(num_curr_devices - num_prev_devices) + for device in device_list.iter() { + let Ok(desc) = device.device_descriptor() else { + continue; + }; + // Some firmware modes may expose a different PID; use VID-based prefilter, + // then still prefer known Keystone VID/PID path. + if !crate::keystone::is_valid_keystone(desc.vendor_id(), desc.product_id()) { + continue; + } + + let handle = match device.open() { + Ok(handle) => handle, + Err(err) => { + error!("Failed to open Keystone device: {err}"); + errors.push(RemoteWalletError::Hid(format!( + "Failed to open Keystone device {:04x}:{:04x}: {err}", + desc.vendor_id(), + desc.product_id() + ))); + continue; + } + }; + let mut keystone = match KeystoneWallet::new(device.clone(), handle) { + Ok(keystone) => keystone, + Err(err) => { + error!("Error initializing Keystone USB transport: {err}"); + errors.push(err); + continue; + } + }; + match keystone.read_device(&device) { + Ok(info) => { + keystone.pretty_path = info.get_pretty_path(); + trace!("Found Keystone device: {info:?}"); + detected_devices.push(Device { + path: info.host_device_path.clone(), + info, + wallet_type: RemoteWalletType::Keystone(Rc::new(keystone)), + }) + } + Err(err) => { + error!("Error connecting to Keystone device: {err}"); + errors.push(err); + } + } + } } #[cfg(not(feature = "hidapi"))] @@ -299,6 +386,36 @@ pub struct Device { pub enum RemoteWalletType { Ledger(Rc), Trezor(Rc), + #[cfg(feature = "keystone")] + Keystone(Rc), +} + +impl RemoteWalletType { + pub fn get_pubkey( + &self, + derivation_path: &DerivationPath, + confirm_key: bool, + ) -> Result { + match self { + Self::Ledger(wallet) => wallet.get_pubkey(derivation_path, confirm_key), + Self::Trezor(wallet) => wallet.get_pubkey(derivation_path, confirm_key), + #[cfg(feature = "keystone")] + Self::Keystone(wallet) => wallet.get_pubkey(derivation_path, confirm_key), + } + } + + pub fn sign_message( + &self, + derivation_path: &DerivationPath, + message: &[u8], + ) -> Result { + match self { + Self::Ledger(wallet) => wallet.sign_message(derivation_path, message), + Self::Trezor(wallet) => wallet.sign_message(derivation_path, message), + #[cfg(feature = "keystone")] + Self::Keystone(wallet) => wallet.sign_message(derivation_path, message), + } + } } /// Remote wallet information. diff --git a/reserved-account-keys/src/lib.rs b/reserved-account-keys/src/lib.rs index 49fdae78d6a..0f9cf44021b 100644 --- a/reserved-account-keys/src/lib.rs +++ b/reserved-account-keys/src/lib.rs @@ -3,7 +3,7 @@ //! New reserved account keys may be added as long as they specify a feature //! gate that transitions the key into read-only at an epoch boundary. #![cfg_attr(feature = "frozen-abi", feature(min_specialization))] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] use { agave_feature_set::FeatureSet, solana_pubkey::Pubkey, diff --git a/rpc-client-api/Cargo.toml b/rpc-client-api/Cargo.toml index 1605b8ac2bf..54fbe96d175 100644 --- a/rpc-client-api/Cargo.toml +++ b/rpc-client-api/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -31,3 +33,6 @@ thiserror = { workspace = true } [dev-dependencies] test-case = { workspace = true } + +[lints] +workspace = true diff --git a/rpc-client-api/src/custom_error.rs b/rpc-client-api/src/custom_error.rs index 620ae497268..7f39d6c7a43 100644 --- a/rpc-client-api/src/custom_error.rs +++ b/rpc-client-api/src/custom_error.rs @@ -29,6 +29,7 @@ pub const JSON_RPC_SERVER_ERROR_EPOCH_REWARDS_PERIOD_ACTIVE: i64 = -32017; pub const JSON_RPC_SERVER_ERROR_SLOT_NOT_EPOCH_BOUNDARY: i64 = -32018; pub const JSON_RPC_SERVER_ERROR_LONG_TERM_STORAGE_UNREACHABLE: i64 = -32019; pub const JSON_RPC_SERVER_ERROR_FILTER_TRANSACTION_NOT_FOUND: i64 = -32020; +pub const JSON_RPC_SERVER_ERROR_NO_SLOT_HISTORY: i64 = -32021; #[derive(Error, Debug)] #[allow(clippy::large_enum_variant)] @@ -83,6 +84,8 @@ pub enum RpcCustomError { LongTermStorageUnreachable, #[error("FilterTransactionNotFound")] FilterTransactionNotFound { signature: String }, + #[error("NoSlotHistory")] + NoSlotHistory, } #[derive(Debug, Serialize, Deserialize)] @@ -267,6 +270,11 @@ impl From for Error { message: format!("Transaction {signature} not found"), data: None, }, + RpcCustomError::NoSlotHistory => Self { + code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_NO_SLOT_HISTORY), + message: "No slot history".to_string(), + data: None, + }, } } } diff --git a/rpc-client-nonce-utils/Cargo.toml b/rpc-client-nonce-utils/Cargo.toml index ebc3b4ec388..4b0a16a51c9 100644 --- a/rpc-client-nonce-utils/Cargo.toml +++ b/rpc-client-nonce-utils/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] default = [] @@ -43,3 +45,6 @@ solana-signer = { workspace = true } solana-system-interface = { workspace = true } solana-transaction = { workspace = true } tokio = { workspace = true, features = ["full"] } + +[lints] +workspace = true diff --git a/rpc-client-types/Cargo.toml b/rpc-client-types/Cargo.toml index 5b9b13cee73..a5f0d7813a3 100644 --- a/rpc-client-types/Cargo.toml +++ b/rpc-client-types/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -38,3 +40,6 @@ thiserror = { workspace = true } const_format = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } solana-signature = { workspace = true } + +[lints] +workspace = true diff --git a/rpc-client-types/src/request.rs b/rpc-client-types/src/request.rs index 6593dff3e25..439ebb7de9c 100644 --- a/rpc-client-types/src/request.rs +++ b/rpc-client-types/src/request.rs @@ -48,6 +48,7 @@ pub enum RpcRequest { GetSlot, GetSlotLeader, GetSlotLeaders, + GetAgGenesisCert, GetStorageTurn, GetStorageTurnRate, GetSlotsPerSegment, @@ -114,6 +115,7 @@ impl RpcRequest { RpcRequest::GetSlot => "getSlot", RpcRequest::GetSlotLeader => "getSlotLeader", RpcRequest::GetSlotLeaders => "getSlotLeaders", + RpcRequest::GetAgGenesisCert => "getAgGenesisCert", RpcRequest::GetStakeMinimumDelegation => "getStakeMinimumDelegation", RpcRequest::GetStorageTurn => "getStorageTurn", RpcRequest::GetStorageTurnRate => "getStorageTurnRate", diff --git a/rpc-client/Cargo.toml b/rpc-client/Cargo.toml index e73d25aea09..f228d96f0f6 100644 --- a/rpc-client/Cargo.toml +++ b/rpc-client/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] default = ["spinner"] @@ -20,6 +22,7 @@ agave-unstable-api = [] spinner = ["dep:indicatif"] [dependencies] +agave-votor-messages = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } bincode = { workspace = true } @@ -35,6 +38,7 @@ serde_json = { workspace = true } solana-account = { workspace = true } solana-account-decoder = { workspace = true } solana-account-decoder-client-types = { workspace = true, features = ["zstd"] } +solana-bls-signatures = { workspace = true } solana-clock = { workspace = true } solana-commitment-config = { workspace = true } solana-epoch-info = { workspace = true, features = ["serde"] } @@ -52,6 +56,7 @@ solana-transaction-status-client-types = { workspace = true } solana-version = { workspace = true } solana-vote-interface = { workspace = true } tokio = { workspace = true, features = ["full"] } +wincode = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } @@ -64,6 +69,10 @@ solana-hash = { workspace = true, features = ["atomic"] } solana-keypair = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } solana-signer = { workspace = true } +solana-system-interface = { workspace = true, features = ["bincode"] } solana-system-transaction = { workspace = true } static_assertions = { workspace = true } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/rpc-client/src/http_sender.rs b/rpc-client/src/http_sender.rs index a75e8f7297a..29ea499d152 100644 --- a/rpc-client/src/http_sender.rs +++ b/rpc-client/src/http_sender.rs @@ -161,14 +161,12 @@ impl RpcSender for HttpSender { && too_many_requests_retries > 0 { let mut duration = Duration::from_millis(500); - if let Some(retry_after) = response.headers().get(RETRY_AFTER) { - if let Ok(retry_after) = retry_after.to_str() { - if let Ok(retry_after) = retry_after.parse::() { - if retry_after < 120 { - duration = Duration::from_secs(retry_after); - } - } - } + if let Some(retry_after) = response.headers().get(RETRY_AFTER) + && let Ok(retry_after) = retry_after.to_str() + && let Ok(retry_after) = retry_after.parse::() + && retry_after < 120 + { + duration = Duration::from_secs(retry_after); } too_many_requests_retries -= 1; diff --git a/rpc-client/src/mock_sender.rs b/rpc-client/src/mock_sender.rs index 8d5db296f5e..d44e957f6ee 100644 --- a/rpc-client/src/mock_sender.rs +++ b/rpc-client/src/mock_sender.rs @@ -2,15 +2,21 @@ use { crate::rpc_sender::*, + agave_votor_messages::{ + consensus_message::Block, + wire::{WireBlockCertMessage, WireCertSignature}, + }, async_trait::async_trait, base64::{Engine, prelude::BASE64_STANDARD}, serde_json::{Number, Value, json}, solana_account_decoder_client_types::{ UiAccount, UiAccountData, UiAccountEncoding, token::UiTokenAmount, }, + solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_clock::{Slot, UnixTimestamp}, solana_epoch_info::EpochInfo, solana_epoch_schedule::EpochSchedule, + solana_hash::Hash, solana_instruction::{TRANSACTION_LEVEL_STACK_HEIGHT, error::InstructionError}, solana_message::MessageHeader, solana_pubkey::Pubkey, @@ -28,7 +34,7 @@ use { }, }, solana_signature::Signature, - solana_transaction::{Transaction, versioned::TransactionVersion}, + solana_transaction::versioned::{TransactionVersion, VersionedTransaction}, solana_transaction_error::{TransactionError, TransactionResult}, solana_transaction_status_client_types::{ EncodedConfirmedBlock, EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, @@ -171,6 +177,16 @@ impl RpcSender for MockSender { block_height: 34, transaction_count: Some(123), })?, + "getAgGenesisCert" => { + let cert = WireBlockCertMessage { + block: Block { slot: 0, block_id: Hash::default() }, + signature: WireCertSignature { + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + } + }; + serde_json::to_value(Some(cert))? + } "getSignatureStatuses" => { let status: TransactionResult<()> = if self.url == "account_in_use" { Err(TransactionError::AccountInUse) @@ -350,7 +366,7 @@ impl RpcSender for MockSender { } else { let tx_str = params.as_array().unwrap()[0].as_str().unwrap().to_string(); let data = BASE64_STANDARD.decode(tx_str).unwrap(); - let tx: Transaction = bincode::deserialize(&data).unwrap(); + let tx: VersionedTransaction = wincode::deserialize(&data).unwrap(); tx.signatures[0].to_string() }; Value::String(signature) diff --git a/rpc-client/src/nonblocking/rpc_client.rs b/rpc-client/src/nonblocking/rpc_client.rs index 19077bffba7..c710e217d9d 100644 --- a/rpc-client/src/nonblocking/rpc_client.rs +++ b/rpc-client/src/nonblocking/rpc_client.rs @@ -19,8 +19,8 @@ use { }, rpc_sender::*, }, + agave_votor_messages::wire::WireBlockCertMessage, base64::{Engine, prelude::BASE64_STANDARD}, - bincode::serialize, futures::join, log::*, serde_json::{Value, json}, @@ -56,6 +56,7 @@ use { time::{Duration, Instant}, }, tokio::time::sleep, + wincode::{SchemaWrite, config::DefaultConfig}, }; /// A client of a remote Solana node. @@ -2096,6 +2097,30 @@ impl RpcClient { }) } + /// Returns Alpenglow's genesis certificate. + /// + /// # RPC Reference + /// + /// This method corresponds directly to the [`getAgGenesisCert`] RPC method. + /// + /// [`getAgGenesisCert`]: https://solana.com/docs/rpc/http/getaggenesiscert + /// + /// # Examples + /// + /// ``` + /// # use solana_rpc_client_api::client_error::Error; + /// # use solana_rpc_client::nonblocking::rpc_client::RpcClient; + /// # futures::executor::block_on(async { + /// # let rpc_client = RpcClient::new_mock("succeeds".to_string()); + /// let cert = rpc_client.get_ag_genesis_cert().await?; + /// # Ok::<(), Error>(()) + /// # })?; + /// # Ok::<(), Error>(()) + /// ``` + pub async fn get_ag_genesis_cert(&self) -> ClientResult> { + self.send(RpcRequest::GetAgGenesisCert, Value::Null).await + } + /// Get block production for the current epoch. /// /// # RPC Reference @@ -4383,16 +4408,16 @@ impl RpcClient { } = serde_json::from_value::>>(result_json)?; trace!("Response account {pubkey:?} {rpc_account:?}"); let response = { - if let Some(rpc_account) = rpc_account { - if let UiAccountData::Json(account_data) = rpc_account.data { - let token_account_type: TokenAccountType = - serde_json::from_value(account_data.parsed)?; - if let TokenAccountType::Account(token_account) = token_account_type { - return Ok(Response { - context, - value: Some(token_account), - }); - } + if let Some(rpc_account) = rpc_account + && let UiAccountData::Json(account_data) = rpc_account.data + { + let token_account_type: TokenAccountType = + serde_json::from_value(account_data.parsed)?; + if let TokenAccountType::Account(token_account) = token_account_type { + return Ok(Response { + context, + value: Some(token_account), + }); } } Err(Into::::into(RpcError::ForUser(format!( @@ -4786,10 +4811,9 @@ impl RpcClient { "wait_for_balance_with_commitment [{run}] {balance_result:?} {expected_balance:?}" ); if let (Some(expected_balance), Ok(balance_result)) = (expected_balance, balance_result) + && expected_balance == balance_result { - if expected_balance == balance_result { - return Ok(balance_result); - } + return Ok(balance_result); } run += 1; } @@ -4942,20 +4966,46 @@ impl RpcClient { &self, commitment: CommitmentConfig, ) -> ClientResult<(Hash, u64)> { - let RpcBlockhash { - blockhash, - last_valid_block_height, + Ok(self + .get_latest_blockhash_with_commitment_and_context(commitment) + .await? + .value) + } + + /// Returns the most recent blockhash and last valid block height along with the response + /// context, which includes the slot at which the node observed the blockhash. + /// + /// # RPC Reference + /// + /// This method corresponds directly to the [`getLatestBlockhash`] RPC method and uses the + /// provided [commitment level][cl]. + /// + /// [cl]: https://solana.com/docs/rpc#configuring-state-commitment + /// [`getLatestBlockhash`]: https://solana.com/docs/rpc/http/getlatestblockhash + pub async fn get_latest_blockhash_with_commitment_and_context( + &self, + commitment: CommitmentConfig, + ) -> RpcResult<(Hash, u64)> { + let Response { + context, + value: + RpcBlockhash { + blockhash, + last_valid_block_height, + }, } = self .send::>(RpcRequest::GetLatestBlockhash, json!([commitment])) - .await? - .value; + .await?; let blockhash = blockhash.parse().map_err(|_| { ClientError::new_with_request( RpcError::ParseError("Hash".to_string()).into(), RpcRequest::GetLatestBlockhash, ) })?; - Ok((blockhash, last_valid_block_height)) + Ok(Response { + context, + value: (blockhash, last_valid_block_height), + }) } /// Checks whether a blockhash is still valid for submitting transactions. @@ -5014,10 +5064,10 @@ impl RpcClient { let mut num_retries = 0; let start = Instant::now(); while start.elapsed().as_secs() < 5 { - if let Ok(new_blockhash) = self.get_latest_blockhash().await { - if new_blockhash != *blockhash { - return Ok(new_blockhash); - } + if let Ok(new_blockhash) = self.get_latest_blockhash().await + && new_blockhash != *blockhash + { + return Ok(new_blockhash); } debug!("Got same blockhash ({blockhash:?}), will retry..."); @@ -5060,9 +5110,9 @@ impl RpcClient { fn serialize_and_encode(input: &T, encoding: UiTransactionEncoding) -> ClientResult where - T: serde::ser::Serialize, + T: SchemaWrite, { - let serialized = serialize(input) + let serialized = wincode::serialize(input) .map_err(|e| ClientErrorKind::Custom(format!("Serialization failed: {e}")))?; let encoded = match encoding { UiTransactionEncoding::Base58 => bs58::encode(serialized).into_string(), diff --git a/rpc-client/src/rpc_client.rs b/rpc-client/src/rpc_client.rs index 422c8c98ee9..db45d8e3a9b 100644 --- a/rpc-client/src/rpc_client.rs +++ b/rpc-client/src/rpc_client.rs @@ -17,7 +17,7 @@ use { nonblocking::{self, rpc_client::get_rpc_request_str}, rpc_sender::*, }, - serde::Serialize, + agave_votor_messages::wire::WireBlockCertMessage, serde_json::Value, solana_account::{Account, ReadableAccount}, solana_account_decoder::UiAccount, @@ -45,6 +45,7 @@ use { }, solana_vote_interface::state::MAX_LOCKOUT_HISTORY, std::{net::SocketAddr, str::FromStr, sync::Arc, time::Duration}, + wincode::{SchemaWrite, config::DefaultConfig}, }; #[derive(Default)] @@ -94,7 +95,7 @@ impl SerializableMessage for v0::Message { /// Trait used to add support for versioned transactions to RPC APIs while /// retaining backwards compatibility -pub trait SerializableTransaction: Serialize { +pub trait SerializableTransaction: SchemaWrite { fn get_signature(&self) -> &Signature; fn get_recent_blockhash(&self) -> &Hash; fn uses_durable_nonce(&self) -> bool; @@ -1828,6 +1829,27 @@ impl RpcClient { self.invoke((self.rpc_client.as_ref()).get_slot_leaders(start_slot, limit)) } + /// Returns Alpenglow's genesis certificate. + /// + /// # RPC Reference + /// + /// This method corresponds directly to the [`getAgGenesisCert`] RPC method. + /// + /// [`getAgGenesisCert`]: https://solana.com/docs/rpc/http/getaggenesiscert + /// + /// # Examples + /// + /// ``` + /// # use solana_rpc_client_api::client_error::Error; + /// # use solana_rpc_client::rpc_client::RpcClient; + /// # let rpc_client = RpcClient::new_mock("succeeds".to_string()); + /// let cert = rpc_client.get_ag_genesis_cert()?; + /// # Ok::<(), Error>(()) + /// ``` + pub fn get_ag_genesis_cert(&self) -> ClientResult> { + self.invoke((self.rpc_client.as_ref()).get_ag_genesis_cert()) + } + /// Get block production for the current epoch. /// /// # RPC Reference @@ -4211,6 +4233,25 @@ impl RpcClient { self.invoke((self.rpc_client.as_ref()).get_latest_blockhash_with_commitment(commitment)) } + /// Returns the most recent blockhash and last valid block height along with the response + /// context, which includes the slot at which the node observed the blockhash. + /// + /// # RPC Reference + /// + /// This method corresponds directly to the [`getLatestBlockhash`] RPC method and uses the + /// provided [commitment level][cl]. + /// + /// [cl]: https://solana.com/docs/rpc#configuring-state-commitment + /// [`getLatestBlockhash`]: https://solana.com/docs/rpc/http/getlatestblockhash + pub fn get_latest_blockhash_with_commitment_and_context( + &self, + commitment: CommitmentConfig, + ) -> RpcResult<(Hash, u64)> { + self.invoke( + (self.rpc_client.as_ref()).get_latest_blockhash_with_commitment_and_context(commitment), + ) + } + /// Checks whether a blockhash is still valid for submitting transactions. /// /// # RPC Reference @@ -4322,7 +4363,7 @@ mod tests { crate::mock_sender::PUBKEY, assert_matches::assert_matches, base64::{Engine, prelude::BASE64_STANDARD}, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, jsonrpc_core::{Error, IoHandler, Params, futures::prelude::*}, jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder}, serde_json::{Number, json}, @@ -4331,9 +4372,12 @@ mod tests { solana_hash::Hash, solana_instruction::error::InstructionError, solana_keypair::Keypair, - solana_message::{MessageHeader, compiled_instruction::CompiledInstruction}, + solana_message::{ + MessageHeader, VersionedMessage, compiled_instruction::CompiledInstruction, v1, + }, solana_rpc_client_api::client_error::ErrorKind, solana_signer::Signer, + solana_system_interface::instruction as system_instruction, solana_system_transaction as system_transaction, solana_transaction_error::TransactionError, std::{io, thread}, @@ -4357,7 +4401,7 @@ mod tests { } fn _test_send() { - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); thread::spawn(move || { let rpc_addr = "0.0.0.0:0".parse().unwrap(); let mut io = IoHandler::default(); @@ -5080,7 +5124,7 @@ mod tests { let serialized_message = message.serialize(); let serialized_message_base64 = BASE64_STANDARD.encode(serialized_message); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); thread::spawn(move || { let rpc_addr = "0.0.0.0:0".parse().unwrap(); let mut io = IoHandler::default(); @@ -5124,4 +5168,51 @@ mod tests { let fee: u64 = rpc_client.get_fee_for_message(&message).unwrap(); assert_eq!(fee, 42); } + + #[test] + fn test_get_latest_blockhash_with_commitment_and_context() { + let rpc_client = RpcClient::new_mock("succeeds".to_string()); + let expected_blockhash: Hash = PUBKEY.parse().unwrap(); + + let response = rpc_client + .get_latest_blockhash_with_commitment_and_context(CommitmentConfig::default()) + .unwrap(); + assert_eq!(response.context.slot, 1); + assert_eq!(response.value, (expected_blockhash, 1234)); + + let value = rpc_client + .get_latest_blockhash_with_commitment(CommitmentConfig::default()) + .unwrap(); + assert_eq!(value, response.value); + } + + #[test] + fn test_send_transaction_v1() { + let rpc_client = RpcClient::new_mock("succeeds".to_string()); + + let key = Keypair::new(); + let to = Pubkey::new_unique(); + let blockhash = Hash::default(); + let ix = system_instruction::transfer(&key.pubkey(), &to, 50); + let tx = VersionedTransaction::try_new( + VersionedMessage::V1( + v1::Message::try_compile(&key.pubkey(), &[ix], blockhash).unwrap(), + ), + &[key], + ) + .unwrap(); + + let signature = rpc_client.send_transaction(&tx); + assert_eq!(signature.unwrap(), tx.signatures[0]); + + let rpc_client = RpcClient::new_mock("fails".to_string()); + + let signature = rpc_client.send_transaction(&tx); + assert!(signature.is_err()); + + // Test bad signature returned from rpc node + let rpc_client = RpcClient::new_mock("malicious".to_string()); + let signature = rpc_client.send_transaction(&tx); + assert!(signature.is_err()); + } } diff --git a/rpc-test/Cargo.toml b/rpc-test/Cargo.toml index 1b6064bce4b..bcc56c1c491 100644 --- a/rpc-test/Cargo.toml +++ b/rpc-test/Cargo.toml @@ -31,7 +31,6 @@ solana-net-utils = { workspace = true } solana-pubsub-client = { workspace = true } solana-rpc-client = { workspace = true } solana-rpc-client-api = { workspace = true } -solana-test-validator = { workspace = true } solana-tpu-client = { workspace = true } solana-tpu-client-next = { workspace = true } solana-transaction-status = { workspace = true } @@ -50,4 +49,8 @@ solana-rent = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } solana-system-transaction = { workspace = true } +solana-test-validator = { workspace = true, features = ["agave-unstable-api", "dev-context-only-utils"] } solana-transaction = { workspace = true } + +[lints] +workspace = true diff --git a/rpc-test/tests/nonblocking.rs b/rpc-test/tests/nonblocking.rs index c276d80ad65..05e1a50e1ff 100644 --- a/rpc-test/tests/nonblocking.rs +++ b/rpc-test/tests/nonblocking.rs @@ -15,7 +15,9 @@ use { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_tpu_send_transaction() { - let (test_validator, mint_keypair) = TestValidatorGenesis::default().start_async().await; + let (test_validator, mint_keypair) = TestValidatorGenesis::default_for_tests() + .start_async() + .await; let rpc_client = Arc::new(test_validator.get_async_rpc_client()); let mut tpu_client = TpuClient::new( "tpu_client_test", @@ -49,7 +51,9 @@ async fn test_tpu_send_transaction() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_tpu_cache_slot_updates() { - let (test_validator, _) = TestValidatorGenesis::default().start_async().await; + let (test_validator, _) = TestValidatorGenesis::default_for_tests() + .start_async() + .await; let rpc_client = Arc::new(test_validator.get_async_rpc_client()); let exit = Arc::new(AtomicBool::new(false)); let mut leader_tpu_service = LeaderTpuService::new( diff --git a/rpc-test/tests/rpc.rs b/rpc-test/tests/rpc.rs index 7c9b2bcb2f9..9ae8c64605c 100644 --- a/rpc-test/tests/rpc.rs +++ b/rpc-test/tests/rpc.rs @@ -106,11 +106,11 @@ fn test_rpc_send_tx() { let result: Option = serde_json::from_value(json["result"]["value"][0].clone()).unwrap(); - if let Some(result) = result.as_ref() { - if result.err.is_none() { - confirmed_tx = true; - break; - } + if let Some(result) = result.as_ref() + && result.err.is_none() + { + confirmed_tx = true; + break; } sleep(Duration::from_millis(500)); diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index d27ca71ec82..dbc053bcf81 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -18,11 +20,16 @@ name = "solana_rpc" [features] agave-unstable-api = [] -dev-context-only-utils = ["solana-ledger/dev-context-only-utils", "solana-rpc/dev-context-only-utils"] +dev-context-only-utils = [ + "agave-votor-messages/dev-context-only-utils", + "solana-ledger/dev-context-only-utils", + "solana-rpc/dev-context-only-utils", +] [dependencies] agave-feature-set = { workspace = true } agave-snapshots = { workspace = true } +agave-votor-messages = { workspace = true } base64 = { workspace = true } bs58 = { workspace = true } crossbeam-channel = { workspace = true } @@ -56,6 +63,7 @@ solana-genesis-config = { workspace = true } solana-gossip = { workspace = true } solana-hash = { workspace = true } solana-keypair = { workspace = true } +solana-leader-schedule = { workspace = true } solana-ledger = { workspace = true } solana-measure = { workspace = true } solana-message = { workspace = true } @@ -112,7 +120,7 @@ solana-nonce-account = { workspace = true } solana-program-option = { workspace = true } solana-program-runtime = { path = "../program-runtime", features = ["agave-unstable-api"] } solana-rent = { workspace = true } -solana-rpc = { path = ".", features = ["dev-context-only-utils"] } +solana-rpc = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime-transaction = { path = "../runtime-transaction", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-sdk-ids = { workspace = true } @@ -124,3 +132,6 @@ solana-vote-interface = { workspace = true } spl-pod = { workspace = true } symlink = { workspace = true } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/rpc/src/cluster_tpu_info.rs b/rpc/src/cluster_tpu_info.rs index d67271f731d..edb040da1ac 100644 --- a/rpc/src/cluster_tpu_info.rs +++ b/rpc/src/cluster_tpu_info.rs @@ -1,8 +1,12 @@ use { - solana_clock::NUM_CONSECUTIVE_LEADER_SLOTS, + agave_votor_messages::migration::MigrationStatus, + solana_clock::Slot, solana_gossip::{cluster_info::ClusterInfo, contact_info::Protocol}, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, + solana_ledger::leader_schedule_cache::LeaderScheduleCache, solana_poh::poh_recorder::PohRecorder, solana_pubkey::Pubkey, + solana_runtime::commitment::BlockCommitmentCache, solana_send_transaction_service::tpu_info::TpuInfo, std::{ collections::HashMap, @@ -16,17 +20,78 @@ use { pub struct ClusterTpuInfo { cluster_info: Arc, poh_recorder: Arc>, + block_commitment_cache: Arc>, + leader_schedule_cache: Arc, + migration_status: Arc, recent_peers: HashMap, // values are socket address for QUIC protocol } impl ClusterTpuInfo { - pub fn new(cluster_info: Arc, poh_recorder: Arc>) -> Self { + pub fn new( + cluster_info: Arc, + poh_recorder: Arc>, + block_commitment_cache: Arc>, + leader_schedule_cache: Arc, + migration_status: Arc, + ) -> Self { Self { cluster_info, poh_recorder, + block_commitment_cache, + leader_schedule_cache, + migration_status, recent_peers: HashMap::new(), } } + + fn leader_pubkeys(&self, max_count: u64) -> Vec { + let current_slot = self.commitment_current_slot(); + // Commitment-cache slots represent banks we have already processed, so + // transactions can only land in later slots. + let target_slot = current_slot.saturating_add(1); + if self + .migration_status + .should_have_alpenglow_ticks(target_slot) + { + self.alpenglow_leader_pubkeys(target_slot, max_count) + } else { + self.poh_leader_pubkeys(max_count) + } + } + + fn commitment_current_slot(&self) -> Slot { + let commitment_slots = self + .block_commitment_cache + .read() + .unwrap() + .commitment_slots(); + commitment_slots + .slot + .max(commitment_slots.root) + .max(commitment_slots.highest_confirmed_slot) + .max(commitment_slots.highest_super_majority_root) + } + + fn alpenglow_leader_pubkeys(&self, current_slot: Slot, max_count: u64) -> Vec { + (0..max_count) + .filter_map(|i| { + let target_slot = current_slot + .saturating_add(i.saturating_mul(NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64)); + self.leader_schedule_cache + .slot_leader_at(target_slot, None) + .map(|leader| leader.id) + }) + .collect() + } + + fn poh_leader_pubkeys(&self, max_count: u64) -> Vec { + let recorder = self.poh_recorder.read().unwrap(); + (0..max_count) + .filter_map(|i| { + recorder.leader_after_n_slots(i * NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64) + }) + .collect() + } } impl TpuInfo for ClusterTpuInfo { @@ -41,29 +106,20 @@ impl TpuInfo for ClusterTpuInfo { } fn get_leader_tpus(&self, max_count: u64) -> Vec<&SocketAddr> { - let recorder = self.poh_recorder.read().unwrap(); - let leaders: Vec<_> = (0..max_count) - .filter_map(|i| recorder.leader_after_n_slots(i * NUM_CONSECUTIVE_LEADER_SLOTS)) - .collect(); - drop(recorder); + let leaders = self.leader_pubkeys(max_count); let mut unique_leaders = vec![]; for leader in leaders.iter() { - if let Some(addr) = self.recent_peers.get(leader) { - if !unique_leaders.contains(&addr) { - unique_leaders.push(addr); - } + if let Some(addr) = self.recent_peers.get(leader) + && !unique_leaders.contains(&addr) + { + unique_leaders.push(addr); } } unique_leaders } fn get_not_unique_leader_tpus(&self, max_count: u64) -> Vec<&SocketAddr> { - let recorder = self.poh_recorder.read().unwrap(); - let leader_pubkeys: Vec<_> = (0..max_count) - .filter_map(|i| recorder.leader_after_n_slots(i * NUM_CONSECUTIVE_LEADER_SLOTS)) - .collect(); - drop(recorder); - leader_pubkeys + self.leader_pubkeys(max_count) .iter() .filter_map(|leader_pubkey| self.recent_peers.get(leader_pubkey)) .collect() @@ -87,6 +143,7 @@ mod test { genesis_utils::{ GenesisConfigInfo, ValidatorVoteKeypairs, create_genesis_config_with_vote_accounts, }, + leader_schedule_utils::slot_leader_at, }, solana_signer::Signer, solana_time_utils::timestamp, @@ -119,6 +176,7 @@ mod test { ); let bank = Arc::new(Bank::new_for_tests(&genesis_config)); + let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank)); let (poh_recorder, _entry_receiver) = PohRecorder::new( 0, bank.last_blockhash(), @@ -126,7 +184,7 @@ mod test { Some((2, 2)), bank.ticks_per_slot(), Arc::new(blockstore), - &Arc::new(LeaderScheduleCache::new_from_bank(&bank)), + &leader_schedule_cache, &PohConfig::default(), Arc::new(AtomicBool::default()), ); @@ -151,8 +209,13 @@ mod test { cluster_info.insert_info(validator1_contact_info); cluster_info.insert_info(validator2_contact_info); - let mut leader_info = - ClusterTpuInfo::new(cluster_info, Arc::new(RwLock::new(poh_recorder))); + let mut leader_info = ClusterTpuInfo::new( + cluster_info, + Arc::new(RwLock::new(poh_recorder)), + Arc::new(RwLock::new(BlockCommitmentCache::new_for_tests())), + leader_schedule_cache, + Arc::new(MigrationStatus::default()), + ); leader_info.refresh_recent_peers(); let mut refreshed_recent_peers = leader_info.recent_peers.keys().copied().collect::>(); @@ -181,6 +244,7 @@ mod test { ); let bank = Arc::new(Bank::new_for_tests(&genesis_config)); + let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank)); let (poh_recorder, _entry_receiver) = PohRecorder::new( 0, bank.last_blockhash(), @@ -188,7 +252,7 @@ mod test { Some((2, 2)), bank.ticks_per_slot(), Arc::new(blockstore), - &Arc::new(LeaderScheduleCache::new_from_bank(&bank)), + &leader_schedule_cache, &PohConfig::default(), Arc::new(AtomicBool::default()), ); @@ -220,11 +284,14 @@ mod test { .iter() .cloned() .collect(); - let leader_info = ClusterTpuInfo { + let mut leader_info = ClusterTpuInfo::new( cluster_info, - poh_recorder: Arc::new(RwLock::new(poh_recorder)), - recent_peers: recent_peers.clone(), - }; + Arc::new(RwLock::new(poh_recorder)), + Arc::new(RwLock::new(BlockCommitmentCache::new_for_tests())), + leader_schedule_cache, + Arc::new(MigrationStatus::default()), + ); + leader_info.recent_peers = recent_peers.clone(); let slot = bank.slot(); let first_leader = @@ -239,7 +306,7 @@ mod test { ); let second_leader = solana_runtime::leader_schedule_utils::slot_leader_at( - slot + NUM_CONSECUTIVE_LEADER_SLOTS, + slot + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot, &bank, ) .unwrap(); @@ -255,7 +322,7 @@ mod test { ); let third_leader = solana_runtime::leader_schedule_utils::slot_leader_at( - slot + (2 * NUM_CONSECUTIVE_LEADER_SLOTS), + slot + (2 * NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot), &bank, ) .unwrap(); @@ -280,4 +347,97 @@ mod test { assert_eq!(leader_info.get_not_unique_leader_tpus(x).len(), x as usize); } } + + #[test] + fn test_get_leader_tpus_uses_commitment_slot_after_alpenglow() { + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Blockstore::open(ledger_path.path()).unwrap(); + + let validator_vote_keypairs0 = ValidatorVoteKeypairs::new_rand(); + let validator_vote_keypairs1 = ValidatorVoteKeypairs::new_rand(); + let validator_vote_keypairs2 = ValidatorVoteKeypairs::new_rand(); + let validator_keypairs = vec![ + &validator_vote_keypairs0, + &validator_vote_keypairs1, + &validator_vote_keypairs2, + ]; + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_vote_accounts( + 1_000_000_000, + &validator_keypairs, + vec![10_000; 3], + ); + let bank = Arc::new(Bank::new_for_tests(&genesis_config)); + let leader_schedule_cache = Arc::new(LeaderScheduleCache::new_from_bank(&bank)); + + let (poh_recorder, _entry_receiver) = PohRecorder::new( + 0, + bank.last_blockhash(), + bank.clone(), + Some((2, 2)), + bank.ticks_per_slot(), + Arc::new(blockstore), + &leader_schedule_cache, + &PohConfig::default(), + Arc::new(AtomicBool::default()), + ); + + let node_keypair = Arc::new(Keypair::new()); + let cluster_info = Arc::new(ClusterInfo::new( + ContactInfo::new_localhost(&node_keypair.pubkey(), timestamp()), + node_keypair, + SocketAddrSpace::Unspecified, + )); + + let validator0_socket = SocketAddr::from((Ipv4Addr::LOCALHOST, 1112)); + let validator1_socket = SocketAddr::from((Ipv4Addr::LOCALHOST, 2223)); + let validator2_socket = SocketAddr::from((Ipv4Addr::LOCALHOST, 3334)); + let recent_peers: HashMap<_, _> = [ + ( + validator_vote_keypairs0.node_keypair.pubkey(), + validator0_socket, + ), + ( + validator_vote_keypairs1.node_keypair.pubkey(), + validator1_socket, + ), + ( + validator_vote_keypairs2.node_keypair.pubkey(), + validator2_socket, + ), + ] + .iter() + .cloned() + .collect(); + + let completed_alpenglow_slot = (1..64) + .find(|slot| { + slot_leader_at(*slot, &bank).unwrap() + != slot_leader_at(slot.saturating_add(1), &bank).unwrap() + }) + .expect("leader schedule should rotate within 64 slots"); + let completed_slot_leader = slot_leader_at(completed_alpenglow_slot, &bank).unwrap(); + let target_slot_leader = + slot_leader_at(completed_alpenglow_slot.saturating_add(1), &bank).unwrap(); + + let mut leader_info = ClusterTpuInfo::new( + cluster_info, + Arc::new(RwLock::new(poh_recorder)), + Arc::new(RwLock::new(BlockCommitmentCache::new_for_tests_with_slots( + completed_alpenglow_slot, + completed_alpenglow_slot, + ))), + leader_schedule_cache, + Arc::new(MigrationStatus::post_migration_status()), + ); + leader_info.recent_peers = recent_peers.clone(); + + assert_eq!( + leader_info.get_not_unique_leader_tpus(1), + vec![recent_peers.get(&target_slot_leader).unwrap()] + ); + assert_ne!( + leader_info.get_not_unique_leader_tpus(1), + vec![recent_peers.get(&completed_slot_leader).unwrap()] + ); + } } diff --git a/rpc/src/optimistically_confirmed_bank_tracker.rs b/rpc/src/optimistically_confirmed_bank_tracker.rs index 77db08aeffa..c553b623707 100644 --- a/rpc/src/optimistically_confirmed_bank_tracker.rs +++ b/rpc/src/optimistically_confirmed_bank_tracker.rs @@ -34,7 +34,7 @@ pub struct OptimisticallyConfirmedBank { } impl OptimisticallyConfirmedBank { - pub fn locked_from_bank_forks_root(bank_forks: &Arc>) -> Arc> { + pub fn locked_from_bank_forks_root(bank_forks: &RwLock) -> Arc> { Arc::new(RwLock::new(Self { bank: bank_forks.read().unwrap().root_bank(), })) @@ -288,10 +288,10 @@ impl OptimisticallyConfirmedBankTracker { ) { debug!("received bank notification: {notification:?} event: {dependency_work:?}"); - if let Some(tracker) = dependency_tracker.as_ref() { - if let Some(dependency_work) = dependency_work { - tracker.wait_for_dependency(dependency_work); - } + if let Some(tracker) = dependency_tracker.as_ref() + && let Some(dependency_work) = dependency_work + { + tracker.wait_for_dependency(dependency_work); } match notification { BankNotification::OptimisticallyConfirmed(slot) => { @@ -417,7 +417,7 @@ impl OptimisticallyConfirmedBankTracker { mod tests { use { super::*, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_ledger::genesis_utils::{GenesisConfigInfo, create_genesis_config}, solana_runtime::{bank::SlotLeader, commitment::BlockCommitmentCache, dependency_tracker}, std::sync::atomic::AtomicU64, @@ -585,7 +585,7 @@ mod tests { let bank5 = bank_forks.read().unwrap().get(5).unwrap(); let mut bank_notification_senders = Vec::new(); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); bank_notification_senders.push(sender); let subscribers = Some(Arc::new(RwLock::new(bank_notification_senders))); diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index bbeef742949..09449c61c08 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -8,6 +8,7 @@ use { parsed_token_accounts::*, rpc_cache::LargestAccountsCache, rpc_health::*, }, agave_snapshots::{paths as snapshot_paths, snapshot_config::SnapshotConfig}, + agave_votor_messages::wire::{WireBlockCertMessage, WireCertSignature}, base64::{Engine, prelude::BASE64_STANDARD}, crossbeam_channel::{Receiver, Sender, unbounded}, jsonrpc_core::{ @@ -273,13 +274,13 @@ impl JsonRpcRequestProcessor { min_context_slot, } = config; let bank = self.bank(commitment); - if let Some(min_context_slot) = min_context_slot { - if bank.slot() < min_context_slot { - return Err(RpcCustomError::MinContextSlotNotReached { - context_slot: bank.slot(), - } - .into()); + if let Some(min_context_slot) = min_context_slot + && bank.slot() < min_context_slot + { + return Err(RpcCustomError::MinContextSlotNotReached { + context_slot: bank.slot(), } + .into()); } Ok(bank) } @@ -448,7 +449,7 @@ impl JsonRpcRequestProcessor { #[cfg(test)] pub fn new_from_bank(bank: Bank, socket_addr_space: SocketAddrSpace) -> Self { use { - crate::rpc_service::service_runtime, + crate::rpc_service::service_runtime, crossbeam_channel::bounded, solana_send_transaction_service::test_utils::create_client_for_tests, }; @@ -467,7 +468,7 @@ impl JsonRpcRequestProcessor { }); let my_tpu_address = cluster_info.my_contact_info().tpu(Protocol::QUIC).unwrap(); - let (transaction_sender, transaction_receiver) = unbounded(); + let (transaction_sender, transaction_receiver) = bounded(1024); let config = JsonRpcConfig::default(); let JsonRpcConfig { @@ -480,7 +481,7 @@ impl JsonRpcRequestProcessor { let client = create_client_for_tests(runtime.handle().clone(), my_tpu_address, None, 1); SendTransactionService::new( - &bank_forks, + bank_forks.clone(), transaction_receiver, client, SendTransactionServiceConfig { @@ -920,6 +921,18 @@ impl JsonRpcRequestProcessor { bank.epoch_schedule().clone() } + pub fn get_ag_genesis_cert(&self) -> Option { + let bank = self.bank(Some(CommitmentConfig::finalized())); + bank.get_alpenglow_genesis_certificate() + .map(|c| WireBlockCertMessage { + block: c.block, + signature: WireCertSignature { + signature: c.signature.signature, + bitmap: c.signature.bitmap, + }, + }) + } + pub fn get_balance( &self, pubkey: &Pubkey, @@ -1164,10 +1177,10 @@ impl JsonRpcRequestProcessor { ) = vote_accounts .iter() .filter_map(|(vote_pubkey, (activated_stake, account))| { - if let Some(filter_by_vote_pubkey) = filter_by_vote_pubkey { - if *vote_pubkey != filter_by_vote_pubkey { - return None; - } + if let Some(filter_by_vote_pubkey) = filter_by_vote_pubkey + && *vote_pubkey != filter_by_vote_pubkey + { + return None; } let vote_state_view = account.vote_state_view(); @@ -1349,14 +1362,14 @@ impl JsonRpcRequestProcessor { } Ok::(encoded_block) }; - if result.is_err() { - if let Some(bigtable_ledger_storage) = &self.bigtable_ledger_storage { - let bigtable_result = bigtable_ledger_storage.get_confirmed_block(slot).await; - self.check_bigtable_result(&bigtable_result)?; - let encoded_block_future: OptionFuture<_> = - bigtable_result.ok().map(encode_block).into(); - return encoded_block_future.await.transpose(); - } + if result.is_err() + && let Some(bigtable_ledger_storage) = &self.bigtable_ledger_storage + { + let bigtable_result = bigtable_ledger_storage.get_confirmed_block(slot).await; + self.check_bigtable_result(&bigtable_result)?; + let encoded_block_future: OptionFuture<_> = + bigtable_result.ok().map(encode_block).into(); + return encoded_block_future.await.transpose(); } self.check_slot_cleaned_up(&result, slot)?; let encoded_block_future: OptionFuture<_> = result @@ -1601,14 +1614,14 @@ impl JsonRpcRequestProcessor { { let result = self.blockstore.get_rooted_block_time(slot); self.check_blockstore_root(&result, slot)?; - if result.is_err() { - if let Some(bigtable_ledger_storage) = &self.bigtable_ledger_storage { - let bigtable_result = bigtable_ledger_storage.get_confirmed_block(slot).await; - self.check_bigtable_result(&bigtable_result)?; - return Ok(bigtable_result - .ok() - .and_then(|confirmed_block| confirmed_block.block_time)); - } + if result.is_err() + && let Some(bigtable_ledger_storage) = &self.bigtable_ledger_storage + { + let bigtable_result = bigtable_ledger_storage.get_confirmed_block(slot).await; + self.check_bigtable_result(&bigtable_result)?; + return Ok(bigtable_result + .ok() + .and_then(|confirmed_block| confirmed_block.block_time)); } self.check_slot_cleaned_up(&result, slot)?; Ok(result.ok()) @@ -1955,21 +1968,17 @@ impl JsonRpcRequestProcessor { } else { // Long-term storage is not enabled. // Return an error to the user if either before/until were provided but not found. - if !found_before { - if let Some(signature) = before { - return Err(RpcCustomError::FilterTransactionNotFound { - signature: signature.to_string(), - } - .into()); + if !found_before && let Some(signature) = before { + return Err(RpcCustomError::FilterTransactionNotFound { + signature: signature.to_string(), } + .into()); } - if !found_until { - if let Some(signature) = until { - return Err(RpcCustomError::FilterTransactionNotFound { - signature: signature.to_string(), - } - .into()); + if !found_until && let Some(signature) = until { + return Err(RpcCustomError::FilterTransactionNotFound { + signature: signature.to_string(), } + .into()); } } } @@ -2446,11 +2455,11 @@ impl JsonRpcRequestProcessor { pub(crate) fn optimize_filters(filters: &mut [RpcFilterType]) { filters.iter_mut().for_each(|filter_type| { - if let RpcFilterType::Memcmp(compare) = filter_type { - if let Err(err) = compare.convert_to_raw_bytes() { - // All filters should have been previously verified - warn!("Invalid filter: bytes could not be decoded, {err}"); - } + if let RpcFilterType::Memcmp(compare) = filter_type + && let Err(err) = compare.convert_to_raw_bytes() + { + // All filters should have been previously verified + warn!("Invalid filter: bytes could not be decoded, {err}"); } }) } @@ -2988,7 +2997,7 @@ pub mod rpc_minimal { // RPC interface that only depends on immediate Bank data // Expected to be provided by API nodes pub mod rpc_bank { - use super::*; + use {super::*, agave_votor_messages::wire::WireBlockCertMessage}; #[rpc] pub trait BankData { type Metadata; @@ -3029,6 +3038,10 @@ pub mod rpc_bank { limit: u64, ) -> Result>; + #[rpc(meta, name = "getAgGenesisCert")] + fn get_ag_genesis_cert(&self, meta: Self::Metadata) + -> Result>; + #[rpc(meta, name = "getBlockProduction")] fn get_block_production( &self, @@ -3104,6 +3117,14 @@ pub mod rpc_bank { .collect()) } + fn get_ag_genesis_cert( + &self, + meta: Self::Metadata, + ) -> Result> { + debug!("get_ag_genesis_cert rpc request received"); + Ok(meta.get_ag_genesis_cert()) + } + fn get_block_production( &self, meta: Self::Metadata, @@ -3136,7 +3157,9 @@ pub mod rpc_bank { } }; - let slot_history = bank.get_slot_history(); + let Some(slot_history) = bank.get_slot_history() else { + return Err(RpcCustomError::NoSlotHistory.into()); + }; if first_slot < slot_history.oldest() { return Err(Error::invalid_params(format!( "firstSlot, {}, is too small; min {}", @@ -3162,11 +3185,11 @@ pub mod rpc_bank { let mut slot = first_slot; for identity in slot_leaders { - if let Some(ref filter_by_identity) = filter_by_identity { - if identity != *filter_by_identity { - slot += 1; - continue; - } + if let Some(ref filter_by_identity) = filter_by_identity + && identity != *filter_by_identity + { + slot += 1; + continue; } let entry = block_production.entry(identity).or_default(); @@ -3786,10 +3809,7 @@ pub mod rpc_full { config: Option, ) -> Result { debug!("request_airdrop rpc request received"); - trace!( - "request_airdrop id={} lamports={} config: {:?}", - pubkey_str, lamports, &config - ); + trace!("request_airdrop id={pubkey_str} lamports={lamports} config: {config:?}"); let faucet_addr = meta.config.faucet_addr.ok_or_else(Error::invalid_request)?; let pubkey = verify_pubkey(&pubkey_str)?; @@ -4366,8 +4386,19 @@ fn rpc_perf_sample_from_perf_sample(slot: u64, sample: PerfSample) -> RpcPerfSam } } +// Base58 encoding is deprecated, not increased for v1 transactions const MAX_BASE58_SIZE: usize = 1683; // Golden, bump if PACKET_DATA_SIZE changes -const MAX_BASE64_SIZE: usize = 1644; // Golden, bump if PACKET_DATA_SIZE changes +// Cap base64 based on the largest supported transaction version, +// which is v1 (4096 bytes). +const MAX_BASE64_SIZE: usize = 5464; // ceil(4096 / 3) * 4 +// Legacy/v0 transactions are still capped at PACKET_DATA_SIZE (1232 bytes). +const MAX_BASE64_LEGACY_SIZE: usize = 1644; // ceil(1232 / 3) * 4 + +// V1+ messages start with 0x81 (or higher). In base64 the top 6 bits of 0x81 +// Comparing the first two chars against "gQ" detects V1+ before decoding. +// Revisit before V3 which will break this shortcut +const V1_BASE64_PREFIX_LOWER_BOUND: &[u8] = b"gQ"; + fn decode_and_deserialize( encoded: String, encoding: TransactionBinaryEncoding, @@ -4375,7 +4406,7 @@ fn decode_and_deserialize( where T: for<'a> wincode::SchemaRead<'a, wincode::config::DefaultConfig, Dst = T>, { - let wire_output = match encoding { + let (wire_output, max_raw_size) = match encoding { TransactionBinaryEncoding::Base58 => { inc_new_counter_info!("rpc-base58_encoded_tx", 1); if encoded.len() > MAX_BASE58_SIZE { @@ -4387,32 +4418,44 @@ where PACKET_DATA_SIZE, ))); } - bs58::decode(encoded) + let bytes = bs58::decode(encoded) .into_vec() - .map_err(|e| Error::invalid_params(format!("invalid base58 encoding: {e:?}")))? + .map_err(|e| Error::invalid_params(format!("invalid base58 encoding: {e:?}")))?; + // base58 is deprecated, unchanged max size + (bytes, PACKET_DATA_SIZE) } TransactionBinaryEncoding::Base64 => { inc_new_counter_info!("rpc-base64_encoded_tx", 1); - if encoded.len() > MAX_BASE64_SIZE { + let (max_encoded_size, max_raw_size) = if encoded + .as_bytes() + .get(..2) + .is_some_and(|p| p >= V1_BASE64_PREFIX_LOWER_BOUND) + { + (MAX_BASE64_SIZE, solana_message::v1::MAX_TRANSACTION_SIZE) + } else { + (MAX_BASE64_LEGACY_SIZE, PACKET_DATA_SIZE) + }; + if encoded.len() > max_encoded_size { return Err(Error::invalid_params(format!( "base64 encoded {} too large: {} bytes (max: encoded/raw {}/{})", type_name::(), encoded.len(), - MAX_BASE64_SIZE, - PACKET_DATA_SIZE, + max_encoded_size, + max_raw_size, ))); } - BASE64_STANDARD + let bytes = BASE64_STANDARD .decode(encoded) - .map_err(|e| Error::invalid_params(format!("invalid base64 encoding: {e:?}")))? + .map_err(|e| Error::invalid_params(format!("invalid base64 encoding: {e:?}")))?; + (bytes, max_raw_size) } }; - if wire_output.len() > PACKET_DATA_SIZE { + if wire_output.len() > max_raw_size { return Err(Error::invalid_params(format!( "decoded {} too large: {} bytes (max: {} bytes)", type_name::(), wire_output.len(), - PACKET_DATA_SIZE + max_raw_size ))); } @@ -4421,7 +4464,7 @@ where Error::invalid_params(format!( "failed to deserialize {}: {}", type_name::(), - &err.to_string() + err )) }) .map(|output| (wire_output, output)) @@ -4491,15 +4534,17 @@ pub fn populate_blockstore_for_tests( blockstore: Arc, max_complete_transaction_status_slot: Arc, ) { + use crossbeam_channel::bounded; + let slot = bank.slot(); let parent_slot = bank.parent_slot(); let shreds = solana_ledger::blockstore::entries_to_test_shreds(&entries, slot, parent_slot, true, 0); - blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.insert_shreds(shreds, false).unwrap(); blockstore.set_roots(std::iter::once(&slot)).unwrap(); - let (transaction_status_sender, transaction_status_receiver) = unbounded(); - let (replay_vote_sender, _replay_vote_receiver) = unbounded(); + let (transaction_status_sender, transaction_status_receiver) = bounded(1024); + let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let tss_exit = Arc::new(AtomicBool::new(false)); let transaction_status_service = crate::transaction_status_service::TransactionStatusService::new( @@ -4571,6 +4616,7 @@ pub mod tests { solana_message::{ Message, MessageHeader, SimpleAddressLoader, VersionedMessage, v0::{self, MessageAddressTableLookup}, + v1, }, solana_nonce::{self as nonce, state::DurableNonce}, solana_program_option::COption, @@ -4612,7 +4658,6 @@ pub mod tests { vote_instruction, vote_state::{MAX_LOCKOUT_HISTORY, TowerSync, VoteInit, VoteStateVersions}, }, - spl_pod::optional_keys::OptionalNonZeroPubkey, spl_token_2022_interface::{ extension::{ BaseStateWithExtensionsMut, ExtensionType, StateWithExtensionsMut, @@ -6919,7 +6964,7 @@ pub mod tests { let client = create_client_for_tests(runtime.handle().clone(), my_tpu_address, None, 1); SendTransactionService::new( - &bank_forks, + bank_forks.clone(), receiver, client, SendTransactionServiceConfig { @@ -7231,7 +7276,7 @@ pub mod tests { ); SendTransactionService::new( - &bank_forks, + bank_forks, receiver, client, SendTransactionServiceConfig { @@ -8007,8 +8052,7 @@ pub mod tests { let mint_close_authority = mint_state .init_extension::(true) .unwrap(); - mint_close_authority.close_authority = - OptionalNonZeroPubkey::try_from(Some(owner)).unwrap(); + mint_close_authority.close_authority = owner.into(); let mint_account = AccountSharedData::from(Account { lamports: 111, @@ -8515,8 +8559,7 @@ pub mod tests { let mint_close_authority = mint_state .init_extension::(true) .unwrap(); - mint_close_authority.close_authority = - OptionalNonZeroPubkey::try_from(Some(owner)).unwrap(); + mint_close_authority.close_authority = owner.into(); if let Some(interest_bearing_config) = interest_bearing_config.as_mut() { interest_bearing_config.initialization_timestamp = bank.clock().unix_timestamp.saturating_sub(1_000_000).into(); @@ -9111,17 +9154,21 @@ pub mod tests { #[test] fn test_worst_case_encoded_tx_goldens() { - let ff_tx = vec![0xffu8; PACKET_DATA_SIZE]; - let tx58 = bs58::encode(&ff_tx).into_string(); + // Base58 is capped at the legacy PACKET_DATA_SIZE since base58 encoding + // is deprecated and not extended to v1-sized transactions. + let ff_tx58_input = vec![0xffu8; PACKET_DATA_SIZE]; + let tx58 = bs58::encode(&ff_tx58_input).into_string(); assert_eq!(tx58.len(), MAX_BASE58_SIZE); - let tx64 = BASE64_STANDARD.encode(&ff_tx); + + // Base64 is sized for the largest supported transaction version (v1). + let ff_tx64_input = vec![0xffu8; solana_message::v1::MAX_TRANSACTION_SIZE]; + let tx64 = BASE64_STANDARD.encode(&ff_tx64_input); assert_eq!(tx64.len(), MAX_BASE64_SIZE); } #[test] - fn test_decode_and_deserialize_too_large_payloads_fail() { - // +2 because +1 still fits in base64 encoded worst-case - let too_big = PACKET_DATA_SIZE + 2; + fn test_decode_and_deserialize_too_large_encoded_base58_payload_fails() { + let too_big = PACKET_DATA_SIZE + 1; let tx_ser = vec![0xffu8; too_big]; let tx58 = bs58::encode(&tx_ser).into_string(); @@ -9134,18 +9181,50 @@ pub mod tests { encoded/raw {MAX_BASE58_SIZE}/{PACKET_DATA_SIZE})", )) ); + } + + #[test] + fn test_decode_and_deserialize_too_large_encoded_base64_v1_payload_fails() { + // +3 because +2 still fits in base64 encoded worst-case + let too_big = solana_message::v1::MAX_TRANSACTION_SIZE + 3; + let mut tx_ser = vec![0xffu8; too_big]; + tx_ser[0] = solana_message::v1::V1_PREFIX; + + let tx64 = BASE64_STANDARD.encode(&tx_ser); + let tx64_len = tx64.len(); + assert_eq!( + decode_and_deserialize::(tx64, TransactionBinaryEncoding::Base64) + .unwrap_err(), + Error::invalid_params(format!( + "base64 encoded solana_transaction::Transaction too large: {tx64_len} bytes (max: \ + encoded/raw {MAX_BASE64_SIZE}/{})", + solana_message::v1::MAX_TRANSACTION_SIZE, + )) + ); + } + + #[test] + fn test_decode_and_deserialize_too_large_encoded_base64_legacy_payload_fails() { + // Legacy/v0 payloads must bail out before decoding when they exceed the + // legacy base64 cap, even if they would fit under the v1 cap. + let too_big = PACKET_DATA_SIZE + 3; + let tx_ser = vec![0x00u8; too_big]; let tx64 = BASE64_STANDARD.encode(&tx_ser); let tx64_len = tx64.len(); + assert!(tx64_len > MAX_BASE64_LEGACY_SIZE && tx64_len < MAX_BASE64_SIZE); assert_eq!( decode_and_deserialize::(tx64, TransactionBinaryEncoding::Base64) .unwrap_err(), Error::invalid_params(format!( "base64 encoded solana_transaction::Transaction too large: {tx64_len} bytes (max: \ - encoded/raw {MAX_BASE64_SIZE}/{PACKET_DATA_SIZE})", + encoded/raw {MAX_BASE64_LEGACY_SIZE}/{PACKET_DATA_SIZE})", )) ); + } + #[test] + fn test_decode_and_deserialize_too_large_payloads_fail() { let too_big = PACKET_DATA_SIZE + 1; let tx_ser = vec![0x00u8; too_big]; let tx58 = bs58::encode(&tx_ser).into_string(); @@ -9207,6 +9286,46 @@ pub mod tests { ); } + #[test] + fn test_decode_and_deserialize_v1_sized_payload_passes_size_checks() { + // V1-prefixed payload at the V1 max size. Random bytes will fail + // wincode deserialization, but should pass the size gates. + let mut tx_ser = vec![0xffu8; solana_message::v1::MAX_TRANSACTION_SIZE]; + tx_ser[0] = solana_message::v1::V1_PREFIX; + + let tx64 = BASE64_STANDARD.encode(&tx_ser); + let err = + decode_and_deserialize::(tx64, TransactionBinaryEncoding::Base64) + .unwrap_err(); + assert_eq!(err.code, ErrorCode::InvalidParams); + assert!( + err.message.starts_with( + "failed to deserialize solana_transaction::versioned::VersionedTransaction" + ), + "unexpected error: {}", + err.message + ); + } + + #[test] + fn test_decode_and_deserialize_v1_oversize_payload_fails() { + // V1-prefixed payload one byte over the V1 max. + let too_big = solana_message::v1::MAX_TRANSACTION_SIZE + 1; + let mut tx_ser = vec![0xffu8; too_big]; + tx_ser[0] = solana_message::v1::V1_PREFIX; + + let tx64 = BASE64_STANDARD.encode(&tx_ser); + assert_eq!( + decode_and_deserialize::(tx64, TransactionBinaryEncoding::Base64) + .unwrap_err(), + Error::invalid_params(format!( + "decoded solana_transaction::versioned::VersionedTransaction too large: {too_big} \ + bytes (max: {} bytes)", + solana_message::v1::MAX_TRANSACTION_SIZE, + )) + ); + } + #[test] fn test_sanitize_unsanitary() { let unsanitary_tx58 = "ju9xZWuDBX4pRxX2oZkTjxU5jB4SSTgEGhX8bQ8PURNzyzqKMPPpNvWihx8zUe\ @@ -9333,6 +9452,32 @@ pub mod tests { let response: RpcResponse = parse_success_result(rpc.handle_request_sync(request)); assert_eq!(response.value, TEST_SIGNATURE_FEE); } + + { + const PRIORITY_FEE: u64 = 42; + let v1_msg = VersionedMessage::V1(v1::Message::new( + MessageHeader { + num_required_signatures: 1, + ..MessageHeader::default() + }, + v1::TransactionConfig { + priority_fee: Some(PRIORITY_FEE), + ..v1::TransactionConfig::empty() + }, + recent_blockhash, + vec![Pubkey::new_unique()], + vec![], + )); + + let request = create_test_request( + "getFeeForMessage", + Some(json!([ + BASE64_STANDARD.encode(wincode::serialize(&v1_msg).unwrap()) + ])), + ); + let response: RpcResponse = parse_success_result(rpc.handle_request_sync(request)); + assert_eq!(response.value, TEST_SIGNATURE_FEE + PRIORITY_FEE); + } } #[test] diff --git a/rpc/src/rpc_cache.rs b/rpc/src/rpc_cache.rs index e47d6a4dcf7..ed97dd9cca2 100644 --- a/rpc/src/rpc_cache.rs +++ b/rpc/src/rpc_cache.rs @@ -32,10 +32,10 @@ impl LargestAccountsCache { filter: &Option, ) -> Option<(u64, Vec)> { self.cache.get(filter).and_then(|value| { - if let Ok(elapsed) = value.cached_time.elapsed() { - if elapsed < Duration::from_secs(self.duration) { - return Some((value.slot, value.accounts.clone())); - } + if let Ok(elapsed) = value.cached_time.elapsed() + && elapsed < Duration::from_secs(self.duration) + { + return Some((value.slot, value.accounts.clone())); } None }) diff --git a/rpc/src/rpc_service.rs b/rpc/src/rpc_service.rs index 4fb9d692e7f..56453fd040e 100644 --- a/rpc/src/rpc_service.rs +++ b/rpc/src/rpc_service.rs @@ -349,52 +349,50 @@ impl RequestMiddleware for RpcRequestMiddleware { fn on_request(&self, request: hyper::Request) -> RequestMiddlewareAction { trace!("request uri: {}", request.uri()); - if let Some(ref snapshot_config) = self.snapshot_config { - if request.uri().path() == FULL_SNAPSHOT_REQUEST_PATH - || request.uri().path() == INCREMENTAL_SNAPSHOT_REQUEST_PATH - { - // Convenience redirect to the latest snapshot - let full_snapshot_archive_info = - snapshot_paths::get_highest_full_snapshot_archive_info( - &snapshot_config.full_snapshot_archives_dir, - ); - let snapshot_archive_info = - if let Some(full_snapshot_archive_info) = full_snapshot_archive_info { - if request.uri().path() == FULL_SNAPSHOT_REQUEST_PATH { - Some(full_snapshot_archive_info.snapshot_archive_info().clone()) - } else { - snapshot_paths::get_highest_incremental_snapshot_archive_info( - &snapshot_config.incremental_snapshot_archives_dir, - full_snapshot_archive_info.slot(), - ) - .map(|incremental_snapshot_archive_info| { - incremental_snapshot_archive_info - .snapshot_archive_info() - .clone() - }) - } + if let Some(ref snapshot_config) = self.snapshot_config + && (request.uri().path() == FULL_SNAPSHOT_REQUEST_PATH + || request.uri().path() == INCREMENTAL_SNAPSHOT_REQUEST_PATH) + { + // Convenience redirect to the latest snapshot + let full_snapshot_archive_info = snapshot_paths::get_highest_full_snapshot_archive_info( + &snapshot_config.full_snapshot_archives_dir, + ); + let snapshot_archive_info = + if let Some(full_snapshot_archive_info) = full_snapshot_archive_info { + if request.uri().path() == FULL_SNAPSHOT_REQUEST_PATH { + Some(full_snapshot_archive_info.snapshot_archive_info().clone()) } else { - None - }; - return if let Some(snapshot_archive_info) = snapshot_archive_info { - RpcRequestMiddleware::redirect(&format!( - "/{}", - snapshot_archive_info - .path - .file_name() - .unwrap_or_else(|| std::ffi::OsStr::new("")) - .to_str() - .unwrap_or("") - )) + snapshot_paths::get_highest_incremental_snapshot_archive_info( + &snapshot_config.incremental_snapshot_archives_dir, + full_snapshot_archive_info.slot(), + ) + .map(|incremental_snapshot_archive_info| { + incremental_snapshot_archive_info + .snapshot_archive_info() + .clone() + }) + } } else { - RpcRequestMiddleware::not_found() - } - .into(); + None + }; + return if let Some(snapshot_archive_info) = snapshot_archive_info { + RpcRequestMiddleware::redirect(&format!( + "/{}", + snapshot_archive_info + .path + .file_name() + .unwrap_or_else(|| std::ffi::OsStr::new("")) + .to_str() + .unwrap_or("") + )) + } else { + RpcRequestMiddleware::not_found() } + .into(); } if let Some(path) = match_supply_path(request.uri().path()) { - process_rest(&self.bank_forks, path) + process_rest(self.bank_forks.clone(), path) } else if self.is_file_get_path(request.uri().path()) { self.process_file_get(request.uri().path()) } else if request.uri().path() == "/health" { @@ -433,7 +431,7 @@ async fn calculate_circulating_supply_async(bank: &Arc) -> Result>, path: &str) -> Option { +async fn handle_rest(bank_forks: &RwLock, path: &str) -> Option { match path { "/v0/circulating-supply" => { let bank = bank_forks.read().unwrap().root_bank(); @@ -452,8 +450,7 @@ async fn handle_rest(bank_forks: &Arc>, path: &str) -> Option< } } -fn process_rest(bank_forks: &Arc>, path: &str) -> RequestMiddlewareAction { - let bank_forks = bank_forks.clone(); +fn process_rest(bank_forks: Arc>, path: &str) -> RequestMiddlewareAction { let path = path.to_string(); RequestMiddlewareAction::Respond { @@ -512,9 +509,16 @@ impl JsonRpcService { config.rpc_config.rpc_blocking_threads, config.rpc_config.rpc_niceness_adj, ); - let leader_info = config - .poh_recorder - .map(|recorder| ClusterTpuInfo::new(config.cluster_info.clone(), recorder)); + let migration_status = config.bank_forks.read().unwrap().migration_status(); + let leader_info = config.poh_recorder.clone().map(|recorder| { + ClusterTpuInfo::new( + config.cluster_info.clone(), + recorder, + config.block_commitment_cache.clone(), + config.leader_schedule_cache.clone(), + migration_status.clone(), + ) + }); let RpcTpuClientArgs(identity_keypair, tpu_client_socket, client_runtime, cancel) = config.rpc_tpu_client_args; @@ -685,7 +689,7 @@ impl JsonRpcService { ); let _send_transaction_service = Arc::new(SendTransactionService::new( - &bank_forks, + bank_forks.clone(), receiver, client.clone(), send_transaction_service_config, @@ -716,7 +720,7 @@ impl JsonRpcService { let request_middleware = RpcRequestMiddleware::new( ledger_path, snapshot_config, - bank_forks.clone(), + bank_forks, health.clone(), ); let server = ServerBuilder::with_meta_extractor( diff --git a/rpc/src/rpc_subscription_tracker.rs b/rpc/src/rpc_subscription_tracker.rs index 838c712502d..bae372d6c16 100644 --- a/rpc/src/rpc_subscription_tracker.rs +++ b/rpc/src/rpc_subscription_tracker.rs @@ -19,7 +19,7 @@ use { fmt, sync::{ Arc, RwLock, Weak, - atomic::{AtomicU64, Ordering}, + atomic::{AtomicU64, AtomicUsize, Ordering}, }, }, thiserror::Error, @@ -186,6 +186,10 @@ struct SubscriptionControlInner { subscriptions: DashMap, next_id: AtomicU64, max_active_subscriptions: usize, + // Number of live `SubscriptionToken`s issued by `subscribe()`. Counts + // duplicate subscribers to the same params separately so the configured + // cap is enforced per subscriber, not per deduplicated upstream stream. + subscriber_count: AtomicUsize, sender: crossbeam_channel::Sender, broadcast_sender: broadcast::Sender, counter: TokenCounter, @@ -201,6 +205,7 @@ impl SubscriptionControl { subscriptions: DashMap::new(), next_id: AtomicU64::new(0), max_active_subscriptions, + subscriber_count: AtomicUsize::new(0), sender, broadcast_sender, counter: TokenCounter::new("rpc_pubsub_total_subscriptions"), @@ -216,54 +221,69 @@ impl SubscriptionControl { "Total existing subscriptions: {}", self.0.subscriptions.len() ); - let count = self.0.subscriptions.len(); - let create_token_and_weak_ref = |id, params| { - let token = SubscriptionToken( - Arc::new(SubscriptionTokenInner { - control: Arc::clone(&self.0), - params, - id, - }), - self.0.counter.create_token(), - ); - let weak_ref = WeakSubscriptionTokenRef(Arc::downgrade(&token.0), token.0.id); - (token, weak_ref) + // Reserve a subscriber slot up front. The cap is per live token holder, + // not per deduplicated upstream stream, so duplicate subscribes to the + // same params each consume their own slot. + let subscriber_guard = SubscriberCountGuard::try_reserve(&self.0).ok_or_else(|| { + inc_new_counter_info!("rpc-subscription-refused-limit-reached", 1); + Error::TooManySubscriptions + })?; + + let create_inner = |id, params| { + Arc::new(SubscriptionTokenInner { + control: Arc::clone(&self.0), + params, + id, + }) }; match self.0.subscriptions.entry(params) { DashEntry::Occupied(mut entry) => match entry.get().0.upgrade() { - Some(token_ref) => Ok(SubscriptionToken(token_ref, self.0.counter.create_token())), + Some(inner) => Ok(SubscriptionToken( + inner, + self.0.counter.create_token(), + subscriber_guard, + )), // This means the last Arc for this Weak pointer entered the drop just before us, // but could not remove the entry since we are holding the write lock. // See `Drop` implementation for `SubscriptionTokenInner` for further info. None => { - let (token, weak_ref) = - create_token_and_weak_ref(entry.get().1, entry.key().clone()); + let inner = create_inner(entry.get().1, entry.key().clone()); + let weak_ref = WeakSubscriptionTokenRef(Arc::downgrade(&inner), inner.id); entry.insert(weak_ref); - Ok(token) + Ok(SubscriptionToken( + inner, + self.0.counter.create_token(), + subscriber_guard, + )) } }, DashEntry::Vacant(entry) => { - if count >= self.0.max_active_subscriptions { - inc_new_counter_info!("rpc-subscription-refused-limit-reached", 1); - return Err(Error::TooManySubscriptions); - } let id = SubscriptionId::from(self.0.next_id.fetch_add(1, Ordering::AcqRel)); - let (token, weak_ref) = create_token_and_weak_ref(id, entry.key().clone()); + let inner = create_inner(id, entry.key().clone()); + let weak_ref = WeakSubscriptionTokenRef(Arc::downgrade(&inner), id); let _ = self .0 .sender - .send(NotificationEntry::Subscribed(token.0.params.clone(), id).into()); + .send(NotificationEntry::Subscribed(inner.params.clone(), id).into()); entry.insert(weak_ref); datapoint_info!( "rpc-subscription", ("total", self.0.subscriptions.len(), i64) ); - Ok(token) + Ok(SubscriptionToken( + inner, + self.0.counter.create_token(), + subscriber_guard, + )) } } } + pub fn subscriber_total(&self) -> usize { + self.0.subscriber_count.load(Ordering::Relaxed) + } + pub fn total(&self) -> usize { self.0.subscriptions.len() } @@ -480,7 +500,6 @@ impl SubscriptionsTracker { } } - #[allow(clippy::collapsible_if)] pub fn unsubscribe(&mut self, params: SubscriptionParams, id: SubscriptionId) { match ¶ms { SubscriptionParams::Logs(params) => { @@ -500,20 +519,16 @@ impl SubscriptionsTracker { } _ => {} } - if params.is_commitment_watcher() { - if self.commitment_watchers.remove(&id).is_none() { - warn!("Subscriptions inconsistency (missing entry in commitment_watchers)"); - } + if params.is_commitment_watcher() && self.commitment_watchers.remove(&id).is_none() { + warn!("Subscriptions inconsistency (missing entry in commitment_watchers)"); } - if params.is_gossip_watcher() { - if self.gossip_watchers.remove(&id).is_none() { - warn!("Subscriptions inconsistency (missing entry in gossip_watchers)"); - } + if params.is_gossip_watcher() && self.gossip_watchers.remove(&id).is_none() { + warn!("Subscriptions inconsistency (missing entry in gossip_watchers)"); } - if params.is_node_progress_watcher() { - if self.node_progress_watchers.remove(¶ms).is_none() { - warn!("Subscriptions inconsistency (missing entry in node_progress_watchers)"); - } + if params.is_node_progress_watcher() + && self.node_progress_watchers.remove(¶ms).is_none() + { + warn!("Subscriptions inconsistency (missing entry in node_progress_watchers)"); } } @@ -551,7 +566,6 @@ impl fmt::Debug for SubscriptionTokenInner { } impl Drop for SubscriptionTokenInner { - #[allow(clippy::collapsible_if)] fn drop(&mut self) { match self.control.subscriptions.entry(self.params.clone()) { DashEntry::Vacant(_) => { @@ -577,11 +591,36 @@ impl Drop for SubscriptionTokenInner { } } -// allowing dead code here to appease clippy, but unsure if/how the CounterToken is actually used. -// further investigation would be necessary before removing -#[allow(dead_code)] -#[derive(Clone)] -pub struct SubscriptionToken(Arc, CounterToken); +// RAII guard for one subscriber slot. The slot is reserved atomically against +// the configured cap by `try_reserve` (returns `None` if the cap is reached) +// and released on drop. Intentionally not `Clone`: each holder must go through +// `try_reserve` so duplicates cannot bypass the cap. +struct SubscriberCountGuard(Arc); + +impl SubscriberCountGuard { + fn try_reserve(control: &Arc) -> Option { + let max = control.max_active_subscriptions; + control + .subscriber_count + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| { + (current < max).then_some(current + 1) + }) + .ok()?; + Some(Self(Arc::clone(control))) + } +} + +impl Drop for SubscriberCountGuard { + fn drop(&mut self) { + self.0.subscriber_count.fetch_sub(1, Ordering::Relaxed); + } +} + +pub struct SubscriptionToken( + Arc, + #[allow(dead_code)] CounterToken, + #[allow(dead_code)] SubscriberCountGuard, +); impl SubscriptionToken { pub fn id(&self) -> SubscriptionId { @@ -598,6 +637,7 @@ mod tests { use { super::*, crate::rpc_pubsub_service::PubSubConfig, + crossbeam_channel::bounded, solana_ledger::genesis_utils::{GenesisConfigInfo, create_genesis_config}, solana_runtime::bank::Bank, }; @@ -609,7 +649,7 @@ mod tests { impl ControlWrapper { fn new() -> Self { - let (sender, receiver) = crossbeam_channel::unbounded(); + let (sender, receiver) = bounded(1024); let (broadcast_sender, _broadcast_receiver) = broadcast::channel(42); let control = SubscriptionControl::new( @@ -655,12 +695,75 @@ mod tests { control.assert_unsubscribed(&SubscriptionParams::Slot, 0); } + #[test] + fn recreates_inner_when_weak_upgrade_fails() { + // Exercise the race-window branch in `subscribe()` where a stale + // `WeakSubscriptionTokenRef` is still in the DashMap because the + // previous holder's `SubscriptionTokenInner::drop` is blocked on the + // entry lock. A fresh subscribe must recreate the inner and reuse the + // stored SubscriptionId rather than allocating a new one. + let (sender, receiver) = bounded(1024); + let (broadcast_sender, _broadcast_receiver) = broadcast::channel(42); + let control = SubscriptionControl::new(2, sender, broadcast_sender); + + let stored_id = SubscriptionId::from(42); + let dead_weak = { + let arc = Arc::new(SubscriptionTokenInner { + control: Arc::clone(&control.0), + params: SubscriptionParams::Slot, + id: stored_id, + }); + let weak = Arc::downgrade(&arc); + WeakSubscriptionTokenRef(weak, stored_id) + // `arc` drops here; its Drop sees Vacant and logs a warning. + }; + control + .0 + .subscriptions + .insert(SubscriptionParams::Slot, dead_weak); + + let token = control.subscribe(SubscriptionParams::Slot).unwrap(); + assert_eq!(token.id(), stored_id); + assert_eq!(control.total(), 1); + assert_eq!(control.subscriber_total(), 1); + // Drain notifications emitted by the test (Drop-on-vacant warning is + // log-only; the only entry should be the Subscribed from the recreate). + while receiver.try_recv().is_ok() {} + drop(token); + } + + #[test] + fn duplicate_params_consume_separate_subscriber_slots() { + // Cap of 1 must reject a second subscriber even when params match an + // existing subscription (GHSA: duplicate-params cap bypass). + let (sender, _receiver) = bounded(1024); + let (broadcast_sender, _broadcast_receiver) = broadcast::channel(42); + let control = SubscriptionControl::new(1, sender, broadcast_sender); + + let token1 = control.subscribe(SubscriptionParams::Slot).unwrap(); + assert_eq!(control.total(), 1); + assert_eq!(control.subscriber_total(), 1); + + assert!(matches!( + control.subscribe(SubscriptionParams::Slot), + Err(Error::TooManySubscriptions) + )); + assert_eq!(control.subscriber_total(), 1); + + drop(token1); + // After the only holder drops, a fresh subscriber should fit again. + let token2 = control.subscribe(SubscriptionParams::Slot).unwrap(); + assert_eq!(control.subscriber_total(), 1); + drop(token2); + assert_eq!(control.subscriber_total(), 0); + } + #[test] fn notify_subscribe_multiple() { let control = ControlWrapper::new(); let token1 = control.control.subscribe(SubscriptionParams::Slot).unwrap(); control.assert_subscribed(&SubscriptionParams::Slot, 0); - let token2 = token1.clone(); + let token2 = control.control.subscribe(SubscriptionParams::Slot).unwrap(); drop(token1); let token3 = control.control.subscribe(SubscriptionParams::Slot).unwrap(); drop(token3); diff --git a/rpc/src/rpc_subscriptions.rs b/rpc/src/rpc_subscriptions.rs index 37604500dcd..88b2b2b6e8d 100644 --- a/rpc/src/rpc_subscriptions.rs +++ b/rpc/src/rpc_subscriptions.rs @@ -136,7 +136,7 @@ impl std::fmt::Debug for NotificationEntry { fn check_commitment_and_notify( params: &P, subscription: &SubscriptionInfo, - bank_forks: &Arc>, + bank_forks: &RwLock, slot: Slot, bank_method: B, filter_results: F, @@ -318,9 +318,6 @@ impl RpcNotifier { // just as the notifier generates a notification for it. let _ = self.sender.send(notification); - inc_new_counter_info!("rpc-pubsub-messages", 1); - inc_new_counter_info!("rpc-pubsub-bytes", buf_arc.len()); - self.recent_items.lock().unwrap().push(buf_arc); } } @@ -339,10 +336,10 @@ fn filter_block_result_txs( .collect(), }; - if block.transactions.is_empty() { - if let BlockSubscriptionKind::MentionsAccountOrProgram(_) = params.kind { - return Ok(None); - } + if block.transactions.is_empty() + && let BlockSubscriptionKind::MentionsAccountOrProgram(_) = params.kind + { + return Ok(None); } let block = ConfirmedBlock::from(block) @@ -483,6 +480,11 @@ fn initial_last_notified_slot( #[derive(Default)] struct PubsubNotificationStats { since: Option, + notify_slot_count: u64, + notify_slot_update_count: u64, + notify_vote_count: u64, + notify_root_count: u64, + notify_signature_count: u64, notification_entry_processing_count: u64, notification_entry_processing_time_us: u64, } @@ -496,6 +498,15 @@ impl PubsubNotificationStats { } datapoint_info!( "pubsub_notification_entries", + ("notify_slot_count", self.notify_slot_count, i64), + ( + "notify_slot_update_count", + self.notify_slot_update_count, + i64 + ), + ("notify_vote_count", self.notify_vote_count, i64), + ("notify_root_count", self.notify_root_count, i64), + ("notify_signature_count", self.notify_signature_count, i64), ( "notification_entry_processing_count", self.notification_entry_processing_count, @@ -777,7 +788,7 @@ impl RpcSubscriptions { .get(&SubscriptionParams::Slot) { debug!("slot notify: {slot_info:?}"); - inc_new_counter_info!("rpc-subscription-notify-slot", 1); + stats.notify_slot_count += 1; notifier.notify(slot_info, sub, false); } } @@ -786,7 +797,8 @@ impl RpcSubscriptions { .node_progress_watchers() .get(&SubscriptionParams::SlotsUpdates) { - inc_new_counter_info!("rpc-subscription-notify-slots-updates", 1); + debug!("slot update notify: {slot_update:?}"); + stats.notify_slot_update_count += 1; notifier.notify(slot_update, sub, false); } } @@ -806,7 +818,7 @@ impl RpcSubscriptions { signature: signature.to_string(), }; debug!("vote notify: {vote_info:?}"); - inc_new_counter_info!("rpc-subscription-notify-vote", 1); + stats.notify_vote_count += 1; notifier.notify(&rpc_vote, sub, false); } } @@ -816,7 +828,7 @@ impl RpcSubscriptions { .get(&SubscriptionParams::Root) { debug!("root notify: {root:?}"); - inc_new_counter_info!("rpc-subscription-notify-root", 1); + stats.notify_root_count += 1; notifier.notify(root, sub, false); } } @@ -857,6 +869,7 @@ impl RpcSubscriptions { subscription.params() { if params.enable_received_notification { + stats.notify_signature_count += 1; notifier.notify( RpcResponse::from(RpcNotificationResponse { context: RpcNotificationContext { slot }, @@ -895,7 +908,7 @@ impl RpcSubscriptions { fn notify_watchers( max_complete_transaction_status_slot: Arc, subscriptions: &HashMap>, - bank_forks: &Arc>, + bank_forks: &RwLock, blockstore: &Blockstore, commitment_slots: &CommitmentSlots, notifier: &RpcNotifier, @@ -2720,7 +2733,9 @@ pub(crate) mod tests { let bank2 = Bank::new_from_parent(bank0, SlotLeader::default(), 2); bank_forks.write().unwrap().insert(bank2); - let alice = Keypair::from_base58_string("sfLnS4rZ5a8gXke3aGxCgM6usFAVPxLUaBSRdssGY9uS5eoiEWQ41CqDcpXbcekpKsie8Lyy3LNFdhEvjUE1wd9"); + let alice = Keypair::from_base58_string( + "sfLnS4rZ5a8gXke3aGxCgM6usFAVPxLUaBSRdssGY9uS5eoiEWQ41CqDcpXbcekpKsie8Lyy3LNFdhEvjUE1wd9", + ); let optimistically_confirmed_bank = OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks); diff --git a/rpc/src/transaction_status_service.rs b/rpc/src/transaction_status_service.rs index 516755d5491..4c241e4f5a0 100644 --- a/rpc/src/transaction_status_service.rs +++ b/rpc/src/transaction_status_service.rs @@ -259,10 +259,10 @@ impl TransactionStatusService { blockstore.write_batch(batch)?; } - if let Some(dependency_tracker) = dependency_tracker.as_ref() { - if let Some(work_id) = work_id { - dependency_tracker.mark_this_and_all_previous_work_processed(work_id); - } + if let Some(dependency_tracker) = dependency_tracker.as_ref() + && let Some(work_id) = work_id + { + dependency_tracker.mark_this_and_all_previous_work_processed(work_id); } } TransactionStatusMessage::Freeze(bank) => { @@ -349,7 +349,7 @@ pub(crate) mod tests { super::*, crate::transaction_notifier_interface::TransactionNotifier, agave_reserved_account_keys::ReservedAccountKeys, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, dashmap::DashMap, solana_account::state_traits::StateMut, solana_account_decoder::{ @@ -442,7 +442,7 @@ pub(crate) mod tests { let genesis_config = create_genesis_config(2).genesis_config; let (bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); - let (transaction_status_sender, transaction_status_receiver) = unbounded(); + let (transaction_status_sender, transaction_status_receiver) = bounded(1024); let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()) .expect("Expected to be able to open database ledger"); @@ -570,7 +570,7 @@ pub(crate) mod tests { let genesis_config = create_genesis_config(2).genesis_config; let (bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); - let (transaction_status_sender, transaction_status_receiver) = unbounded(); + let (transaction_status_sender, transaction_status_receiver) = bounded(1024); let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()) .expect("Expected to be able to open database ledger"); diff --git a/runtime-transaction/Cargo.toml b/runtime-transaction/Cargo.toml index 607c4eca939..ade769c8f5f 100644 --- a/runtime-transaction/Cargo.toml +++ b/runtime-transaction/Cargo.toml @@ -7,10 +7,12 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] diff --git a/runtime-transaction/src/lib.rs b/runtime-transaction/src/lib.rs index 176d884c868..36ca46f04d3 100644 --- a/runtime-transaction/src/lib.rs +++ b/runtime-transaction/src/lib.rs @@ -4,6 +4,7 @@ mod instruction_data_len; pub(crate) mod instruction_meta; pub mod runtime_transaction; +pub mod sanitize_config; pub mod signature_details; pub mod transaction_meta; pub mod transaction_with_meta; diff --git a/runtime-transaction/src/runtime_transaction/sdk_transactions.rs b/runtime-transaction/src/runtime_transaction/sdk_transactions.rs index 611d6986a55..3288fd25f80 100644 --- a/runtime-transaction/src/runtime_transaction/sdk_transactions.rs +++ b/runtime-transaction/src/runtime_transaction/sdk_transactions.rs @@ -1,15 +1,13 @@ use { - super::{ComputeBudgetInstructionDetails, RuntimeTransaction}, + super::RuntimeTransaction, crate::{ instruction_meta::InstructionMeta, transaction_meta::{ - CachedTransactionMeta, TransactionConfiguration, TransactionMeta, - VersionedTransactionConfiguration, + CachedTransactionMeta, TransactionMeta, VersionedTransactionConfiguration, }, transaction_with_meta::TransactionWithMeta, }, - solana_message::{AddressLoader, TransactionSignatureDetails, VersionedMessage}, - solana_program_entrypoint::HEAP_LENGTH, + solana_message::{AddressLoader, TransactionSignatureDetails}, solana_pubkey::Pubkey, solana_svm_transaction::instruction::SVMInstruction, solana_transaction::{ @@ -56,27 +54,10 @@ impl RuntimeTransaction { precompile_signature_details.num_secp256r1_instruction_signatures, ); - let versioned_transaction_config = match &sanitized_versioned_tx.get_message().message { - VersionedMessage::V1(msg) => { - VersionedTransactionConfiguration::V1(TransactionConfiguration { - priority_fee_lamports: msg.config.priority_fee.unwrap_or(0), - compute_unit_limit: msg.config.compute_unit_limit.unwrap_or(0), - loaded_accounts_data_size_limit: msg - .config - .loaded_accounts_data_size_limit - .unwrap_or(0), - updated_heap_bytes: msg.config.heap_size.unwrap_or(HEAP_LENGTH as u32), - }) - } - _ => VersionedTransactionConfiguration::LegacyAndV0( - ComputeBudgetInstructionDetails::try_from( - sanitized_versioned_tx - .get_message() - .program_instructions_iter() - .map(|(program_id, ix)| (program_id, SVMInstruction::from(ix))), - )?, - ), - }; + let versioned_transaction_config = + VersionedTransactionConfiguration::try_from_sanitized_versioned_message( + sanitized_versioned_tx.get_message(), + )?; Ok(Self { transaction: sanitized_versioned_tx, diff --git a/runtime-transaction/src/runtime_transaction/transaction_view.rs b/runtime-transaction/src/runtime_transaction/transaction_view.rs index 110c43f5e63..452d2e4bc86 100644 --- a/runtime-transaction/src/runtime_transaction/transaction_view.rs +++ b/runtime-transaction/src/runtime_transaction/transaction_view.rs @@ -256,6 +256,7 @@ impl TransactionWithMeta for RuntimeTransaction>::try_new( transaction, @@ -313,7 +317,8 @@ mod tests { ) { let bytes = wincode::serialize(&original_transaction).unwrap(); let transaction_view = - SanitizedTransactionView::try_new_sanitized(&bytes[..], true).unwrap(); + SanitizedTransactionView::try_new_sanitized(&bytes[..], &sanitize_config(true)) + .unwrap(); let runtime_transaction = RuntimeTransaction::>::try_new( transaction_view, MessageHash::Compute, @@ -380,7 +385,8 @@ mod tests { let bytes = wincode::serialize(&original_transaction.to_versioned_transaction()).unwrap(); let transaction_view = - SanitizedTransactionView::try_new_sanitized(&bytes[..], true).unwrap(); + SanitizedTransactionView::try_new_sanitized(&bytes[..], &sanitize_config(true)) + .unwrap(); let runtime_transaction = RuntimeTransaction::>::try_new( transaction_view, MessageHash::Compute, @@ -467,8 +473,11 @@ mod tests { Hash::new_unique(), ))) .unwrap(); - let transaction_view = - SanitizedTransactionView::try_new_sanitized(&serialized_transaction[..], true).unwrap(); + let transaction_view = SanitizedTransactionView::try_new_sanitized( + &serialized_transaction[..], + &sanitize_config(true), + ) + .unwrap(); let runtime_transaction = RuntimeTransaction::>::try_new( transaction_view, MessageHash::Compute, diff --git a/runtime-transaction/src/sanitize_config.rs b/runtime-transaction/src/sanitize_config.rs new file mode 100644 index 00000000000..f16fa5aa682 --- /dev/null +++ b/runtime-transaction/src/sanitize_config.rs @@ -0,0 +1,25 @@ +//! Canonical [`SanitizeConfig`] construction for agave. +//! +//! `agave-transaction-view` does not depend on agave crates, so the protocol +//! limits enforced during sanitization are supplied by the caller. This module +//! is the single place where those limits are sourced from the agave constants. + +use { + agave_transaction_view::sanitize::SanitizeConfig, + solana_compute_budget::compute_budget_limits::{MAX_HEAP_FRAME_BYTES, MIN_HEAP_FRAME_BYTES}, + solana_transaction_context::{MAX_ACCOUNTS_PER_INSTRUCTION, MAX_INSTRUCTION_TRACE_LENGTH}, +}; + +/// Returns the [`SanitizeConfig`] with current protocol limits. +/// +/// `enable_instruction_accounts_limit` should reflect the +/// `limit_instruction_accounts` (SIMD-406) feature activation. +pub fn sanitize_config(enable_instruction_accounts_limit: bool) -> SanitizeConfig { + SanitizeConfig { + min_requested_heap_size: MIN_HEAP_FRAME_BYTES, + max_requested_heap_size: MAX_HEAP_FRAME_BYTES, + max_instructions: MAX_INSTRUCTION_TRACE_LENGTH, + max_accounts_per_instruction: enable_instruction_accounts_limit + .then_some(MAX_ACCOUNTS_PER_INSTRUCTION), + } +} diff --git a/runtime-transaction/src/transaction_meta.rs b/runtime-transaction/src/transaction_meta.rs index 56bb3fe1b70..d638a16b8b3 100644 --- a/runtime-transaction/src/transaction_meta.rs +++ b/runtime-transaction/src/transaction_meta.rs @@ -19,7 +19,13 @@ use { }, solana_compute_budget_instruction::compute_budget_instruction_details::ComputeBudgetInstructionDetails, solana_hash::Hash, - solana_message::TransactionSignatureDetails, + solana_message::{ + SanitizedMessage, SanitizedVersionedMessage, TransactionSignatureDetails, VersionedMessage, + v1::TransactionConfig, + }, + solana_program_entrypoint::HEAP_LENGTH, + solana_pubkey::Pubkey, + solana_svm_transaction::{instruction::SVMInstruction, svm_message::SVMStaticMessage}, solana_transaction::TransactionError, }; @@ -54,6 +60,14 @@ pub struct TransactionConfiguration { } impl TransactionConfiguration { + pub fn try_from_sanitized_message( + message: &SanitizedMessage, + feature_set: &FeatureSet, + ) -> Result { + VersionedTransactionConfiguration::try_from_sanitized_message(message)? + .try_into_config(feature_set) + } + /// Compute the compute unit price in micro-lamports per compute unit. /// /// Note: that this will return an effective price according to the actual @@ -77,6 +91,51 @@ pub(crate) enum VersionedTransactionConfiguration { } impl VersionedTransactionConfiguration { + pub(crate) fn try_from_sanitized_message( + message: &SanitizedMessage, + ) -> Result { + match message { + SanitizedMessage::V1(message) => Ok(Self::from_v1_config(&message.message.config)), + SanitizedMessage::Legacy(_) | SanitizedMessage::V0(_) => { + Self::try_from_legacy_and_v0_instructions( + SVMStaticMessage::program_instructions_iter(message), + ) + } + } + } + + pub(crate) fn try_from_sanitized_versioned_message( + message: &SanitizedVersionedMessage, + ) -> Result { + match &message.message { + VersionedMessage::V1(message) => Ok(Self::from_v1_config(&message.config)), + VersionedMessage::Legacy(_) | VersionedMessage::V0(_) => { + Self::try_from_legacy_and_v0_instructions( + message + .program_instructions_iter() + .map(|(program_id, ix)| (program_id, SVMInstruction::from(ix))), + ) + } + } + } + + fn from_v1_config(config: &TransactionConfig) -> Self { + Self::V1(TransactionConfiguration { + priority_fee_lamports: config.priority_fee.unwrap_or(0), + compute_unit_limit: config.compute_unit_limit.unwrap_or(0), + loaded_accounts_data_size_limit: config.loaded_accounts_data_size_limit.unwrap_or(0), + updated_heap_bytes: config.heap_size.unwrap_or(HEAP_LENGTH as u32), + }) + } + + fn try_from_legacy_and_v0_instructions<'a>( + instructions: impl Iterator)> + Clone, + ) -> Result { + Ok(Self::LegacyAndV0( + ComputeBudgetInstructionDetails::try_from(instructions)?, + )) + } + pub(crate) fn try_into_config( &self, feature_set: &FeatureSet, diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index d8a7326d38a..09102d1d316 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -19,7 +21,6 @@ name = "solana_runtime" [features] agave-unstable-api = [] dev-context-only-utils = [ - "agave-feature-set/dev-context-only-utils", "agave-votor-messages/dev-context-only-utils", "solana-accounts-db/dev-context-only-utils", "solana-program-binaries", @@ -66,8 +67,10 @@ arrayref = { workspace = true } assert_matches = { workspace = true } base64 = { workspace = true } bincode = { workspace = true } +bitvec = { workspace = true } bytemuck = { workspace = true } crossbeam-channel = { workspace = true } +crossbeam-utils = { workspace = true } dashmap = { workspace = true, features = ["rayon", "raw-api", "serde"] } imbl = { workspace = true, features = ["rayon", "serde"] } itertools = { workspace = true } @@ -76,7 +79,6 @@ log = { workspace = true } mockall = { workspace = true } num-derive = { workspace = true } num-traits = { workspace = true } -num_cpus = { workspace = true } percentage = { workspace = true } qualifier_attr = { workspace = true } rand = { workspace = true } @@ -92,7 +94,7 @@ solana-account = { workspace = true } solana-account-info = { workspace = true } solana-accounts-db = { workspace = true } solana-address-lookup-table-interface = { workspace = true } -solana-bls-signatures = { workspace = true } +solana-bls-signatures = { workspace = true, features = ["wincode"] } solana-builtins = { workspace = true } solana-client-traits = { workspace = true } solana-clock = { workspace = true } @@ -140,7 +142,7 @@ solana-poh-config = { workspace = true } solana-precompile-error = { workspace = true } solana-program-binaries = { workspace = true, optional = true } solana-program-runtime = { workspace = true, features = ["metrics"] } -solana-pubkey = { workspace = true, features = ["rand"] } +solana-pubkey = { workspace = true, features = ["rand", "wincode"] } solana-rayon-threadlimit = { workspace = true } solana-rent = { workspace = true } solana-reward-info = { workspace = true } @@ -153,8 +155,8 @@ solana-sha256-hasher = { workspace = true } solana-signature = { workspace = true } solana-signer = { workspace = true } solana-signer-store = { workspace = true } -solana-slot-hashes = { workspace = true } -solana-slot-history = { workspace = true } +solana-slot-hashes = { workspace = true, features = ["wincode"] } +solana-slot-history = { workspace = true, features = ["wincode"] } solana-stake-interface = { workspace = true } solana-svm = { workspace = true, features = ["metrics"] } solana-svm-callback = { workspace = true } @@ -179,7 +181,6 @@ spl-generic-token = { workspace = true } static_assertions = { workspace = true } strum = { workspace = true, features = ["derive"] } strum_macros = { workspace = true } -symlink = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } wincode = { workspace = true } diff --git a/runtime/src/account_saver.rs b/runtime/src/account_saver.rs index 6e554ef1810..78eb22a1630 100644 --- a/runtime/src/account_saver.rs +++ b/runtime/src/account_saver.rs @@ -80,6 +80,7 @@ pub fn collect_accounts_to_store<'a, T: SVMMessage>( transaction, transaction_ref, &executed_tx.loaded_transaction.accounts, + &executed_tx.loaded_transaction.touched_flags, ); } else { collect_accounts_for_failed_tx( @@ -109,12 +110,18 @@ fn collect_accounts_for_successful_tx<'a, T: SVMMessage>( transaction: &'a T, transaction_ref: Option<&'a SanitizedTransaction>, transaction_accounts: &'a [KeyedAccountSharedData], + touched_flags: &[bool], ) { for (i, (address, account)) in (0..transaction.account_keys().len()).zip(transaction_accounts) { if !transaction.is_writable(i) { continue; } + // Skip write-locked accounts the transaction left unmodified. + if !touched_flags[i] { + continue; + } + // Accounts that are invoked and also not passed as an instruction // account to a program don't need to be stored because it's assumed // to be impossible for a committable transaction to modify an @@ -174,9 +181,15 @@ mod tests { solana_system_interface::{instruction as system_instruction, program as system_program}, solana_transaction::{Transaction, sanitized::SanitizedTransaction}, solana_transaction_error::{TransactionError, TransactionResult as Result}, - std::collections::HashMap, + std::collections::{HashMap, HashSet}, }; + /// Builds touched flags for `num_total` accounts with the first + /// `num_touched` of them marked as touched. + fn touched_flags_for_test(num_touched: usize, num_total: usize) -> Box<[bool]> { + (0..num_total).map(|index| index < num_touched).collect() + } + fn new_sanitized_tx( from_keypairs: &T, message: Message, @@ -252,16 +265,22 @@ mod tests { ]; let tx1 = new_sanitized_tx(&[&keypair1], message, Hash::default()); + let touched0 = + touched_flags_for_test(transaction_accounts0.len(), transaction_accounts0.len()); let loaded0 = LoadedTransaction { accounts: transaction_accounts0, + touched_flags: touched0, fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::default(), compute_budget: SVMTransactionExecutionBudget::default(), loaded_accounts_data_size: 0, }; + let touched1 = + touched_flags_for_test(transaction_accounts1.len(), transaction_accounts1.len()); let loaded1 = LoadedTransaction { accounts: transaction_accounts1, + touched_flags: touched1, fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::default(), compute_budget: SVMTransactionExecutionBudget::default(), @@ -303,6 +322,65 @@ mod tests { } } + #[test] + fn test_collect_accounts_to_store_skips_untouched_accounts() { + let fee_payer = Keypair::new(); + let (touched_key, untouched_key) = (solana_pubkey::new_rand(), solana_pubkey::new_rand()); + + let fee_payer_account = AccountSharedData::new(100, 0, &Pubkey::default()); + let touched_account = AccountSharedData::new(1, 0, &Pubkey::default()); + let untouched_account = AccountSharedData::new(2, 0, &Pubkey::default()); + let program_account = AccountSharedData::new(3, 0, &Pubkey::default()); + + // Writable accounts at indices 0, 1, 2, plus a readonly program at index 3. + let instructions = vec![CompiledInstruction::new(3, &(), vec![1, 2])]; + let message = Message::new_with_compiled_instructions( + 1, // num_required_signatures + 0, // num_readonly_signed_accounts + 1, // num_readonly_unsigned_accounts -> only the program is readonly + vec![ + fee_payer.pubkey(), + touched_key, + untouched_key, + native_loader::id(), + ], + Hash::default(), + instructions, + ); + let transaction_accounts = vec![ + (message.account_keys[0], fee_payer_account), + (message.account_keys[1], touched_account), + (message.account_keys[2], untouched_account), + (message.account_keys[3], program_account), + ]; + let tx = new_sanitized_tx(&[&fee_payer], message, Hash::default()); + + // The processor marks the fee payer (index 0) and every account the VM + // modified. Here only index 1 was modified; the writable account at + // index 2 was left untouched and must not be written back. + let loaded = LoadedTransaction { + touched_flags: touched_flags_for_test(2, transaction_accounts.len()), + accounts: transaction_accounts, + ..Default::default() + }; + + let txs = vec![tx]; + let processing_results = vec![new_executed_processing_result(Ok(()), loaded)]; + + let transaction_refs: Option> = None; + let (collected_accounts, _transactions) = + collect_accounts_to_store(&txs, &transaction_refs, &processing_results); + + // Only the fee payer and the touched writable account are stored; the + // untouched writable account and the readonly program are skipped. + let collected_keys = + HashSet::from_iter(collected_accounts.into_iter().map(|(pubkey, _act)| *pubkey)); + assert_eq!( + collected_keys, + HashSet::from([fee_payer.pubkey(), touched_key]), + ); + } + #[test] fn test_collect_accounts_for_failed_tx_rollback_fee_payer_only() { let from = keypair_from_seed(&[1; 32]).unwrap(); @@ -322,8 +400,13 @@ mod tests { let from_account_pre = AccountSharedData::new(4242, 0, &Pubkey::default()); + let touched_flags = + touched_flags_for_test(transaction_accounts.len(), transaction_accounts.len()); let loaded = LoadedTransaction { accounts: transaction_accounts, + // Worst case: every writable account appears modified, yet a failed + // tx must still persist only its rollback accounts. + touched_flags, fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::FeePayerOnly { fee_payer: (from_address, from_account_pre.clone()), @@ -412,8 +495,13 @@ mod tests { AccountSharedData::new_data(42, &nonce_state, &system_program::id()).unwrap(); let from_account_pre = AccountSharedData::new(4242, 0, &Pubkey::default()); + let touched_flags = + touched_flags_for_test(transaction_accounts.len(), transaction_accounts.len()); let loaded = LoadedTransaction { accounts: transaction_accounts, + // Worst case: every writable account appears modified, yet a failed + // tx must still persist only its rollback accounts. + touched_flags, fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::SeparateNonceAndFeePayer { nonce: (nonce_address, nonce_account_pre.clone()), @@ -519,8 +607,13 @@ mod tests { let nonce_account_pre = AccountSharedData::new_data(42, &nonce_state, &system_program::id()).unwrap(); + let touched_flags = + touched_flags_for_test(transaction_accounts.len(), transaction_accounts.len()); let loaded = LoadedTransaction { accounts: transaction_accounts, + // Worst case: every writable account appears modified, yet a failed + // tx must still persist only its rollback accounts. + touched_flags, fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::SameNonceAndFeePayer { nonce: (nonce_address, nonce_account_pre.clone()), diff --git a/runtime/src/accounts_background_service.rs b/runtime/src/accounts_background_service.rs index c350363f090..656497f0d22 100644 --- a/runtime/src/accounts_background_service.rs +++ b/runtime/src/accounts_background_service.rs @@ -34,7 +34,12 @@ use { }, }; -const INTERVAL_MS: u64 = 100; +/// Limit the maximum frequency that the ABS main loop can run. +/// If the loop ran for less than this duration, sleep the remainder. +/// E.g. with a min interval of 100 millis, the loop will run a maximum +/// of 10 times per second. Lower frequency is allowed, and occurs +/// when longer-running tasks are triggered. +const MIN_LOOP_INTERVAL: Duration = Duration::from_millis(100); // Set the clean interval duration to be approximately how long before the next incremental // snapshot request is received, plus some buffer. The default incremental snapshot interval is // 100 slots, which ends up being 40 seconds plus buffer. @@ -565,8 +570,12 @@ impl AccountsBackgroundService { previous_shrink_time = Instant::now(); } } - stats.record_and_maybe_submit(start_time.elapsed()); - sleep(Duration::from_millis(INTERVAL_MS)); + + let loop_dur = start_time.elapsed(); + stats.record_and_maybe_submit(loop_dur); + if let Some(sleep_dur) = MIN_LOOP_INTERVAL.checked_sub(loop_dur) { + sleep(sleep_dur); + } } info!("AccountsBackgroundService has stopped"); is_running.store(false, Ordering::Relaxed); @@ -719,7 +728,7 @@ fn cmp_snapshot_request_kinds_by_priority( mod test { use { super::*, crate::genesis_utils::create_genesis_config, - agave_snapshots::snapshot_config::SnapshotConfig, crossbeam_channel::unbounded, + agave_snapshots::snapshot_config::SnapshotConfig, crossbeam_channel::bounded, solana_account::AccountSharedData, solana_epoch_schedule::EpochSchedule, solana_leader_schedule::SlotLeader, solana_pubkey::Pubkey, }; @@ -728,7 +737,7 @@ mod test { fn test_accounts_background_service_remove_dead_slots() { let genesis = create_genesis_config(10); let bank0 = Arc::new(Bank::new_for_tests(&genesis.genesis_config)); - let (pruned_banks_sender, pruned_banks_receiver) = unbounded(); + let (pruned_banks_sender, pruned_banks_receiver) = bounded(1024); let pruned_banks_request_handler = PrunedBanksRequestHandler { pruned_banks_receiver, }; @@ -768,7 +777,7 @@ mod test { let snapshot_config = SnapshotConfig::default(); let pending_snapshot_packages = Arc::new(Mutex::new(PendingSnapshotPackages::default())); - let (snapshot_request_sender, snapshot_request_receiver) = crossbeam_channel::unbounded(); + let (snapshot_request_sender, snapshot_request_receiver) = bounded(1024); let snapshot_controller = Arc::new(SnapshotController::new( snapshot_request_sender.clone(), snapshot_config, @@ -908,7 +917,7 @@ mod test { /// Ensure that we can prune banks with the same slot (if they were on different forks) #[test] fn test_pruned_banks_request_handler_handle_request() { - let (pruned_banks_sender, pruned_banks_receiver) = crossbeam_channel::unbounded(); + let (pruned_banks_sender, pruned_banks_receiver) = bounded(1024); let pruned_banks_request_handler = PrunedBanksRequestHandler { pruned_banks_receiver, }; diff --git a/runtime/src/alpenglow_epoch_type.rs b/runtime/src/alpenglow_epoch_type.rs new file mode 100644 index 00000000000..b00190743b6 --- /dev/null +++ b/runtime/src/alpenglow_epoch_type.rs @@ -0,0 +1,226 @@ +use { + crate::bank::{Bank, MAX_ALPENGLOW_VOTE_ACCOUNTS}, + solana_account::{AccountSharedData, ReadableAccount}, + solana_clock::{Epoch, Slot}, + solana_pubkey::Pubkey, + solana_sdk_ids::system_program, + solana_vote::vote_account::VoteAccounts, + std::{cmp::Ordering, collections::HashMap, sync::LazyLock}, + wincode::{SchemaRead, SchemaWrite}, +}; + +/// For alpenglow rewards we only reward lamports earned in the previous epoch. +/// For this prior epoch we need to know the delegated stake for each vote account. +/// Note that this is not the same as `epoch_stakes`, which is calculated an epoch +/// in advance. +#[derive(Debug)] +pub(crate) struct RewardEpochDelegatedStakes { + pub(crate) epoch: Epoch, + pub(crate) delegated_stakes: HashMap, +} + +#[derive(Debug, SchemaRead, SchemaWrite)] +struct RewardEpochDelegatedStakesAccount { + pub(crate) epoch: Epoch, + pub(crate) delegated_stakes: Vec, +} + +#[derive(Debug, Clone, SchemaRead, SchemaWrite)] +struct RewardEpochDelegatedStake { + pub(crate) vote_pubkey: Pubkey, + pub(crate) delegated_stake: u64, +} + +/// The off-curve account where we store the bounded reward-epoch delegated stake +/// denominators used for non-Tower reward recalculation after snapshot restore. +static REWARD_EPOCH_DELEGATED_STAKES_ACCOUNT: LazyLock = LazyLock::new(|| { + let (pubkey, _) = Pubkey::find_program_address( + &[b"reward_epoch_delegated_stakes"], + &agave_feature_set::alpenglow::id(), + ); + pubkey +}); + +impl RewardEpochDelegatedStakesAccount { + pub(crate) fn max_size() -> usize { + static REWARD_EPOCH_DELEGATED_STAKES_ACCOUNT_MAX_SIZE: LazyLock = + LazyLock::new(|| { + let account = RewardEpochDelegatedStakesAccount { + epoch: u64::MAX, + delegated_stakes: vec![ + RewardEpochDelegatedStake { + vote_pubkey: Pubkey::new_from_array([u8::MAX; 32]), + delegated_stake: u64::MAX, + }; + MAX_ALPENGLOW_VOTE_ACCOUNTS + ], + }; + wincode::serialized_size(&account) + .expect("reward epoch delegated stakes account must serialize") + .try_into() + .expect( + "reward epoch delegated stakes account serialized size must fit in usize", + ) + }); + + *REWARD_EPOCH_DELEGATED_STAKES_ACCOUNT_MAX_SIZE + } +} + +impl RewardEpochDelegatedStakes { + pub(crate) fn set(&self, bank: &Bank, distribution_vote_accounts: &VoteAccounts) { + assert!( + distribution_vote_accounts.len() <= MAX_ALPENGLOW_VOTE_ACCOUNTS, + "reward epoch delegated stakes account must be bounded by MAX_ALPENGLOW_VOTE_ACCOUNTS" + ); + + let mut delegated_stakes = distribution_vote_accounts + .delegated_stakes() + .map( + |(vote_pubkey, _delegated_stake)| RewardEpochDelegatedStake { + vote_pubkey: *vote_pubkey, + delegated_stake: self + .delegated_stakes + .get(vote_pubkey) + .copied() + .unwrap_or_default(), + }, + ) + .collect::>(); + delegated_stakes.sort_unstable_by_key(|stake| stake.vote_pubkey); + + let account = RewardEpochDelegatedStakesAccount { + epoch: self.epoch, + delegated_stakes, + }; + let data = wincode::serialize(&account).unwrap(); + let lamports = bank + .get_minimum_balance_for_rent_exemption(RewardEpochDelegatedStakesAccount::max_size()); + let mut account = AccountSharedData::new(lamports, data.len(), &system_program::ID); + account.set_data_from_slice(&data); + + bank.store_account_and_update_capitalization( + &REWARD_EPOCH_DELEGATED_STAKES_ACCOUNT, + &account, + ); + } + + pub(crate) fn get(bank: &Bank) -> Option { + let account = bank.get_account(&REWARD_EPOCH_DELEGATED_STAKES_ACCOUNT)?; + (!account.data().is_empty()).then(|| { + let account: RewardEpochDelegatedStakesAccount = wincode::deserialize(account.data()) + .expect("Couldn't deserialize reward epoch delegated stakes"); + assert!( + account.delegated_stakes.len() <= MAX_ALPENGLOW_VOTE_ACCOUNTS, + "reward epoch delegated stakes account exceeds MAX_ALPENGLOW_VOTE_ACCOUNTS" + ); + account.into() + }) + } +} + +impl From for RewardEpochDelegatedStakes { + fn from(account: RewardEpochDelegatedStakesAccount) -> Self { + Self { + epoch: account.epoch, + delegated_stakes: account + .delegated_stakes + .into_iter() + .map(|stake| (stake.vote_pubkey, stake.delegated_stake)) + .collect(), + } + } +} + +#[derive(Debug)] +pub(crate) enum AlpenglowEpochType { + /// This is a full tower epoch. + Tower, + /// The epoch started in tower and then switched to alpenglow + MigrationEpoch { + num_tower_slots: Slot, + num_ag_slots: Slot, + migration_epoch: Epoch, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes, + }, + /// This is a full alpenglow epoch + Alpenglow { + migration_epoch: Epoch, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes, + }, +} + +impl AlpenglowEpochType { + pub(crate) fn is_alpenglow_or_migration_epoch(bank: &Bank, epoch: Epoch) -> bool { + debug_assert!(epoch < bank.epoch()); + bank.get_alpenglow_migration_slot() + .map(|migration_slot| bank.epoch_schedule().get_epoch(migration_slot) <= epoch) + .unwrap_or(false) + } + + /// Returns `AlpenglowEpochType` for the given `epoch`. + /// + /// Calling this function with an epoch >= `Bank::epoch` can return false information as it is + /// possible that we have not observed the genesis cert yet but will in the upcoming slots. + /// + /// We pass `reward_epoch_delegated_stakes` as a closure to avoid the potential deserialization + /// of `REWARD_EPOCH_DELEGATED_STAKES_ACCOUNT` in the Tower case. + pub(crate) fn get( + bank: &Bank, + epoch: Epoch, + reward_epoch_delegated_stakes: impl FnOnce() -> Option, + ) -> Self { + debug_assert!(epoch < bank.epoch()); + let Some(migration_slot) = bank.get_alpenglow_migration_slot() else { + return Self::Tower; + }; + let migration_epoch = bank.epoch_schedule().get_epoch(migration_slot); + match migration_epoch.cmp(&epoch) { + Ordering::Less => { + let reward_epoch_delegated_stakes = + reward_epoch_delegated_stakes().unwrap_or_else(|| { + panic!( + "Missing reward epoch delegated stakes for non-Tower reward epoch \ + {epoch}" + ) + }); + assert_eq!( + reward_epoch_delegated_stakes.epoch, epoch, + "reward epoch delegated stakes must match rewarded epoch" + ); + Self::Alpenglow { + migration_epoch, + reward_epoch_delegated_stakes, + } + } + Ordering::Greater => Self::Tower, + Ordering::Equal => { + let first_slot_in_epoch = bank + .epoch_schedule() + .get_first_slot_in_epoch(migration_epoch); + // + 1 because the migration_slot is still tower. + let num_tower_slots = migration_slot - first_slot_in_epoch + 1; + let slots_in_epoch = bank.epoch_schedule().get_slots_in_epoch(migration_epoch); + let num_ag_slots = slots_in_epoch - num_tower_slots; + assert_eq!(slots_in_epoch, num_tower_slots + num_ag_slots); + let reward_epoch_delegated_stakes = + reward_epoch_delegated_stakes().unwrap_or_else(|| { + panic!( + "Missing reward epoch delegated stakes for non-Tower reward epoch \ + {epoch}" + ) + }); + assert_eq!( + reward_epoch_delegated_stakes.epoch, epoch, + "reward epoch delegated stakes must match rewarded epoch" + ); + Self::MigrationEpoch { + num_tower_slots, + num_ag_slots, + migration_epoch, + reward_epoch_delegated_stakes, + } + } + } + } +} diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index e88be995e7c..10f22de5daa 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -33,9 +33,15 @@ //! It offers a high-level API that signs transactions //! on behalf of the caller, and a low-level API for when they have //! already been signed and verified. +pub use { + crate::slot_params::DEFAULT_MAX_ENTRY_BYTES_PER_SLOT, + partitioned_epoch_rewards::KeyedRewardsAndNumPartitions, solana_leader_schedule::SlotLeader, + solana_reward_info::RewardType, +}; use { crate::{ account_saver::collect_accounts_to_store, + alpenglow_epoch_type::AlpenglowEpochType, bank::{ entry_bytes_budget::EntryBytesBudget, metrics::*, @@ -56,27 +62,32 @@ use { rent_collector::RentCollector, reward_info::RewardInfo, runtime_config::RuntimeConfig, + slot_params::{SlotParams, SlotParamsArchive}, stake_account::StakeAccount, stake_history::StakeHistory as CowStakeHistory, stake_weighted_timestamp::{ MAX_ALLOWABLE_DRIFT_PERCENTAGE_FAST, MAX_ALLOWABLE_DRIFT_PERCENTAGE_SLOW_V2, MaxAllowableDrift, calculate_stake_weighted_timestamp, }, - stakes::{DeserializableStakes, SerdeStakesToStakeFormat, Stakes, StakesCache}, + stakes::{ + DelegatedStakes, DeserializableStakes, SerdeStakesToStakeFormat, Stakes, StakesCache, + }, status_cache::{SlotDelta, StatusCache}, transaction_batch::{OwnedOrBorrowed, TransactionBatch}, }, - accounts_lt_hash::{CacheValue as AccountsLtHashCacheValue, Stats as AccountsLtHashStats}, + accounts_lt_hash::AccountsLtHashAsyncProgress, agave_bls_cert_verify::cert_verify::{self, Error as CertVerifyError}, agave_feature_set::{self as feature_set, FeatureSet}, agave_precompiles::{get_precompile, get_precompiles, is_precompile}, agave_reserved_account_keys::ReservedAccountKeys, agave_snapshots::snapshot_hash::SnapshotHash, agave_votor_messages::{ - consensus_message::Certificate, migration::GENESIS_CERTIFICATE_ACCOUNT, + certificate::{CertSignature, Certificate, GenesisCert}, + migration::GENESIS_CERTIFICATE_ACCOUNT, + unverified_vote_message::UnverifiedCertificate, + wire::{WireBlockCertMessage, WireCertSignature}, }, ahash::AHashSet, - dashmap::DashMap, log::*, partitioned_epoch_rewards::PartitionedRewardsCalculation, rayon::ThreadPool, @@ -106,12 +117,7 @@ use { }, solana_cluster_type::ClusterType, solana_compute_budget::compute_budget::ComputeBudget, - solana_compute_budget_instruction::instructions_processor::process_compute_budget_instructions, - solana_cost_model::{ - block_cost_limits::simd_0286_block_limit, - cost_tracker::CostTracker, - shred_limit::{DEFAULT_MAX_CODE_SHREDS_PER_SLOT, DEFAULT_MAX_DATA_SHREDS_PER_SLOT}, - }, + solana_cost_model::cost_tracker::CostTracker, solana_epoch_info::EpochInfo, solana_epoch_schedule::EpochSchedule, solana_feature_gate_interface as feature, @@ -138,20 +144,24 @@ use { solana_pubkey::Pubkey, solana_rent::Rent, solana_runtime_transaction::{ - runtime_transaction::RuntimeTransaction, transaction_with_meta::TransactionWithMeta, + runtime_transaction::RuntimeTransaction, transaction_meta::TransactionConfiguration, + transaction_with_meta::TransactionWithMeta, + }, + solana_sdk_ids::{ + bpf_loader_upgradeable, incinerator, native_loader, system_program, sysvar as sysvar_id, }, - solana_sdk_ids::{bpf_loader_upgradeable, incinerator, native_loader, system_program}, solana_sha256_hasher::hashv, solana_signature::Signature, solana_slot_hashes::SlotHashes, solana_slot_history::{Check, SlotHistory}, solana_stake_interface::{ - stake_history::StakeHistory, state::Delegation, sysvar::stake_history, + stake_history::{SIZE as STAKE_HISTORY_ACCOUNT_SIZE, StakeHistory}, + state::Delegation, + sysvar::stake_history, }, solana_svm::{ account_loader::LoadedTransaction, account_overrides::AccountOverrides, - program_loader::load_program_with_pubkey, transaction_balances::{BalanceCollector, SvmTokenInfo}, transaction_commit_result::{CommittedTransaction, TransactionCommitResult}, transaction_error_metrics::TransactionErrorMetrics, @@ -174,7 +184,6 @@ use { solana_system_transaction as system_transaction, solana_sysvar::{self as sysvar, SysvarSerialize, last_restart_slot::LastRestartSlot}, solana_sysvar_id::SysvarId, - solana_time_utils::years_as_slots, solana_transaction::{ Transaction, TransactionVerificationMode, sanitized::{MAX_TX_ACCOUNT_LOCKS, MessageHash, SanitizedTransaction}, @@ -205,6 +214,7 @@ use { }, time::{Duration, Instant}, }, + thiserror::Error, }; #[cfg(feature = "dev-context-only-utils")] use { @@ -217,16 +227,8 @@ use { solana_nonce as nonce, solana_nonce_account::{SystemAccountKind, get_system_account_kind}, solana_program_runtime::sysvar_cache::SysvarCache, + solana_svm::program_loader::load_program_with_pubkey, }; -pub use { - partitioned_epoch_rewards::KeyedRewardsAndNumPartitions, solana_leader_schedule::SlotLeader, - solana_reward_info::RewardType, -}; - -/// params to `verify_accounts_hash` -struct VerifyAccountsHashConfig { - require_rooted_bank: bool, -} mod accounts_lt_hash; mod address_lookup_table; @@ -250,9 +252,12 @@ pub const MAX_LEADER_SCHEDULE_STAKES: Epoch = 5; /// only the top 2000 validators by stake will be present in vote account structures. pub const MAX_ALPENGLOW_VOTE_ACCOUNTS: usize = 2000; -/// The admission cost in lamports that is burned from the vote account of -/// validators that are a part of the voting set -pub const VAT_TO_BURN_PER_EPOCH: u64 = 1_600_000_000; // 1.6 SOL +/// Default 400ms-slot Validator Admission Ticket burn amount. +/// +/// Use this for conservative genesis/test funding defaults. Runtime VAT +/// filtering and burns must use the bank's effective slot params instead. +pub const DEFAULT_VAT_TO_BURN_PER_EPOCH: u64 = + crate::slot_params::LEGACY_SLOT_PARAMS.vat_to_burn_per_epoch(); /// The off-curve account where we store the Alpenglow clock. The clock sysvar has seconds /// resolution while the Alpenglow clock has nanosecond resolution. @@ -273,7 +278,6 @@ pub type BankSlotDelta = SlotDelta>; pub struct SquashTiming { pub squash_accounts_ms: u64, pub squash_accounts_cache_ms: u64, - pub squash_accounts_index_ms: u64, pub squash_cache_ms: u64, } @@ -281,7 +285,6 @@ impl AddAssign for SquashTiming { fn add_assign(&mut self, rhs: Self) { self.squash_accounts_ms += rhs.squash_accounts_ms; self.squash_accounts_cache_ms += rhs.squash_accounts_cache_ms; - self.squash_accounts_index_ms += rhs.squash_accounts_index_ms; self.squash_cache_ms += rhs.squash_cache_ms; } } @@ -459,6 +462,16 @@ impl TransactionLogCollector { } } +#[derive(Error, Debug, Serialize, Deserialize)] +pub enum VATHealthError { + #[error("vote account not found")] + VoteAccountNotFound, + #[error("missing BLS pubkey")] + NoBLSPubkey, + #[error("insufficient lamports in vote account: {0} < {1}")] + InsufficientFundsInVoteAccount(u64, u64), +} + /// Bank's common fields shared by all supported snapshot versions for deserialization. /// Sync fields with BankFieldsToSerialize! This is paired with it. /// All members are made public to remain Bank's members private and to make versioned deserializer workable on this @@ -586,6 +599,7 @@ impl PartialEq for Bank { status_cache: _, blockhash_queue, max_processing_age, + partitioned_rewards_stake_account_stores_per_block, ancestors: _, hash, parent_hash, @@ -606,6 +620,7 @@ impl PartialEq for Bank { ns_per_slot, genesis_creation_time, slots_per_year, + slot_params: _, slot, bank_id: _, epoch, @@ -621,6 +636,7 @@ impl PartialEq for Bank { #[cfg(feature = "dev-context-only-utils")] hash_overrides, accounts_lt_hash, + is_alpenglow, // TODO: Confirm if all these fields are intentionally ignored! rewards: _, cluster_type: _, @@ -643,8 +659,7 @@ impl PartialEq for Bank { compute_budget: _, transaction_account_lock_limit: _, fee_structure: _, - cache_for_accounts_lt_hash: _, - stats_for_accounts_lt_hash: _, + accounts_lt_hash_async_progress: _, block_id, expected_bank_hash: _, bank_hash_stats: _, @@ -656,6 +671,8 @@ impl PartialEq for Bank { } = self; *blockhash_queue.read().unwrap() == *other.blockhash_queue.read().unwrap() && *max_processing_age == other.max_processing_age + && *partitioned_rewards_stake_account_stores_per_block + == other.partitioned_rewards_stake_account_stores_per_block && *hash.read().unwrap() == *other.hash.read().unwrap() && parent_hash == &other.parent_hash && parent_slot == &other.parent_slot @@ -665,7 +682,7 @@ impl PartialEq for Bank { && signature_count.load(Relaxed) == other.signature_count.load(Relaxed) && capitalization.load(Relaxed) == other.capitalization.load(Relaxed) && max_tick_height == &other.max_tick_height - && hashes_per_tick == &other.hashes_per_tick + && *hashes_per_tick.read().unwrap() == *other.hashes_per_tick.read().unwrap() && ticks_per_slot == &other.ticks_per_slot && ns_per_slot == &other.ns_per_slot && genesis_creation_time == &other.genesis_creation_time @@ -687,6 +704,7 @@ impl PartialEq for Bank { *hash_overrides.lock().unwrap() == *other.hash_overrides.lock().unwrap()) && *accounts_lt_hash.lock().unwrap() == *other.accounts_lt_hash.lock().unwrap() && *block_id.read().unwrap() == *other.block_id.read().unwrap() + && is_alpenglow.load(Relaxed) == other.is_alpenglow() } } @@ -731,9 +749,6 @@ impl BankFieldsToSerialize { pub enum RewardCalculationEvent<'a, 'b> { Staking(&'a Pubkey, &'b InflationPointCalculationEvent), } -/// Default maximum serialized entry bytes a bank may accept in one slot. -pub const DEFAULT_MAX_ENTRY_BYTES_PER_SLOT: u64 = 20 * 1024 * 1024; // 20 MiB - /// type alias is not supported for trait in rust yet. As a workaround, we define the /// `RewardCalcTracer` trait explicitly and implement it on any type that implement /// `Fn(&RewardCalculationEvent) + Send + Sync`. @@ -811,6 +826,9 @@ pub struct Bank { /// Maximum age in slots a blockhash can be for a tx to be processed. max_processing_age: usize, + /// Number of stake accounts to store in each block during partitioned rewards. + partitioned_rewards_stake_account_stores_per_block: u64, + /// The set of parents including this bank pub ancestors: Ancestors, @@ -860,7 +878,7 @@ pub struct Bank { max_tick_height: u64, /// The number of hashes in each tick. None value means hashing is disabled. - hashes_per_tick: Option, + hashes_per_tick: RwLock>, /// The number of ticks in each slot. ticks_per_slot: u64, @@ -874,6 +892,9 @@ pub struct Bank { /// The number of slots per year, used for inflation slots_per_year: f64, + /// Slot-scoped parameter history used for slot-relative parameter lookups. + slot_params: SlotParamsArchive, + /// Bank slot (i.e. block) slot: Slot, @@ -974,17 +995,8 @@ pub struct Bank { /// The value is only meaningful after freezing. accounts_lt_hash: Mutex, - /// A cache of *the initial state* of accounts modified in this slot - /// - /// The accounts lt hash needs both the initial and final state of each - /// account that was modified in this slot. Cache the initial state here. - /// - /// Note: The initial state must be strictly from an ancestor, - /// and not an intermediate state within this slot. - cache_for_accounts_lt_hash: DashMap, - - /// Stats related to the accounts lt hash - stats_for_accounts_lt_hash: AccountsLtHashStats, + /// Track progress of the asynchronous accounts lt hashing for this Bank. + accounts_lt_hash_async_progress: AccountsLtHashAsyncProgress, /// The unique identifier for the corresponding block for this bank. /// None for banks that have not yet completed replay or for leader banks as we cannot populate block_id @@ -1007,6 +1019,9 @@ pub struct Bank { /// currently write to this during replay, as we process block components one at a time, and /// read from this once replay is complete. pub block_component_processor: RwLock, + + /// Cached Alpenglow migration state, derived from the genesis certificate account. + is_alpenglow: AtomicBool, } #[derive(Debug, Default)] @@ -1039,7 +1054,7 @@ pub struct ProcessedTransactionCounts { /// Account stats for computing the bank hash /// This struct is serialized and stored in the snapshot. -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct BankHashStats { pub num_updated_accounts: u64, @@ -1123,6 +1138,8 @@ struct NewEpochBundle { /// Vote accounts computed from the stakes cache for the current /// (distribution) epoch *before* applying any VAT filtering. unfiltered_distribution_vote_accounts: VoteAccounts, + /// Current effective stake delegated to each vote account pubkey. + delegated_stakes: DelegatedStakes, /// Vote accounts computed from the stakes cache for the current /// (distribution) epoch *after* applying VAT filtering (or an unfiltered /// clone when VAT is disabled). @@ -1132,13 +1149,30 @@ struct NewEpochBundle { update_rewards_with_thread_pool_time_us: u64, } +fn create_stake_history_account( + stake_history: &StakeHistory, + (lamports, rent_epoch): InheritableAccountFields, +) -> AccountSharedData { + let data_len = + STAKE_HISTORY_ACCOUNT_SIZE.max(bincode::serialized_size(stake_history).unwrap() as usize); + let mut account = AccountSharedData::new(lamports, data_len, &sysvar_id::id()); + account.serialize_data(stake_history).unwrap(); + account.set_rent_epoch(rent_epoch); + account +} + impl Bank { fn default_with_accounts(accounts: Accounts) -> Self { + let partitioned_rewards_stake_account_stores_per_block = accounts + .accounts_db + .partitioned_epoch_rewards_config + .stake_account_stores_per_block; let mut bank = Self { rc: BankRc::new(accounts), status_cache: Arc::>::default(), blockhash_queue: RwLock::::default(), max_processing_age: MAX_PROCESSING_AGE, + partitioned_rewards_stake_account_stores_per_block, ancestors: Ancestors::default(), hash: RwLock::::default(), parent_hash: Hash::default(), @@ -1154,11 +1188,12 @@ impl Bank { signature_count: AtomicU64::default(), capitalization: AtomicU64::default(), max_tick_height: u64::default(), - hashes_per_tick: Option::::default(), + hashes_per_tick: RwLock::default(), ticks_per_slot: u64::default(), ns_per_slot: u128::default(), genesis_creation_time: UnixTimestamp::default(), slots_per_year: f64::default(), + slot_params: SlotParamsArchive::default(), slot: Slot::default(), bank_id: BankId::default(), epoch: Epoch::default(), @@ -1196,13 +1231,13 @@ impl Bank { #[cfg(feature = "dev-context-only-utils")] hash_overrides: Arc::new(Mutex::new(HashOverrides::default())), accounts_lt_hash: Mutex::new(AccountsLtHash(LtHash::identity())), - cache_for_accounts_lt_hash: DashMap::default(), - stats_for_accounts_lt_hash: AccountsLtHashStats::default(), + accounts_lt_hash_async_progress: AccountsLtHashAsyncProgress::new(), block_id: RwLock::new(None), expected_bank_hash: RwLock::new(None), bank_hash_stats: AtomicBankHashStats::default(), epoch_rewards_calculation_cache: Arc::new(Mutex::new(HashMap::default())), block_component_processor: RwLock::new(BlockComponentProcessor::default()), + is_alpenglow: AtomicBool::new(false), }; bank.transaction_processor = @@ -1278,6 +1313,9 @@ impl Bank { bank.update_last_restart_slot(); bank.transaction_processor .fill_missing_sysvar_cache_entries(&bank); + if bank.get_alpenglow_genesis_certificate().is_some() { + bank.set_is_alpenglow(); + } bank } @@ -1381,12 +1419,15 @@ impl Bank { epoch, blockhash_queue, max_processing_age: parent.max_processing_age, + partitioned_rewards_stake_account_stores_per_block: parent + .partitioned_rewards_stake_account_stores_per_block, // TODO: clean this up, so much special-case copying... - hashes_per_tick: parent.hashes_per_tick, + hashes_per_tick: RwLock::new(parent.hashes_per_tick()), ticks_per_slot: parent.ticks_per_slot, ns_per_slot: parent.ns_per_slot, genesis_creation_time: parent.genesis_creation_time, slots_per_year: parent.slots_per_year, + slot_params: parent.slot_params.clone(), epoch_schedule, rent_collector: Self::get_rent_collector_from(&parent.rent_collector, epoch), max_tick_height: slot @@ -1453,21 +1494,19 @@ impl Bank { #[cfg(feature = "dev-context-only-utils")] hash_overrides: parent.hash_overrides.clone(), accounts_lt_hash: Mutex::new(parent.accounts_lt_hash.lock().unwrap().clone()), - cache_for_accounts_lt_hash: DashMap::default(), - stats_for_accounts_lt_hash: AccountsLtHashStats::default(), + accounts_lt_hash_async_progress: AccountsLtHashAsyncProgress::new(), block_id: RwLock::new(None), expected_bank_hash: RwLock::new(None), bank_hash_stats: AtomicBankHashStats::default(), epoch_rewards_calculation_cache: parent.epoch_rewards_calculation_cache.clone(), block_component_processor: RwLock::new(BlockComponentProcessor::default()), + is_alpenglow: AtomicBool::new(parent.is_alpenglow()), }; let (_, ancestors_time_us) = measure_us!({ - let mut ancestors = Vec::with_capacity(1 + new.parents().len()); + let mut ancestors = Vec::with_capacity(parent.ancestors.len() + 1); ancestors.push(new.slot()); - new.parents().iter().for_each(|p| { - ancestors.push(p.slot()); - }); + ancestors.extend(new.parents_iter().map(|parent| parent.slot())); new.ancestors = Ancestors::from(ancestors); }); @@ -1484,7 +1523,6 @@ impl Bank { slot, parent.slot(), new.block_height, - prepare_timings.num_accounts_modified_this_slot, NewBankTimings { bank_rc_creation_time_us, total_elapsed_time_us: time.as_us(), @@ -1504,8 +1542,6 @@ impl Bank { cache_preparation_time_us: prepare_timings.cache_preparation_time_us, update_sysvars_time_us: prepare_timings.update_sysvars_time_us, fill_sysvar_cache_time_us: prepare_timings.fill_sysvar_cache_time_us, - populate_cache_for_accounts_lt_hash_us: prepare_timings - .populate_cache_for_accounts_lt_hash_us, }, ); @@ -1548,11 +1584,6 @@ impl Bank { .checked_div(2) .unwrap(); - let program_cache = self - .transaction_processor - .global_program_cache - .read() - .unwrap(); let mut epoch_boundary_preparation = self .transaction_processor .epoch_boundary_preparation @@ -1566,27 +1597,14 @@ impl Bank { epoch_boundary_preparation.programs_to_recompile.pop() { drop(epoch_boundary_preparation); - drop(program_cache); - if let Some((recompiled, last_modification_slot)) = load_program_with_pubkey( - self, - &upcoming_environment, - &key, - self.slot, - &mut ExecuteTimings::default(), - ) { - recompiled.stats.merge_from(&program_to_recompile.stats); - let mut program_cache = self - .transaction_processor - .global_program_cache - .write() - .unwrap(); - program_cache.assign_program( + self.transaction_processor + .prepare_one_program_for_upcoming_feature_set( + self, + self.check_program_deployment_slot(), &upcoming_environment, - key, - last_modification_slot, - recompiled, + &key, + &program_to_recompile.stats, ); - } } } else if slot_index.saturating_add(slots_in_recompilation_phase) >= slots_in_epoch { // Anticipate the upcoming program runtime environment for the next epoch, @@ -1600,7 +1618,12 @@ impl Bank { let changed_program_runtime_environment = *upcoming_environment != *new_environment; if changed_program_runtime_environment { upcoming_environment = new_environment; - epoch_boundary_preparation.programs_to_recompile = program_cache + let program_cache_guard = self + .transaction_processor + .global_program_cache + .read() + .unwrap(); + epoch_boundary_preparation.programs_to_recompile = program_cache_guard .get_flattened_entries() .into_iter() .map(|(id, _last_modification_slot, entry)| (id, entry)) @@ -1616,23 +1639,26 @@ impl Bank { } } - pub fn prune_program_cache( - &self, - new_root_slot: Slot, - new_root_epoch: Epoch, - bank_forks: &BankForks, - ) { + pub fn prune_program_cache(&self, bank_forks: &BankForks) { let upcoming_environment = self .transaction_processor .epoch_boundary_preparation .write() .unwrap() - .reroot(new_root_epoch); + .reroot(self.epoch()); self.transaction_processor .global_program_cache .write() .unwrap() - .prune(new_root_slot, upcoming_environment, bank_forks); + .prune( + self.slot(), + upcoming_environment.map(|_| { + ProgramRuntimeEnvironment::clone( + &self.transaction_processor.program_runtime_environment, + ) + }), + bank_forks, + ); } pub fn prune_program_cache_by_deployment_slot(&self, deployment_slot: Slot) { @@ -1649,6 +1675,12 @@ impl Bank { .new_warmup_cooldown_rate_epoch(&self.epoch_schedule) } + fn use_fixed_point_stake_math(&self) -> bool { + self.feature_set + .snapshot() + .upgrade_bpf_stake_program_to_v5_1 + } + /// Get cached vote account state from the past few epochs so that some vote /// state configuration changes are delayed before being used in reward /// calculation. @@ -1691,14 +1723,21 @@ impl Bank { let stakes = self.stakes_cache.stakes(); let stake_delegations = stakes.stake_delegations_vec(); let ( - (stake_history, unfiltered_distribution_vote_accounts), + ( + stake_history, + unfiltered_distribution_vote_accounts, + delegated_stakes, + reward_epoch_delegated_stakes, + ), calculate_activated_stake_time_us, ) = measure_us!(stakes.calculate_activated_stake( self.epoch(), thread_pool, self.new_warmup_cooldown_rate_epoch(), - &stake_delegations + &stake_delegations, + self.use_fixed_point_stake_math(), )); + debug_assert_eq!(reward_epoch_delegated_stakes.epoch, rewarded_epoch); // Apply stake rewards and commission using the distribution vote-account // snapshot that matches VAT admission filtering when enabled. @@ -1711,6 +1750,13 @@ impl Bank { } else { unfiltered_distribution_vote_accounts.clone() }; + if AlpenglowEpochType::is_alpenglow_or_migration_epoch(self, rewarded_epoch) { + assert!( + self.feature_set.snapshot().validator_admission_ticket, + "Alpenglow should not be activated before the VAT" + ); + reward_epoch_delegated_stakes.set(self, &filtered_distribution_vote_accounts); + } let cached_vote_accounts = self.get_cached_vote_accounts(rewarded_epoch, &filtered_distribution_vote_accounts); let (rewards_calculation, update_rewards_with_thread_pool_time_us) = @@ -1719,6 +1765,7 @@ impl Bank { stake_delegations, cached_vote_accounts, rewarded_epoch, + reward_epoch_delegated_stakes, reward_calc_tracer, thread_pool, rewards_metrics, @@ -1726,6 +1773,7 @@ impl Bank { NewEpochBundle { stake_history, unfiltered_distribution_vote_accounts, + delegated_stakes, filtered_distribution_vote_accounts, rewards_calculation, calculate_activated_stake_time_us, @@ -1754,6 +1802,7 @@ impl Bank { let NewEpochBundle { stake_history, unfiltered_distribution_vote_accounts, + delegated_stakes, filtered_distribution_vote_accounts, rewards_calculation, calculate_activated_stake_time_us, @@ -1769,6 +1818,7 @@ impl Bank { epoch, stake_history, unfiltered_distribution_vote_accounts, + delegated_stakes, ); // Save a snapshot of stakes for use in consensus and stake weighted networking @@ -1780,7 +1830,7 @@ impl Bank { // Distribute rewards commission to vote accounts and cache stake rewards // for partitioned distribution in the upcoming slots. - let (epoch_validator_rewards, begin_partitioned_rewards_time_us) = + let (epoch_rewards, begin_partitioned_rewards_time_us) = measure_us!(self.begin_partitioned_rewards( parent_epoch, parent_slot, @@ -1793,11 +1843,11 @@ impl Bank { // the vote reward account state should be created at the epoch boundary in which we // activate alpenglow as it will need info from the previous epoch. if self.feature_set.snapshot().alpenglow { + let epoch_start_capitalization = parent_capitalization; EpochInflationAccountState::new_epoch_update_account( self, - parent_epoch, - parent_capitalization, - epoch_validator_rewards, + epoch_start_capitalization, + epoch_rewards, ); } @@ -1923,8 +1973,15 @@ impl Bank { let (_, update_sysvars_time_us) = measure_us!({ self.update_slot_hashes(); self.update_stake_history(Some(parent_epoch)); - // Keep setting the tower clock sysvar till we have not migrated to Alpenglow. - if self.get_alpenglow_genesis_certificate().is_none() { + + if self.is_alpenglow() { + // Alpenglow banks have the timestamp populated via the footer + // We only populate the slot here + self.update_clock_slot_for_alpenglow(); + } else { + // PoH banks have the timestamp and slot populated at the beginning + // Note: The first alpenglow bank will have the timestamp populated + // here at the beginning as well as at the end via the footer - this is intentional. self.update_clock(Some(parent_epoch)); } self.update_last_restart_slot() @@ -1935,34 +1992,12 @@ impl Bank { .fill_missing_sysvar_cache_entries(self) ); - let (num_accounts_modified_this_slot, populate_cache_for_accounts_lt_hash_us) = - measure_us!({ - // The cache for accounts lt hash needs to be made aware of accounts modified - // before transaction processing begins. Otherwise we may calculate the wrong - // accounts lt hash due to having the wrong initial state of the account. The - // lt hash cache's initial state must always be from an ancestor, and cannot be - // an intermediate state within this Bank's slot. If the lt hash cache has the - // wrong initial account state, we'll mix out the wrong lt hash value, and thus - // have the wrong overall accounts lt hash, and diverge. - let accounts_modified_this_slot = - self.rc.accounts.accounts_db.get_pubkeys_for_slot(slot); - let num_accounts_modified_this_slot = accounts_modified_this_slot.len(); - for pubkey in accounts_modified_this_slot { - self.cache_for_accounts_lt_hash - .entry(pubkey) - .or_insert(AccountsLtHashCacheValue::BankNew); - } - num_accounts_modified_this_slot - }); - PrepareBlockExecutionStats { update_epoch_time_us, distribute_rewards_time_us, cache_preparation_time_us, update_sysvars_time_us, fill_sysvar_cache_time_us, - num_accounts_modified_this_slot, - populate_cache_for_accounts_lt_hash_us, } } @@ -2046,11 +2081,17 @@ impl Bank { let stakes_accounts_load_duration = now.elapsed(); let rent = Self::load_rent_from_account_for_snapshot_load(&bank_rc.accounts, &ancestors); + let partitioned_rewards_stake_account_stores_per_block = bank_rc + .accounts + .accounts_db + .partitioned_epoch_rewards_config + .stake_account_stores_per_block; let mut bank = Self { rc: bank_rc, status_cache: Arc::>::default(), blockhash_queue: RwLock::new(fields.blockhash_queue), max_processing_age: MAX_PROCESSING_AGE, + partitioned_rewards_stake_account_stores_per_block, ancestors, hash: RwLock::new(fields.hash), parent_hash: fields.parent_hash, @@ -2066,11 +2107,12 @@ impl Bank { signature_count: AtomicU64::new(fields.signature_count), capitalization: AtomicU64::new(fields.capitalization), max_tick_height: fields.max_tick_height, - hashes_per_tick: fields.hashes_per_tick, + hashes_per_tick: RwLock::new(fields.hashes_per_tick), ticks_per_slot: fields.ticks_per_slot, ns_per_slot: fields.ns_per_slot, genesis_creation_time: fields.genesis_creation_time, slots_per_year: fields.slots_per_year, + slot_params: SlotParamsArchive::default(), slot, bank_id: 0, epoch, @@ -2114,15 +2156,19 @@ impl Bank { #[cfg(feature = "dev-context-only-utils")] hash_overrides: Arc::new(Mutex::new(HashOverrides::default())), accounts_lt_hash: Mutex::new(fields.accounts_lt_hash), - cache_for_accounts_lt_hash: DashMap::default(), - stats_for_accounts_lt_hash: AccountsLtHashStats::default(), + accounts_lt_hash_async_progress: AccountsLtHashAsyncProgress::new(), block_id: RwLock::new(fields.block_id), bank_hash_stats: AtomicBankHashStats::new(&fields.bank_hash_stats), epoch_rewards_calculation_cache: Arc::new(Mutex::new(HashMap::default())), expected_bank_hash: RwLock::new(None), block_component_processor: RwLock::new(BlockComponentProcessor::default()), + is_alpenglow: AtomicBool::new(false), }; + if bank.get_alpenglow_genesis_certificate().is_some() { + bank.set_is_alpenglow(); + } + // Sanity assertions between bank snapshot and genesis config // Consider removing from serializable bank state // (BankFieldsToSerialize/BankFieldsToDeserialize) and initializing @@ -2133,22 +2179,10 @@ impl Bank { snapshot and genesis.bin might pertain to different clusters" ); assert_eq!(bank.ticks_per_slot, genesis_config.ticks_per_slot); - assert_eq!( - bank.ns_per_slot, - genesis_config.poh_config.target_tick_duration.as_nanos() - * genesis_config.ticks_per_slot as u128 - ); assert_eq!(bank.max_tick_height, (bank.slot + 1) * bank.ticks_per_slot); - assert_eq!( - bank.slots_per_year, - years_as_slots( - 1.0, - &genesis_config.poh_config.target_tick_duration, - bank.ticks_per_slot, - ) - ); assert_eq!(bank.epoch_schedule, genesis_config.epoch_schedule); + bank.refresh_slot_params_from_snapshot(genesis_config); bank.initialize_after_snapshot_restore(|| rewards_calculation_thread_pool); datapoint_info!( @@ -2203,7 +2237,7 @@ impl Bank { signature_count: self.signature_count.load(Relaxed), capitalization: self.capitalization.load(Relaxed), max_tick_height: self.max_tick_height, - hashes_per_tick: self.hashes_per_tick, + hashes_per_tick: *self.hashes_per_tick.read().unwrap(), ticks_per_slot: self.ticks_per_slot, ns_per_slot: self.ns_per_slot, genesis_creation_time: self.genesis_creation_time, @@ -2278,17 +2312,21 @@ impl Bank { } pub fn status_cache_ancestors(&self) -> Vec { - let mut roots = self.status_cache.read().unwrap().roots().clone(); - let min = roots.iter().min().cloned().unwrap_or(0); - for ancestor in self.ancestors.keys() { - if ancestor >= min { - roots.insert(ancestor); + let (min, mut ancestors) = { + let status_cache = self.status_cache.read().unwrap(); + let roots = status_cache.roots(); + let mut ancestors = Vec::with_capacity(roots.len() + self.ancestors.len()); + let mut min = Slot::MAX; + for root in roots { + ancestors.push(*root); + min = min.min(*root); } - } + (if roots.is_empty() { 0 } else { min }, ancestors) + }; - let mut ancestors: Vec<_> = roots.into_iter().collect(); - #[expect(clippy::stable_sort_primitive)] - ancestors.sort(); + ancestors.extend(self.ancestors.iter().filter(|ancestor| *ancestor >= min)); + ancestors.sort_unstable(); + ancestors.dedup(); ancestors } @@ -2411,6 +2449,31 @@ impl Bank { }); } + /// In Alpenglow the clock sysvar's timestamp is populated from the block footer. + /// The timestamp value on the block footer is used as an estimate for when the block *ended*. + /// This is applied at the end of execution on the bank for use in the child. + /// + /// However we still need to update the slot and epoch fields for the clock sysvar at the *start* + /// of the bank, as transactions executing in this bank need to be able to read these values. + /// This function updates the slot and epoch fields while preserving the timestamp fields from the parent + /// bank's footer. + fn update_clock_slot_for_alpenglow(&self) { + let clock = self.clock(); + let clock = sysvar::clock::Clock { + slot: self.slot, + epoch: self.epoch_schedule().get_epoch(self.slot), + leader_schedule_epoch: self.epoch_schedule().get_leader_schedule_epoch(self.slot), + epoch_start_timestamp: clock.epoch_start_timestamp, + unix_timestamp: clock.unix_timestamp, + }; + self.update_sysvar_account(&sysvar::clock::id(), |account| { + create_account( + &clock, + self.inherit_specially_retained_account_fields(account), + ) + }); + } + pub fn update_last_restart_slot(&self) { // First, see what the currently stored last restart slot is. let current_last_restart_slot = self @@ -2458,16 +2521,15 @@ impl Bank { }); // Simply force fill sysvar cache rather than checking which sysvar was // actually updated since tests don't need to be optimized for performance. - self.transaction_processor.reset_sysvar_cache(); self.transaction_processor - .fill_missing_sysvar_cache_entries(self); + .reset_and_fill_sysvar_cache_entries(self); } fn update_slot_history(&self) { self.update_sysvar_account(&sysvar::slot_history::id(), |account| { let mut slot_history = account .as_ref() - .map(|account| from_account::(account).unwrap()) + .map(|account| wincode::deserialize::(account.data()).unwrap()) .unwrap_or_default(); slot_history.add(self.slot()); create_account( @@ -2481,7 +2543,7 @@ impl Bank { self.update_sysvar_account(&sysvar::slot_hashes::id(), |account| { let mut slot_hashes = account .as_ref() - .map(|account| from_account::(account).unwrap()) + .map(|account| wincode::deserialize::(account.data()).unwrap()) .unwrap_or_default(); slot_hashes.add(self.parent_slot, self.parent_hash); create_account( @@ -2491,8 +2553,9 @@ impl Bank { }); } - pub fn get_slot_history(&self) -> SlotHistory { - from_account(&self.get_account(&sysvar::slot_history::id()).unwrap()).unwrap() + pub fn get_slot_history(&self) -> Option { + wincode::deserialize::(self.get_account(&sysvar::slot_history::id())?.data()) + .ok() } fn update_epoch_stakes( @@ -2557,6 +2620,7 @@ impl Bank { return; } + let vat_to_burn_per_epoch = self.vat_to_burn_per_epoch(); let vote_accounts = epoch_stakes.stakes().vote_accounts(); debug_assert!(vote_accounts.len() <= 2000); // +1 for the incinerator account @@ -2568,11 +2632,11 @@ impl Bank { // accounts with non-zero stake and sufficient balance. for (vote_pubkey, _stake) in vote_accounts.delegated_stakes() { let mut account = self.get_account(vote_pubkey).unwrap(); - total_vat += VAT_TO_BURN_PER_EPOCH; + total_vat += vat_to_burn_per_epoch; account.set_lamports( account .lamports() - .checked_sub(VAT_TO_BURN_PER_EPOCH) + .checked_sub(vat_to_burn_per_epoch) .expect( "Vote accounts should have already been filtered to contain enough \ balance for the VAT", @@ -2627,18 +2691,198 @@ impl Bank { } // if I'm the first Bank in an epoch, ensure stake_history is updated self.update_sysvar_account(&stake_history::id(), |account| { - create_account::( + create_stake_history_account( self.stakes_cache.stakes().history(), self.inherit_specially_retained_account_fields(account), ) }); } + /// Rebuilds slot-param state from the current feature set. + fn refresh_slot_params(&mut self) { + self.refresh_slot_params_with_baseline(self.slot_params.baseline_params()); + } + + fn refresh_slot_params_from_snapshot(&mut self, genesis_config: &GenesisConfig) { + let (feature_set, _) = self.compute_active_feature_set(false); + self.refresh_slot_params_with_baseline( + self.snapshot_restore_slot_params_baseline(genesis_config, &feature_set), + ); + } + + /// Rebuilds cached slot params while preserving the supplied slot-0 baseline. + /// + /// The cache is not serialized into snapshots; it is reconstructed from + /// existing Bank fields during genesis and snapshot restore. + fn refresh_slot_params_with_baseline(&mut self, baseline_params: SlotParams) { + self.slot_params = + SlotParamsArchive::new(&self.feature_set, &self.epoch_schedule, baseline_params); + } + + /// Builds slot-0 params from the genesis config. + fn genesis_config_slot_params( + genesis_config: &GenesisConfig, + partitioned_rewards_stake_account_stores_per_block: u64, + ) -> SlotParams { + SlotParams::genesis_baseline( + genesis_config.ns_per_slot(), + genesis_config.slots_per_year(), + genesis_config.hashes_per_tick(), + partitioned_rewards_stake_account_stores_per_block, + ) + } + + /// Builds the slot-param baseline from the restored bank fields. + /// + /// Snapshot fields represent the cluster's current reality. This can + /// differ from genesis for values, such as `hashes_per_tick`, that were + /// changed by older feature gates before slot-time reductions existed. + fn restored_bank_slot_params(&self) -> SlotParams { + SlotParams::genesis_baseline( + self.ns_per_slot, + self.slots_per_year, + self.hashes_per_tick(), + self.partitioned_rewards_stake_account_stores_per_block, + ) + } + + /// Returns true if any slot-time reduction has taken effect by this bank. + /// + /// Feature activation happens in one epoch, but slot params become effective + /// at the start of the following epoch. + fn any_slot_time_reduction_effective( + &self, + feature_set: &FeatureSet, + ns_per_slot: u128, + ) -> bool { + SlotParamsArchive::any_slot_time_reduction_effective( + &self.epoch_schedule, + self.slot, + feature_set, + ns_per_slot, + ) + } + + /// Selects the slot-param baseline to use when reconstructing from snapshot. + /// + /// Before any slot-time reduction is effective, the baseline should match + /// restored bank fields because historical non-slot-time feature gates may + /// have already changed some values away from genesis. Once a slot-time + /// reduction is effective, keep the genesis baseline so historical lookups + /// for pre-reduction slots remain correct. + fn snapshot_restore_slot_params_baseline( + &self, + genesis_config: &GenesisConfig, + feature_set: &FeatureSet, + ) -> SlotParams { + if self.any_slot_time_reduction_effective(feature_set, genesis_config.ns_per_slot()) { + Self::genesis_config_slot_params( + genesis_config, + self.partitioned_rewards_stake_account_stores_per_block, + ) + } else { + // Default to whatever is in the bank if we've never enabled any + // slot time reductions. This prevents resetting any slot params + // that may have been changed previously back to genesis. + self.restored_bank_slot_params() + } + } + + /// Returns the slot params effective at `slot`. + fn slot_params_at_slot(&self, slot: Slot) -> SlotParams { + self.slot_params.params_at_slot(slot) + } + + /// Returns the slot params that should be effective for this bank's slot. + fn current_slot_params(&self) -> SlotParams { + self.slot_params_at_slot(self.slot) + } + + /// Returns the Validator Admission Ticket burn for this bank's slot params. + pub(crate) fn vat_to_burn_per_epoch(&self) -> u64 { + self.current_slot_params().vat_to_burn_per_epoch() + } + + pub fn get_vat_health_for_next_epoch( + &self, + vote_account_pubkey: &Pubkey, + ) -> std::result::Result<(), VATHealthError> { + let vote_accounts = self.vote_accounts(); + + let Some((_, vote_account)) = vote_accounts.get(vote_account_pubkey) else { + return Err(VATHealthError::VoteAccountNotFound); + }; + + if vote_account + .vote_state_view() + .bls_pubkey_compressed() + .is_none() + { + return Err(VATHealthError::NoBLSPubkey); + } + + let my_balance = vote_account.lamports(); + let minimum_vote_account_balance_for_vat = self.minimum_vote_account_balance_for_vat(); + if vote_account.lamports() < minimum_vote_account_balance_for_vat { + return Err(VATHealthError::InsufficientFundsInVoteAccount( + my_balance, + minimum_vote_account_balance_for_vat, + )); + } + + Ok(()) + } + + /// Returns the effective slot duration for `slot`. + pub fn ns_per_slot_at_slot(&self, slot: Slot) -> u128 { + self.slot_params_at_slot(slot).ns_per_slot() + } + + /// Returns slots/year for the slot params active at `epoch` start. + fn slots_per_year_for_epoch(&self, epoch: Epoch) -> f64 { + let first_slot = self.epoch_schedule().get_first_slot_in_epoch(epoch); + self.slot_params_at_slot(first_slot).slots_per_year() + } + + /// Returns the wall-clock duration in years for `[start_slot, end_slot)`. + fn slot_range_duration_in_years(&self, start_slot: Slot, end_slot: Slot) -> f64 { + if start_slot >= end_slot { + return 0.0; + } + + let mut cursor = start_slot; + let mut params = self.slot_params.baseline_params(); + let mut duration = 0.0; + + for (effective_slot, effective_params) in self.slot_params.param_transitions() { + if effective_slot <= start_slot { + params = effective_params; + continue; + } + if effective_slot >= end_slot { + break; + } + + duration += (effective_slot - cursor) as f64 / params.slots_per_year(); + cursor = effective_slot; + params = effective_params; + } + + duration + (end_slot - cursor) as f64 / params.slots_per_year() + } + + /// Returns the exact wall-clock duration in nanoseconds for `start_slot..=end_slot`. + pub fn slot_range_duration_nanos(&self, start_slot: Slot, end_slot: Slot) -> u128 { + self.slot_params + .slot_range_duration_nanos(start_slot, end_slot) + } + pub fn epoch_duration_in_years(&self, epoch: Epoch) -> f64 { // period: time that has passed as a fraction of a year, basically the length of // an epoch as a fraction of a year // calculated as: slots_elapsed / (slots / year) - self.epoch_schedule().get_slots_in_epoch(epoch) as f64 / self.slots_per_year + self.epoch_schedule().get_slots_in_epoch(epoch) as f64 + / self.slots_per_year_for_epoch(epoch) } pub fn max_processing_age(&self) -> usize { @@ -2665,22 +2909,27 @@ impl Bank { }) } + /// Returns slots since inflation started, aligned to the first slot used for rewards accrual. fn get_inflation_num_slots(&self) -> u64 { + let inflation_start_slot = self.inflation_start_slot_aligned_to_rewards(); + self.epoch_schedule().get_first_slot_in_epoch(self.epoch()) - inflation_start_slot + } + + /// Returns the inflation rewards start slot aligned to an epoch boundary. + fn inflation_start_slot_aligned_to_rewards(&self) -> Slot { let inflation_activation_slot = self.get_inflation_start_slot(); - // Normalize inflation_start to align with the start of rewards accrual. - let inflation_start_slot = self.epoch_schedule().get_first_slot_in_epoch( + self.epoch_schedule().get_first_slot_in_epoch( self.epoch_schedule() .get_epoch(inflation_activation_slot) .saturating_sub(1), - ); - self.epoch_schedule().get_first_slot_in_epoch(self.epoch()) - inflation_start_slot + ) } + /// Returns elapsed inflation time in years for slots since inflation started. pub fn slot_in_year_for_inflation(&self) -> f64 { let num_slots = self.get_inflation_num_slots(); - - // calculated as: num_slots / (slots / year) - num_slots as f64 / self.slots_per_year + let inflation_start_slot = self.inflation_start_slot_aligned_to_rewards(); + self.slot_range_duration_in_years(inflation_start_slot, inflation_start_slot + num_slots) } /// For a given `capitalization` (total_supply in lamports) and `epoch`, returns the @@ -2727,14 +2976,22 @@ impl Bank { (slot_delta <= slots_per_epoch) .then_some((*pubkey, (last_timestamp.slot, last_timestamp.timestamp))) }); - let slot_duration = Duration::from_nanos(self.ns_per_slot as u64); + let elapsed_slot_duration = |from_slot: Slot, to_slot: Slot| { + if from_slot >= to_slot { + Duration::ZERO + } else { + Duration::from_nanos_u128( + self.slot_range_duration_nanos(from_slot.saturating_add(1), to_slot), + ) + } + }; let epoch = self.epoch_schedule().get_epoch(self.slot()); let stakes = self.epoch_vote_accounts(epoch)?; let stake_weighted_timestamp = calculate_stake_weighted_timestamp( recent_timestamps, stakes, self.slot(), - slot_duration, + elapsed_slot_duration, epoch_start_timestamp, max_allowable_drift, ); @@ -2789,7 +3046,7 @@ impl Bank { self.freeze_started.store(true, Relaxed); // updating the accounts lt hash must happen *outside* of hash_internal_state() so // that rehash() can be called and *not* modify self.accounts_lt_hash. - self.update_accounts_lt_hash(); + self.finish_accounts_lt_hash_updates(); *hash = self.hash_internal_state(); self.rc.accounts.accounts_db.mark_slot_frozen(self.slot()); } @@ -2801,10 +3058,10 @@ impl Bank { self.freeze(); let computed_hash = self.hash(); - if let Some(expected_hash) = self.expected_bank_hash() { - if expected_hash != computed_hash { - return Err((expected_hash, computed_hash)); - } + if let Some(expected_hash) = self.expected_bank_hash() + && expected_hash != computed_hash + { + return Err((expected_hash, computed_hash)); } Ok(()) } @@ -2839,17 +3096,16 @@ impl Bank { self.freeze(); //this bank and all its parents are now on the rooted path - let mut roots = vec![self.slot()]; - roots.append(&mut self.parents().iter().map(|p| p.slot()).collect()); + let mut roots = Vec::with_capacity(self.ancestors.len()); + roots.push(self.slot()); + roots.extend(self.parents_iter().map(|parent| parent.slot())); - let mut total_index_us = 0; let mut total_cache_us = 0; let mut squash_accounts_time = Measure::start("squash_accounts_time"); for slot in roots.iter().rev() { // root forks cannot be purged let add_root_timing = self.rc.accounts.add_root(*slot); - total_index_us += add_root_timing.index_us; total_cache_us += add_root_timing.cache_us; } squash_accounts_time.stop(); @@ -2857,14 +3113,14 @@ impl Bank { *self.rc.parent.write().unwrap() = None; let mut squash_cache_time = Measure::start("squash_cache_time"); - roots - .iter() - .for_each(|slot| self.status_cache.write().unwrap().add_root(*slot)); + self.status_cache + .write() + .unwrap() + .add_roots(roots.iter().copied()); squash_cache_time.stop(); SquashTiming { squash_accounts_ms: squash_accounts_time.as_ms(), - squash_accounts_index_ms: total_index_us / 1000, squash_accounts_cache_ms: total_cache_us / 1000, squash_cache_ms: squash_cache_time.as_ms(), } @@ -2916,6 +3172,7 @@ impl Bank { self.stakes_cache = StakesCache::new(Stakes::new_from_accounts_for_genesis( self.new_warmup_cooldown_rate_epoch(), genesis_config.accounts.iter(), + self.use_fixed_point_stake_math(), )); // After storing genesis accounts, the bank stakes cache will be warmed @@ -2939,7 +3196,7 @@ impl Bank { genesis_config.fee_rate_governor.lamports_per_signature, ); - self.hashes_per_tick = genesis_config.hashes_per_tick(); + self.hashes_per_tick = RwLock::new(genesis_config.hashes_per_tick()); self.ticks_per_slot = genesis_config.ticks_per_slot(); self.ns_per_slot = genesis_config.ns_per_slot(); self.genesis_creation_time = genesis_config.creation_time; @@ -2947,6 +3204,10 @@ impl Bank { self.slots_per_year = genesis_config.slots_per_year(); self.epoch_schedule = genesis_config.epoch_schedule.clone(); + self.refresh_slot_params_with_baseline(Self::genesis_config_slot_params( + genesis_config, + self.partitioned_rewards_stake_account_stores_per_block, + )); self.inflation = Arc::new(RwLock::new(genesis_config.inflation)); @@ -3010,8 +3271,8 @@ impl Bank { self.rent_collector.rent.burn_percent = burn_percent; } - pub fn set_hashes_per_tick(&mut self, hashes_per_tick: Option) { - self.hashes_per_tick = hashes_per_tick; + pub fn set_hashes_per_tick(&self, hashes_per_tick: Option) { + *self.hashes_per_tick.write().unwrap() = hashes_per_tick; } /// Return the last block hash registered. @@ -3052,28 +3313,19 @@ impl Bank { blockhash_queue.get_lamports_per_signature(message.recent_blockhash()) } .or_else(|| { - self.load_message_nonce_data(message) + self.load_message_nonce_data(message, false) .map(|(_nonce_address, nonce_data)| nonce_data.get_lamports_per_signature()) })?; - Some(self.get_fee_for_message_with_lamports_per_signature(message)) - } - pub fn get_fee_for_message_with_lamports_per_signature( - &self, - message: &impl SVMMessage, - ) -> u64 { - let prioritization_fee = process_compute_budget_instructions( - message.program_instructions_iter(), - &self.feature_set, - ) - .unwrap_or_default() - .get_prioritization_fee(); - solana_fee::calculate_fee( + let transaction_configuration = + TransactionConfiguration::try_from_sanitized_message(message, &self.feature_set) + .ok()?; + Some(solana_fee::calculate_fee( message, self.fee_structure().lamports_per_signature, - prioritization_fee, + transaction_configuration.priority_fee_lamports, FeeFeatures::from(self.feature_set.as_ref()), - ) + )) } pub fn get_blockhash_last_valid_block_height(&self, blockhash: &Hash) -> Option { @@ -3093,25 +3345,48 @@ impl Bank { /// - If `get_alpenglow_genesis_certificate` is called before the marker is processed by replay /// this account will be empty. /// - If `get_alpenglow_genesis_certificate` is called after the marker is processed, we return the certificate - pub fn get_alpenglow_genesis_certificate(&self) -> Option { + pub fn get_alpenglow_genesis_certificate(&self) -> Option { let acct = self.get_account(&GENESIS_CERTIFICATE_ACCOUNT)?; (!acct.data().is_empty()).then(|| { // The address is known in advance, so the account could already exist if it was prefunded. // However this account cannot be written to except by us in `set_alpenglow_genesis_certificate`, // so this deserialize is safe if the account is non-empty - wincode::deserialize(acct.data()) - .expect("Programmer error deserializing genesis certificate") + let cert: WireBlockCertMessage = wincode::deserialize(acct.data()) + .expect("Programmer error deserializing genesis certificate"); + GenesisCert { + block: cert.block, + signature: CertSignature { + signature: cert.signature.signature, + bitmap: cert.signature.bitmap, + }, + } }) } + pub fn is_alpenglow(&self) -> bool { + self.is_alpenglow.load(Relaxed) + } + + fn set_is_alpenglow(&self) { + self.is_alpenglow.store(true, Relaxed); + } + /// For use in the first Alpenglow block, set the genesis certificate. - pub fn set_alpenglow_genesis_certificate(&self, cert: &Certificate) { - let data = wincode::serialize(cert).unwrap(); + pub fn set_alpenglow_genesis_certificate(&self, cert: &GenesisCert) { + let cert = WireBlockCertMessage { + block: cert.block, + signature: WireCertSignature { + signature: cert.signature.signature, + bitmap: cert.signature.bitmap.clone(), + }, + }; + let data = wincode::serialize(&cert).unwrap(); let lamports = Rent::default().minimum_balance(data.len()); let mut cert_acct = AccountSharedData::new(lamports, data.len(), &system_program::ID); cert_acct.set_data_from_slice(&data); self.store_account_and_update_capitalization(&GENESIS_CERTIFICATE_ACCOUNT, &cert_acct); + self.set_is_alpenglow(); } /// Update the clock sysvar from a block footer's nanosecond timestamp. @@ -3161,6 +3436,9 @@ impl Bank { alpenclock_acct.set_data_from_slice(&data); self.store_account_and_update_capitalization(&NANOSECOND_CLOCK_ACCOUNT, &alpenclock_acct); + + self.transaction_processor + .reset_and_fill_sysvar_cache_entries(self); } /// Get the nanosecond clock value. Returns `None` if the nanosecond clock has not been @@ -3178,13 +3456,14 @@ impl Bank { pub fn confirmed_last_blockhash(&self) -> Hash { const NUM_BLOCKHASH_CONFIRMATIONS: usize = 3; - let parents = self.parents(); - if parents.is_empty() { - self.last_blockhash() - } else { - let index = NUM_BLOCKHASH_CONFIRMATIONS.min(parents.len() - 1); - parents[index].last_blockhash() + let mut last_parent = None; + for (index, parent) in self.parents_iter().enumerate() { + if index == NUM_BLOCKHASH_CONFIRMATIONS { + return parent.last_blockhash(); + } + last_parent = Some(parent); } + last_parent.map_or_else(|| self.last_blockhash(), |parent| parent.last_blockhash()) } /// Forget all signatures. Useful for benchmarking. @@ -3547,6 +3826,7 @@ impl Bank { }, drop_on_failure: false, all_or_nothing: false, + strict_nonce_size_check: false, }, ); @@ -3644,7 +3924,7 @@ impl Bank { let current_account = self.get_account_with_fixed_root(&slot_history_id); let slot_history = current_account .as_ref() - .map(|account| from_account::(account).unwrap()) + .map(|account| wincode::deserialize::(account.data()).unwrap()) .unwrap_or_default(); if slot_history.check(self.slot()) == Check::Found { let ancestors = Ancestors::from(self.proper_ancestors().collect::>()); @@ -3709,6 +3989,7 @@ impl Bank { sanitized_txs, batch.lock_results(), max_age, + processing_config.strict_nonce_size_check, error_counters, )); timings.saturating_add_in_place(ExecuteTimingType::CheckUs, check_us); @@ -3722,7 +4003,7 @@ impl Bank { let processing_environment = TransactionProcessingEnvironment { blockhash, blockhash_lamports_per_signature, - alpenglow_migration_succeeded: self.get_alpenglow_genesis_certificate().is_some(), + alpenglow_migration_succeeded: self.is_alpenglow(), epoch_total_stake: self.get_current_epoch_total_stake(), feature_set: self.feature_set.runtime_features(), program_runtime_environments: ProgramRuntimeEnvironments::new( @@ -4053,6 +4334,7 @@ impl Bank { let to_store = (self.slot(), accounts_to_store.as_slice()); self.update_bank_hash_stats(&to_store); + self.enqueue_accounts_lt_hash_updates(&to_store); // See https://github.com/solana-labs/solana/pull/31455 for discussion // on *not* updating the index within a threadpool. self.rc @@ -4280,6 +4562,7 @@ impl Bank { recording_config, drop_on_failure: false, all_or_nothing: false, + strict_nonce_size_check: false, }, ); @@ -4393,19 +4676,23 @@ impl Bank { /// Compute all the parents of the bank in order pub fn parents(&self) -> Vec> { - let mut parents = vec![]; + self.parents_iter().collect() + } + + pub(crate) fn parents_iter(&self) -> impl Iterator> + '_ { let mut bank = self.parent(); - while let Some(parent) = bank { - parents.push(parent.clone()); + core::iter::from_fn(move || { + let parent = bank.take()?; bank = parent.parent(); - } - parents + Some(parent) + }) } /// Compute all the parents of the bank including this bank itself pub fn parents_inclusive(self: Arc) -> Vec> { - let mut parents = self.parents(); - parents.insert(0, self); + let mut parents = Vec::with_capacity(self.ancestors.len()); + parents.push(Arc::clone(&self)); + parents.extend(self.parents_iter()); parents } @@ -4419,6 +4706,7 @@ impl Bank { assert!(!self.freeze_started()); let mut m = Measure::start("stakes_cache.check_and_store"); let new_warmup_cooldown_rate_epoch = self.new_warmup_cooldown_rate_epoch(); + let use_fixed_point_stake_math = self.use_fixed_point_stake_math(); (0..accounts.len()).for_each(|i| { accounts.account(i, |account| { @@ -4426,13 +4714,11 @@ impl Bank { account.pubkey(), &account, new_warmup_cooldown_rate_epoch, + use_fixed_point_stake_math, ) }) }); - self.update_bank_hash_stats(&accounts); - self.rc - .accounts - .store_accounts_par(accounts, None, &self.ancestors); + self.store_accounts_without_stakes_cache(accounts); m.stop(); self.rc .accounts @@ -4449,6 +4735,7 @@ impl Bank { fn store_accounts_without_stakes_cache<'a>(&self, accounts: impl StorableAccounts<'a>) { assert!(!self.freeze_started()); self.update_bank_hash_stats(&accounts); + self.enqueue_accounts_lt_hash_updates(&accounts); self.rc .accounts .store_accounts_par(accounts, None, &self.ancestors); @@ -4513,24 +4800,78 @@ impl Bank { self.rc.accounts.clone() } + /// Recomputes cost tracker limits from active feature state. fn apply_cost_tracker_limits_for_active_features(&mut self) { + let params = self.current_slot_params(); + let cost_limits = + params.cost_limits(self.feature_set.snapshot().raise_block_limits_to_100m); + let mut cost_tracker = self.write_cost_tracker().unwrap(); - let block_cost_limit = if self.feature_set.snapshot().raise_block_limits_to_100m { - simd_0286_block_limit() - } else { - cost_tracker.get_block_limit() - }; - let account_cost_limit = block_cost_limit.saturating_mul(40).saturating_div(100); - let vote_cost_limit = cost_tracker.get_vote_limit(); - let allocated_data_size_limit = cost_tracker.get_allocated_data_size_limit(); - cost_tracker.set_limits( - account_cost_limit, - block_cost_limit, - vote_cost_limit, - allocated_data_size_limit, + cost_tracker.set_limits(cost_limits); + } + + /// Recomputes this bank's effective partitioned-reward write budget. + fn apply_partitioned_epoch_rewards_config_for_active_features(&mut self) { + self.partitioned_rewards_stake_account_stores_per_block = self + .current_slot_params() + .partitioned_epoch_rewards_stake_account_stores_per_block(); + } + + /// Applies slot-time changes for fields serialized into snapshots. + fn apply_slot_time_persistent_changes(&mut self) { + let params = self.current_slot_params(); + self.ns_per_slot = params.ns_per_slot(); + self.slots_per_year = params.slots_per_year(); + self.rent_collector.slots_per_year = params.slots_per_year(); + if !self.feature_set.is_active(&feature_set::alpenglow::id()) + && self.hashes_per_tick().is_some() + { + self.set_hashes_per_tick(params.hashes_per_tick()); + } + } + + /// Verifies bank fields are consistent with current slot params. + fn assert_bank_matches_slot_params(&self) { + let params = self.current_slot_params(); + assert_eq!( + self.ns_per_slot, + params.ns_per_slot(), + "snapshot slot-time ns_per_slot mismatch" + ); + assert_eq!( + self.slots_per_year.to_bits(), + params.slots_per_year().to_bits(), + "snapshot slot-time slots_per_year mismatch" + ); + assert_eq!( + self.rent_collector.slots_per_year.to_bits(), + params.slots_per_year().to_bits(), + "snapshot slot-time rent_collector.slots_per_year mismatch" + ); + let hashes_per_tick = self.hashes_per_tick(); + if !self.feature_set.is_active(&feature_set::alpenglow::id()) && hashes_per_tick.is_some() { + assert_eq!( + hashes_per_tick, + params.hashes_per_tick(), + "snapshot slot-time hashes_per_tick mismatch" + ); + } + assert_eq!( + self.entry_bytes_budget().slot_limit(), + params.max_entry_bytes_per_slot(), + "snapshot slot-time entry byte budget mismatch" ); } + /// Applies slot-time changes for runtime-only fields. This function is + /// expected to be idempotent. + fn apply_slot_time_runtime_changes(&mut self) { + self.entry_bytes_consumed = + EntryBytesBudget::new(self.current_slot_params().max_entry_bytes_per_slot()); + self.apply_cost_tracker_limits_for_active_features(); + self.apply_partitioned_epoch_rewards_config_for_active_features(); + } + fn apply_simd_0339_invoke_cost_changes(&mut self) { let simd_0268_active = self.feature_set.snapshot().raise_cpi_nesting_limit_to_8; let compute_budget = self @@ -4552,11 +4893,10 @@ impl Bank { Arc::new(reserved_keys) }; - // Cost-Tracker is not serialized in snapshot or any configs. - // We must apply previously activated features related to limits here - // so that the initial bank state is consistent with the feature set. - // Cost-tracker limits are propagated through children banks. - self.apply_cost_tracker_limits_for_active_features(); + // Many fields are not serialized in snapshot or any configs. Rebuild + // them from the feature set so the initial bank state is consistent. + self.refresh_slot_params(); + self.apply_slot_time_runtime_changes(); self.apply_simd_0339_invoke_cost_changes(); let program_runtime_environment = @@ -4782,10 +5122,10 @@ impl Bank { pubkey: &Pubkey, ) -> Option<(AccountSharedData, Slot)> { let just_self: Ancestors = Ancestors::from(vec![self.slot()]); - if let Some((account, slot)) = self.load_slow_with_fixed_root(&just_self, pubkey) { - if slot == self.slot() { - return Some((account, slot)); - } + if let Some((account, slot)) = self.load_slow_with_fixed_root(&just_self, pubkey) + && slot == self.slot() + { + return Some((account, slot)); } None } @@ -4848,16 +5188,16 @@ impl Bank { /// /// Limit changes are delayed by an epoch, so a root bank can derive the /// limit for any slot inside the shred intake window. - pub fn max_data_shreds_per_slot_for_slot(&self, _slot: Slot) -> u32 { - DEFAULT_MAX_DATA_SHREDS_PER_SLOT + pub fn max_data_shreds_per_slot_for_slot(&self, slot: Slot) -> u32 { + self.slot_params_at_slot(slot).max_data_shreds_per_slot() } /// Returns the code shred limit applicable to `slot`. /// /// Limit changes are delayed by an epoch, so a root bank can derive the /// limit for any slot inside the shred intake window. - pub fn max_code_shreds_per_slot_for_slot(&self, _slot: Slot) -> u32 { - DEFAULT_MAX_CODE_SHREDS_PER_SLOT + pub fn max_code_shreds_per_slot_for_slot(&self, slot: Slot) -> u32 { + self.slot_params_at_slot(slot).max_code_shreds_per_slot() } pub fn max_entry_bytes_per_slot(&self) -> u64 { @@ -4889,10 +5229,10 @@ impl Bank { &self, signature: &Signature, ) -> Option> { - if let Some((slot, status)) = self.get_signature_status_slot(signature) { - if slot <= self.slot() { - return Some(status); - } + if let Some((slot, status)) = self.get_signature_status_slot(signature) + && slot <= self.slot() + { + return Some(status); } None } @@ -5010,12 +5350,7 @@ impl Bank { pub fn run_final_hash_calc(&self) { self.force_flush_accounts_cache(); // note that this slot may not be a root - _ = self.verify_accounts( - VerifyAccountsHashConfig { - require_rooted_bank: false, - }, - None, - ); + _ = self.verify_accounts(None); } /// Verify the account state as part of startup, typically from a snapshot. @@ -5031,31 +5366,9 @@ impl Bank { /// /// Only intended to be called at startup, or from tests/ledger-tool. #[must_use] - fn verify_accounts( - &self, - config: VerifyAccountsHashConfig, - calculated_accounts_lt_hash: Option<&AccountsLtHash>, - ) -> bool { + fn verify_accounts(&self, calculated_accounts_lt_hash: Option<&AccountsLtHash>) -> bool { let accounts_db = &self.rc.accounts.accounts_db; - let slot = self.slot(); - - if config.require_rooted_bank && !accounts_db.accounts_index.is_alive_root(slot) { - if let Some(parent) = self.parent() { - info!( - "slot {slot} is not a root, so verify accounts hash on parent bank at slot {}", - parent.slot(), - ); - // The calculated_accounts_lt_hash parameter is only valid for the current slot, so - // we must fall back to calculating the accounts lt hash with the index. - return parent.verify_accounts(config, None); - } else { - // this will result in mismatch errors - // accounts hash calc doesn't include unrooted slots - panic!("cannot verify accounts hash because slot {slot} is not a root"); - } - } - fn check_lt_hash( expected_accounts_lt_hash: &AccountsLtHash, calculated_accounts_lt_hash: &AccountsLtHash, @@ -5256,12 +5569,7 @@ impl Bank { let (verified_accounts, verify_accounts_time_us) = measure_us!({ let should_verify_accounts = !self.rc.accounts.accounts_db.skip_initial_hash_calc; if should_verify_accounts { - self.verify_accounts( - VerifyAccountsHashConfig { - require_rooted_bank: false, - }, - calculated_accounts_lt_hash, - ) + self.verify_accounts(calculated_accounts_lt_hash) } else { info!("Verifying accounts... Skipped."); true @@ -5317,8 +5625,8 @@ impl Bank { } /// Return the number of hashes per tick - pub fn hashes_per_tick(&self) -> &Option { - &self.hashes_per_tick + pub fn hashes_per_tick(&self) -> Option { + *self.hashes_per_tick.read().unwrap() } /// Return the number of ticks per slot @@ -5388,6 +5696,7 @@ impl Bank { ) { debug_assert_eq!(txs.len(), processing_results.len()); let new_warmup_cooldown_rate_epoch = self.new_warmup_cooldown_rate_epoch(); + let use_fixed_point_stake_math = self.use_fixed_point_stake_math(); txs.iter() .zip(processing_results) .filter_map(|(tx, processing_result)| { @@ -5409,8 +5718,12 @@ impl Bank { .for_each(|(pubkey, account)| { // note that this could get timed to: self.rc.accounts.accounts_db.stats.stakes_cache_check_and_store_us, // but this code path is captured separately in ExecuteTimingType::UpdateStakesCacheUs - self.stakes_cache - .check_and_store(pubkey, account, new_warmup_cooldown_rate_epoch); + self.stakes_cache.check_and_store( + pubkey, + account, + new_warmup_cooldown_rate_epoch, + use_fixed_point_stake_math, + ); }); } @@ -5443,26 +5756,25 @@ impl Bank { } /// Verify a BLS certificate's signature using this bank's epoch stakes. - /// - /// Returns (stake present in certificate, total stake in validator set) on success. pub fn verify_certificate( &self, - cert: &Certificate, - ) -> std::result::Result<(u64, u64), CertVerifyError> { + cert: UnverifiedCertificate, + ) -> std::result::Result { let slot = cert.cert_type.slot(); let epoch_stakes = self .epoch_stakes_from_slot(slot) .ok_or(CertVerifyError::MissingRankMap)?; let key_to_rank_map = epoch_stakes.bls_pubkey_to_rank_map(); - let total_stake = epoch_stakes.total_stake(); + let total_stake = key_to_rank_map.total_stake(); - let stake = cert_verify::verify_certificate(cert, key_to_rank_map.len(), |rank| { - key_to_rank_map - .get_pubkey_stake_entry(rank) - .map(|entry| (entry.stake, entry.bls_pubkey)) - })?; + let cert = + cert_verify::verify_certificate(cert, key_to_rank_map.len(), total_stake, |rank| { + key_to_rank_map + .get_pubkey_stake_entry(rank) + .map(|entry| (entry.stake, entry.bls_pubkey)) + })?; - Ok((stake, total_stake)) + Ok(cert) } pub fn epoch_stakes_map(&self) -> &HashMap { @@ -5651,12 +5963,14 @@ impl Bank { let mut feature_set = Arc::make_mut(&mut self.feature_set).clone(); feature_set.deactivate(id); self.feature_set = Arc::new(feature_set); + self.refresh_slot_params(); } pub fn activate_feature(&mut self, id: &Pubkey) { let mut feature_set = Arc::make_mut(&mut self.feature_set).clone(); feature_set.activate(id, 0); self.feature_set = Arc::new(feature_set); + self.refresh_slot_params(); } pub fn fill_bank_with_ticks_for_tests(&self) { @@ -5695,6 +6009,10 @@ impl Bank { } self.compute_and_apply_features_after_snapshot_restore(); + self.stakes_cache.refresh_delegated_stakes( + self.new_warmup_cooldown_rate_epoch(), + self.use_fixed_point_stake_math(), + ); self.recalculate_partitioned_rewards_if_active(rewards_thread_pool_builder); @@ -5732,6 +6050,7 @@ impl Bank { self.feature_set = Arc::new(feature_set); self.apply_activated_features(); + self.assert_bank_matches_slot_params(); } /// This is called from each epoch boundary @@ -5740,17 +6059,18 @@ impl Bank { let (feature_set, new_feature_activations) = self.compute_active_feature_set(include_pending); self.feature_set = Arc::new(feature_set); + self.refresh_slot_params(); // Update activation slot of features in `new_feature_activations` for feature_id in new_feature_activations.iter() { - if let Some(mut account) = self.get_account_with_fixed_root(feature_id) { - if let Some(mut feature) = feature::state::from_account(&account) { - feature.activated_at = Some(self.slot()); - if feature::state::to_account(&feature, &mut account).is_some() { - self.store_account(feature_id, &account); - } - info!("Feature {} activated at slot {}", feature_id, self.slot()); + if let Some(mut account) = self.get_account_with_fixed_root(feature_id) + && let Some(mut feature) = feature::state::from_account(&account) + { + feature.activated_at = Some(self.slot()); + if feature::state::to_account(&feature, &mut account).is_some() { + self.store_account(feature_id, &account); } + info!("Feature {} activated at slot {}", feature_id, self.slot()); } } @@ -5822,35 +6142,46 @@ impl Bank { self.fee_rate_governor.burn_percent = solana_fee_calculator::DEFAULT_BURN_PERCENT; } + // Apply unconditionally: this is relatively cheap and idempotent. + self.apply_slot_time_persistent_changes(); + self.apply_slot_time_runtime_changes(); + self.apply_new_builtin_program_feature_transitions(&new_feature_activations); - if new_feature_activations.contains(&feature_set::raise_block_limits_to_100m::id()) { - self.apply_cost_tracker_limits_for_active_features(); - } - if new_feature_activations.contains(&feature_set::replace_spl_token_with_p_token::id()) { - if let Err(e) = self.upgrade_loader_v2_program_with_loader_v3_program( + if new_feature_activations.contains(&feature_set::replace_spl_token_with_p_token::id()) + && let Err(e) = self.upgrade_loader_v2_program_with_loader_v3_program( &feature_set::replace_spl_token_with_p_token::SPL_TOKEN_PROGRAM_ID, &feature_set::replace_spl_token_with_p_token::PTOKEN_PROGRAM_BUFFER, self.feature_set .snapshot() .relax_programdata_account_check_migration, "replace_spl_token_with_p_token", - ) { - warn!( - "Failed to replace SPL Token with p-token buffer '{}': {e}", - feature_set::replace_spl_token_with_p_token::PTOKEN_PROGRAM_BUFFER, - ); - } + ) + { + warn!( + "Failed to replace SPL Token with p-token buffer '{}': {e}", + feature_set::replace_spl_token_with_p_token::PTOKEN_PROGRAM_BUFFER, + ); } - if new_feature_activations.contains(&feature_set::upgrade_bpf_stake_program_to_v5::id()) { - if let Err(e) = self.upgrade_core_bpf_program( + if new_feature_activations.contains(&feature_set::upgrade_bpf_stake_program_to_v5::id()) + && let Err(e) = self.upgrade_core_bpf_program( &solana_sdk_ids::stake::id(), &feature_set::upgrade_bpf_stake_program_to_v5::buffer::id(), "upgrade_stake_program_to_v5", - ) { - error!("Failed to upgrade Core BPF Stake program: {e}"); - } + ) + { + error!("Failed to upgrade Core BPF Stake program: {e}"); + } + + if new_feature_activations.contains(&feature_set::upgrade_bpf_stake_program_to_v5_1::id()) + && let Err(e) = self.upgrade_core_bpf_program( + &solana_sdk_ids::stake::id(), + &feature_set::upgrade_bpf_stake_program_to_v5_1::buffer::id(), + "upgrade_stake_program_to_v5_1", + ) + { + error!("Failed to upgrade Core BPF Stake program: {e}"); } } @@ -5859,37 +6190,37 @@ impl Bank { new_feature_activations: &AHashSet, ) { for builtin in BUILTINS.iter() { - if let Some(feature_id) = builtin.enable_feature_id { - if new_feature_activations.contains(&feature_id) { - self.add_builtin( - builtin.program_id, - builtin.name, - ProgramCacheEntry::new_builtin( - self.feature_set.activated_slot(&feature_id).unwrap_or(0), - builtin.name.len(), - builtin.register_fn, - ), - ); - } + if let Some(feature_id) = builtin.enable_feature_id + && new_feature_activations.contains(&feature_id) + { + self.add_builtin( + builtin.program_id, + builtin.name, + ProgramCacheEntry::new_builtin( + self.feature_set.activated_slot(&feature_id).unwrap_or(0), + builtin.name.len(), + builtin.register_fn, + ), + ); } if let Some(core_bpf_migration_config) = &builtin.core_bpf_migration_config { // If the builtin is set to be migrated to Core BPF on feature // activation, perform the migration which will remove it from // the builtins list and the cache. - if new_feature_activations.contains(&core_bpf_migration_config.feature_id) { - if let Err(e) = self.migrate_builtin_to_core_bpf( + if new_feature_activations.contains(&core_bpf_migration_config.feature_id) + && let Err(e) = self.migrate_builtin_to_core_bpf( &builtin.program_id, core_bpf_migration_config, self.feature_set .snapshot() .relax_programdata_account_check_migration, - ) { - warn!( - "Failed to migrate builtin {} to Core BPF: {}", - builtin.name, e - ); - } + ) + { + warn!( + "Failed to migrate builtin {} to Core BPF: {}", + builtin.name, e + ); } }; } @@ -5898,29 +6229,28 @@ impl Bank { // Stateless builtins do not have an `enable_feature_id` since they // do not exist on-chain. for stateless_builtin in STATELESS_BUILTINS.iter() { - if let Some(core_bpf_migration_config) = &stateless_builtin.core_bpf_migration_config { - if new_feature_activations.contains(&core_bpf_migration_config.feature_id) { - if let Err(e) = self.migrate_builtin_to_core_bpf( - &stateless_builtin.program_id, - core_bpf_migration_config, - self.feature_set - .snapshot() - .relax_programdata_account_check_migration, - ) { - warn!( - "Failed to migrate stateless builtin {} to Core BPF: {}", - stateless_builtin.name, e - ); - } - } + if let Some(core_bpf_migration_config) = &stateless_builtin.core_bpf_migration_config + && new_feature_activations.contains(&core_bpf_migration_config.feature_id) + && let Err(e) = self.migrate_builtin_to_core_bpf( + &stateless_builtin.program_id, + core_bpf_migration_config, + self.feature_set + .snapshot() + .relax_programdata_account_check_migration, + ) + { + warn!( + "Failed to migrate stateless builtin {} to Core BPF: {}", + stateless_builtin.name, e + ); } } for precompile in get_precompiles() { - if let Some(feature_id) = &precompile.feature { - if new_feature_activations.contains(feature_id) { - self.add_precompile(&precompile.program_id); - } + if let Some(feature_id) = &precompile.feature + && new_feature_activations.contains(feature_id) + { + self.add_precompile(&precompile.program_id); } } } @@ -5942,20 +6272,20 @@ impl Bank { for feature_id in self.feature_set.inactive() { let mut activated = None; - if let Some(account) = self.get_account_with_fixed_root(feature_id) { - if let Some(feature) = feature::state::from_account(&account) { - match feature.activated_at { - None if include_pending => { - // Feature activation is pending - pending.insert(*feature_id); - activated = Some(slot); - } - Some(activation_slot) if slot >= activation_slot => { - // Feature has been activated already - activated = Some(activation_slot); - } - _ => {} + if let Some(account) = self.get_account_with_fixed_root(feature_id) + && let Some(feature) = feature::state::from_account(&account) + { + match feature.activated_at { + None if include_pending => { + // Feature activation is pending + pending.insert(*feature_id); + activated = Some(slot); + } + Some(activation_slot) if slot >= activation_slot => { + // Feature has been activated already + activated = Some(activation_slot); } + _ => {} } } if let Some(slot) = activated { @@ -6108,10 +6438,10 @@ impl Bank { } pub fn is_in_slot_hashes_history(&self, slot: &Slot) -> bool { - if slot < &self.slot { - if let Ok(slot_hashes) = self.transaction_processor.sysvar_cache().get_slot_hashes() { - return slot_hashes.get(slot).is_some(); - } + if slot < &self.slot + && let Ok(slot_hashes) = self.transaction_processor.sysvar_cache().get_slot_hashes() + { + return slot_hashes.get(slot).is_some(); } false } @@ -6218,13 +6548,13 @@ impl Bank { /// Minimum balance a vote account must hold to survive SIMD-0357 filtering /// under the current feature set. When `alpenglow` is active the threshold /// also includes one epoch's worth of VAT burn. - fn minimum_vote_account_balance_for_vat(&self) -> u64 { + pub fn minimum_vote_account_balance_for_vat(&self) -> u64 { let vote_account_rent_exempt_minimum = self .rent_collector .rent .minimum_balance(VoteStateV4::size_of()); if self.feature_set.snapshot().alpenglow { - vote_account_rent_exempt_minimum + VAT_TO_BURN_PER_EPOCH + vote_account_rent_exempt_minimum + self.vat_to_burn_per_epoch() } else { vote_account_rent_exempt_minimum } @@ -6283,6 +6613,11 @@ impl Bank { solana_sha256_hasher::hashv(&[parent_block_id.as_ref(), bank.hash().as_ref()]); bank.set_block_id(Some(block_id)); } + + pub(crate) fn get_alpenglow_migration_slot(&self) -> Option { + let genesis_cert = self.get_alpenglow_genesis_certificate()?; + Some(genesis_cert.block.slot) + } } impl InvokeContextCallback for Bank { @@ -6326,8 +6661,8 @@ impl TransactionProcessingCallback for Bank { .load_with_fixed_root(&self.ancestors, pubkey) } - fn inspect_account(&self, address: &Pubkey, account_state: AccountState, is_writable: bool) { - self.inspect_account_for_accounts_lt_hash(address, &account_state, is_writable); + fn inspect_account(&self, _address: &Pubkey, _account_state: AccountState, _is_writable: bool) { + // nothing to do here } } @@ -6380,7 +6715,7 @@ impl Bank { bank.signature_count = AtomicU64::new(fields.signature_count); bank.capitalization = AtomicU64::new(fields.capitalization); bank.max_tick_height = fields.max_tick_height; - bank.hashes_per_tick = fields.hashes_per_tick; + bank.hashes_per_tick = RwLock::new(fields.hashes_per_tick); bank.ticks_per_slot = fields.ticks_per_slot; bank.ns_per_slot = fields.ns_per_slot; bank.genesis_creation_time = fields.genesis_creation_time; @@ -6408,6 +6743,12 @@ impl Bank { bank.transaction_processor = TransactionBatchProcessor::new_uninitialized(slot, epoch); bank.accounts_lt_hash = Mutex::new(fields.accounts_lt_hash); bank.bank_hash_stats = AtomicBankHashStats::new(&fields.bank_hash_stats); + bank.refresh_slot_params_with_baseline(SlotParams::genesis_baseline( + bank.ns_per_slot, + bank.slots_per_year, + bank.hashes_per_tick(), + bank.partitioned_rewards_stake_account_stores_per_block, + )); bank } @@ -6734,6 +7075,11 @@ impl Bank { minimized_account_set.insert(*pubkey); }); } + + /// Returns true when this bank is using slot params beyond its genesis baseline. + pub fn slot_time_reduction_active(&self) -> bool { + self.ns_per_slot != self.slot_params.baseline_params().ns_per_slot() + } } /// Returns a thread pool intended to be used for reward calculation. This diff --git a/runtime/src/bank/accounts_lt_hash.rs b/runtime/src/bank/accounts_lt_hash.rs index 6388b322f2f..508c4c6fbdb 100644 --- a/runtime/src/bank/accounts_lt_hash.rs +++ b/runtime/src/bank/accounts_lt_hash.rs @@ -1,388 +1,375 @@ use { super::Bank, - rayon::prelude::*, - solana_account::{AccountSharedData, accounts_equal}, - solana_accounts_db::accounts_db::AccountsDb, - solana_hash::Hash, + crossbeam_utils::CachePadded, + rayon::{ThreadPool, ThreadPoolBuilder}, + solana_account::{AccountSharedData, ReadableAccount}, + solana_accounts_db::{accounts_db::AccountsDb, storable_accounts::StorableAccounts}, solana_lattice_hash::lt_hash::LtHash, - solana_measure::{meas_dur, measure::Measure}, solana_pubkey::Pubkey, - solana_svm_callback::AccountState, std::{ - ops::AddAssign, - sync::atomic::{AtomicU64, Ordering}, - time::Duration, + array, hint, + mem::size_of, + sync::{ + Arc, LazyLock, Mutex, + atomic::{AtomicU64, AtomicUsize, Ordering}, + }, + time::Instant, }, }; -impl Bank { - /// Updates the accounts lt hash - /// - /// When freezing a bank, we compute and update the accounts lt hash. - /// For each account modified in this bank, we: - /// - mix out its previous state, and - /// - mix in its current state - /// - /// Since this function is non-idempotent, it should only be called once per bank. - pub fn update_accounts_lt_hash(&self) { - let delta_lt_hash = self.calculate_delta_lt_hash(); - let mut accounts_lt_hash = self.accounts_lt_hash.lock().unwrap(); - accounts_lt_hash.0.mix_in(&delta_lt_hash); - } +/// Number of threads for the async accounts hasher thread pool. +const NUM_ACCOUNTS_HASHER_THREADS: usize = 4; - /// Calculates the lt hash *of only this slot* - /// - /// This can be thought of as akin to the accounts delta hash. - /// - /// For each account modified in this bank, we: - /// - mix out its previous state, and - /// - mix in its current state - /// - /// This function is idempotent, and may be called more than once. - fn calculate_delta_lt_hash(&self) -> LtHash { - let measure_total = Measure::start(""); - let slot = self.slot(); - - // If we don't find the account in the cache, we need to go load it. - // We want the version of the account *before* it was written in this slot. - // Bank::ancestors *includes* this slot, so we need to remove it before loading. - let strictly_ancestors = { - let mut ancestors = self.ancestors.clone(); - ancestors.remove(&self.slot()); - ancestors - }; +// Maximum size, in bytes, for the seen-accounts freelist. +const MAX_BYTES_SEEN_ACCOUNTS_FREELIST: usize = 10_000_000; - if slot == 0 { - // Slot 0 is special when calculating the accounts lt hash. - // Primordial accounts (those in genesis) that are modified by transaction processing - // in slot 0 will have Alive entries in the accounts lt hash cache. - // When calculating the accounts lt hash, if an account was initially alive, we mix - // *out* its previous lt hash value. In slot 0, we haven't stored any previous lt hash - // values (since it is in the first slot), yet we'd still mix out these accounts! - // This produces the incorrect accounts lt hash. - // From the perspective of the accounts lt hash, in slot 0 we cannot have any accounts - // as previously alive. So to work around this issue, we clear the cache. - // And since `strictly_ancestors` is empty, loading the previous version of the account - // from accounts db will return `None` (aka Dead), which is the correct behavior. - assert!(strictly_ancestors.is_empty()); - self.cache_for_accounts_lt_hash.clear(); +impl Bank { + /// Enqueues the accounts lt hash updates for `accounts` to the accounts hasher thread pool. + pub fn enqueue_accounts_lt_hash_updates<'a>(&self, accounts: &impl StorableAccounts<'a>) { + if accounts.is_empty() { + return; } - // Get all the accounts stored in this slot. - // Since this bank is in the middle of being frozen, it hasn't been rooted. - // That means the accounts should all be in the write cache, and loading will be fast. - let (accounts_curr, time_loading_accounts_curr) = meas_dur!({ - self.rc + let seen_accounts_freelist = seen_accounts_freelist(); + let mut seen_accounts = seen_accounts_freelist.try_pop().unwrap_or_default(); + let async_progress = &self.accounts_lt_hash_async_progress; + let thread_pool = accounts_hasher_thread_pool(); + + // process accounts in reverse because we must only count the latest version of each account + for index in (0..accounts.len()).rev() { + let address = accounts.pubkey(index); + if !seen_accounts.insert(*address) { + // we've already enqueued a newer update for the same account; skip this one + continue; + } + let prev_account = self + .rc .accounts - .accounts_db - .get_pubkey_account_for_slot(slot) - }); - let num_accounts_total = accounts_curr.len(); - - #[derive(Debug, Default)] - struct Stats { - num_cache_misses: usize, - num_accounts_unmodified: usize, - time_loading_accounts_prev: Duration, - time_comparing_accounts: Duration, - time_computing_hashes: Duration, - time_mixing_hashes: Duration, - } - impl AddAssign for Stats { - fn add_assign(&mut self, other: Self) { - self.num_cache_misses += other.num_cache_misses; - self.num_accounts_unmodified += other.num_accounts_unmodified; - self.time_loading_accounts_prev += other.time_loading_accounts_prev; - self.time_comparing_accounts += other.time_comparing_accounts; - self.time_computing_hashes += other.time_computing_hashes; - self.time_mixing_hashes += other.time_mixing_hashes; + .load_with_fixed_root_do_not_populate_read_cache(&self.ancestors, address) + .map(|(account, _slot)| account); + let curr_account = accounts.account(index, |account| { + (account.lamports() != 0).then(|| account.take_account()) + }); + if prev_account.is_none() && curr_account.is_none() { + // the account was ephemeral; skip it + } else { + // the account was modified; enqueue this update + async_progress.spawn( + thread_pool, + AccountsLtHashUpdate { + address: *address, + prev_account, + curr_account, + }, + ); } } - let do_calculate_delta_lt_hash = || { - // Work on chunks of 128 pubkeys, which is 4 KiB. - // And 4 KiB is likely the smallest a real page size will be. - // And a single page is likely the smallest size a disk read will actually read. - // This can be tuned larger, but likely not smaller. - const CHUNK_SIZE: usize = 128; - accounts_curr - .par_iter() - .fold_chunks( - CHUNK_SIZE, - || (LtHash::identity(), Stats::default()), - |mut accum, (pubkey, curr_account)| { - // load the initial state of the account - let (initial_state_of_account, measure_load) = meas_dur!({ - let cache_value = self - .cache_for_accounts_lt_hash - .get(pubkey) - .map(|entry| entry.value().clone()); - match cache_value { - Some(CacheValue::InspectAccount(initial_state_of_account)) => { - initial_state_of_account - } - Some(CacheValue::BankNew) | None => { - accum.1.num_cache_misses += 1; - // If the initial state of the account is not in the accounts - // lt hash cache, or is explicitly unknown, then it is likely - // this account was stored *outside* of transaction processing - // (e.g. creating a new bank). - // Do not populate the read cache, as this account likely will - // not be accessed again soon. - let account_slot = self - .rc - .accounts - .load_with_fixed_root_do_not_populate_read_cache( - &strictly_ancestors, - pubkey, - ); - match account_slot { - Some((account, _slot)) => { - InitialStateOfAccount::Alive(account) - } - None => InitialStateOfAccount::Dead, - } - } - } - }); - accum.1.time_loading_accounts_prev += measure_load; - - // mix out the previous version of the account - match initial_state_of_account { - InitialStateOfAccount::Dead => { - // nothing to do here - } - InitialStateOfAccount::Alive(prev_account) => { - let (are_accounts_equal, measure_is_equal) = - meas_dur!(accounts_equal(curr_account, &prev_account)); - accum.1.time_comparing_accounts += measure_is_equal; - if are_accounts_equal { - // this account didn't actually change, so skip it for lt hashing - accum.1.num_accounts_unmodified += 1; - return accum; - } - let (prev_lt_hash, measure_hashing) = - meas_dur!(AccountsDb::lt_hash_account(&prev_account, pubkey)); - let (_, measure_mixing) = - meas_dur!(accum.0.mix_out(&prev_lt_hash.0)); - accum.1.time_computing_hashes += measure_hashing; - accum.1.time_mixing_hashes += measure_mixing; - } - } - - // mix in the new version of the account - let (curr_lt_hash, measure_hashing) = - meas_dur!(AccountsDb::lt_hash_account(curr_account, pubkey)); - let (_, measure_mixing) = meas_dur!(accum.0.mix_in(&curr_lt_hash.0)); - accum.1.time_computing_hashes += measure_hashing; - accum.1.time_mixing_hashes += measure_mixing; - - accum - }, - ) - .reduce( - || (LtHash::identity(), Stats::default()), - |mut accum, elem| { - accum.0.mix_in(&elem.0); - accum.1 += elem.1; - accum - }, - ) + // reclaim the seen accounts hashset + seen_accounts_freelist.try_push(seen_accounts); + } + + /// Updates the accounts lt hash. + /// + /// When freezing a bank, we compute and update the accounts lt hash. + /// For each account modified in this bank, we: + /// - mix out its previous state, and + /// - mix in its current state + /// + /// This function waits for any in-flight jobs on the accounts hasher threads, + /// computes their combined delta lt hash, then mixes it into the bank. + pub fn finish_accounts_lt_hash_updates(&self) { + let timer = Instant::now(); + let num_jobs_total = { + let mut accounts_lt_hash = self.accounts_lt_hash.lock().unwrap(); + self.accounts_lt_hash_async_progress + .finish(&mut accounts_lt_hash.0) }; - let (delta_lt_hash, stats) = self - .rc - .accounts - .accounts_db - .thread_pool_foreground - .install(do_calculate_delta_lt_hash); + let finish_time = timer.elapsed(); - let total_time = measure_total.end_as_duration(); - let num_accounts_modified = - num_accounts_total.saturating_sub(stats.num_accounts_unmodified); + let seen_accounts_freelist_stats = seen_accounts_freelist().stats(); datapoint_info!( "bank-accounts_lt_hash", - ("slot", slot, i64), - ("num_accounts_total", num_accounts_total, i64), - ("num_accounts_modified", num_accounts_modified, i64), - ( - "num_accounts_unmodified", - stats.num_accounts_unmodified, - i64 - ), - ("num_cache_misses", stats.num_cache_misses, i64), - ("total_us", total_time.as_micros(), i64), - ( - "loading_accounts_curr_us", - time_loading_accounts_curr.as_micros(), - i64 - ), + ("slot", self.slot(), i64), + ("num_jobs", num_jobs_total, i64), + ("finish_us", finish_time.as_micros(), i64), ( - "par_loading_accounts_prev_us", - stats.time_loading_accounts_prev.as_micros(), + "seen_accounts_freelist_num_containers", + seen_accounts_freelist_stats.num_containers, i64 ), ( - "par_comparing_accounts_us", - stats.time_comparing_accounts.as_micros(), + "seen_accounts_freelist_capacity_elems", + seen_accounts_freelist_stats.capacity_elems, i64 ), ( - "par_computing_hashes_us", - stats.time_computing_hashes.as_micros(), - i64 - ), - ( - "par_mixing_hashes_us", - stats.time_mixing_hashes.as_micros(), - i64 - ), - ( - "num_inspect_account_hits", - self.stats_for_accounts_lt_hash - .num_inspect_account_hits - .load(Ordering::Relaxed), - i64 - ), - ( - "num_inspect_account_misses", - self.stats_for_accounts_lt_hash - .num_inspect_account_misses - .load(Ordering::Relaxed), - i64 - ), - ( - "num_inspect_account_after_frozen", - self.stats_for_accounts_lt_hash - .num_inspect_account_after_frozen - .load(Ordering::Relaxed), - i64 - ), - ( - "inspect_account_lookup_ns", - self.stats_for_accounts_lt_hash - .inspect_account_lookup_time_ns - .load(Ordering::Relaxed), - i64 - ), - ( - "inspect_account_insert_ns", - self.stats_for_accounts_lt_hash - .inspect_account_insert_time_ns - .load(Ordering::Relaxed), + "seen_accounts_freelist_capacity_bytes", + seen_accounts_freelist_stats.capacity_bytes, i64 ), ); + } +} - delta_lt_hash +/// Struct for tracking progress of the asynchronous accounts lt hashing for a Bank. +pub struct AccountsLtHashAsyncProgress { + // Note: use [Mutex>] and *not* [CachePadded>]. + // - In both ways each mutex is on its own separate cache line. + // - In both ways the size used for each element, including padding, is the same. + // - Only this way ensures that each LtHash is placed for aligned SIMD/AVX access. + // + // Here's the layout of [Mutex>; 2] + // + // │element 0 │element 1 + // │ │ + // ▼───────┬─────────┬────────────────▼───────┬─────────┬────────────────┐ + // │ Mutex │ padding │ LtHash │ Mutex │ padding │ LtHash │ + // ├───────┼─────────┼────────────────┼───────┼─────────┼────────────────┤ + // │ │ │ │ │ │ │ + // │0 │6 │128 <-- aligned │2176 │2182 │2304 │4352 + // + // + // And here's the layout of [CachePadded>; 2] + // + // │element 0 │element 1 + // │ │ + // ▼───────┬────────────────┬─────────▼───────┬────────────────┬─────────┐ + // │ Mutex │ LtHash │ padding │ Mutex │ LtHash │ padding │ + // ├───────┼────────────────┼─────────┼───────┼────────────────┼─────────┤ + // │ │ │ │ │ │ │ + // │0 │6 <-- unaligned │2054 │2176 │2182 │4230 │4352 + // + accumulators: Arc<[Mutex>; NUM_ACCOUNTS_HASHER_THREADS]>, + num_jobs_pending: Arc, + num_jobs_total: AtomicU64, +} + +impl AccountsLtHashAsyncProgress { + /// Creates a new AccountsLtHashAsyncProgress variable, which is suitable for a new Bank. + pub fn new() -> Self { + Self { + accumulators: Arc::new(array::from_fn(|_| { + Mutex::new(CachePadded::new(LtHash::identity())) + })), + num_jobs_pending: Arc::new(AtomicUsize::new(0)), + num_jobs_total: AtomicU64::new(0), + } + } + + /// Enqueues `update` into `thread_pool` for asynchronous processing. + fn spawn(&self, thread_pool: &'static ThreadPool, update: AccountsLtHashUpdate) { + self.num_jobs_pending.fetch_add(1, Ordering::Relaxed); + self.num_jobs_total.fetch_add(1, Ordering::Relaxed); + thread_pool.spawn({ + let accumulators = Arc::clone(&self.accumulators); + let num_jobs_pending = Arc::clone(&self.num_jobs_pending); + move || { + // SAFETY: We always call from the same/correct Rayon thread pool. + let worker_index = thread_pool.current_thread_index().unwrap(); + + // SAFETY: There are num_threads accumulators, and each + // thread's index shall always be in range 0..num_threads. + debug_assert!(worker_index < accumulators.len()); + let accumulator = unsafe { accumulators.get_unchecked(worker_index) }; + + Self::process(&mut accumulator.lock().unwrap(), update); + + // Decrementing the number of pending jobs MUST happen *after* + // accumulating the result. This ensures `finish()` cannot + // observe zero pending jobs until all workers are done. + num_jobs_pending.fetch_sub(1, Ordering::Relaxed); + } + }); } - /// Caches initial state of writeable accounts + /// Waits for all pending jobs to complete, then mixes the results into `lt_hash`. /// - /// If a transaction account is writeable, cache its initial account state. - /// The initial state is needed when computing the accounts lt hash for the slot, and caching - /// the initial state saves us from having to look it up on disk later. - pub fn inspect_account_for_accounts_lt_hash( - &self, - address: &Pubkey, - account_state: &AccountState, - is_writable: bool, - ) { - if !is_writable { - // if the account is not writable, then it cannot be modified; nothing to do here + /// Returns the number of asynchronous jobs completed. + /// + /// Note: Since an LtHash is large, `lt_hash` is passed as an in-out parameter. + /// This it to avoid Rust compiler bug that fails to perform return value optimization. + fn finish(&self, lt_hash: &mut LtHash) -> u64 { + while self.num_jobs_pending.load(Ordering::Relaxed) > 0 { + // Spin, do not yield! This is called by Bank::freeze() and we want to be fast. + hint::spin_loop(); + } + + for thread_accumulator in self.accumulators.iter() { + lt_hash.mix_in(&thread_accumulator.lock().unwrap()); + } + self.num_jobs_total.load(Ordering::Relaxed) + } + + /// Processes `update` and mixes the result into `accum_lt_hash`. + /// + /// Note: Since an LtHash is large, `accum_lt_hash` is passed as an in-out parameter. + /// This it to avoid Rust compiler bug that fails to perform return value optimization. + fn process(accum_lt_hash: &mut LtHash, update: AccountsLtHashUpdate) { + let AccountsLtHashUpdate { + address, + prev_account, + curr_account, + } = update; + if let Some(prev_account) = prev_account { + let prev_lt_hash = AccountsDb::lt_hash_account(&prev_account, &address); + accum_lt_hash.mix_out(&prev_lt_hash.0); + } + if let Some(curr_account) = curr_account { + let curr_lt_hash = AccountsDb::lt_hash_account(&curr_account, &address); + accum_lt_hash.mix_in(&curr_lt_hash.0); + } + } +} + +/// A single accounts lt hash update to process. +#[derive(Debug)] +struct AccountsLtHashUpdate { + address: Pubkey, + prev_account: Option, + curr_account: Option, +} + +/// Get the freelist of hashsets to use for seen accounts. +fn seen_accounts_freelist() -> &'static HashSetFreelist { + // Derived empirically while observing an unstaked node on mnb. + // Should end up being the same number as replay threads. + const MAX_CONTAINERS: usize = 50; + static FREELIST: LazyLock> = LazyLock::new(|| { + HashSetFreelist::new(MAX_CONTAINERS, Some(MAX_BYTES_SEEN_ACCOUNTS_FREELIST)) + }); + &FREELIST +} + +/// Freelist of containers, to avoid repeat allocations/deallocations. +#[derive(Debug)] +struct HashSetFreelist { + /// the maximum number of containers this freelist will hold + max_containers: usize, + + /// the maximum capacity, in elements, this freelist will hold + max_capacity: Option, + + inner: Mutex>, +} + +impl HashSetFreelist { + /// Creates a new, empty, freelist. + /// + /// max_containers: + /// * The maximum number of containers this freelist can hold. + /// + /// max_bytes: + /// * The maximum number of bytes this freelist can hold. + /// * This value corresponds to the total capacity across all the containers in the freelist. + /// * If `None`, there is no maximum. + fn new(max_containers: usize, max_bytes: Option) -> Self { + let max_capacity = max_bytes.map(|max_bytes| max_bytes / size_of::()); + Self { + max_containers, + max_capacity, + inner: Mutex::new(HashSetFreelistInner { + list: Vec::with_capacity(max_containers), + total_capacity: 0, + }), + } + } + + /// Pushes `container` on to the freelist (IFF its capacity is greater than zero). + fn try_push(&self, mut container: ahash::HashSet) { + // If the capacity is zero, then the container never allocated. + // In that case, don't waste time putting it back into the freelist, + // since there's nothing of value to reuse. + // + // Else, check if pushing the container would exceed the max capacity of the freelist. + // If so, also do not put it back into the freelist. + let capacity = container.capacity(); + if capacity == 0 { return; } - // Only insert the account the *first* time we see it. - // We want to capture the value of the account *before* any modifications during this slot. - let (is_in_cache, lookup_time) = - meas_dur!(self.cache_for_accounts_lt_hash.contains_key(address)); - if !is_in_cache { - // We need to check if the bank is frozen. In order to do that safely, we - // must hold a read lock on Bank::hash to read the frozen state. - let freeze_guard = self.freeze_lock(); - let is_frozen = *freeze_guard != Hash::default(); - if is_frozen { - // If the bank is frozen, do not add this account to the cache. - // It is possible for the leader to be executing transactions after freeze has - // started, i.e. while any deferred changes to account state is finishing up. - // This means the transaction could load an account *after* it was modified by the - // deferred changes, which would be the wrong initial state of the account. - // Inserting the wrong initial state of an account into the cache will end up - // producing the wrong accounts lt hash. - self.stats_for_accounts_lt_hash - .num_inspect_account_after_frozen - .fetch_add(1, Ordering::Relaxed); - return; - } - let (_, insert_time) = meas_dur!({ - self.cache_for_accounts_lt_hash - .entry(*address) - .or_insert_with(|| { - let initial_state_of_account = match account_state { - AccountState::Dead => InitialStateOfAccount::Dead, - AccountState::Alive(account) => { - InitialStateOfAccount::Alive((*account).clone()) - } - }; - CacheValue::InspectAccount(initial_state_of_account) - }); - }); - drop(freeze_guard); - - self.stats_for_accounts_lt_hash - .num_inspect_account_misses - .fetch_add(1, Ordering::Relaxed); - self.stats_for_accounts_lt_hash - .inspect_account_insert_time_ns - // N.B. this needs to be nanoseconds because it can be so fast - .fetch_add(insert_time.as_nanos() as u64, Ordering::Relaxed); - } else { - // The account is already in the cache, so nothing to do here other than update stats. - self.stats_for_accounts_lt_hash - .num_inspect_account_hits - .fetch_add(1, Ordering::Relaxed); + // container must be empty to be reused, so do it here outside of the lock + container.clear(); + + let mut inner = self.inner.lock().unwrap(); + + if inner.list.len() >= self.max_containers { + // the num containers would exceed the max, do not push + return; + } + + let Some(new_total_capacity) = inner.total_capacity.checked_add(capacity) else { + // the new total capacity would overflow, do not push + return; + }; + + let max_capacity = self.max_capacity.unwrap_or(usize::MAX); + if new_total_capacity > max_capacity { + // the new total capacity would exceed the max, do not push + return; } - self.stats_for_accounts_lt_hash - .inspect_account_lookup_time_ns - // N.B. this needs to be nanoseconds because it can be so fast - .fetch_add(lookup_time.as_nanos() as u64, Ordering::Relaxed); + inner.list.push(container); + inner.total_capacity = new_total_capacity; + } + + /// Pops a container off the freelist and returns it. + /// + /// The returned container will always be empty. + fn try_pop(&self) -> Option> { + let mut inner = self.inner.lock().unwrap(); + let container = inner.list.pop()?; + assert!(container.is_empty()); + inner.total_capacity -= container.capacity(); + Some(container) + } + + /// Returns a snapshot of the freelist's stats. + fn stats(&self) -> FreelistStats { + let inner = self.inner.lock().unwrap(); + let num_containers = inner.list.len(); + let capacity_elems = inner.total_capacity; + drop(inner); + FreelistStats { + num_containers, + capacity_elems, + capacity_bytes: capacity_elems.saturating_mul(size_of::()), + } } } -/// Stats related to accounts lt hash -#[derive(Debug, Default)] -pub struct Stats { - /// the number of times the cache already contained the account being inspected - num_inspect_account_hits: AtomicU64, - /// the number of times the cache *did not* already contain the account being inspected - num_inspect_account_misses: AtomicU64, - /// the number of times an account was inspected after the bank was frozen - num_inspect_account_after_frozen: AtomicU64, - /// time spent checking if accounts are in the cache - inspect_account_lookup_time_ns: AtomicU64, - /// time spent inserting accounts into the cache - inspect_account_insert_time_ns: AtomicU64, +/// The mutable state of a [`HashSetFreelist`], guarded by a single lock. +#[derive(Debug)] +struct HashSetFreelistInner { + /// the containers available for reuse + list: Vec>, + /// the capacity, in elements, across all the containers in the freelist + total_capacity: usize, } -/// The initial state of an account prior to being modified in this slot/transaction -#[derive(Debug, Clone, PartialEq)] -pub enum InitialStateOfAccount { - /// The account was initially dead - Dead, - /// The account was initially alive - Alive(AccountSharedData), +/// A snapshot of a freelist's stats. +#[derive(Debug, Eq, PartialEq)] +struct FreelistStats { + /// the number of containers held by the freelist + num_containers: usize, + /// the capacity, in elements, across all the containers in the freelist + capacity_elems: usize, + /// the capacity, in bytes, across all the containers in the freelist + capacity_bytes: usize, } -/// The value type for the accounts lt hash cache -#[derive(Debug, Clone, PartialEq)] -pub enum CacheValue { - /// The value was inserted by `inspect_account()`. - /// This means we will have the initial state of the account. - InspectAccount(InitialStateOfAccount), - /// The value was inserted by `Bank::new()`. - /// This means we will *not* have the initial state of the account. - BankNew, +/// Returns the thread pool for asynchronous accounts hashing. +/// +/// Note, the thread pool will be created on first call. +fn accounts_hasher_thread_pool() -> &'static ThreadPool { + static THREAD_POOL: LazyLock = LazyLock::new(|| { + ThreadPoolBuilder::new() + .num_threads(NUM_ACCOUNTS_HASHER_THREADS) + .thread_name(|i| format!("solAcctsHashr{i:02}")) + .build() + .expect("new accounts hasher rayon threadpool") + }); + &THREAD_POOL } #[cfg(test)] @@ -395,7 +382,7 @@ mod tests { }, agave_feature_set::FeatureSet, agave_snapshots::snapshot_config::SnapshotConfig, - solana_account::{ReadableAccount as _, WritableAccount as _}, + ahash::HashSetExt as _, solana_accounts_db::{ accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsDbConfig}, accounts_index::{ACCOUNTS_INDEX_CONFIG_FOR_TESTING, AccountsIndexConfig, IndexLimit}, @@ -403,13 +390,19 @@ mod tests { solana_cluster_type::ClusterType, solana_fee_calculator::FeeRateGovernor, solana_genesis_config::{self, GenesisConfig}, + solana_hash::Hash, solana_keypair::Keypair, solana_leader_schedule::SlotLeader, solana_native_token::LAMPORTS_PER_SOL, solana_pubkey::{self as pubkey, Pubkey}, solana_rent::Rent, solana_signer::Signer as _, - std::{cmp, iter, str::FromStr as _, sync::Arc}, + std::{ + cmp, iter, + str::FromStr as _, + sync::{Arc, Barrier}, + thread, + }, tempfile::TempDir, test_case::{test_case, test_matrix}, }; @@ -495,7 +488,7 @@ mod tests { bank.transfer(amount, &mint_keypair, &keypair5.pubkey()) .unwrap(); - // manually freeze the bank to trigger update_accounts_lt_hash() to run + // manually freeze the bank to trigger updating the accounts lt hash bank.freeze(); let prev_accounts_lt_hash = bank.accounts_lt_hash.lock().unwrap().clone(); @@ -550,10 +543,9 @@ mod tests { // store account 5 into this new bank, unchanged bank.store_account(&keypair5.pubkey(), prev_account5.as_ref().unwrap()); - // freeze the bank to trigger update_accounts_lt_hash() to run + // freeze the bank to trigger updating the accounts lt hash bank.freeze(); - let actual_delta_lt_hash = bank.calculate_delta_lt_hash(); let post_accounts_lt_hash = bank.accounts_lt_hash.lock().unwrap().clone(); let post_mint = bank.get_account_with_fixed_root(&mint_keypair.pubkey()); let post_account1 = bank.get_account_with_fixed_root(&keypair1.pubkey()); @@ -574,21 +566,18 @@ mod tests { .map(|address| bank.get_account_with_fixed_root(address)) .collect(); - let mut expected_delta_lt_hash = LtHash::identity(); let mut expected_accounts_lt_hash = prev_accounts_lt_hash; let mut updater = |address: &Pubkey, prev: Option, post: Option| { // if there was an alive account, mix out if let Some(prev) = prev { let prev_lt_hash = AccountsDb::lt_hash_account(&prev, address); - expected_delta_lt_hash.mix_out(&prev_lt_hash.0); expected_accounts_lt_hash.0.mix_out(&prev_lt_hash.0); } // mix in the new one let post = post.unwrap_or_default(); let post_lt_hash = AccountsDb::lt_hash_account(&post, address); - expected_delta_lt_hash.mix_in(&post_lt_hash.0); expected_accounts_lt_hash.0.mix_in(&post_lt_hash.0); }; updater(&mint_keypair.pubkey(), prev_mint, post_mint); @@ -605,15 +594,7 @@ mod tests { ); } - // now make sure the delta lt hashes match - let expected = expected_delta_lt_hash.checksum(); - let actual = actual_delta_lt_hash.checksum(); - assert_eq!( - expected, actual, - "delta_lt_hash, expected: {expected}, actual: {actual}", - ); - - // ...and the accounts lt hashes match too + // now make sure the accounts lt hashes match let expected = expected_accounts_lt_hash.0.checksum(); let actual = post_accounts_lt_hash.0.checksum(); assert_eq!( @@ -626,7 +607,7 @@ mod tests { /// /// This test does a simple transfer in slot 0 so that a primordial account is modified. /// - /// See the comments in calculate_delta_lt_hash() for more information. + /// Slot 0 is special because primordial accounts have no previous accounts lt hash entry. #[test_case(Features::None; "no features")] #[test_case(Features::All; "all features")] fn test_slot0_accounts_lt_hash(features: Features) { @@ -640,7 +621,7 @@ mod tests { bank.transfer(LAMPORTS_PER_SOL, &mint_keypair, &Pubkey::new_unique()) .unwrap(); - // manually freeze the bank to trigger update_accounts_lt_hash() to run + // manually freeze the bank to trigger updating the accounts lt hash bank.freeze(); let actual_accounts_lt_hash = bank.accounts_lt_hash.lock().unwrap().clone(); @@ -653,114 +634,6 @@ mod tests { assert_eq!(actual_accounts_lt_hash, calculated_accounts_lt_hash); } - #[test_case(Features::None; "no features")] - #[test_case(Features::All; "all features")] - fn test_inspect_account_for_accounts_lt_hash(features: Features) { - let (genesis_config, _mint_keypair) = genesis_config_with(features); - let (bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); - - // the cache should start off empty - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 0); - - // ensure non-writable accounts are *not* added to the cache - bank.inspect_account_for_accounts_lt_hash( - &Pubkey::new_unique(), - &AccountState::Dead, - false, - ); - bank.inspect_account_for_accounts_lt_hash( - &Pubkey::new_unique(), - &AccountState::Alive(&AccountSharedData::default()), - false, - ); - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 0); - - // ensure *new* accounts are added to the cache - let address = Pubkey::new_unique(); - bank.inspect_account_for_accounts_lt_hash(&address, &AccountState::Dead, true); - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 1); - assert!(bank.cache_for_accounts_lt_hash.contains_key(&address)); - - // ensure *existing* accounts are added to the cache - let address = Pubkey::new_unique(); - let initial_lamports = 123; - let mut account = AccountSharedData::new(initial_lamports, 0, &Pubkey::default()); - bank.inspect_account_for_accounts_lt_hash(&address, &AccountState::Alive(&account), true); - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 2); - if let CacheValue::InspectAccount(InitialStateOfAccount::Alive(cached_account)) = bank - .cache_for_accounts_lt_hash - .get(&address) - .unwrap() - .value() - { - assert_eq!(*cached_account, account); - } else { - panic!("wrong initial state for account"); - }; - - // ensure if an account is modified multiple times that we only cache the *first* one - let updated_lamports = account.lamports() + 1; - account.set_lamports(updated_lamports); - bank.inspect_account_for_accounts_lt_hash(&address, &AccountState::Alive(&account), true); - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 2); - if let CacheValue::InspectAccount(InitialStateOfAccount::Alive(cached_account)) = bank - .cache_for_accounts_lt_hash - .get(&address) - .unwrap() - .value() - { - assert_eq!(cached_account.lamports(), initial_lamports); - } else { - panic!("wrong initial state for account"); - }; - - // and ensure multiple updates are handled correctly when the account is initially dead - { - let address = Pubkey::new_unique(); - bank.inspect_account_for_accounts_lt_hash(&address, &AccountState::Dead, true); - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 3); - match bank - .cache_for_accounts_lt_hash - .get(&address) - .unwrap() - .value() - { - CacheValue::InspectAccount(InitialStateOfAccount::Dead) => { - // this is expected, nothing to do here - } - _ => panic!("wrong initial state for account"), - }; - - bank.inspect_account_for_accounts_lt_hash( - &address, - &AccountState::Alive(&AccountSharedData::default()), - true, - ); - assert_eq!(bank.cache_for_accounts_lt_hash.len(), 3); - match bank - .cache_for_accounts_lt_hash - .get(&address) - .unwrap() - .value() - { - CacheValue::InspectAccount(InitialStateOfAccount::Dead) => { - // this is expected, nothing to do here - } - _ => panic!("wrong initial state for account"), - }; - } - - // ensure accounts are *not* added to the cache if the bank is frozen - // N.B. this test should remain *last*, as Bank::freeze() is not meant to be undone - bank.freeze(); - let address = Pubkey::new_unique(); - let num_cache_entries_prev = bank.cache_for_accounts_lt_hash.len(); - bank.inspect_account_for_accounts_lt_hash(&address, &AccountState::Dead, true); - let num_cache_entries_curr = bank.cache_for_accounts_lt_hash.len(); - assert_eq!(num_cache_entries_curr, num_cache_entries_prev); - assert!(!bank.cache_for_accounts_lt_hash.contains_key(&address)); - } - #[test_case(Features::None; "no features")] #[test_case(Features::All; "all features")] fn test_calculate_accounts_lt_hash_at_startup_from_index(features: Features) { @@ -932,39 +805,6 @@ mod tests { assert_eq!(roundtrip_bank, *bank); } - /// Ensure that accounts written in Bank::new() are added to the accounts lt hash cache. - #[test_case(Features::None; "no features")] - #[test_case(Features::All; "all features")] - fn test_accounts_lt_hash_cache_values_from_bank_new(features: Features) { - let (genesis_config, _mint_keypair) = genesis_config_with(features); - let (mut bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); - - let slot = bank.slot() + 1; - bank = - Bank::new_from_parent_with_bank_forks(&bank_forks, bank, SlotLeader::default(), slot); - - // These are the two accounts *currently* added to the bank during Bank::new(). - // More accounts could be added later, so if the test fails, inspect the actual cache - // accounts and update the expected cache accounts as necessary. - let expected_cache = &[ - ( - Pubkey::from_str_const("SysvarC1ock11111111111111111111111111111111"), - CacheValue::BankNew, - ), - ( - Pubkey::from_str_const("SysvarS1otHashes111111111111111111111111111"), - CacheValue::BankNew, - ), - ]; - let mut actual_cache: Vec<_> = bank - .cache_for_accounts_lt_hash - .iter() - .map(|entry| (*entry.key(), entry.value().clone())) - .collect(); - actual_cache.sort_unstable_by_key(|a| a.0); - assert_eq!(expected_cache, actual_cache.as_slice()); - } - /// Ensure that the snapshot hash is correct #[test_case(Features::None; "no features")] #[test_case(Features::All; "all features")] @@ -1024,4 +864,84 @@ mod tests { assert_eq!(roundtrip_bank, *bank); } + + /// Ensure freelist respects max size. + #[test] + fn test_freelist_max_capacity() { + type Container = ahash::HashSet; + + // This test uses a hashbrown container, which has some special power-of-two sizing plus + // a buffer. So create the container first, and use that to derive the max capacity. + let container = Container::with_capacity(77); + + let max_capacity = container.capacity(); + let max_bytes = max_capacity * size_of::(); + let mut freelist = HashSetFreelist::new(10, Some(max_bytes)); + + // pushing a container that is too big will not actually push + freelist.try_push(Container::with_capacity(max_capacity + 1)); + let stats0 = freelist.stats(); + assert_eq!(stats0.num_containers, 0); + assert_eq!(stats0.capacity_elems, 0); + assert_eq!(stats0.capacity_bytes, 0); + + // pushing a container that is not too big will actually push + freelist.try_push(container); + let stats1 = freelist.stats(); + assert_eq!(stats1.num_containers, 1); + assert_eq!(stats1.capacity_elems, max_capacity); + assert_eq!(stats1.capacity_bytes, max_bytes); + + // pushing a container that would exceed capacity will not push + freelist.try_push(Container::with_capacity(1)); + assert_eq!(freelist.stats(), stats1); + + // ...but, if we remove the limit, push should work again + freelist.max_capacity = None; + let container = Container::with_capacity(1); + let container_capacity = container.capacity(); + freelist.try_push(container); + let stats2 = freelist.stats(); + assert_eq!(stats2.num_containers, 2); + assert_eq!(stats2.capacity_elems, max_capacity + container_capacity); + assert_eq!( + stats2.capacity_bytes, + max_bytes + container_capacity * size_of::(), + ); + } + + /// Ensure concurrent pushes do not exceed the freelist's max capacity. + #[test] + fn test_freelist_concurrent_push() { + // This test uses a hashbrown container, which has some special power-of-two sizing plus + // a buffer. So create the container first, and use that to derive the max capacity. + let container = ahash::HashSet::::with_capacity(77); + + let num_threads = 16; + let barrier = Arc::new(Barrier::new(num_threads)); + let max_capacity = container.capacity(); + let max_bytes = max_capacity * size_of::(); + let freelist = Arc::new(HashSetFreelist::new(num_threads, Some(max_bytes))); + + let threads: Vec<_> = iter::repeat_with(|| { + let container = container.clone(); + let freelist = Arc::clone(&freelist); + let barrier = Arc::clone(&barrier); + thread::spawn(move || { + barrier.wait(); + freelist.try_push(container); + }) + }) + .take(num_threads) + .collect(); + + for thread in threads { + thread.join().unwrap(); + } + + let stats = freelist.stats(); + assert_eq!(stats.num_containers, 1); + assert_eq!(stats.capacity_elems, max_capacity); + assert_eq!(stats.capacity_bytes, max_bytes); + } } diff --git a/runtime/src/bank/builtins/core_bpf_migration/mod.rs b/runtime/src/bank/builtins/core_bpf_migration/mod.rs index c3d4d21e383..405980be78a 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/mod.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/mod.rs @@ -83,13 +83,13 @@ impl Bank { authority_address: buffer_authority, } = bincode::deserialize(&source.buffer_account.data()[..buffer_metadata_size])? { - if let Some(provided_authority) = upgrade_authority_address { - if upgrade_authority_address != buffer_authority { - return Err(CoreBpfMigrationError::UpgradeAuthorityMismatch( - provided_authority, - buffer_authority, - )); - } + if let Some(provided_authority) = upgrade_authority_address + && upgrade_authority_address != buffer_authority + { + return Err(CoreBpfMigrationError::UpgradeAuthorityMismatch( + provided_authority, + buffer_authority, + )); } let elf = &source.buffer_account.data()[buffer_metadata_size..]; diff --git a/runtime/src/bank/builtins/core_bpf_migration/source_buffer.rs b/runtime/src/bank/builtins/core_bpf_migration/source_buffer.rs index cde8c0b911a..cf7f68c5a99 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/source_buffer.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/source_buffer.rs @@ -35,15 +35,14 @@ impl SourceBuffer { // The buffer account should have the correct state. let buffer_metadata_size = UpgradeableLoaderState::size_of_buffer_metadata(); - if buffer_account.data().len() >= buffer_metadata_size { - if let UpgradeableLoaderState::Buffer { .. } = + if buffer_account.data().len() >= buffer_metadata_size + && let UpgradeableLoaderState::Buffer { .. } = bincode::deserialize(&buffer_account.data()[..buffer_metadata_size])? - { - return Ok(Self { - buffer_address: *buffer_address, - buffer_account, - }); - } + { + return Ok(Self { + buffer_address: *buffer_address, + buffer_account, + }); } Err(CoreBpfMigrationError::InvalidBufferAccount(*buffer_address)) } diff --git a/runtime/src/bank/builtins/core_bpf_migration/target_core_bpf.rs b/runtime/src/bank/builtins/core_bpf_migration/target_core_bpf.rs index e904aad5f20..d40b5506a19 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/target_core_bpf.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/target_core_bpf.rs @@ -72,19 +72,18 @@ impl TargetCoreBpf { // The program data account should have the correct state. let programdata_metadata_size = UpgradeableLoaderState::size_of_programdata_metadata(); - if program_data_account.data().len() >= programdata_metadata_size { - if let UpgradeableLoaderState::ProgramData { + if program_data_account.data().len() >= programdata_metadata_size + && let UpgradeableLoaderState::ProgramData { upgrade_authority_address, .. } = bincode::deserialize(&program_data_account.data()[..programdata_metadata_size])? - { - return Ok(Self { - program_address: *program_address, - program_data_address, - program_data_account, - upgrade_authority_address, - }); - } + { + return Ok(Self { + program_address: *program_address, + program_data_address, + program_data_account, + upgrade_authority_address, + }); } Err(CoreBpfMigrationError::InvalidProgramDataAccount( program_data_address, diff --git a/runtime/src/bank/check_transactions.rs b/runtime/src/bank/check_transactions.rs index fe75107b953..2a04926e0f6 100644 --- a/runtime/src/bank/check_transactions.rs +++ b/runtime/src/bank/check_transactions.rs @@ -1,11 +1,12 @@ use { super::{Bank, BankStatusCache}, agave_feature_set::FeatureSet, + solana_account::ReadableAccount, solana_accounts_db::blockhash_queue::BlockhashQueue, solana_clock::{MAX_TRANSACTION_FORWARDING_DELAY, Slot}, solana_compute_budget::compute_budget::SVMTransactionExecutionBudget, solana_fee::{FeeFeatures, calculate_fee_details}, - solana_nonce::state::{Data as NonceData, DurableNonce}, + solana_nonce::state::{Data as NonceData, DurableNonce, State as NonceState}, solana_nonce_account as nonce_account, solana_program_runtime::execution_budget::SVMTransactionExecutionAndFeeBudgetLimits, solana_pubkey::Pubkey, @@ -41,6 +42,7 @@ impl Bank { self.max_processing_age() .saturating_sub(max_tx_fwd_delay) .saturating_sub(forward_transactions_to_leader_at_slot_offset as usize), + false, &mut error_counters, ) } @@ -50,6 +52,7 @@ impl Bank { sanitized_txs: &[impl core::borrow::Borrow], lock_results: &[TransactionResult<()>], max_age: usize, + strict_nonce_size_check: bool, error_counters: &mut TransactionErrorMetrics, ) -> Vec { self.check_transactions_with_processed_slots( @@ -57,6 +60,7 @@ impl Bank { lock_results, max_age, false, + strict_nonce_size_check, error_counters, ) .0 @@ -68,14 +72,16 @@ impl Bank { lock_results: &[TransactionResult<()>], max_age: usize, collect_processed_slots: bool, + strict_nonce_size_check: bool, error_counters: &mut TransactionErrorMetrics, ) -> (Vec, Option>>) { let lock_results = self.filter_v1_transactions(sanitized_txs, lock_results); let lock_results = self.check_age_and_compute_budget_limits( sanitized_txs, - &lock_results, + lock_results, max_age, + strict_nonce_size_check, error_counters, ); self.check_status_cache( @@ -86,33 +92,33 @@ impl Bank { ) } - fn filter_v1_transactions( + fn filter_v1_transactions<'a, Tx: TransactionWithMeta>( &self, - sanitized_txs: &[impl core::borrow::Borrow], - lock_results: &[TransactionResult<()>], - ) -> Vec> { + sanitized_txs: &'a [impl core::borrow::Borrow], + lock_results: &'a [TransactionResult<()>], + ) -> impl Iterator> + 'a { + let enable_tx_v1 = self.feature_set.snapshot().enable_tx_v1; // Discard v1 transactions until feature gate is activated. sanitized_txs .iter() .zip(lock_results) - .map(|(tx, lock_result)| match lock_result { + .map(move |(tx, lock_result)| match lock_result { Err(err) => Err(err.clone()), Ok(()) - if !self.feature_set.snapshot().enable_tx_v1 - && tx.borrow().version() == TransactionVersion::Number(1) => + if !enable_tx_v1 && tx.borrow().version() == TransactionVersion::Number(1) => { Err(TransactionError::UnsupportedVersion) } Ok(()) => Ok(()), }) - .collect() } fn check_age_and_compute_budget_limits( &self, sanitized_txs: &[impl core::borrow::Borrow], - lock_results: &[TransactionResult<()>], + lock_results: impl IntoIterator>, max_age: usize, + strict_nonce_size_check: bool, error_counters: &mut TransactionErrorMetrics, ) -> Vec { let hash_queue = self.blockhash_queue.read().unwrap(); @@ -173,9 +179,10 @@ impl Bank { &hash_queue, error_counters, compute_budget_and_limits, + strict_nonce_size_check, ) } - Err(e) => Err(e.clone()), + Err(e) => Err(e), }) .collect() } @@ -188,6 +195,7 @@ impl Bank { hash_queue: &BlockhashQueue, error_counters: &mut TransactionErrorMetrics, compute_budget: SVMTransactionExecutionAndFeeBudgetLimits, + strict_nonce_size_check: bool, ) -> TransactionCheckResult { let recent_blockhash = tx.recent_blockhash(); if hash_queue @@ -196,7 +204,7 @@ impl Bank { { Ok(CheckedTransactionDetails::new(None, compute_budget)) } else if let Some((nonce_address, _)) = - self.check_nonce_transaction_validity(tx, next_durable_nonce) + self.check_nonce_transaction_validity(tx, next_durable_nonce, strict_nonce_size_check) { Ok(CheckedTransactionDetails::new( Some(nonce_address), @@ -212,13 +220,15 @@ impl Bank { &self, message: &impl SVMMessage, next_durable_nonce: &DurableNonce, + strict_nonce_size_check: bool, ) -> Option<(Pubkey, u64)> { let nonce_is_advanceable = message.recent_blockhash() != next_durable_nonce.as_hash(); if !nonce_is_advanceable { return None; } - let (nonce_address, nonce_data) = self.load_message_nonce_data(message)?; + let (nonce_address, nonce_data) = + self.load_message_nonce_data(message, strict_nonce_size_check)?; let previous_lamports_per_signature = nonce_data.get_lamports_per_signature(); Some((nonce_address, previous_lamports_per_signature)) @@ -227,9 +237,13 @@ impl Bank { pub(super) fn load_message_nonce_data( &self, message: &impl SVMMessage, + strict_nonce_size_check: bool, ) -> Option<(Pubkey, NonceData)> { let nonce_address = message.get_durable_nonce()?; let nonce_account = self.get_account_with_fixed_root(nonce_address)?; + if strict_nonce_size_check && nonce_account.data().len() != NonceState::size() { + return None; + } let nonce_data = nonce_account::verify_nonce_account(&nonce_account, message.recent_blockhash())?; @@ -239,12 +253,11 @@ impl Bank { fn check_status_cache( &self, sanitized_txs: &[impl core::borrow::Borrow], - lock_results: Vec, + mut lock_results: Vec, collect_processed_slots: bool, error_counters: &mut TransactionErrorMetrics, ) -> (Vec, Option>>) { // Do allocation before acquiring the lock on the status cache. - let mut check_results = Vec::with_capacity(sanitized_txs.len()); let mut processed_slots = if collect_processed_slots { Some(Vec::with_capacity(sanitized_txs.len())) } else { @@ -252,27 +265,24 @@ impl Bank { }; let rcache = self.status_cache.read().unwrap(); - for (sanitized_tx_ref, lock_result) in sanitized_txs.iter().zip(lock_results) { - let sanitized_tx = sanitized_tx_ref.borrow(); - - let (result, processed_slot) = if lock_result.is_ok() { - if let Some(slot) = self.get_processed_slot(sanitized_tx, &rcache) { - error_counters.already_processed += 1; - (Err(TransactionError::AlreadyProcessed), Some(slot)) - } else { - (lock_result, None) - } + for (sanitized_tx_ref, lock_result) in sanitized_txs.iter().zip(lock_results.iter_mut()) { + let processed_slot = if lock_result.is_ok() { + self.get_processed_slot(sanitized_tx_ref.borrow(), &rcache) } else { - (lock_result, None) + None }; - check_results.push(result); + if processed_slot.is_some() { + error_counters.already_processed += 1; + *lock_result = Err(TransactionError::AlreadyProcessed); + } + if let Some(processed_slots) = processed_slots.as_mut() { processed_slots.push(processed_slot) } } - (check_results, processed_slots) + (lock_results, processed_slots) } fn get_processed_slot( @@ -299,7 +309,9 @@ mod tests { setup_nonce_with_bank, }, }, - solana_account::state_traits::StateMut, + solana_account::{ + AccountSharedData, ReadableAccount, WritableAccount, state_traits::StateMut, + }, solana_hash::Hash, solana_keypair::Keypair, solana_message::{ @@ -351,7 +363,7 @@ mod tests { bank.store_account(&nonce_pubkey, &nonce_account); assert_eq!( - bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce()), + bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce(), false), Some((nonce_pubkey, STALE_LAMPORTS_PER_SIGNATURE)), ); } @@ -373,7 +385,40 @@ mod tests { &nonce_hash, )); assert!( - bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce()) + bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce(), false) + .is_none() + ); + } + + #[test] + fn test_check_nonce_transaction_validity_strict_nonce_size_check_fail() { + let (bank, _mint_keypair, custodian_keypair, nonce_keypair, _) = + setup_nonce_with_bank(10_000_000, |_| {}, 5_000_000, 250_000, None).unwrap(); + let custodian_pubkey = custodian_keypair.pubkey(); + let nonce_pubkey = nonce_keypair.pubkey(); + + let nonce_hash = get_nonce_blockhash(&bank, &nonce_pubkey).unwrap(); + let message = new_sanitized_message(Message::new_with_blockhash( + &[ + system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey), + system_instruction::transfer(&custodian_pubkey, &nonce_pubkey, 100_000), + ], + Some(&custodian_pubkey), + &nonce_hash, + )); + + let nonce_account = bank.get_account(&nonce_pubkey).unwrap(); + let mut resized_nonce_account = AccountSharedData::new( + nonce_account.lamports(), + NonceState::size() + 1, + nonce_account.owner(), + ); + resized_nonce_account.data_as_mut_slice()[..nonce_account.data().len()] + .copy_from_slice(nonce_account.data()); + bank.store_account(&nonce_pubkey, &resized_nonce_account); + + assert!( + bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce(), true) .is_none() ); } @@ -399,6 +444,7 @@ mod tests { bank.check_nonce_transaction_validity( &new_sanitized_message(message), &bank.next_durable_nonce(), + false, ) .is_none() ); @@ -423,7 +469,7 @@ mod tests { &nonce_hash, )); assert!( - bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce()) + bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce(), false) .is_none() ); } @@ -444,7 +490,7 @@ mod tests { &Hash::default(), )); assert!( - bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce()) + bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce(), false) .is_none() ); } @@ -502,7 +548,7 @@ mod tests { .unwrap(); assert_eq!( - bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce()), + bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce(), false), None, ); } @@ -561,15 +607,7 @@ mod tests { let filtered = Bank::default_for_tests().filter_v1_transactions(&txs, &lock_results); - assert!(matches!(filtered[0], Err(TransactionError::AccountInUse))); - assert!(matches!( - filtered[1], - Err(TransactionError::TooManyAccountLocks) - )); - assert!(matches!( - filtered[2], - Err(TransactionError::WouldExceedMaxBlockCostLimit) - )); + assert!(filtered.eq(lock_results.iter().cloned())); } #[test] @@ -579,11 +617,7 @@ mod tests { let filtered = Bank::default_for_tests().filter_v1_transactions(&txs, &lock_results); - assert_eq!(filtered.len(), 1); - assert!(matches!( - filtered[0], - Err(TransactionError::UnsupportedVersion) - )); + assert!(filtered.eq([Err(TransactionError::UnsupportedVersion)])); } #[test] @@ -595,7 +629,7 @@ mod tests { let filtered = bank.filter_v1_transactions(&txs, &lock_results); - assert_eq!(filtered, vec![Ok(())]); + assert!(filtered.eq([Ok(())])); } #[test] @@ -608,7 +642,7 @@ mod tests { let filtered = Bank::default_for_tests().filter_v1_transactions(&txs, &lock_results); - assert_eq!(filtered, vec![Ok(()), Ok(())]); + assert!(filtered.eq([Ok(()), Ok(())])); } #[test] @@ -628,15 +662,11 @@ mod tests { let filtered = Bank::default_for_tests().filter_v1_transactions(&txs, &lock_results); - assert!(matches!(filtered[0], Ok(()))); - assert!(matches!( - filtered[1], - Err(TransactionError::UnsupportedVersion) - )); - assert!(matches!(filtered[2], Err(TransactionError::AccountInUse))); - assert!(matches!( - filtered[3], - Err(TransactionError::TooManyAccountLocks) - )); + assert!(filtered.eq([ + Ok(()), + Err(TransactionError::UnsupportedVersion), + Err(TransactionError::AccountInUse), + Err(TransactionError::TooManyAccountLocks), + ])); } } diff --git a/runtime/src/bank/fee_distribution.rs b/runtime/src/bank/fee_distribution.rs index f5e1dcb9b4a..1a9972d4294 100644 --- a/runtime/src/bank/fee_distribution.rs +++ b/runtime/src/bank/fee_distribution.rs @@ -19,7 +19,7 @@ use { }; #[derive(Error, Debug, PartialEq)] -enum DepositFeeError { +pub(super) enum DepositFeeError { #[error("fee account became rent paying")] InvalidRentPayingAccount, #[error("lamport overflow")] @@ -30,6 +30,19 @@ enum DepositFeeError { ReservedCollector, } +/// Helper enum used to distinguish external collector types allowed by +/// SIMD-0232. +/// +/// The term "external" is used to exclude the vote account itself, which is a +/// valid collector. +pub(super) enum ExternalCollectorType { + /// A rent-exempt, non-incinerator, non-reserved account owned by the system + /// program + SystemAccount, + /// Specifically, the incinerator account, denoted by `incinerator::id()` + Incinerator, +} + #[derive(Default)] pub struct FeeDistribution { deposit: u64, @@ -56,10 +69,6 @@ impl Bank { // form of transaction fees as well. pub(super) fn distribute_transaction_fee_details(&self) { let fee_details = self.collector_fee_details.read().unwrap(); - if fee_details.total_transaction_fee() == 0 { - // nothing to distribute, exit early - return; - } let FeeDistribution { deposit, burn } = self.calculate_reward_and_burn_fee_details(&fee_details); @@ -90,10 +99,6 @@ impl Bank { &self, fee_details: &CollectorFeeDetails, ) -> FeeDistribution { - if fee_details.transaction_fee == 0 { - return FeeDistribution::default(); - } - let burn = fee_details.transaction_fee * self.burn_percent() / 100; let deposit = fee_details .priority_fee @@ -188,7 +193,7 @@ impl Bank { .checked_add_lamports(fees) .map_err(|_| DepositFeeError::LamportOverflow)?; if collector_id != &self.leader.vote_address { - Bank::check_collector_account( + Bank::collector_type_checked( collector_id, pre_lamports, &account, @@ -228,14 +233,20 @@ impl Bank { Ok(account.lamports()) } - fn check_collector_account( + /// Checks if a collector account adheres to the rules outlined in SIMD-0232: + /// * system program owned account + /// * rent-exempt after depositing inflation rewards commission + /// * not a reserved account + /// + /// Returns the kind of collector + pub(super) fn collector_type_checked( collector_id: &Pubkey, pre_lamports: u64, account: &AccountSharedData, reserved_account_keys: &ReservedAccountKeys, rent: &Rent, relax_post_execution_balance_checks: bool, - ) -> Result<(), DepositFeeError> { + ) -> Result { if !system_program::check_id(account.owner()) { return Err(DepositFeeError::InvalidAccountOwner); } @@ -245,15 +256,18 @@ impl Bank { } // Don't perform rent check on the incinerator, so that the deposit - // always works. The incinerator is cleaned right after this step - if *collector_id != incinerator::id() - && !rent.is_exempt(account.lamports(), account.data().len()) - && (!relax_post_execution_balance_checks || pre_lamports == 0) - { - return Err(DepositFeeError::InvalidRentPayingAccount); + // always works. The incinerator is run at the end of a block + if *collector_id == incinerator::id() { + Ok(ExternalCollectorType::Incinerator) + } else { + if !rent.is_exempt(account.lamports(), account.data().len()) + && (!relax_post_execution_balance_checks || pre_lamports == 0) + { + Err(DepositFeeError::InvalidRentPayingAccount) + } else { + Ok(ExternalCollectorType::SystemAccount) + } } - - Ok(()) } } diff --git a/runtime/src/bank/metrics.rs b/runtime/src/bank/metrics.rs index 712253282a9..a1ba882e253 100644 --- a/runtime/src/bank/metrics.rs +++ b/runtime/src/bank/metrics.rs @@ -44,7 +44,6 @@ pub(crate) struct NewBankTimings { pub(crate) cache_preparation_time_us: u64, pub(crate) update_sysvars_time_us: u64, pub(crate) fill_sysvar_cache_time_us: u64, - pub(crate) populate_cache_for_accounts_lt_hash_us: u64, } pub(crate) struct PrepareBlockExecutionStats { @@ -53,8 +52,6 @@ pub(crate) struct PrepareBlockExecutionStats { pub(crate) cache_preparation_time_us: u64, pub(crate) update_sysvars_time_us: u64, pub(crate) fill_sysvar_cache_time_us: u64, - pub(crate) num_accounts_modified_this_slot: usize, - pub(crate) populate_cache_for_accounts_lt_hash_us: u64, } pub(crate) fn report_new_epoch_metrics( @@ -122,7 +119,6 @@ pub(crate) fn report_new_bank_metrics( slot: Slot, parent_slot: Slot, block_height: u64, - num_accounts_modified_this_slot: usize, timings: NewBankTimings, ) { datapoint_info!( @@ -172,16 +168,6 @@ pub(crate) fn report_new_bank_metrics( timings.fill_sysvar_cache_time_us, i64 ), - ( - "num_accounts_modified_this_slot", - num_accounts_modified_this_slot, - i64 - ), - ( - "populate_cache_for_accounts_lt_hash_us", - timings.populate_cache_for_accounts_lt_hash_us, - i64 - ), ); } diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs index edbe252816f..3d17c778a4b 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -1,16 +1,23 @@ use { super::{ Bank, CachedVoteAccounts, CalculateValidatorRewardsResult, EpochRewardCalculateParamInfo, - PartitionedRewardsCalculation, PartitionedStakeReward, PartitionedStakeRewards, - REWARD_CALCULATION_NUM_BLOCKS, RewardCommission, RewardCommissionAccounts, - RewardCommissionAccountsStorable, RewardCommissions, StakeRewardCalculation, + InflationReward, PartitionedRewardsCalculation, PartitionedStakeReward, + PartitionedStakeRewards, REWARD_CALCULATION_NUM_BLOCKS, RewardCommission, + RewardCommissionAccounts, RewardCommissionAccountsStorable, RewardCommissionLamportAmounts, + RewardCommissions, StakeRewardCalculation, epoch_rewards_hasher::hash_rewards_into_partitions, }, crate::{ - bank::{RewardCalcTracer, RewardCalculationEvent, RewardsMetrics, null_tracer}, + alpenglow_epoch_type::{AlpenglowEpochType, RewardEpochDelegatedStakes}, + bank::{ + RewardCalcTracer, RewardCalculationEvent, RewardsMetrics, + fee_distribution::ExternalCollectorType, null_tracer, + }, inflation_rewards::{ - adjust_delegation_for_rent, - points::{CalculationEnvironment, DelegatedVoteState, PointValue, calculate_points}, + delegation_may_need_adjustment, + points::{ + CalculationEnvironment, DelegatedVoteState, PointValue, calculate_points_for_tower, + }, redeem_rewards, }, reward_info::RewardInfo, @@ -29,12 +36,15 @@ use { solana_reward_info::RewardType, solana_stake_interface::{stake_history::StakeHistory, state::Delegation}, solana_sysvar::epoch_rewards::EpochRewards, - std::sync::{Arc, atomic::Ordering::Relaxed}, + std::sync::{ + Arc, + atomic::{AtomicU64, Ordering::Relaxed}, + }, }; #[derive(Debug)] -struct DelegationRewards { - stake_reward: PartitionedStakeReward, +struct InflationRewardWithCommission { + inflation: InflationReward, commission_pubkey: Pubkey, reward_commission: RewardCommission, } @@ -46,6 +56,61 @@ struct RewardsAccumulator { total_stake_rewards_lamports: u64, } +/// Merge the lamport and `is_vote_account` fields of two `RewardCommission`s +/// +/// This pays special attention to the case where `is_vote_account` does not +/// match, which can happen in the following situation: +/// +/// * a vote account A sets the inflation collector to valid system account B +/// * at some point in the future, that system account B gets allocated and +/// initialized as a vote account B +/// * vote account B sets itself as the inflation reward collector +/// +/// In that situation, the rewards for vote account A will get burned, but the +/// rewards for vote account B will not. According to the rules of SIMD-0232, +/// a collector account must either be the vote account itself or a system +/// account that fulfills certain criteria. In the case of vote account A, we +/// are already sure that the collector account is invalid. +/// +/// NOTE: if vote account B sets a system account as its inflation collector, +/// then the commission lamports for vote account A will NOT get burned here, +/// but will get burned during `load_and_reward_commission_accounts` +fn accumulate_lamports(src: &RewardCommission, dst: &mut RewardCommission) { + match (src.is_vote_account, dst.is_vote_account) { + (false, true) => { + // Don't accumulate, burn everything in the source + // reward commission entry. + // + // NOTE: There shouldn't be any burned lamports in the + // source entry, but we're defensive + dst.burned_lamports = dst + .burned_lamports + .saturating_add(src.commission_lamports) + .saturating_add(src.burned_lamports); + } + (true, false) => { + // The commission lamports on the source are the only + // ones that get distributed, all others get burned. + // + // NOTE: There shouldn't be any burned lamports in the + // destination entry, but we're defensive + dst.is_vote_account = true; + dst.burned_lamports = dst + .burned_lamports + .saturating_add(dst.commission_lamports) + .saturating_add(src.burned_lamports); + dst.commission_lamports = src.commission_lamports; + } + _ => { + // Normal case, just accumulate both + dst.commission_lamports = dst + .commission_lamports + .saturating_add(src.commission_lamports); + dst.burned_lamports = dst.burned_lamports.saturating_add(src.burned_lamports); + } + } +} + impl RewardsAccumulator { fn add_reward( &mut self, @@ -56,9 +121,7 @@ impl RewardsAccumulator { self.reward_commissions .entry(commission_pubkey) .and_modify(|dst_reward_commission| { - dst_reward_commission.commission_lamports = dst_reward_commission - .commission_lamports - .saturating_add(reward_commission.commission_lamports) + accumulate_lamports(&reward_commission, dst_reward_commission); }) .or_insert(reward_commission); self.num_stake_rewards = self.num_stake_rewards.saturating_add(1); @@ -84,9 +147,7 @@ impl RewardsAccumulator { dst.reward_commissions .entry(commission_pubkey) .and_modify(|dst_reward_commission: &mut RewardCommission| { - dst_reward_commission.commission_lamports = dst_reward_commission - .commission_lamports - .saturating_add(reward_commission.commission_lamports) + accumulate_lamports(&reward_commission, dst_reward_commission); }) .or_insert(reward_commission); } @@ -102,7 +163,9 @@ impl Bank { /// Begin the process of calculating and distributing rewards. /// This process can take multiple slots. /// - /// Returns the epoch validator rewards. + /// Returns the total rewards that will be distributed in this epoch (to both validators and + /// stakers) minus rewards sent to the incinerator. This is the total amount the capitalization + /// will increase by after all the rewards have been paid. pub(in crate::bank) fn begin_partitioned_rewards( &mut self, parent_epoch: Epoch, @@ -112,7 +175,11 @@ impl Bank { rewards_metrics: &mut RewardsMetrics, thread_pool: &ThreadPool, ) -> u64 { - let distributed_rewards = self.distribute_reward_commissions( + let RewardCommissionLamportAmounts { + distributed_lamports, + distributed_to_incinerator_lamports, + burned_lamports, + } = self.distribute_reward_commissions( parent_epoch, rewards_calculation, rewards_metrics, @@ -135,10 +202,11 @@ impl Bank { self.set_epoch_reward_status_calculation(distribution_starting_block_height, stake_rewards); self.create_epoch_rewards_sysvar( - distributed_rewards, + distributed_lamports + distributed_to_incinerator_lamports + burned_lamports, distribution_starting_block_height, num_partitions, point_value, + 0, // block_rewards ); datapoint_info!( @@ -149,7 +217,10 @@ impl Bank { ("parent_slot", parent_slot, i64), ("parent_block_height", parent_block_height, i64), ); - distributed_rewards + distributed_lamports + + rewards_calculation + .stake_rewards + .total_stake_rewards_lamports } // Calculate rewards from previous epoch and distribute reward commissions @@ -158,7 +229,8 @@ impl Bank { stake_history: &StakeHistory, stake_delegations: Vec<(&Pubkey, &StakeAccount)>, cached_vote_accounts: CachedVoteAccounts<'_>, - prev_epoch: Epoch, + rewarded_epoch: Epoch, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes, reward_calc_tracer: Option, thread_pool: &ThreadPool, metrics: &mut RewardsMetrics, @@ -191,7 +263,8 @@ impl Bank { stake_history, stake_delegations, cached_vote_accounts, - prev_epoch, + rewarded_epoch, + reward_epoch_delegated_stakes, reward_calc_tracer, thread_pool, metrics, @@ -203,14 +276,15 @@ impl Bank { rewards_calculation } - /// Returns the total amount of commission lamports distributed. + /// Returns a tuple containing the total amount of commission lamports + /// distributed and the total amount of lamports burned. pub(in crate::bank) fn distribute_reward_commissions( &mut self, prev_epoch: Epoch, rewards_calculation: &PartitionedRewardsCalculation, rewards_metrics: &mut RewardsMetrics, thread_pool: &ThreadPool, - ) -> u64 { + ) -> RewardCommissionLamportAmounts { let PartitionedRewardsCalculation { reward_commissions, stake_rewards, @@ -234,7 +308,12 @@ impl Bank { reward_commission_accounts.accounts_with_rewards.len(), load_and_reward_commission_accounts_us, ); - let total_reward_commissions = reward_commission_accounts.total_reward_commission_lamports; + + let RewardCommissionLamportAmounts { + distributed_lamports, + distributed_to_incinerator_lamports, + burned_lamports, + } = reward_commission_accounts.amounts; self.store_commission_accounts_partitioned(&reward_commission_accounts, rewards_metrics); self.update_reward_commissions(&reward_commission_accounts); @@ -244,16 +323,30 @@ impl Bank { } = stake_rewards; // verify that we didn't pay any more than we expected to - assert!(point_value.rewards >= total_reward_commissions + total_stake_rewards_lamports); + assert!( + point_value.rewards + >= distributed_lamports + + distributed_to_incinerator_lamports + + burned_lamports + + total_stake_rewards_lamports, + "point_value={point_value:?}, distributed_lamports={distributed_lamports}, \ + distributed_to_incinerator_lamports={distributed_to_incinerator_lamports} \ + burned_lamports={burned_lamports}, \ + total_stake_rewards_lamports={total_stake_rewards_lamports}" + ); info!( "distributed reward commissions: {} out of {}, remaining {}", - total_reward_commissions, point_value.rewards, total_stake_rewards_lamports + distributed_lamports + distributed_to_incinerator_lamports + burned_lamports, + point_value.rewards, + total_stake_rewards_lamports ); let num_stake_accounts = self.stakes_cache.stakes().stake_delegations().len(); let num_vote_accounts = *num_filtered_vote_accounts; - self.capitalization - .fetch_add(total_reward_commissions, Relaxed); + self.capitalization.fetch_add( + distributed_lamports + distributed_to_incinerator_lamports, + Relaxed, + ); let active_stake = if let Some(stake_history_entry) = self.stakes_cache.stakes().history().get(prev_epoch) @@ -267,7 +360,13 @@ impl Bank { "epoch_rewards", ("slot", self.slot, i64), ("epoch", prev_epoch, i64), - ("validator_rewards", total_reward_commissions, i64), + ("validator_rewards", distributed_lamports, i64), + ( + "validator_rewards_to_incinerator", + distributed_to_incinerator_lamports, + i64 + ), + ("validator_rewards_burned", burned_lamports, i64), ("active_stake", active_stake, i64), ("pre_capitalization", *capitalization, i64), ("post_capitalization", self.capitalization(), i64), @@ -275,7 +374,7 @@ impl Bank { ("num_vote_accounts", num_vote_accounts, i64), ); - total_reward_commissions + reward_commission_accounts.amounts } fn store_commission_accounts_partitioned( @@ -303,12 +402,13 @@ impl Bank { stake_delegations: Vec<(&'a Pubkey, &'a StakeAccount)>, cached_vote_accounts: CachedVoteAccounts<'_>, rewarded_epoch: Epoch, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes, reward_calc_tracer: Option, thread_pool: &ThreadPool, metrics: &mut RewardsMetrics, ) -> PartitionedRewardsCalculation { let capitalization = self.capitalization(); - let validator_rewards_lamports = + let epoch_inflation_rewards = self.calculate_epoch_inflation_rewards(capitalization, rewarded_epoch); // `distribution_epoch_vote_accounts` is the post-VAT-filter snapshot // produced upstream of this call (or unfiltered when VAT is off), @@ -326,7 +426,8 @@ impl Bank { stake_delegations, cached_vote_accounts, rewarded_epoch, - validator_rewards_lamports, + epoch_inflation_rewards, + reward_epoch_delegated_stakes, reward_calc_tracer, thread_pool, metrics, @@ -348,22 +449,27 @@ impl Bank { } /// Calculate epoch reward and return stake rewards and commissions. + #[allow(clippy::too_many_arguments)] fn calculate_validator_rewards<'a>( &self, stake_history: &StakeHistory, stake_delegations: Vec<(&'a Pubkey, &'a StakeAccount)>, cached_vote_accounts: CachedVoteAccounts<'_>, rewarded_epoch: Epoch, - rewards: u64, + epoch_inflation_rewards: u64, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes, reward_calc_tracer: Option, thread_pool: &ThreadPool, metrics: &mut RewardsMetrics, ) -> Option { + let ag_epoch_type = + AlpenglowEpochType::get(self, rewarded_epoch, || Some(reward_epoch_delegated_stakes)); self.calculate_reward_points_partitioned( stake_history, &stake_delegations, &cached_vote_accounts, - rewards, + epoch_inflation_rewards, + &ag_epoch_type, thread_pool, metrics, ) @@ -375,6 +481,7 @@ impl Bank { cached_vote_accounts, rewarded_epoch, point_value.clone(), + &ag_epoch_type, thread_pool, reward_calc_tracer, metrics, @@ -431,7 +538,10 @@ impl Bank { delay_commission_updates: bool, commission_rate_in_basis_points: bool, adjust_delegations_for_rent: bool, - ) -> Option { + ag_epoch_type: &AlpenglowEpochType, + custom_commission_collector: bool, + use_fixed_point_stake_math: bool, + ) -> Option { // curry closure to add the contextual stake_pubkey let reward_calc_tracer = reward_calc_tracer.as_ref().map(|outer| { // inner @@ -446,7 +556,6 @@ impl Bank { distribution_epoch_vote_accounts, } = cached_vote_accounts; - let stake_pubkey = *stake_pubkey; let vote_pubkey = stake_account.delegation().voter_pubkey; let current_lamports = stake_account.lamports(); @@ -454,40 +563,44 @@ impl Bank { .rent_collector .rent .minimum_balance(stake_account.data_len()); - let mut stake = *stake_account.stake(); + let stake = *stake_account.stake(); let Some(vote_account) = distribution_epoch_vote_accounts.get(&vote_pubkey) else { debug!("could not find vote account {vote_pubkey} in cache"); // Even if the vote account doesn't exist, there might still be a // need to adjust the stake delegation if adjust_delegations_for_rent { - let delegation = stake.delegation.stake; - let stake_was_adjusted = adjust_delegation_for_rent( - &mut stake.delegation, - rewarded_epoch, - delegation, + if delegation_may_need_adjustment( + stake.delegation.stake, + stake.delegation.stake, current_lamports, minimum_lamports, - ); - if stake_was_adjusted { - debug!("delegation for stake {stake_pubkey} was adjusted"); - let stake_reward = PartitionedStakeReward { - stake_pubkey, + ) { + debug!( + "delegation for stake {stake_pubkey} may be adjusted at distribution, \ + unless lamports are transferred before distribution block" + ); + let inflation = InflationReward { stake, stake_reward: 0, - commission_bps: 0, + commission_bps: (!custom_commission_collector).then_some(0), }; + // Set `is_vote_account` to `false` in order to deliberately + // fail during commission collector checks. This avoids + // creating a reward entry during payout. let reward_commission = RewardCommission { - commission_bps: 0, + commission_bps: (!custom_commission_collector).then_some(0), commission_lamports: 0, + burned_lamports: 0, + is_vote_account: false, }; - return Some(DelegationRewards { - stake_reward, + return Some(InflationRewardWithCommission { + inflation, commission_pubkey: vote_pubkey, reward_commission, }); } else { - debug!("delegation for stake {stake_pubkey} was not adjusted"); + debug!("delegation for stake {stake_pubkey} will not be adjusted"); return None; } } else { @@ -530,25 +643,36 @@ impl Bank { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math, }, reward_calc_tracer, + ag_epoch_type, current_lamports, minimum_lamports, ) { Ok((stake_reward, commission_lamports, stake)) => { - let stake_reward = PartitionedStakeReward { - stake_pubkey, + let inflation = InflationReward { stake, stake_reward, - commission_bps, + commission_bps: (!custom_commission_collector).then_some(commission_bps), + }; + let (commission_pubkey, is_vote_account) = if custom_commission_collector { + let commission_pubkey = *vote_state + .inflation_rewards_collector() + .unwrap_or(&vote_pubkey); + (commission_pubkey, commission_pubkey == vote_pubkey) + } else { + (vote_pubkey, true) }; let reward_commission = RewardCommission { - commission_bps, + commission_bps: (!custom_commission_collector).then_some(commission_bps), commission_lamports, + burned_lamports: 0, + is_vote_account, }; - Some(DelegationRewards { - stake_reward, - commission_pubkey: vote_pubkey, + Some(InflationRewardWithCommission { + inflation, + commission_pubkey, reward_commission, }) } @@ -561,6 +685,7 @@ impl Bank { /// Calculates epoch rewards for stake/commission accounts /// Returns commission accounts, stake rewards, and the sum of all stake rewards in lamports + #[allow(clippy::too_many_arguments)] fn calculate_stake_rewards_and_commissions<'a>( &self, stake_history: &StakeHistory, @@ -568,17 +693,20 @@ impl Bank { cached_vote_accounts: CachedVoteAccounts<'_>, rewarded_epoch: Epoch, point_value: PointValue, + ag_epoch_type: &AlpenglowEpochType, thread_pool: &ThreadPool, reward_calc_tracer: Option, metrics: &mut RewardsMetrics, ) -> (RewardCommissions, StakeRewardCalculation) { let new_warmup_cooldown_rate_epoch = self.new_warmup_cooldown_rate_epoch(); let feature_snapshot = self.feature_set.snapshot(); + let use_fixed_point_stake_math = feature_snapshot.upgrade_bpf_stake_program_to_v5_1; let delay_commission_updates = feature_snapshot.delay_commission_updates; let commission_rate_in_basis_points = feature_snapshot.commission_rate_in_basis_points; // Name intentionally doesn't match -- "adjust delegations for rent" is // part of relaxing post-exec min balance checks. let adjust_delegations_for_rent = feature_snapshot.relax_post_exec_min_balance_check; + let custom_commission_collector = feature_snapshot.custom_commission_collector; let mut measure_redeem_rewards = Measure::start("redeem-rewards"); // For N stake delegations, where N is >1,000,000, we produce: @@ -609,18 +737,24 @@ impl Bank { delay_commission_updates, commission_rate_in_basis_points, adjust_delegations_for_rent, + ag_epoch_type, + custom_commission_collector, + use_fixed_point_stake_math, ); let (stake_reward, maybe_reward_record) = match maybe_reward_record { Some(res) => { - let DelegationRewards { - stake_reward, + let InflationRewardWithCommission { + inflation, commission_pubkey, reward_commission, } = res; - let stakers_reward = stake_reward.stake_reward; + let stakers_reward = inflation.stake_reward; ( - Some(stake_reward), + Some(PartitionedStakeReward { + stake_pubkey: **stake_pubkey, + inflation, + }), Some((stakers_reward, commission_pubkey, reward_commission)), ) } @@ -682,7 +816,8 @@ impl Bank { stake_history: &StakeHistory, stake_delegations: &Vec<(&'a Pubkey, &'a StakeAccount)>, cached_vote_accounts: &CachedVoteAccounts<'_>, - rewards: u64, + epoch_inflation_rewards: u64, + ag_epoch_type: &AlpenglowEpochType, thread_pool: &ThreadPool, metrics: &RewardsMetrics, ) -> Option { @@ -693,6 +828,25 @@ impl Bank { let solana_vote_program: Pubkey = solana_vote_program::id(); let new_warmup_cooldown_rate_epoch = self.new_warmup_cooldown_rate_epoch(); + match ag_epoch_type { + AlpenglowEpochType::Alpenglow { .. } => { + // In alpenglow, we do not need to compute `PointValue::points` as the final + // rewards are simply the total credits stored in the vote account. We just need + // to return a `Some` value with valid rewards. + return Some(PointValue { + rewards: epoch_inflation_rewards, + points: 0, + }); + } + AlpenglowEpochType::Tower => { + // For tower we need to compute the valid `PointValue::points`. + } + AlpenglowEpochType::MigrationEpoch { .. } => { + // For the migrating epoch, we need to compute the tower portion of `PointValue::points`. + } + } + + let use_fixed_point_stake_math = self.use_fixed_point_stake_math(); let (points, measure_us) = measure_us!(thread_pool.install(|| { stake_delegations .par_iter() @@ -707,11 +861,12 @@ impl Bank { return 0; } - calculate_points( + calculate_points_for_tower( stake_account.stake_state(), DelegatedVoteState::from(vote_account.vote_state_view()), stake_history, new_warmup_cooldown_rate_epoch, + use_fixed_point_stake_math, ) .unwrap_or(0) }) @@ -719,7 +874,10 @@ impl Bank { })); metrics.calculate_points_us.fetch_add(measure_us, Relaxed); - (points > 0).then_some(PointValue { rewards, points }) + (points > 0).then_some(PointValue { + rewards: epoch_inflation_rewards, + points, + }) } /// If rewards are still active, recalculates partitioned stake rewards and @@ -770,6 +928,9 @@ impl Bank { stake_delegations, cached_vote_accounts, } = self.get_epoch_params_for_recalculation(rewarded_epoch, &stakes); + let ag_epoch_type = AlpenglowEpochType::get(self, rewarded_epoch, || { + RewardEpochDelegatedStakes::get(self) + }); // On recalculation, only the `StakeRewardCalculation::stake_rewards` // field is relevant. It is assumed that reward commission accounts have @@ -791,6 +952,7 @@ impl Bank { cached_vote_accounts, rewarded_epoch, point_value, + &ag_epoch_type, thread_pool, null_tracer(), &mut RewardsMetrics::default(), // This is required, but not reporting anything at the moment @@ -814,6 +976,14 @@ impl Bank { reward_commissions: &RewardCommissions, thread_pool: &ThreadPool, ) -> RewardCommissionAccounts { + let reserved_account_keys = &self.reserved_account_keys; + let rent = &self.rent_collector().rent; + let feature_snapshot = self.feature_set.snapshot(); + let relax_post_exec_min_balance_check = feature_snapshot.relax_post_exec_min_balance_check; + let custom_commission_collector = feature_snapshot.custom_commission_collector; + let total_non_incinerator_burned_lamports = AtomicU64::new(0); + let total_incinerator_lamports = AtomicU64::new(0); + let accounts_with_rewards: Vec<_> = thread_pool.install(|| { reward_commissions .par_iter() @@ -823,31 +993,77 @@ impl Bank { RewardCommission { commission_bps, commission_lamports, - .. + burned_lamports, + is_vote_account, }, )| { - let Some(mut commission_account) = - self.get_account_with_fixed_root_no_cache(commission_pubkey) - else { - debug!( - "commission account {commission_pubkey} missing at distribution \ - time" - ); - return None; + let maybe_commission_account = + self.get_account_with_fixed_root_no_cache(commission_pubkey); + let mut commission_account = if custom_commission_collector { + // If the account doesn't exist, the vote commission + // may be enough lamports to cover rent-exemption + // and properly create the commission account. + maybe_commission_account.unwrap_or_default() + } else { + // Before SIMD-0232, commission accounts were always + // vote accounts, which cannot be closed unless the + // account hasn't voted for at least a full epoch. + // This means that `maybe_commission_account` should + // always exist. + let Some(commission_account) = maybe_commission_account else { + debug!( + "commission account {commission_pubkey} missing at \ + distribution time" + ); + return None; + }; + commission_account }; + if *burned_lamports != 0 { + total_non_incinerator_burned_lamports + .fetch_add(*burned_lamports, Relaxed); + } + let pre_lamports = commission_account.lamports(); if let Err(err) = commission_account.checked_add_lamports(*commission_lamports) { debug!("reward redemption failed for {commission_pubkey}: {err:?}"); + total_non_incinerator_burned_lamports + .fetch_add(*commission_lamports, Relaxed); return None; } + if !is_vote_account { + match Self::collector_type_checked( + commission_pubkey, + pre_lamports, + &commission_account, + reserved_account_keys, + rent, + relax_post_exec_min_balance_check, + ) { + Ok(ExternalCollectorType::SystemAccount) => {} + Ok(ExternalCollectorType::Incinerator) => { + total_incinerator_lamports + .fetch_add(*commission_lamports, Relaxed); + } + Err(err) => { + debug!( + "reward redemption failed for {commission_pubkey} due to \ + commission account error: {err:?}" + ); + total_non_incinerator_burned_lamports + .fetch_add(*commission_lamports, Relaxed); + return None; + } + } + } Some(( *commission_pubkey, RewardInfo { reward_type: RewardType::Voting, lamports: *commission_lamports as i64, post_balance: commission_account.lamports(), - commission_bps: Some(*commission_bps), + commission_bps: *commission_bps, }, commission_account, )) @@ -855,13 +1071,21 @@ impl Bank { ) .collect() }); - let total_reward_commission_lamports = accounts_with_rewards + + let distributed_to_incinerator_lamports = total_incinerator_lamports.into_inner(); + let distributed_lamports = accounts_with_rewards .iter() .map(|(_, info, _)| info.lamports as u64) - .sum(); + .sum::() + .checked_sub(distributed_to_incinerator_lamports) + .expect("incinerator lamports must be a subset of all distributed lamports"); RewardCommissionAccounts { accounts_with_rewards, - total_reward_commission_lamports, + amounts: RewardCommissionLamportAmounts { + distributed_lamports, + distributed_to_incinerator_lamports, + burned_lamports: total_non_incinerator_burned_lamports.into_inner(), + }, } } @@ -897,6 +1121,7 @@ mod tests { }, bank_forks::BankForks, genesis_utils::{self, GenesisConfigInfo, deactivate_features}, + runtime_config::RuntimeConfig, stake_account::StakeAccount, stake_utils, stakes::{Stakes, tests::create_staked_node_accounts}, @@ -908,13 +1133,17 @@ mod tests { solana_account::{ AccountSharedData, ReadableAccount, accounts_equal, state_traits::StateMut, }, - solana_accounts_db::partitioned_rewards::PartitionedEpochRewardsConfig, + solana_accounts_db::{ + accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsDbConfig}, + partitioned_rewards::PartitionedEpochRewardsConfig, + }, solana_bls_signatures::keypair::Keypair as BLSKeypair, solana_clock::Clock, solana_epoch_schedule::EpochSchedule, solana_keypair::Keypair, solana_native_token::LAMPORTS_PER_SOL, solana_rent::Rent, + solana_sdk_ids::incinerator, solana_signer::Signer, solana_stake_interface::{ stake_flags::StakeFlags, @@ -925,12 +1154,19 @@ mod tests { }, solana_vote_program::vote_state::{self, create_bls_proof_of_possession}, std::{ - collections::HashSet, + collections::{HashMap, HashSet}, sync::{Arc, RwLock, RwLockReadGuard}, }, - test_case::test_case, + test_case::{test_case, test_matrix}, }; + fn reward_epoch_delegated_stakes_for_tests(epoch: Epoch) -> RewardEpochDelegatedStakes { + RewardEpochDelegatedStakes { + epoch, + delegated_stakes: HashMap::default(), + } + } + #[test] fn test_store_commission_accounts_partitioned() { let (genesis_config, _mint_keypair) = create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); @@ -962,12 +1198,12 @@ mod tests { *info, commission_account.clone(), )); - reward_commission_accounts.total_reward_commission_lamports += info.lamports as u64; + reward_commission_accounts.amounts.distributed_lamports += info.lamports as u64; } let metrics = RewardsMetrics::default(); - let total_reward_commissions = reward_commission_accounts.total_reward_commission_lamports; + let total_reward_commissions = reward_commission_accounts.amounts.distributed_lamports; bank.store_commission_accounts_partitioned(&reward_commission_accounts, &metrics); assert_eq!( num_reward_commissions, @@ -998,7 +1234,7 @@ mod tests { let expected = 0; let reward_commission_accounts = RewardCommissionAccounts::default(); let metrics = RewardsMetrics::default(); - let total_reward_commissions = reward_commission_accounts.total_reward_commission_lamports; + let total_reward_commissions = reward_commission_accounts.amounts.distributed_lamports; bank.store_commission_accounts_partitioned(&reward_commission_accounts, &metrics); assert_eq!( @@ -1030,7 +1266,7 @@ mod tests { let expected_rewards = 100_000_000_000; let stakes = bank.stakes_cache.stakes(); - let rewarded_epoch = 1; + let rewarded_epoch = 0; let EpochRewardCalculateParamInfo { stake_history, stake_delegations, @@ -1042,6 +1278,7 @@ mod tests { cached_vote_accounts, rewarded_epoch, expected_rewards, + reward_epoch_delegated_stakes_for_tests(rewarded_epoch), null_tracer(), &thread_pool, &mut rewards_metrics, @@ -1093,6 +1330,7 @@ mod tests { &stake_delegations, &cached_vote_accounts, expected_rewards, + &AlpenglowEpochType::Tower, &thread_pool, &rewards_metrics, ); @@ -1126,6 +1364,7 @@ mod tests { &stake_delegations, &cached_vote_accounts, expected_rewards, + &AlpenglowEpochType::Tower, &thread_pool, &rewards_metrics, ); @@ -1133,6 +1372,74 @@ mod tests { assert!(point_value.is_none()); } + #[test] + fn test_begin_partitioned_rewards_returns_total_capitalization_increase() { + let (genesis_config, _mint_keypair) = create_genesis_config(1_000 * LAMPORTS_PER_SOL); + let mut bank = Bank::new_for_tests(&genesis_config); + let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); + + let commission_pubkey = Pubkey::new_unique(); + { + let mut commission_account = AccountSharedData::default(); + commission_account.set_lamports(1); + bank.store_account_and_update_capitalization(&commission_pubkey, &commission_account); + } + + let commission_lamports = 123; + let stake_reward_lamports = 456; + let mut reward_commissions = RewardCommissions::default(); + reward_commissions.insert( + commission_pubkey, + RewardCommission { + commission_bps: Some(0), + commission_lamports, + burned_lamports: 0, + is_vote_account: true, + }, + ); + let stake_rewards = [Some(PartitionedStakeReward { + stake_pubkey: Pubkey::new_unique(), + inflation: InflationReward { + stake: Stake { + delegation: Delegation::default(), + credits_observed: 0, + }, + stake_reward: stake_reward_lamports, + commission_bps: Some(0), + }, + })] + .into_iter() + .collect::(); + let rewards_calculation = PartitionedRewardsCalculation { + reward_commissions, + stake_rewards: StakeRewardCalculation { + stake_rewards: Arc::new(stake_rewards), + total_stake_rewards_lamports: stake_reward_lamports, + }, + capitalization: bank.capitalization(), + point_value: PointValue { + rewards: commission_lamports + stake_reward_lamports, + points: 1, + }, + num_filtered_vote_accounts: 1, + }; + let mut rewards_metrics = RewardsMetrics::default(); + + let rewards = bank.begin_partitioned_rewards( + bank.epoch().saturating_sub(1), + bank.parent_slot(), + bank.block_height(), + &rewards_calculation, + &mut rewards_metrics, + &thread_pool, + ); + + assert_eq!(rewards, commission_lamports + stake_reward_lamports); + let epoch_rewards = bank.get_epoch_rewards_sysvar(); + assert_eq!(epoch_rewards.distributed_rewards, commission_lamports); + assert_eq!(epoch_rewards.total_rewards, rewards); + } + struct EpochOperations { epoch: Epoch, vote_operations: Vec<(Pubkey, VoteOperations)>, @@ -1140,18 +1447,19 @@ mod tests { #[derive(Default)] struct VoteOperations { - expected_reward_commission: Option, + expect_reward: bool, + // Additional lamport amount to ignore in collector account, used for + // VAT burns in Alpenglow when the incinerator is an inflation collector + extra_reward_lamport_amount: Option, // ops to perform before epoch ends create_with_balance: Option, delegate_stake_amount: Option, new_commission: Option, earned_credits: Option, + new_inflation_rewards_collector: Option, } - fn recalculate_reward_commission_for_tests( - bank: &Bank, - commission_pubkey: &Pubkey, - ) -> Option { + fn recalculate_reward_commissions_for_tests(bank: &Bank) -> RewardCommissions { let epoch_rewards_sysvar = bank.get_epoch_rewards_sysvar(); let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); assert!(epoch_rewards_sysvar.active); @@ -1175,23 +1483,47 @@ mod tests { cached_vote_accounts, rewarded_epoch, point_value, + &AlpenglowEpochType::Tower, &thread_pool, null_tracer(), &mut RewardsMetrics::default(), // This is required, but not reporting anything at the moment ); - reward_commissions.get(commission_pubkey).map(|rc| { + reward_commissions + } + + fn recalculate_reward_commission_for_tests( + bank: &Bank, + commission_pubkey: &Pubkey, + ) -> Option { + let reward_commissions = recalculate_reward_commissions_for_tests(bank); + reward_commissions.get(commission_pubkey).and_then(|rc| { + let commission_account = bank.get_account(commission_pubkey).unwrap_or_default(); // In the recalculation path, commissions have already been // distributed — so the current account balance already includes // them. We report that balance as the post_balance. - let post_balance = bank - .get_account(commission_pubkey) - .map(|a| a.lamports()) - .unwrap_or(0); - RewardInfo { - reward_type: RewardType::Voting, - lamports: rc.commission_lamports as i64, - post_balance, - commission_bps: Some(rc.commission_bps), + let post_balance = commission_account.lamports(); + + // This is artificial, but mimics the actual distribution logic a + // bit better + if *commission_account.owner() != solana_vote_program::id() + && Bank::collector_type_checked( + commission_pubkey, + post_balance.saturating_sub(rc.commission_lamports), + &commission_account, + &bank.reserved_account_keys, + &bank.rent_collector().rent, + true, + ) + .is_err() + { + None + } else { + Some(RewardInfo { + reward_type: RewardType::Voting, + lamports: rc.commission_lamports as i64, + post_balance, + commission_bps: rc.commission_bps, + }) } }) } @@ -1297,6 +1629,12 @@ mod tests { }); } + if let Some(inflation_rewards_collector) = vote_op.new_inflation_rewards_collector { + modify_vote_state(&|vote_state: &mut VoteStateV4| { + vote_state.inflation_rewards_collector = inflation_rewards_collector; + }); + } + if let Some(earned_credits) = vote_op.earned_credits { modify_vote_state(&|vote_state: &mut VoteStateV4| { let last_credits = vote_state @@ -1320,30 +1658,40 @@ mod tests { Bank::new_from_parent_with_bank_forks(bank_forks, bank, SlotLeader::new_unique(), slot); for (vote_address, vote_op) in &op.vote_operations { - let expected_commission = vote_op.expected_reward_commission; + // some tests delegate before the vote account exists + let collector_address = bank + .get_account(vote_address) + .map(|vote_account| { + VoteStateV4::deserialize(vote_account.data(), vote_address) + .unwrap() + .inflation_rewards_collector + }) + .unwrap_or(*vote_address); let recalculated_vote_reward = - recalculate_reward_commission_for_tests(&bank, vote_address); + recalculate_reward_commission_for_tests(&bank, &collector_address); let vote_reward = bank .rewards .read() .unwrap() .iter() - .find(|(address, _reward)| address == vote_address) + .find(|(address, _reward)| *address == collector_address) .map(|(_address, reward)| *reward); - let prev_vote_balance = prev_bank - .get_account(vote_address) + let prev_collector_balance = prev_bank + .get_account(&collector_address) .unwrap_or_default() .lamports(); - let vote_balance = bank.get_balance(vote_address); + let collector_balance = bank.get_balance(&collector_address); - if let Some(expected_commission) = &expected_commission { - let reward_lamports = vote_balance - prev_vote_balance; + if vote_op.expect_reward { + let reward_lamports = collector_balance + - prev_collector_balance + - vote_op.extra_reward_lamport_amount.unwrap_or(0); let expected_vote_reward = RewardInfo { reward_type: RewardType::Voting, lamports: reward_lamports as i64, - post_balance: vote_balance, - commission_bps: Some(*expected_commission as u16 * 100), + post_balance: collector_balance, + commission_bps: None, }; assert_eq!( @@ -1422,27 +1770,23 @@ mod tests { // Check that if a vote account didn't exist two epochs ago (normal for // new vote accounts), that the reward commission falls back to the // commission from the end of the rewarded epoch. - bank = { - let expected_commission = if delay_commission_updates { 1 } else { 2 }; - apply_epoch_operations( - bank, - bank_forks.as_ref(), - EpochOperations { - epoch: 1, - vote_operations: vec![( - vote_address, - VoteOperations { - new_commission: Some(2), - earned_credits: Some(1000), - expected_reward_commission: Some(expected_commission), - ..VoteOperations::default() - }, - )], - }, - ) - }; + bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 1, + vote_operations: vec![( + vote_address, + VoteOperations { + new_commission: Some(2), + earned_credits: Some(1000), + expect_reward: true, + ..VoteOperations::default() + }, + )], + }, + ); - let expected_commission = if delay_commission_updates { 1 } else { 3 }; apply_epoch_operations( bank, bank_forks.as_ref(), @@ -1453,7 +1797,7 @@ mod tests { VoteOperations { new_commission: Some(3), earned_credits: Some(1000), - expected_reward_commission: Some(expected_commission), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1506,7 +1850,7 @@ mod tests { create_with_balance: Some(LAMPORTS_PER_SOL), new_commission: Some(1), earned_credits: Some(1000), - expected_reward_commission: Some(1), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1526,7 +1870,7 @@ mod tests { VoteOperations { new_commission: Some(2), earned_credits: Some(1000), - expected_reward_commission: Some(1), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1543,7 +1887,7 @@ mod tests { VoteOperations { new_commission: Some(3), earned_credits: Some(1000), - expected_reward_commission: Some(1), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1661,7 +2005,7 @@ mod tests { VoteOperations { new_commission: Some(1), earned_credits: Some(1000), - expected_reward_commission: Some(0), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1680,7 +2024,7 @@ mod tests { VoteOperations { new_commission: Some(2), earned_credits: Some(1000), - expected_reward_commission: Some(0), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1696,7 +2040,7 @@ mod tests { genesis_vote_address, VoteOperations { earned_credits: Some(1000), - expected_reward_commission: Some(1), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1712,7 +2056,7 @@ mod tests { genesis_vote_address, VoteOperations { earned_credits: Some(1000), - expected_reward_commission: Some(2), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1728,7 +2072,7 @@ mod tests { genesis_vote_address, VoteOperations { earned_credits: Some(1000), - expected_reward_commission: Some(2), + expect_reward: true, ..VoteOperations::default() }, )], @@ -1777,6 +2121,7 @@ mod tests { cached_vote_accounts, rewarded_epoch, point_value, + &AlpenglowEpochType::Tower, &thread_pool, reward_calc_tracer, &mut rewards_metrics, @@ -1792,9 +2137,8 @@ mod tests { assert_eq!(vote_rewards_accounts.len(), 1); let reward_commission = vote_rewards_accounts.get(vote_pubkey).unwrap(); let vote_rewards = 0; - let commission_bps = vote_state.inflation_rewards_commission_bps; assert_eq!(reward_commission.commission_lamports, vote_rewards); - assert_eq!(reward_commission.commission_bps, commission_bps); + assert_eq!(reward_commission.commission_bps, None); assert_eq!(stake_reward_calculation.stake_rewards.num_rewards(), 1); let expected_reward = { @@ -1804,10 +2148,12 @@ mod tests { stake.credits_observed = vote_state.credits(); stake.delegation.stake += stake_reward; PartitionedStakeReward { - stake, stake_pubkey, - stake_reward, - commission_bps, + inflation: InflationReward { + stake, + stake_reward, + commission_bps: None, + }, } }; assert_eq!( @@ -1841,7 +2187,7 @@ mod tests { num_rewards_per_block, SLOTS_PER_EPOCH, ); - let rewarded_epoch = bank.epoch(); + let rewarded_epoch = bank.epoch() - 1; let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); let mut rewards_metrics = RewardsMetrics::default(); @@ -1863,6 +2209,7 @@ mod tests { stake_delegations, cached_vote_accounts, rewarded_epoch, + reward_epoch_delegated_stakes_for_tests(rewarded_epoch), null_tracer(), &thread_pool, &mut rewards_metrics, @@ -1984,6 +2331,7 @@ mod tests { stake_delegations, cached_vote_accounts, rewarded_epoch, + reward_epoch_delegated_stakes_for_tests(rewarded_epoch), null_tracer(), &thread_pool, &mut rewards_metrics, @@ -2079,6 +2427,179 @@ mod tests { assert_eq!(bank.epoch_reward_status, EpochRewardStatus::Inactive); } + #[test] + fn test_recalculate_alpenglow_rewards_after_partial_distribution_uses_original_denominator() { + let stake_lamports = 2_000_000_000; + let validator_keypairs = vec![genesis_utils::ValidatorVoteKeypairs::new_rand()]; + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_alpenglow_vote_accounts( + 1_000_000_000 * LAMPORTS_PER_SOL, + &validator_keypairs, + vec![stake_lamports], + ); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + let features_to_deactivate = crate::slot_params::slot_time_feature_ids().to_vec(); + deactivate_features(&mut genesis_config, &features_to_deactivate); + + let mut accounts_db_config: AccountsDbConfig = ACCOUNTS_DB_CONFIG_FOR_TESTING; + accounts_db_config.partitioned_epoch_rewards_config = + PartitionedEpochRewardsConfig::new_for_test(1); + let bank = Bank::new_from_genesis( + &genesis_config, + Arc::new(RuntimeConfig::default()), + Vec::new(), + None, + accounts_db_config, + None, + None, + Arc::default(), + None, + None, + ); + + let vote_pubkey = validator_keypairs[0].vote_keypair.pubkey(); + let vote_account = bank.get_account(&vote_pubkey).unwrap(); + let extra_stake_pubkey = Pubkey::new_unique(); + let extra_stake_account = stake_utils::create_stake_account( + &extra_stake_pubkey, + &vote_pubkey, + &vote_account, + &bank.rent_collector.rent, + stake_lamports, + ); + bank.store_account_and_update_capitalization(&extra_stake_pubkey, &extra_stake_account); + + let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); + let bank = Bank::new_from_parent_with_bank_forks( + bank_forks.as_ref(), + bank, + SlotLeader::default(), + SLOTS_PER_EPOCH, + ); + assert_eq!(bank.epoch(), 1); + + let mut vote_account = bank.get_account(&vote_pubkey).unwrap(); + let VoteStateVersions::V4(mut vote_state) = vote_account + .deserialize_data::() + .unwrap() + else { + panic!("unexpected vote state version"); + }; + let last_credits = vote_state + .epoch_credits + .last() + .map(|(_epoch, final_credits, _initial_credits)| *final_credits) + .unwrap_or_default(); + vote_state + .epoch_credits + .push((bank.epoch(), last_credits + 1_000_000, last_credits)); + vote_account + .serialize_data(&VoteStateVersions::V4(vote_state)) + .unwrap(); + bank.store_account(&vote_pubkey, &vote_account); + + let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); + let mut bank = Bank::new_from_parent( + bank, + SlotLeader::default(), + SLOTS_PER_EPOCH.saturating_mul(2), + ); + assert_eq!(bank.epoch(), 2); + + let EpochRewardStatus::Active(EpochRewardPhase::Calculation(calculation_status)) = + bank.epoch_reward_status.clone() + else { + panic!("{:?} not active calculation", bank.epoch_reward_status); + }; + let original_stake_rewards = calculation_status.all_stake_rewards; + let original_rewards = original_stake_rewards + .enumerated_rewards_iter() + .collect::>(); + assert_eq!(original_rewards.len(), 2); + let (paid_index, paid_reward) = original_rewards[0]; + let (unpaid_index, unpaid_reward) = original_rewards[1]; + assert!(paid_reward.inflation.stake_reward > 0); + assert!(unpaid_reward.inflation.stake_reward > 0); + + // Force exactly one stake reward to be distributed before simulating + // snapshot restore. That write updates StakesCache with a larger + // delegation for the same vote account. + bank.set_epoch_reward_status_distribution( + bank.block_height(), + Arc::clone(&original_stake_rewards), + vec![vec![paid_index], vec![unpaid_index]], + ); + bank.distribute_partitioned_epoch_rewards(); + + let epoch_rewards_sysvar = bank.get_epoch_rewards_sysvar(); + assert!(epoch_rewards_sysvar.active); + let (recalculated_stake_rewards, _partition_indices) = + bank.recalculate_stake_rewards(&epoch_rewards_sysvar, &thread_pool); + let recalculated_unpaid_reward = recalculated_stake_rewards + .enumerated_rewards_iter() + .find_map(|(_index, reward)| { + (reward.stake_pubkey == unpaid_reward.stake_pubkey).then_some(reward) + }) + .expect("unpaid stake reward must still be pending after recalculation"); + + assert_eq!( + unpaid_reward.inflation.stake_reward, recalculated_unpaid_reward.inflation.stake_reward, + "recalculation after partial distribution must use the same AG delegated stake \ + denominator as the original epoch-boundary calculation" + ); + } + + #[test] + fn test_alpenglow_reward_epoch_delegated_stakes_account_is_bounded() { + let num_validators = crate::bank::MAX_ALPENGLOW_VOTE_ACCOUNTS + 1; + let validator_keypairs = (0..num_validators) + .map(|_| genesis_utils::ValidatorVoteKeypairs::new_rand()) + .collect::>(); + // Unique stakes make VAT filtering exclude only the lowest-staked vote + // account instead of dropping a tie group at the boundary. + let stakes = (0..num_validators) + .map(|index| 2_000_000_000 + (num_validators - index) as u64) + .collect::>(); + let filtered_vote_pubkey = validator_keypairs + .last() + .expect("validator keypairs must not be empty") + .vote_keypair + .pubkey(); + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_alpenglow_vote_accounts( + 1_000_000_000 * LAMPORTS_PER_SOL, + &validator_keypairs, + stakes, + ); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + let features_to_deactivate = crate::slot_params::slot_time_feature_ids().to_vec(); + deactivate_features(&mut genesis_config, &features_to_deactivate); + + let (bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + let bank = Bank::new_from_parent_with_bank_forks( + bank_forks.as_ref(), + bank, + SlotLeader::default(), + SLOTS_PER_EPOCH, + ); + + let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes::get(&bank) + .expect("AG reward epoch delegated stakes must be persisted"); + assert_eq!(reward_epoch_delegated_stakes.epoch, bank.epoch() - 1); + assert_eq!( + reward_epoch_delegated_stakes.delegated_stakes.len(), + crate::bank::MAX_ALPENGLOW_VOTE_ACCOUNTS + ); + assert!( + !reward_epoch_delegated_stakes + .delegated_stakes + .contains_key(&filtered_vote_pubkey) + ); + } + #[test] fn test_initialize_after_snapshot_restore() { let expected_num_stake_rewards = 4; @@ -2200,7 +2721,6 @@ mod tests { .enumerated_rewards_iter() .all(|(_, reward)| reward.stake_pubkey != filtered_stake_pubkey) ); - // Simulate snapshot restore: re-apply features from accounts and // rebuild epoch_reward_status from snapshot-stable state. bank.feature_set = Arc::new(FeatureSet::default()); @@ -2246,32 +2766,40 @@ mod tests { accumulator1.add_reward( commission_pubkey_a, RewardCommission { - commission_bps: 1_000, + commission_bps: Some(1_000), commission_lamports: 50, + burned_lamports: 0, + is_vote_account: true, }, 50, ); accumulator1.add_reward( commission_pubkey_b, RewardCommission { - commission_bps: 1_000, + commission_bps: Some(1_000), commission_lamports: 50, + burned_lamports: 0, + is_vote_account: true, }, 50, ); accumulator2.add_reward( commission_pubkey_b, RewardCommission { - commission_bps: 1_000, + commission_bps: Some(1_000), commission_lamports: 30, + burned_lamports: 0, + is_vote_account: true, }, 30, ); accumulator2.add_reward( commission_pubkey_c, RewardCommission { - commission_bps: 1_000, + commission_bps: Some(1_000), commission_lamports: 50, + burned_lamports: 0, + is_vote_account: true, }, 50, ); @@ -2282,28 +2810,28 @@ mod tests { .reward_commissions .get(&commission_pubkey_a) .unwrap(); - assert_eq!(reward_commission_a_1.commission_bps, 1_000); + assert_eq!(reward_commission_a_1.commission_bps, Some(1_000)); assert_eq!(reward_commission_a_1.commission_lamports, 50); let reward_commission_b_1 = accumulator1 .reward_commissions .get(&commission_pubkey_b) .unwrap(); - assert_eq!(reward_commission_b_1.commission_bps, 1_000); + assert_eq!(reward_commission_b_1.commission_bps, Some(1_000)); assert_eq!(reward_commission_b_1.commission_lamports, 50); let reward_commission_b_2 = accumulator2 .reward_commissions .get(&commission_pubkey_b) .unwrap(); - assert_eq!(reward_commission_b_2.commission_bps, 1_000); + assert_eq!(reward_commission_b_2.commission_bps, Some(1_000)); assert_eq!(reward_commission_b_2.commission_lamports, 30); let reward_commission_c_2 = accumulator2 .reward_commissions .get(&commission_pubkey_c) .unwrap(); - assert_eq!(reward_commission_c_2.commission_bps, 1_000); + assert_eq!(reward_commission_c_2.commission_bps, Some(1_000)); assert_eq!(reward_commission_c_2.commission_lamports, 50); let accumulator = accumulator1.accumulate_into_larger(accumulator2); @@ -2314,14 +2842,14 @@ mod tests { .reward_commissions .get(&commission_pubkey_a) .unwrap(); - assert_eq!(reward_commission_a.commission_bps, 1_000); + assert_eq!(reward_commission_a.commission_bps, Some(1_000)); assert_eq!(reward_commission_a.commission_lamports, 50); let reward_commission_b = accumulator .reward_commissions .get(&commission_pubkey_b) .unwrap(); - assert_eq!(reward_commission_b.commission_bps, 1_000); + assert_eq!(reward_commission_b.commission_bps, Some(1_000)); // sum of the reward commissions from both accumulators assert_eq!(reward_commission_b.commission_lamports, 80); @@ -2329,42 +2857,193 @@ mod tests { .reward_commissions .get(&commission_pubkey_c) .unwrap(); - assert_eq!(reward_commission_c.commission_bps, 1_000); + assert_eq!(reward_commission_c.commission_bps, Some(1_000)); assert_eq!(reward_commission_c.commission_lamports, 50); } - #[test] - fn test_epoch_rewards_cache_multiple_forks() { - let (mut genesis_config, _mint_keypair) = - create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + fn check_accumulator( + accumulator: &RewardsAccumulator, + commission_pubkey: &Pubkey, + left_reward: &RewardCommission, + right_reward: &RewardCommission, + ) { + let reward_commission = accumulator + .reward_commissions + .get(commission_pubkey) + .unwrap(); - const NUM_STAKES: usize = 1000; + assert_eq!( + left_reward.is_vote_account || right_reward.is_vote_account, + reward_commission.is_vote_account + ); + match (left_reward.is_vote_account, right_reward.is_vote_account) { + (false, true) => { + assert_eq!( + reward_commission.commission_lamports, + right_reward.commission_lamports + ); + assert_eq!( + reward_commission.burned_lamports, + left_reward.burned_lamports + + right_reward.burned_lamports + + left_reward.commission_lamports + ); + } + (true, false) => { + assert_eq!( + reward_commission.commission_lamports, + left_reward.commission_lamports + ); + assert_eq!( + reward_commission.burned_lamports, + left_reward.burned_lamports + + right_reward.burned_lamports + + right_reward.commission_lamports + ); + } + _ => { + assert_eq!( + reward_commission.commission_lamports, + left_reward.commission_lamports + right_reward.commission_lamports + ); + assert_eq!( + reward_commission.burned_lamports, + left_reward.burned_lamports + right_reward.burned_lamports + ); + } + } + } - for _i in 0..NUM_STAKES { - let vote_pubkey = Pubkey::new_unique(); - let stake_pubkey = Pubkey::new_unique(); + #[test_matrix( + [false, true], + [false, true] + )] + fn test_reward_accumulator_add_rewards( + left_is_vote_account: bool, + right_is_vote_account: bool, + ) { + let mut accumulator = RewardsAccumulator::default(); + let commission_pubkey = Pubkey::new_unique(); + let commission_bps = Some(1_000); - genesis_config.accounts.insert( - vote_pubkey, - vote_state::create_v4_account_with_authorized( - &vote_pubkey, - &vote_pubkey, - [0u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE], - &vote_pubkey, - 0, - &vote_pubkey, - 0, - &vote_pubkey, - 100_000_000_000, - ) - .into(), - ); + let left_reward = RewardCommission { + commission_bps, + commission_lamports: 1, + burned_lamports: 10, + is_vote_account: left_is_vote_account, + }; - let stake_lamports = 1_000_000_000_000; - let stake_account = stake_utils::create_stake_account( - &stake_pubkey, - &vote_pubkey, - &vote_state::create_v4_account_with_authorized( + let right_reward = RewardCommission { + commission_bps, + commission_lamports: 100, + burned_lamports: 1_000, + is_vote_account: right_is_vote_account, + }; + accumulator.add_reward(commission_pubkey, left_reward.clone(), 50); + accumulator.add_reward(commission_pubkey, right_reward.clone(), 50); + + check_accumulator( + &accumulator, + &commission_pubkey, + &left_reward, + &right_reward, + ); + } + + #[test_matrix( + [false, true], + [false, true], + [false, true] + )] + fn test_reward_accumulator_accumulate_into_larger( + left_is_vote_account: bool, + right_is_vote_account: bool, + right_is_larger: bool, + ) { + let mut accumulator1 = RewardsAccumulator::default(); + let mut accumulator2 = RewardsAccumulator::default(); + let commission_pubkey = Pubkey::new_unique(); + let commission_bps = Some(1_000); + + let left_reward = RewardCommission { + commission_bps, + commission_lamports: 1, + burned_lamports: 10, + is_vote_account: left_is_vote_account, + }; + + let right_reward = RewardCommission { + commission_bps, + commission_lamports: 100, + burned_lamports: 1_000, + is_vote_account: right_is_vote_account, + }; + + accumulator1.add_reward(commission_pubkey, left_reward.clone(), 50); + accumulator2.add_reward(commission_pubkey, right_reward.clone(), 50); + + let additional_commission_pubkey = Pubkey::new_unique(); + let additional_reward_commission = RewardCommission { + commission_bps, + commission_lamports: 1, + burned_lamports: 2, + is_vote_account: true, + }; + if right_is_larger { + accumulator2.add_reward( + additional_commission_pubkey, + additional_reward_commission, + 50, + ); + } else { + accumulator1.add_reward( + additional_commission_pubkey, + additional_reward_commission, + 50, + ); + }; + + let accumulator = accumulator1.accumulate_into_larger(accumulator2); + check_accumulator( + &accumulator, + &commission_pubkey, + &left_reward, + &right_reward, + ); + } + + #[test] + fn test_epoch_rewards_cache_multiple_forks() { + let (mut genesis_config, _mint_keypair) = + create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + + const NUM_STAKES: usize = 1000; + + for _i in 0..NUM_STAKES { + let vote_pubkey = Pubkey::new_unique(); + let stake_pubkey = Pubkey::new_unique(); + + genesis_config.accounts.insert( + vote_pubkey, + vote_state::create_v4_account_with_authorized( + &vote_pubkey, + &vote_pubkey, + [0u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE], + &vote_pubkey, + 0, + &vote_pubkey, + 0, + &vote_pubkey, + 100_000_000_000, + ) + .into(), + ); + + let stake_lamports = 1_000_000_000_000; + let stake_account = stake_utils::create_stake_account( + &stake_pubkey, + &vote_pubkey, + &vote_state::create_v4_account_with_authorized( &vote_pubkey, &vote_pubkey, [0u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE], @@ -2597,8 +3276,10 @@ mod tests { reward_commissions.insert( pubkey, RewardCommission { - commission_bps: 0, + commission_bps: Some(0), commission_lamports: 1, // enough to overflow + burned_lamports: 0, + is_vote_account: true, }, ); let result = bank.load_and_reward_commission_accounts(&reward_commissions, &thread_pool); @@ -2612,7 +3293,7 @@ mod tests { let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); let pubkey = solana_pubkey::new_rand(); - let pre_burn_balance = 10 * crate::bank::VAT_TO_BURN_PER_EPOCH; + let pre_burn_balance = 10 * crate::bank::DEFAULT_VAT_TO_BURN_PER_EPOCH; let commission_lamports = 12_345; // Commission is planned against the pre-burn account state. @@ -2623,14 +3304,16 @@ mod tests { reward_commissions.insert( pubkey, RewardCommission { - commission_bps: 500, + commission_bps: Some(500), commission_lamports, + burned_lamports: 0, + is_vote_account: true, }, ); // Simulate the VAT burn that would run in `update_epoch_stakes` // between reward calculation and distribution. - let post_burn_balance = pre_burn_balance - crate::bank::VAT_TO_BURN_PER_EPOCH; + let post_burn_balance = pre_burn_balance - crate::bank::DEFAULT_VAT_TO_BURN_PER_EPOCH; let mut burned_account = commission_account.clone(); burned_account.set_lamports(post_burn_balance); bank.store_account_and_update_capitalization(&pubkey, &burned_account); @@ -2653,7 +3336,7 @@ mod tests { commission_bps: Some(500), } ); - assert_eq!(result.total_reward_commission_lamports, commission_lamports); + assert_eq!(result.amounts.distributed_lamports, commission_lamports); } #[test] @@ -2671,8 +3354,10 @@ mod tests { reward_commissions.insert( pubkey, RewardCommission { - commission_bps, + commission_bps: Some(commission_bps), commission_lamports, + burned_lamports: 0, + is_vote_account: true, }, ); let result = @@ -2693,4 +3378,514 @@ mod tests { } } } + + #[test] + fn test_inflation_rewards_collector() { + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_leader( + 1_000_000 * LAMPORTS_PER_SOL, + &Pubkey::new_unique(), + 42 * LAMPORTS_PER_SOL, + ); + + genesis_config.rent = Rent::default(); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + + let (bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + let vote_address = Pubkey::new_unique(); + + // Vote account just created + let mut bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 0, + vote_operations: vec![( + vote_address, + VoteOperations { + create_with_balance: Some(LAMPORTS_PER_SOL), + new_commission: Some(1), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + ..VoteOperations::default() + }, + )], + }, + ); + + for (epoch, (collector_address, maybe_account, expect_reward)) in [ + // system account with lamports, success + ( + Pubkey::new_unique(), + Some(AccountSharedData::new( + LAMPORTS_PER_SOL, + 0, + &solana_sdk_ids::system_program::id(), + )), + true, + ), + // vote account, success + (vote_address, None, true), + // incinerator, success + (incinerator::id(), None, true), + // non-rent-exempt system account with 1 lamport, success with relaxed checks + ( + Pubkey::new_unique(), + Some(AccountSharedData::new( + 1, + 0, + &solana_sdk_ids::system_program::id(), + )), + true, + ), + // invalid owner, no commission + ( + Pubkey::new_unique(), + Some(AccountSharedData::new( + LAMPORTS_PER_SOL, + 0, + &Pubkey::new_unique(), + )), + false, + ), + // reserved account, no commission + (solana_sdk_ids::native_loader::id(), None, false), + // non-rent-exempt system account, no commission + (Pubkey::new_unique(), None, false), + ] + .into_iter() + .enumerate() + { + if let Some(account) = maybe_account { + bank.store_account(&collector_address, &account); + } + bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: epoch as u64 + 1, + vote_operations: vec![( + vote_address, + VoteOperations { + earned_credits: Some(1), + new_inflation_rewards_collector: Some(collector_address), + expect_reward, + ..VoteOperations::default() + }, + )], + }, + ); + } + } + + #[test] + fn test_inflation_rewards_collector_becomes_rent_exempt() { + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_leader( + 1_000_000 * LAMPORTS_PER_SOL, + &Pubkey::new_unique(), + 42 * LAMPORTS_PER_SOL, + ); + + genesis_config.rent = Rent::default(); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + + let (bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + let vote_address = Pubkey::new_unique(); + + // Vote account just created + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 0, + vote_operations: vec![( + vote_address, + VoteOperations { + create_with_balance: Some(LAMPORTS_PER_SOL), + new_commission: Some(100), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + new_inflation_rewards_collector: Some(Pubkey::new_unique()), + ..VoteOperations::default() + }, + )], + }, + ); + + // next epoch, get reward into new account + let epoch = bank.epoch(); + apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch, + vote_operations: vec![( + vote_address, + VoteOperations { + earned_credits: Some(1), + expect_reward: true, + ..VoteOperations::default() + }, + )], + }, + ); + } + + #[test] + fn test_repeated_inflation_rewards_collector() { + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_leader( + 1_000_000 * LAMPORTS_PER_SOL, + &Pubkey::new_unique(), + 42 * LAMPORTS_PER_SOL, + ); + + genesis_config.rent = Rent::default(); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + + let (bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + + let collector_address = Pubkey::new_unique(); + let vote1_address = Pubkey::new_unique(); + let vote2_address = Pubkey::new_unique(); + // Vote account just created + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 0, + vote_operations: vec![ + ( + vote1_address, + VoteOperations { + create_with_balance: Some(LAMPORTS_PER_SOL), + new_commission: Some(50), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + new_inflation_rewards_collector: Some(collector_address), + ..VoteOperations::default() + }, + ), + ( + vote2_address, + VoteOperations { + create_with_balance: Some(LAMPORTS_PER_SOL), + new_commission: Some(100), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + new_inflation_rewards_collector: Some(collector_address), + ..VoteOperations::default() + }, + ), + ], + }, + ); + + // next epoch, get double reward into collector + let epoch = bank.epoch(); + apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch, + vote_operations: vec![ + ( + vote1_address, + VoteOperations { + earned_credits: Some(1), + expect_reward: true, + ..VoteOperations::default() + }, + ), + ( + vote2_address, + VoteOperations { + earned_credits: Some(1), + expect_reward: true, + ..VoteOperations::default() + }, + ), + ], + }, + ); + } + + #[test] + fn test_invalid_inflation_rewards_collector_burns_sysvar_rewards() { + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_leader( + 1_000_000 * LAMPORTS_PER_SOL, + &Pubkey::new_unique(), + 42 * LAMPORTS_PER_SOL, + ); + + genesis_config.rent = Rent::default(); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + + let (bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + let vote_address = Pubkey::new_unique(); + + // Create a vote account with delegated stake and 100% commission. + // Using 100% commission makes the expected accounting simple: every lamport + // of the epoch reward should go through the commission collector path. + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 0, + vote_operations: vec![( + vote_address, + VoteOperations { + create_with_balance: Some(LAMPORTS_PER_SOL), + new_commission: Some(100), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + ..VoteOperations::default() + }, + )], + }, + ); + + // Simulate a collector account that is invalid at reward distribution time. + // Started as system account but now it's owned by a random program + let invalid_collector = Pubkey::new_unique(); + bank.store_account( + &invalid_collector, + &AccountSharedData::new(LAMPORTS_PER_SOL, 0, &Pubkey::new_unique()), + ); + + // Point the vote account at the invalid collector and advance to the reward + // boundary. Account is not credited, so the commission lamports are burned. + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 1, + vote_operations: vec![( + vote_address, + VoteOperations { + earned_credits: Some(1000), + new_inflation_rewards_collector: Some(invalid_collector), + ..VoteOperations::default() + }, + )], + }, + ); + + let epoch_rewards = bank.get_epoch_rewards_sysvar(); + + // The burned commission lamports must be reflected in the epoch rewards + // sysvar. Since this test uses 100% commission, all calculated rewards + // went through the invalid collector path. + assert!(epoch_rewards.total_rewards > 0); + assert_eq!( + epoch_rewards.total_rewards, + epoch_rewards.distributed_rewards + ); + } + + #[test] + fn test_incinerator_not_included_load_and_reward_commission_accounts() { + let (genesis_config, _mint_keypair) = create_genesis_config(1_000 * LAMPORTS_PER_SOL); + let bank = Bank::new_for_tests(&genesis_config); + let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); + + let pubkey = incinerator::id(); + let mut incinerator_account = AccountSharedData::default(); + incinerator_account.set_lamports(1); + bank.store_account_and_update_capitalization(&pubkey, &incinerator_account); + let mut reward_commissions = RewardCommissions::default(); + + let commission_lamports = 100; + reward_commissions.insert( + pubkey, + RewardCommission { + commission_bps: None, + commission_lamports, + burned_lamports: 0, + is_vote_account: false, + }, + ); + let result = bank.load_and_reward_commission_accounts(&reward_commissions, &thread_pool); + assert_eq!( + result.amounts.distributed_to_incinerator_lamports, + commission_lamports + ); + assert_eq!(result.amounts.distributed_lamports, 0); + assert_eq!(result.amounts.burned_lamports, 0); + } + + #[test] + fn test_inflation_collector_becomes_vote_account_burns_rewards() { + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_leader( + 1_000_000 * LAMPORTS_PER_SOL, + &Pubkey::new_unique(), + 42 * LAMPORTS_PER_SOL, + ); + + genesis_config.rent = Rent::default(); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + + let (bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + let vote_address = Pubkey::new_unique(); + let collector_into_vote_address = Pubkey::new_unique(); + + // Create a normal vote account with a currently valid inflation collector + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 0, + vote_operations: vec![( + vote_address, + VoteOperations { + create_with_balance: Some(LAMPORTS_PER_SOL), + new_commission: Some(100), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + new_inflation_rewards_collector: Some(collector_into_vote_address), + ..VoteOperations::default() + }, + )], + }, + ); + + // New vote account gets nothing + let rewards = bank.get_balance(&collector_into_vote_address); + assert_eq!(rewards, 0); + + // Next epoch gets something + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 1, + vote_operations: vec![( + vote_address, + VoteOperations { + earned_credits: Some(1000), + expect_reward: true, + ..VoteOperations::default() + }, + )], + }, + ); + let pre_balance = bank.get_balance(&collector_into_vote_address); + assert_ne!(pre_balance, 0); + // Fund the converted vote account enough to pass VAT filtering. + let pre_balance = + pre_balance.max(bank.get_minimum_balance_for_rent_exemption(VoteStateV4::size_of())); + + // Transform the collector into a vote account, see that all rewards + // are burned for this epoch + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 2, + vote_operations: vec![ + ( + vote_address, + VoteOperations { + earned_credits: Some(1000), + expect_reward: true, + ..VoteOperations::default() + }, + ), + ( + collector_into_vote_address, + VoteOperations { + create_with_balance: Some(pre_balance), + new_commission: Some(100), + earned_credits: Some(1000), + delegate_stake_amount: Some(LAMPORTS_PER_SOL), + ..VoteOperations::default() + }, + ), + ], + }, + ); + + let vote_reward = bank + .rewards + .read() + .unwrap() + .iter() + .find(|(address, _reward)| *address == collector_into_vote_address) + .map(|(_address, reward)| *reward) + .unwrap(); + assert_eq!(vote_reward.lamports, 0); + + let unchanged_balance = bank.get_balance(&collector_into_vote_address); + assert_eq!(unchanged_balance, pre_balance); + + // `collector_into_vote_address` receives its rewards, but `vote_address` + // has its rewards burned + let bank = apply_epoch_operations( + bank, + bank_forks.as_ref(), + EpochOperations { + epoch: 3, + vote_operations: vec![ + ( + vote_address, + VoteOperations { + earned_credits: Some(1000), + expect_reward: true, + ..VoteOperations::default() + }, + ), + ( + collector_into_vote_address, + VoteOperations { + earned_credits: Some(1000), + expect_reward: true, + ..VoteOperations::default() + }, + ), + ], + }, + ); + + // Some rewards were distributed + let post_balance = bank.get_balance(&collector_into_vote_address); + assert!(post_balance > pre_balance); + + // They're reflected in the reported rewards + let vote_reward = bank + .rewards + .read() + .unwrap() + .iter() + .find(|(address, _reward)| *address == collector_into_vote_address) + .map(|(_address, reward)| *reward) + .unwrap(); + assert_eq!(vote_reward.lamports as u64, post_balance - pre_balance); + + // Some lamports were burned + let reward_commissions = recalculate_reward_commissions_for_tests(&bank); + let reward_commission = reward_commissions + .get(&collector_into_vote_address) + .unwrap(); + assert_ne!(reward_commission.burned_lamports, 0); + + // The burned lamports are included in the epoch rewards sysvar + let epoch_rewards = bank.get_epoch_rewards_sysvar(); + assert_eq!( + reward_commission.burned_lamports + reward_commission.commission_lamports, + epoch_rewards.distributed_rewards + ); + } } diff --git a/runtime/src/bank/partitioned_epoch_rewards/distribution.rs b/runtime/src/bank/partitioned_epoch_rewards/distribution.rs index 2d1f782d7eb..bca6d58c752 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/distribution.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/distribution.rs @@ -9,6 +9,7 @@ use { partitioned_epoch_rewards::EpochRewardPhase, }, stake_account::StakeAccount, + stake_delegation::delegation_effective_stake, }, log::error, serde::{Deserialize, Serialize}, @@ -40,11 +41,40 @@ enum DistributionError { } struct DistributionResults { - lamports_distributed: u64, - lamports_burned: u64, + stake_reward_lamports_minted: u64, + stake_reward_lamports_burned: u64, updated_stake_rewards: StakeRewards, } +/// Adjusts stake delegation based on Rent sysvar parameters. +/// +/// As part of SIMD-0392, if Rent is ever increased, we need to make sure that +/// lamports are not double-counted for the rent-exempt minimum and the stake +/// delegation. This function adjusts the delegation in a Stake if needed, right +/// at distribution time. +fn adjust_delegation_for_rent( + delegation: &mut Delegation, + rewarded_epoch: Epoch, + new_delegation_with_rewards: u64, + lamports_with_rewards: u64, + minimum_lamports: u64, +) { + let new_delegation = std::cmp::min( + new_delegation_with_rewards, + lamports_with_rewards.saturating_sub(minimum_lamports), + ); + + if new_delegation != delegation.stake { + delegation.stake = new_delegation; + // Deactivate stake if needed. This deactivation is immediate, + // unlike a requested deactivation which happens at the next epoch + // boundary + if new_delegation == 0 { + delegation.deactivation_epoch = rewarded_epoch; + } + } +} + impl Bank { /// Process reward distribution for the block if it is inside reward interval. pub(in crate::bank) fn distribute_partitioned_epoch_rewards(&mut self) { @@ -150,18 +180,22 @@ impl Bank { let pre_capitalization = self.capitalization(); let ( DistributionResults { - lamports_distributed, - lamports_burned, + stake_reward_lamports_minted, + stake_reward_lamports_burned, updated_stake_rewards, }, store_stake_accounts_us, ) = measure_us!(self.store_stake_accounts_in_partition(partition_rewards, partition_index)); // increase total capitalization by the distributed rewards - self.capitalization.fetch_add(lamports_distributed, Relaxed); + self.capitalization + .fetch_add(stake_reward_lamports_minted, Relaxed); // decrease distributed capital from epoch rewards sysvar - self.update_epoch_rewards_sysvar(lamports_distributed + lamports_burned); + self.update_epoch_rewards_sysvar( + stake_reward_lamports_minted + stake_reward_lamports_burned, + 0, // debit_block_rewards + ); // update reward history for this partitioned distribution self.update_reward_history_in_partition(&updated_stake_rewards); @@ -174,8 +208,8 @@ impl Bank { partition_index, store_stake_accounts_us, store_stake_accounts_count: updated_stake_rewards.len(), - distributed_rewards: lamports_distributed, - burned_rewards: lamports_burned, + distributed_rewards: stake_reward_lamports_minted, + burned_rewards: stake_reward_lamports_burned, }; report_partitioned_reward_metrics(self, metrics); @@ -202,6 +236,7 @@ impl Bank { partitioned_stake_reward: &PartitionedStakeReward, rent: &Rent, adjust_delegations_for_rent: bool, + use_fixed_point_stake_math: bool, ) -> Result { let stake_account = stakes_cache_accounts .get(&partitioned_stake_reward.stake_pubkey) @@ -217,41 +252,47 @@ impl Bank { ) }; account - .checked_add_lamports(partitioned_stake_reward.stake_reward) + .checked_add_lamports(partitioned_stake_reward.inflation.stake_reward) .map_err(|_| DistributionError::ArithmeticOverflow)?; + + let mut new_stake = partitioned_stake_reward.inflation.stake; if adjust_delegations_for_rent { let minimum_balance = rent.minimum_balance(account.data().len()); - assert!( - partitioned_stake_reward.stake.delegation.stake - <= account.lamports().saturating_sub(minimum_balance), - "stake reward delegation must be consistent with the updated stake account \ - lamport balance" + // The rewarded epoch is right before the distribution epoch + let rewarded_epoch = distribution_epoch.saturating_sub(1); + // The entry in `partitioned_stake_reward` contains the rewards, + // calculated during the calculation phase + let delegation_with_rewards = new_stake.delegation.stake; + adjust_delegation_for_rent( + &mut new_stake.delegation, + rewarded_epoch, + delegation_with_rewards, + account.lamports(), + minimum_balance, ); } else { let expected_delegation = stake .delegation .stake - .saturating_add(partitioned_stake_reward.stake_reward); + .saturating_add(partitioned_stake_reward.inflation.stake_reward); assert_eq!( - expected_delegation, partitioned_stake_reward.stake.delegation.stake, + expected_delegation, new_stake.delegation.stake, "stake reward delegation must be consistent with the updated stake account \ lamport balance" ); } account - .set_state(&StakeStateV2::Stake( - meta, - partitioned_stake_reward.stake, - flags, - )) + .set_state(&StakeStateV2::Stake(meta, new_stake, flags)) .map_err(|_| DistributionError::UnableToSetState)?; - let reward_type = if partitioned_stake_reward.stake.delegation.stake( + let stake_at_distribution_epoch = delegation_effective_stake( + &new_stake.delegation, distribution_epoch, stake_history, new_warmup_cooldown_rate_epoch, - ) == 0 - { + use_fixed_point_stake_math, + ); + let reward_type = if stake_at_distribution_epoch == 0 { RewardType::DeactivatedStake } else { RewardType::Staking @@ -260,9 +301,9 @@ impl Bank { stake_pubkey: partitioned_stake_reward.stake_pubkey, stake_reward_info: StakeRewardInfo { reward_type, - lamports: i64::try_from(partitioned_stake_reward.stake_reward).unwrap(), + lamports: i64::try_from(partitioned_stake_reward.inflation.stake_reward).unwrap(), post_balance: account.lamports(), - commission_bps: Some(partitioned_stake_reward.commission_bps), + commission_bps: partitioned_stake_reward.inflation.commission_bps, }, stake_account: account, }) @@ -286,9 +327,10 @@ impl Bank { // Name intentionally doesn't match -- "adjust delegations for rent" is // part of relaxing post-exec min balance checks. let adjust_delegations_for_rent = feature_snapshot.relax_post_exec_min_balance_check; + let use_fixed_point_stake_math = feature_snapshot.upgrade_bpf_stake_program_to_v5_1; - let mut lamports_distributed = 0; - let mut lamports_burned = 0; + let mut stake_reward_lamports_minted = 0; + let mut stake_reward_lamports_burned = 0; let indices = partition_rewards .partition_indices .get(partition_index as usize) @@ -319,7 +361,8 @@ impl Bank { panic!("partition reward {index} is empty"); }); let stake_pubkey = partitioned_stake_reward.stake_pubkey; - let reward_amount = partitioned_stake_reward.stake_reward; + let stake_reward_amount = partitioned_stake_reward.inflation.stake_reward; + match Self::build_updated_stake_reward( self.epoch, stake_history, @@ -328,25 +371,26 @@ impl Bank { partitioned_stake_reward, rent, adjust_delegations_for_rent, + use_fixed_point_stake_math, ) { Ok(stake_reward) => { - lamports_distributed += reward_amount; + stake_reward_lamports_minted += stake_reward_amount; updated_stake_rewards.push(stake_reward); } Err(err) => { error!( "bank::distribution::store_stake_accounts_in_partition() failed for \ - {stake_pubkey}, {reward_amount} lamports burned: {err:?}" + {stake_pubkey}, {stake_reward_amount} lamports burned: {err:?}" ); - lamports_burned += reward_amount; + stake_reward_lamports_burned += stake_reward_amount; } } } drop(stakes_cache); self.store_accounts((self.slot(), &updated_stake_rewards[..])); DistributionResults { - lamports_distributed, - lamports_burned, + stake_reward_lamports_minted, + stake_reward_lamports_burned, updated_stake_rewards, } } @@ -357,14 +401,18 @@ mod tests { use { super::*, crate::{ + alpenglow_epoch_type::AlpenglowEpochType, bank::{ partitioned_epoch_rewards::{ - PartitionedStakeRewards, REWARD_CALCULATION_NUM_BLOCKS, + InflationReward, PartitionedStakeRewards, REWARD_CALCULATION_NUM_BLOCKS, epoch_rewards_hasher::hash_rewards_into_partitions, tests::convert_rewards, }, tests::create_genesis_config, }, - inflation_rewards::points::PointValue, + inflation_rewards::{ + points::{CalculationEnvironment, DelegatedVoteState, PointValue, null_tracer}, + redeem_rewards, + }, reward_info::RewardInfo, stake_utils, }, @@ -382,7 +430,7 @@ mod tests { }, solana_sysvar as sysvar, solana_vote_interface::state::BLS_PUBLIC_KEY_COMPRESSED_SIZE, - solana_vote_program::vote_state, + solana_vote_program::vote_state::{self, VoteStateV4, handler::VoteStateHandler}, std::sync::Arc, test_case::test_case, }; @@ -500,17 +548,19 @@ mod tests { let bank = Bank::new_for_tests(&genesis_config); // Set up epoch_rewards sysvar with rewards with 1e9 lamports to distribute. - let total_rewards = 1_000_000_000; + let inflation_rewards = 1_000_000_000; + let block_rewards = 0; let num_partitions = 2; // num_partitions is arbitrary and unimportant for this test - let total_points = (total_rewards * 42) as u128; // total_points is arbitrary for the purposes of this test + let total_points = (inflation_rewards * 42) as u128; // total_points is arbitrary for the purposes of this test bank.create_epoch_rewards_sysvar( 0, 42, num_partitions, &PointValue { - rewards: total_rewards, + rewards: inflation_rewards, points: total_points, }, + block_rewards, ); let pre_epoch_rewards_account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); let expected_balance = @@ -547,7 +597,7 @@ mod tests { let epoch_rewards: sysvar::epoch_rewards::EpochRewards = from_account(&post_epoch_rewards_account).unwrap(); - assert_eq!(epoch_rewards.total_rewards, total_rewards); + assert_eq!(epoch_rewards.total_rewards, inflation_rewards); assert_eq!(epoch_rewards.distributed_rewards, rewards_to_distribute,); // Assert that the bank total capital changed by the amount of rewards @@ -599,14 +649,14 @@ mod tests { for i in 0..num_partitions { let DistributionResults { - lamports_distributed, + stake_reward_lamports_minted, updated_stake_rewards, .. } = bank.store_stake_accounts_in_partition(&partitioned_rewards, i as u64); let num_history_updates = bank.update_reward_history_in_partition(&updated_stake_rewards); assert_eq!(updated_stake_rewards.len(), num_history_updates); - total_rewards += lamports_distributed; + total_rewards += stake_reward_lamports_minted; total_num_updates += num_history_updates; } @@ -709,9 +759,11 @@ mod tests { let nonexistent_account = Pubkey::new_unique(); let partitioned_stake_reward = PartitionedStakeReward { stake_pubkey: nonexistent_account, - stake: new_stake, - stake_reward, - commission_bps, + inflation: InflationReward { + stake: new_stake, + stake_reward, + commission_bps: Some(commission_bps), + }, }; let stakes_cache = bank.stakes_cache.stakes(); let stakes_cache_accounts = stakes_cache.stake_delegations(); @@ -724,6 +776,7 @@ mod tests { &partitioned_stake_reward, &rent, adjust_delegations_for_rent, + true, ) .unwrap_err(), DistributionError::AccountNotFound @@ -746,9 +799,11 @@ mod tests { bank.store_account(&overflowing_account, &stake_account); let partitioned_stake_reward = PartitionedStakeReward { stake_pubkey: overflowing_account, - stake: new_stake, - stake_reward, - commission_bps, + inflation: InflationReward { + stake: new_stake, + stake_reward, + commission_bps: Some(commission_bps), + }, }; let stakes_cache = bank.stakes_cache.stakes(); let stakes_cache_accounts = stakes_cache.stake_delegations(); @@ -761,6 +816,7 @@ mod tests { &partitioned_stake_reward, &rent, adjust_delegations_for_rent, + true, ) .unwrap_err(), DistributionError::ArithmeticOverflow @@ -793,9 +849,11 @@ mod tests { bank.store_account(&successful_account, &stake_account); let partitioned_stake_reward = PartitionedStakeReward { stake_pubkey: successful_account, - stake: new_stake, - stake_reward, - commission_bps, + inflation: InflationReward { + stake: new_stake, + stake_reward, + commission_bps: Some(commission_bps), + }, }; let stakes_cache = bank.stakes_cache.stakes(); let stakes_cache_accounts = stakes_cache.stake_delegations(); @@ -832,6 +890,7 @@ mod tests { &partitioned_stake_reward, &rent, adjust_delegations_for_rent, + true, ) .unwrap(), expected_stake_reward @@ -874,9 +933,11 @@ mod tests { bank.store_account(&deactivating_account, &stake_account); let partitioned_stake_reward = PartitionedStakeReward { stake_pubkey: deactivating_account, - stake: deactivating_stake, - stake_reward, - commission_bps, + inflation: InflationReward { + stake: deactivating_stake, + stake_reward, + commission_bps: Some(commission_bps), + }, }; let stakes_cache = bank.stakes_cache.stakes(); let stakes_cache_accounts = stakes_cache.stake_delegations(); @@ -913,6 +974,7 @@ mod tests { &partitioned_stake_reward, &rent, adjust_delegations_for_rent, + true, ) .unwrap(), expected_stake_reward @@ -947,7 +1009,7 @@ mod tests { let expected_total = converted_rewards .enumerated_rewards_iter() - .map(|(_, stake_reward)| stake_reward.stake_reward) + .map(|(_, reward)| reward.inflation.stake_reward) .sum::(); let partitioned_rewards = StartBlockHeightAndPartitionedRewards { @@ -957,10 +1019,10 @@ mod tests { }; let DistributionResults { - lamports_distributed, + stake_reward_lamports_minted, .. } = bank.store_stake_accounts_in_partition(&partitioned_rewards, 0); - assert_eq!(expected_total, lamports_distributed); + assert_eq!(expected_total, stake_reward_lamports_minted); } #[test] @@ -977,10 +1039,10 @@ mod tests { let expected_total = 0; let DistributionResults { - lamports_distributed, + stake_reward_lamports_minted, .. } = bank.store_stake_accounts_in_partition(&partitioned_rewards, 0); - assert_eq!(expected_total, lamports_distributed); + assert_eq!(expected_total, stake_reward_lamports_minted); } #[test] @@ -992,6 +1054,7 @@ mod tests { // Set up epoch_rewards sysvar with rewards with 10e9 lamports to distribute. let total_rewards = 10 * LAMPORTS_PER_SOL; + let block_rewards = 0; let num_partitions = 2; // num_partitions is arbitrary and unimportant for this test let total_points = (total_rewards * 42) as u128; // total_points is arbitrary for the purposes of this test bank.create_epoch_rewards_sysvar( @@ -1002,6 +1065,7 @@ mod tests { rewards: total_rewards, points: total_points, }, + block_rewards, ); let pre_epoch_rewards_account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); let expected_balance = @@ -1077,4 +1141,366 @@ mod tests { // distributed assert_eq!(pre_cap + rewards_to_distribute, post_cap); } + + #[test] + fn test_delegation_adjustment_at_distribution() { + let (mut genesis_config, _mint_keypair) = + create_genesis_config(1_000_000 * LAMPORTS_PER_SOL); + genesis_config.epoch_schedule = EpochSchedule::custom(432000, 432000, false); + let bank = Bank::new_for_tests(&genesis_config); + + // Set up epoch_rewards sysvar with rewards with 10e9 lamports to distribute. + let total_rewards = 10 * LAMPORTS_PER_SOL; + let block_rewards = 0; + let num_partitions = 2; // num_partitions is arbitrary and unimportant for this test + let total_points = (total_rewards * 42) as u128; // total_points is arbitrary for the purposes of this test + bank.create_epoch_rewards_sysvar( + 0, + 42, + num_partitions, + &PointValue { + rewards: total_rewards, + points: total_points, + }, + block_rewards, + ); + let pre_epoch_rewards_account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); + let expected_balance = + bank.get_minimum_balance_for_rent_exemption(pre_epoch_rewards_account.data().len()); + // Expected balance is the sysvar rent-exempt balance + assert_eq!(pre_epoch_rewards_account.lamports(), expected_balance); + + // Use lower lamports per byte for creating, bank has higher amount + let mut lower_rent = bank.rent_collector.rent.clone(); + lower_rent.lamports_per_byte /= 10; + + // Below new minimum, small reward, should normally be destaked + let reward_lamports = 1; + let stake_reward = StakeReward::new_with_pre_stake_account(reward_lamports, 1, &lower_rent); + bank.store_account(&stake_reward.1.stake_pubkey, &stake_reward.0); + + let stake_pubkey = stake_reward.1.stake_pubkey; + let mut stake_account = stake_reward.0; + + let expected_num = 1; + let rewards_to_distribute = stake_reward.1.stake_reward_info.lamports as u64; + let all_rewards = convert_rewards(vec![stake_reward.1]); + + let partitioned_rewards = StartBlockHeightAndPartitionedRewards { + distribution_starting_block_height: bank.block_height() + REWARD_CALCULATION_NUM_BLOCKS, + all_stake_rewards: Arc::new(all_rewards), + partition_indices: vec![(0..expected_num).collect::>()], + }; + + // But we transfer in more lamports before distribution time + stake_account.checked_add_lamports(1_000_000_000).unwrap(); + bank.store_account(&stake_pubkey, &stake_account); + + // Distribute rewards + let pre_cap = bank.capitalization(); + bank.distribute_epoch_rewards_in_partition(&partitioned_rewards, 0); + let post_cap = bank.capitalization(); + let post_epoch_rewards_account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); + + // Assert that epoch rewards sysvar lamports balance does not change + assert_eq!(post_epoch_rewards_account.lamports(), expected_balance); + + let epoch_rewards: sysvar::epoch_rewards::EpochRewards = + from_account(&post_epoch_rewards_account).unwrap(); + assert_eq!(epoch_rewards.total_rewards, total_rewards); + assert_eq!(epoch_rewards.distributed_rewards, rewards_to_distribute,); + + // Assert that the bank total capital changed by the amount of rewards + // distributed + assert_eq!(pre_cap + rewards_to_distribute, post_cap); + + // Check that delegation just gets rewards + let post_account = bank.get_account(&stake_pubkey).unwrap(); + let post_stake_state: StakeStateV2 = post_account.state().unwrap(); + let pre_stake_state: StakeStateV2 = stake_account.state().unwrap(); + assert_eq!( + post_stake_state.delegation().unwrap().stake, + pre_stake_state.delegation().unwrap().stake + reward_lamports as u64 + ); + } + + fn check_rent_adjusted_stake_delegation( + rewarded_epoch: u64, + pre_stake: Stake, + pre_lamports: u64, + new_minimum_balance: u64, + total_rewards: u64, + reward_info: Option<(u64, Stake)>, + ) { + let mut vote_state = VoteStateHandler::new_v4(VoteStateV4::default()); + // put 1 credit to create rewards + vote_state.increment_credits(rewarded_epoch, 1); + let stake_history: &StakeHistory = &StakeHistory::default(); + let new_rate_activation_epoch = None; + let commission_rate_in_basis_points = true; + let adjust_delegations_for_rent = true; + + let maybe_rewards = redeem_rewards( + pre_stake, + vote_state.as_ref_v4().inflation_rewards_commission_bps, + DelegatedVoteState::from(vote_state.as_ref_v4()), + CalculationEnvironment { + rewarded_epoch, + point_value: &PointValue { + rewards: total_rewards, + points: 1, + }, + stake_history, + new_rate_activation_epoch, + commission_rate_in_basis_points, + adjust_delegations_for_rent, + use_fixed_point_stake_math: true, + }, + null_tracer(), + &AlpenglowEpochType::Tower, + pre_lamports, + new_minimum_balance, + ); + + // fake the distribution portion which adjusts the delegation + let maybe_rewards = maybe_rewards + .map(|x| { + let stake_rewards = x.0; + let mut stake = x.2; + let new_delegation_with_rewards = stake.delegation.stake; + adjust_delegation_for_rent( + &mut stake.delegation, + rewarded_epoch, + new_delegation_with_rewards, + pre_lamports + stake_rewards, + new_minimum_balance, + ); + (stake_rewards, stake) + }) + .ok(); + assert_eq!(maybe_rewards, reward_info); + } + + #[test] + fn rent_adjusted_stake_delegation_calculations() { + let old_minimum_balance = 8; + let new_minimum_balance = 9; + let rewarded_epoch = 1; + + // No rewards at all -> updated (all stakes get driven forward if + // inflation is disabled) + check_rent_adjusted_stake_delegation( + rewarded_epoch, + Stake { + delegation: Delegation { + stake: 1, + ..Default::default() + }, + credits_observed: 1, + }, + new_minimum_balance + 1, + new_minimum_balance, + 0, + Some(( + 0, + Stake { + delegation: Delegation { + stake: 1, + ..Default::default() + }, + credits_observed: 1, + }, + )), + ); + + // Stake receives no rewards or delegation adjustment -> no update + check_rent_adjusted_stake_delegation( + rewarded_epoch, + Stake { + delegation: Delegation { + stake: 1, + ..Default::default() + }, + credits_observed: 1, + }, + new_minimum_balance + 1, + new_minimum_balance, + 1, + None, + ); + + // Already destaked -> no update + check_rent_adjusted_stake_delegation( + rewarded_epoch, + Stake { + delegation: Delegation { + stake: 0, + deactivation_epoch: 0, + ..Default::default() + }, + credits_observed: 0, + }, + old_minimum_balance - 1, + new_minimum_balance, + 1, + None, + ); + + // Staked, already below minimum, go further below minimum + // -> destaked, still one lamport of rewards though + check_rent_adjusted_stake_delegation( + rewarded_epoch, + Stake { + delegation: Delegation { + stake: 1, + ..Default::default() + }, + credits_observed: 0, + }, + old_minimum_balance - 1, + new_minimum_balance, + 1, + Some(( + 1, + Stake { + delegation: Delegation { + stake: 0, + deactivation_epoch: rewarded_epoch, + ..Default::default() + }, + credits_observed: 1, + }, + )), + ); + + // Delegation hits exactly 0 -> destaked, no rewards + check_rent_adjusted_stake_delegation( + rewarded_epoch, + Stake { + delegation: Delegation { + stake: 1, + ..Default::default() + }, + credits_observed: 0, + }, + new_minimum_balance, + new_minimum_balance, + 0, + Some(( + 0, + Stake { + delegation: Delegation { + stake: 0, + deactivation_epoch: rewarded_epoch, + ..Default::default() + }, + credits_observed: 1, + }, + )), + ); + + // Delegation decreases to 1 -> still staked + check_rent_adjusted_stake_delegation( + rewarded_epoch, + Stake { + delegation: Delegation { + stake: 2, + ..Default::default() + }, + credits_observed: 0, + }, + new_minimum_balance + 1, + new_minimum_balance, + 0, + Some(( + 0, + Stake { + delegation: Delegation { + stake: 1, + ..Default::default() + }, + credits_observed: 1, + }, + )), + ); + + // Rewards partially cover minimum balance change + // -> decrease stake + // This case is confusing because it pays out 2 lamports in rewards, + // so we adjust minimum up so that even with 2 lamports in rewards, the + // delegation goes down. + check_rent_adjusted_stake_delegation( + rewarded_epoch, + Stake { + delegation: Delegation { + stake: 2, + ..Default::default() + }, + credits_observed: 0, + }, + new_minimum_balance, + new_minimum_balance + 1, + 1, + Some(( + 2, + Stake { + delegation: Delegation { + stake: 1, + ..Default::default() + }, + credits_observed: 1, + }, + )), + ); + + // Rewards cover minimum balance change -> no change in stake + check_rent_adjusted_stake_delegation( + rewarded_epoch, + Stake { + delegation: Delegation { + stake: 1, + ..Default::default() + }, + credits_observed: 0, + }, + new_minimum_balance, + new_minimum_balance, + 1, + Some(( + 1, + Stake { + delegation: Delegation { + stake: 1, + ..Default::default() + }, + credits_observed: 1, + }, + )), + ); + + // Well above new minimum balance -> delegation change capped to rewards + check_rent_adjusted_stake_delegation( + rewarded_epoch, + Stake { + delegation: Delegation { + stake: 1, + ..Default::default() + }, + credits_observed: 0, + }, + new_minimum_balance + 2, + new_minimum_balance, + 1, + Some(( + 1, + Stake { + delegation: Delegation { + stake: 2, + ..Default::default() + }, + credits_observed: 1, + }, + )), + ); + } } diff --git a/runtime/src/bank/partitioned_epoch_rewards/mod.rs b/runtime/src/bank/partitioned_epoch_rewards/mod.rs index 880b7026cc0..6f3874c92cb 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/mod.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/mod.rs @@ -25,17 +25,28 @@ use { /// Distributing rewards to stake accounts begins AFTER this many blocks. const REWARD_CALCULATION_NUM_BLOCKS: u64 = 1; +/// Total reward for a stake account, currently just inflation rewards. #[derive(Debug, Clone, PartialEq)] pub(crate) struct PartitionedStakeReward { /// Stake account address pub stake_pubkey: Pubkey, + /// Inflation reward information + pub inflation: InflationReward, +} + +/// Just the inflation portion of a partitioned stake reward +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct InflationReward { /// `Stake` state to be stored in account pub stake: Stake, /// Stake reward for recording in the Bank on distribution pub stake_reward: u64, /// Reward commission in basis points (0-10,000 representing 0-100%) for /// recording reward info. - pub commission_bps: u16, + // + // Note: This field becomes always `None` once SIMD-0232 is activated. + // After full activation, it can be removed on feature cleanup. + pub commission_bps: Option, } /// A vector of stake rewards. @@ -149,19 +160,44 @@ pub(crate) enum EpochRewardPhase { } #[derive(Debug)] +#[cfg_attr(test, derive(Clone))] pub(super) struct RewardCommission { - pub(super) commission_bps: u16, + // Note: This field becomes always `None` once SIMD-0232 is activated. + // After full activation, it can be removed on feature cleanup. + pub(super) commission_bps: Option, pub(super) commission_lamports: u64, + pub(super) burned_lamports: u64, + pub(super) is_vote_account: bool, } pub(super) type RewardCommissions = HashMap; +/// Helper struct to give the amounts distributed to commission accounts or +/// burned in different manners +#[derive(Debug, Default)] +pub(super) struct RewardCommissionLamportAmounts { + /// Lamports distributed across all commission collectors, except the + /// incinerator. + /// + /// Also the maximum capitalization increase by the end of the first block + /// of the epoch. + pub(super) distributed_lamports: u64, + /// lamports distributed to the incinerator. + /// + /// Tracked separately to give a better upper bound for capitalization at + /// the end of the first block of an epoch, needed for Alpenglow's + /// `EpochInflationAccountState` + pub(super) distributed_to_incinerator_lamports: u64, + /// lamports burned from undistributed commissions + pub(super) burned_lamports: u64, +} + #[derive(Debug, Default)] pub(super) struct RewardCommissionAccounts { /// accounts with rewards to be stored pub(super) accounts_with_rewards: Vec<(Pubkey, RewardInfo, AccountSharedData)>, - /// total lamports across all `accounts_with_rewards` - pub(super) total_reward_commission_lamports: u64, + /// amounts distributed to those accounts, and burned after calculation + pub(super) amounts: RewardCommissionLamportAmounts, } /// Wrapper struct to implement StorableAccounts for RewardCommissionAccounts @@ -349,11 +385,7 @@ impl Bank { /// # stake accounts to store in one block during partitioned reward interval pub(super) fn partitioned_rewards_stake_account_stores_per_block(&self) -> u64 { - self.rc - .accounts - .accounts_db - .partitioned_epoch_rewards_config - .stake_account_stores_per_block + self.partitioned_rewards_stake_account_stores_per_block } /// Calculate the number of blocks required to distribute rewards to all stake accounts. @@ -393,6 +425,7 @@ mod tests { bank_forks::BankForks, genesis_utils::{ GenesisConfigInfo, ValidatorVoteKeypairs, create_genesis_config_with_vote_accounts, + deactivate_features, }, runtime_config::RuntimeConfig, stake_utils, @@ -425,9 +458,11 @@ mod tests { { Some(Self { stake_pubkey: stake_reward.stake_pubkey, - stake, - stake_reward: stake_reward.stake_reward_info.lamports as u64, - commission_bps: stake_reward.stake_reward_info.commission_bps.unwrap(), + inflation: InflationReward { + stake, + stake_reward: stake_reward.stake_reward_info.lamports as u64, + commission_bps: stake_reward.stake_reward_info.commission_bps, + }, }) } else { None @@ -550,6 +585,9 @@ mod tests { stake_account_stores_per_block: u64, advance_num_slots: u64, ) -> (RewardBank, Arc>) { + // Disable slot time reduction features as they will override the custom + // stores per block provided in this test helper. + let features_to_deactivate = crate::slot_params::slot_time_feature_ids().to_vec(); let validator_keypairs = (0..stakes.len()) .map(|_| ValidatorVoteKeypairs::new_rand()) .collect::>(); @@ -558,6 +596,7 @@ mod tests { mut genesis_config, .. } = create_genesis_config_with_vote_accounts(1_000_000_000, &validator_keypairs, stakes); genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + deactivate_features(&mut genesis_config, &features_to_deactivate); let mut accounts_db_config: AccountsDbConfig = ACCOUNTS_DB_CONFIG_FOR_TESTING; accounts_db_config.partitioned_epoch_rewards_config = @@ -690,6 +729,14 @@ mod tests { bank.partitioned_rewards_stake_account_stores_per_block(); assert_eq!(stake_account_stores_per_block, 10); + let (bank, bank_forks) = bank.wrap_with_bank_forks_for_tests(); + let bank = + Bank::new_from_parent_with_bank_forks(&bank_forks, bank, SlotLeader::default(), 1); + assert_eq!( + bank.partitioned_rewards_stake_account_stores_per_block(), + stake_account_stores_per_block + ); + let check_num_reward_distribution_blocks = |num_stakes: u64, expected_num_reward_distribution_blocks: u64| { // Given the short epoch, i.e. 32 slots, we should cap the number of reward distribution blocks to 32/10 = 3. diff --git a/runtime/src/bank/partitioned_epoch_rewards/sysvar.rs b/runtime/src/bank/partitioned_epoch_rewards/sysvar.rs index 06ab9ec96ce..77b7477252a 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/sysvar.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/sysvar.rs @@ -2,16 +2,19 @@ use { super::Bank, crate::inflation_rewards::points::PointValue, log::info, - solana_account::{create_account_shared_data_with_fields as create_account, from_account}, - solana_sysvar as sysvar, + solana_account::{ + ReadableAccount, WritableAccount, create_account_shared_data_with_fields as create_account, + from_account, + }, + solana_clock::INITIAL_RENT_EPOCH, + solana_sysvar::{self as sysvar, epoch_rewards::EpochRewards}, }; impl Bank { /// Helper fn to log epoch_rewards sysvar fn log_epoch_rewards_sysvar(&self, prefix: &str) { if let Some(account) = self.get_account(&sysvar::epoch_rewards::id()) { - let epoch_rewards: sysvar::epoch_rewards::EpochRewards = - from_account(&account).unwrap(); + let epoch_rewards: EpochRewards = from_account(&account).unwrap(); info!("{prefix} epoch_rewards sysvar: {epoch_rewards:?}"); } else { info!("{prefix} epoch_rewards sysvar: none"); @@ -27,12 +30,13 @@ impl Bank { distribution_starting_block_height: u64, num_partitions: u64, point_value: &PointValue, + block_rewards: u64, ) { assert!(point_value.rewards >= distributed_rewards); let parent_blockhash = self.last_blockhash(); - let epoch_rewards = sysvar::epoch_rewards::EpochRewards { + let epoch_rewards = EpochRewards { distribution_starting_block_height, num_partitions, parent_blockhash, @@ -42,6 +46,8 @@ impl Bank { active: true, }; + // Do the first store to create the account from scratch, update + // capitalization if needed, etc self.update_sysvar_account(&sysvar::epoch_rewards::id(), |account| { create_account( &epoch_rewards, @@ -49,6 +55,19 @@ impl Bank { ) }); + // Now add the lamports separately without updating capitalization, + // since block reward lamports already existed + let mut account = self + .get_account_with_fixed_root(&sysvar::epoch_rewards::id()) + .expect("created sysvar account exists"); + + // SAFETY: block rewards come from existing lamports, which cannot + // overflow + account + .checked_add_lamports(block_rewards) + .expect("block rewards and sysvar account rent exemption must fit in a u64"); + self.store_account(&sysvar::epoch_rewards::id(), &account); + self.log_epoch_rewards_sysvar("create"); } @@ -56,6 +75,7 @@ impl Bank { pub(in crate::bank::partitioned_epoch_rewards) fn update_epoch_rewards_sysvar( &self, distributed: u64, + debit_block_reward_lamports: u64, ) { let mut epoch_rewards = self.get_epoch_rewards_sysvar(); assert!(epoch_rewards.active); @@ -69,19 +89,48 @@ impl Bank { ) }); + // Debit the lamports separately without updating capitalization, + // since block reward lamports already existed + let mut account = self + .get_account_with_fixed_root(&sysvar::epoch_rewards::id()) + .expect("created sysvar account exists"); + + // SAFETY: programmer error if we debit too many block rewards + account + .checked_sub_lamports(debit_block_reward_lamports) + .expect("epoch reward sysvar has enough lamports for distribution"); + assert!( + account.lamports() >= self.get_minimum_balance_for_rent_exemption(account.data().len()), + "Sysvar account must have enough for rent exemption after debiting block rewards" + ); + self.store_account(&sysvar::epoch_rewards::id(), &account); + self.log_epoch_rewards_sysvar("update"); } - /// Update EpochRewards sysvar with distributed rewards + /// Update EpochRewards sysvar with distributed rewards and burn any + /// remaining lamports over the rent-exempt reserve pub(in crate::bank::partitioned_epoch_rewards) fn set_epoch_rewards_sysvar_to_inactive(&self) { + const RENT_UNADJUSTED_INITIAL_BALANCE: u64 = 1; + let mut epoch_rewards = self.get_epoch_rewards_sysvar(); assert!(epoch_rewards.total_rewards >= epoch_rewards.distributed_rewards); epoch_rewards.active = false; self.update_sysvar_account(&sysvar::epoch_rewards::id(), |account| { + // Don't use `inherit_specially_retained_account_fields()` to + // ensure that any remaining lamports get burned, lamports are + // set to the rent-exempt minimum during `update_sysvar_account`, + // and capitalization is updated create_account( &epoch_rewards, - self.inherit_specially_retained_account_fields(account), + ( + RENT_UNADJUSTED_INITIAL_BALANCE, + account + .as_ref() + .map(|a| a.rent_epoch()) + .unwrap_or(INITIAL_RENT_EPOCH), + ), ) }); @@ -92,7 +141,7 @@ impl Bank { /// account cannot be found or cannot be deserialized. pub(in crate::bank::partitioned_epoch_rewards) fn get_epoch_rewards_sysvar( &self, - ) -> sysvar::epoch_rewards::EpochRewards { + ) -> EpochRewards { from_account( &self .get_account(&sysvar::epoch_rewards::id()) @@ -107,7 +156,6 @@ mod tests { use { super::*, crate::bank::{SlotLeader, tests::create_genesis_config}, - solana_account::ReadableAccount, solana_epoch_schedule::EpochSchedule, solana_native_token::LAMPORTS_PER_SOL, }; @@ -131,8 +179,10 @@ mod tests { points: total_points, }; + let first_block_rewards = 5_000_000_000; + // create epoch rewards sysvar - let expected_epoch_rewards = sysvar::epoch_rewards::EpochRewards { + let expected_epoch_rewards = EpochRewards { distribution_starting_block_height: 42, num_partitions, parent_blockhash: bank.last_blockhash(), @@ -143,19 +193,34 @@ mod tests { }; let epoch_rewards = bank.get_epoch_rewards_sysvar(); - assert_eq!( - epoch_rewards, - sysvar::epoch_rewards::EpochRewards::default() - ); + assert_eq!(epoch_rewards, EpochRewards::default()); + + let pre_capitalization = bank.capitalization(); + bank.create_epoch_rewards_sysvar(10, 42, num_partitions, &point_value, first_block_rewards); + let post_capitalization = bank.capitalization(); - bank.create_epoch_rewards_sysvar(10, 42, num_partitions, &point_value); let account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); - let expected_balance = bank.get_minimum_balance_for_rent_exemption(account.data().len()); - // Expected balance is the sysvar rent-exempt balance + let rent_exempt_reserve = bank.get_minimum_balance_for_rent_exemption(account.data().len()); + let expected_balance = rent_exempt_reserve + first_block_rewards; + // Expected balance is the sysvar rent-exempt balance plus block rewards assert_eq!(account.lamports(), expected_balance); - let epoch_rewards: sysvar::epoch_rewards::EpochRewards = from_account(&account).unwrap(); + + // Expect capitalization to only change by rent exempt minimum + assert_eq!( + post_capitalization, + pre_capitalization + rent_exempt_reserve + ); + + let epoch_rewards: EpochRewards = from_account(&account).unwrap(); assert_eq!(epoch_rewards, expected_epoch_rewards); + // Unsetting should burn all block rewards + bank.set_epoch_rewards_sysvar_to_inactive(); + assert_eq!( + post_capitalization - first_block_rewards, + bank.capitalization() + ); + // Create a child bank to test parent_blockhash let parent_blockhash = bank.last_blockhash(); let parent_slot = bank.slot(); @@ -165,11 +230,26 @@ mod tests { SlotLeader::default(), parent_slot + 1, ); + let second_block_rewards = 500_000_000; + // Also note that running `create_epoch_rewards_sysvar()` against a bank // with an existing EpochRewards sysvar clobbers the previous values - bank.create_epoch_rewards_sysvar(10, 42, num_partitions, &point_value); + let pre_capitalization = bank.capitalization(); + bank.create_epoch_rewards_sysvar( + 10, + 42, + num_partitions, + &point_value, + second_block_rewards, + ); + let post_capitalization = bank.capitalization(); + + // Capitalization shouldn't change this time, no new lamports minted + // since account already existed, but different amount of lamports on + // account + assert_eq!(post_capitalization, pre_capitalization); - let expected_epoch_rewards = sysvar::epoch_rewards::EpochRewards { + let expected_epoch_rewards = EpochRewards { distribution_starting_block_height: 42, num_partitions, parent_blockhash, @@ -179,16 +259,31 @@ mod tests { active: true, }; + let account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); + let expected_balance = bank.get_minimum_balance_for_rent_exemption(account.data().len()) + + second_block_rewards; + // Expected balance is the sysvar rent-exempt balance with new block rewards + assert_eq!(account.lamports(), expected_balance); + let epoch_rewards = bank.get_epoch_rewards_sysvar(); assert_eq!(epoch_rewards, expected_epoch_rewards); // make a distribution from epoch rewards sysvar - bank.update_epoch_rewards_sysvar(10); + let block_reward_distribution = 1_000_000; + bank.update_epoch_rewards_sysvar(10, block_reward_distribution); let account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); - // Balance should not change - assert_eq!(account.lamports(), expected_balance); - let epoch_rewards: sysvar::epoch_rewards::EpochRewards = from_account(&account).unwrap(); - let expected_epoch_rewards = sysvar::epoch_rewards::EpochRewards { + + // Balance should change + assert_eq!( + account.lamports(), + expected_balance - block_reward_distribution + ); + + // Capitalization should not + assert_eq!(post_capitalization, bank.capitalization()); + + let epoch_rewards: EpochRewards = from_account(&account).unwrap(); + let expected_epoch_rewards = EpochRewards { distribution_starting_block_height: 42, num_partitions, parent_blockhash, @@ -198,5 +293,26 @@ mod tests { active: true, }; assert_eq!(epoch_rewards, expected_epoch_rewards); + + // Unsetting should burn the rest + bank.set_epoch_rewards_sysvar_to_inactive(); + assert_eq!( + bank.capitalization(), + post_capitalization + block_reward_distribution - second_block_rewards + ); + + let account = bank.get_account(&sysvar::epoch_rewards::id()).unwrap(); + let epoch_rewards: EpochRewards = from_account(&account).unwrap(); + let expected_epoch_rewards = EpochRewards { + distribution_starting_block_height: 42, + num_partitions, + parent_blockhash, + total_points, + total_rewards, + distributed_rewards: 20, + active: false, + }; + assert_eq!(epoch_rewards, expected_epoch_rewards); + assert_eq!(account.lamports(), rent_exempt_reserve); } } diff --git a/runtime/src/bank/serde_snapshot.rs b/runtime/src/bank/serde_snapshot.rs index cf146012ae8..99d6606ff8f 100644 --- a/runtime/src/bank/serde_snapshot.rs +++ b/runtime/src/bank/serde_snapshot.rs @@ -332,82 +332,4 @@ mod tests { dbank.fee_rate_governor.lamports_per_signature ); } - - #[cfg(feature = "frozen-abi")] - mod test_bank_serialize { - use { - super::*, - crate::{bank::BankHashStats, serde_snapshot::UnusedIncrementalSnapshotPersistence}, - solana_accounts_db::accounts_hash::AccountsLtHash, - solana_frozen_abi::abi_example::AbiExample, - solana_hash::Hash, - solana_lattice_hash::lt_hash::LtHash, - std::marker::PhantomData, - }; - - // This some what long test harness is required to freeze the ABI of Bank's serialization, - // which is implemented manually by calling serialize_bank_snapshot_with() mainly based on - // get_fields_to_serialize(). However, note that Bank's serialization is coupled with - // snapshot storages as well. - // - // It was avoided to impl AbiExample for Bank by wrapping it around PhantomData inside the - // special wrapper called BankAbiTestWrapper. And internally, it creates an actual bank - // from Bank::default_for_tests(). - // - // In this way, frozen abi can increase the coverage of the serialization code path as much - // as possible. Alternatively, we could derive AbiExample for the minimum set of actually - // serialized fields of bank as an ad-hoc tuple. But that was avoided to avoid maintenance - // burden instead. - // - // Involving the Bank here is preferred conceptually because snapshot abi is - // important and snapshot is just a (rooted) serialized bank at the high level. Only - // abi-freezing bank.get_fields_to_serialize() is kind of relying on the implementation - // detail. - #[cfg_attr( - feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "5ESyZ9Aseo4v1XA2xnC8dENK8MueieQESYfeNcxByCmd") - )] - #[derive(serde::Serialize)] - pub struct BankAbiTestWrapper { - #[serde(serialize_with = "wrapper")] - bank: PhantomData, - } - - pub fn wrapper(_bank: &PhantomData, serializer: S) -> Result - where - S: serde::Serializer, - { - let bank = Bank::default_for_tests(); - bank.set_block_id(Some(Hash::default())); - let snapshot_storages = AccountsDb::example().get_storages(0..1).0; - // ensure there is at least one snapshot storage example for ABI digesting - assert!(!snapshot_storages.is_empty()); - - let incremental_snapshot_persistence = UnusedIncrementalSnapshotPersistence { - full_slot: u64::default(), - full_hash: [1; 32], - full_capitalization: u64::default(), - incremental_hash: [2; 32], - incremental_capitalization: u64::default(), - }; - - let mut bank_fields = bank.get_fields_to_serialize(); - let versioned_epoch_stakes = std::mem::take(&mut bank_fields.versioned_epoch_stakes); - serde_snapshot::serialize_bank_snapshot_with( - serializer, - bank_fields, - BankHashStats::default(), - &snapshot_storages, - ExtraFieldsToSerialize { - lamports_per_signature: bank.fee_rate_governor.lamports_per_signature, - unused_incremental_snapshot_persistence: Some(incremental_snapshot_persistence), - unused_epoch_accounts_hash: Some(Hash::new_unique()), - versioned_epoch_stakes, - accounts_lt_hash: Some(AccountsLtHash(LtHash::identity()).into()), - block_id: Some(Hash::new_unique()), - }, - ) - } - } } diff --git a/runtime/src/bank/sysvar_cache.rs b/runtime/src/bank/sysvar_cache.rs index 92257bbefcb..c9dea815948 100644 --- a/runtime/src/bank/sysvar_cache.rs +++ b/runtime/src/bank/sysvar_cache.rs @@ -4,8 +4,12 @@ use super::Bank; #[cfg(test)] mod tests { use { - super::*, crate::inflation_rewards::points::PointValue, - solana_genesis_config::create_genesis_config, solana_leader_schedule::SlotLeader, + super::*, + crate::{ + genesis_utils::activate_all_features_alpenglow, inflation_rewards::points::PointValue, + }, + solana_genesis_config::create_genesis_config, + solana_leader_schedule::SlotLeader, solana_sysvar::epoch_rewards::EpochRewards, }; @@ -121,6 +125,7 @@ mod tests { // inject a reward sysvar for test let num_partitions = 2; // num_partitions is arbitrary and unimportant for this test let total_points = 42_000; // total_points is arbitrary for the purposes of this test + let block_rewards = 42_000_000; // block_rewards are arbitrary for this test let expected_epoch_rewards = EpochRewards { distribution_starting_block_height: 42, num_partitions, @@ -138,6 +143,7 @@ mod tests { rewards: 100, points: total_points, }, + block_rewards, ); bank1 @@ -161,4 +167,59 @@ mod tests { expected_epoch_rewards, ); } + + #[test] + fn test_alpenglow_clock_cache_updates_before_and_after_footer() { + let (mut genesis_config, _mint_keypair) = create_genesis_config(100_000); + activate_all_features_alpenglow(&mut genesis_config); + let (bank0, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + assert!(bank0.get_alpenglow_genesis_certificate().is_some()); + assert!(bank0.is_alpenglow()); + + let parent_clock = bank0.clock(); + let bank1_slot = bank0.slot() + 1; + let bank1 = Bank::new_from_parent_with_bank_forks( + bank_forks.as_ref(), + bank0, + SlotLeader::default(), + bank1_slot, + ); + assert!(bank1.is_alpenglow()); + + let pre_footer_clock = bank1.clock(); + assert_eq!(pre_footer_clock.slot, bank1_slot); + assert_eq!(pre_footer_clock.unix_timestamp, parent_clock.unix_timestamp); + assert_eq!( + pre_footer_clock.epoch_start_timestamp, + parent_clock.epoch_start_timestamp + ); + + let cached_pre_footer_clock = { + let sysvar_cache = bank1.transaction_processor.sysvar_cache(); + sysvar_cache.get_clock().unwrap() + }; + assert_eq!(cached_pre_footer_clock.slot, bank1_slot); + assert_eq!( + cached_pre_footer_clock.unix_timestamp, + parent_clock.unix_timestamp + ); + + let footer_timestamp_nanos = parent_clock + .unix_timestamp + .saturating_add(42) + .saturating_mul(1_000_000_000); + bank1.update_clock_from_footer(footer_timestamp_nanos); + let footer_timestamp = footer_timestamp_nanos / 1_000_000_000; + + assert_eq!(bank1.clock().slot, bank1_slot); + assert_eq!(bank1.clock().unix_timestamp, footer_timestamp); + + let cached_post_footer_clock = { + let sysvar_cache = bank1.transaction_processor.sysvar_cache(); + sysvar_cache.get_clock().unwrap() + }; + assert_eq!(cached_post_footer_clock.slot, bank1_slot); + assert_eq!(cached_post_footer_clock.unix_timestamp, footer_timestamp); + } } diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index eafff8b5292..270b942c8d7 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -19,6 +19,12 @@ use { }, runtime_config::RuntimeConfig, serde_snapshot::fields_from_stream, + slot_params::{ + DEFAULT_MAX_ENTRY_BYTES_PER_SLOT, LEGACY_HASHES_PER_TICK, LEGACY_SLOT_PARAMS, + SLOT_PARAMS_200MS, SLOT_PARAMS_250MS, SLOT_PARAMS_300MS, SLOT_PARAMS_350MS, SlotParams, + slot_time_feature_gates, slot_time_feature_ids, + }, + stake_delegation::effective_stake, stake_history::StakeHistory, stake_utils, stakes::{DeserializableStakes, InvalidCacheEntryReason, SerdeStakesToStakeFormat, Stakes}, @@ -27,7 +33,8 @@ use { agave_reserved_account_keys::ReservedAccount, ahash::AHashMap, assert_matches::assert_matches, - crossbeam_channel::{bounded, unbounded}, + crossbeam_channel::{TrySendError, bounded}, + dashmap::DashMap, itertools::Itertools, rand::Rng, rayon::{ThreadPool, ThreadPoolBuilder, iter::IntoParallelIterator}, @@ -46,6 +53,7 @@ use { accounts_scan::ScanError, ancestors::Ancestors, blockhash_queue::BlockhashQueue, + partitioned_rewards::PartitionedEpochRewardsConfig, }, solana_client_traits::SyncClient, solana_clock::{ @@ -56,9 +64,13 @@ use { solana_compute_budget::{ compute_budget::ComputeBudget, compute_budget_limits::ComputeBudgetLimits, }, + solana_compute_budget_instruction::instructions_processor::process_compute_budget_instructions, solana_compute_budget_interface::ComputeBudgetInstruction, - solana_cost_model::block_cost_limits::{ - MAX_BLOCK_UNITS, MAX_BLOCK_UNITS_SIMD_0286, MAX_WRITABLE_ACCOUNT_UNITS, + solana_cost_model::{ + block_cost_limits::{ + MAX_BLOCK_UNITS, MAX_BLOCK_UNITS_SIMD_0286, MAX_WRITABLE_ACCOUNT_UNITS, + }, + shred_limit::{DEFAULT_MAX_CODE_SHREDS_PER_SLOT, DEFAULT_MAX_DATA_SHREDS_PER_SLOT}, }, solana_cpi::MAX_RETURN_DATA, solana_epoch_schedule::{EpochSchedule, MINIMUM_SLOTS_PER_EPOCH}, @@ -175,6 +187,12 @@ pub(in crate::bank) fn create_genesis_config(lamports: u64) -> (GenesisConfig, K solana_genesis_config::create_genesis_config(lamports) } +fn create_genesis_config_with_legacy_hashes(lamports: u64) -> (GenesisConfig, Keypair) { + let (mut genesis_config, mint_keypair) = create_genesis_config(lamports); + genesis_config.poh_config.hashes_per_tick = Some(LEGACY_HASHES_PER_TICK); + (genesis_config, mint_keypair) +} + pub(in crate::bank) fn new_sanitized_message(message: Message) -> SanitizedMessage { SanitizedMessage::try_from_legacy_message(message, &ReservedAccountKeys::empty_key_set()) .unwrap() @@ -988,14 +1006,6 @@ fn test_bank_update_rewards_determinism() { } } -impl VerifyAccountsHashConfig { - fn default_for_test() -> Self { - Self { - require_rooted_bank: false, - } - } -} - // Test that purging 0 lamports accounts works. #[test] fn test_purge_empty_accounts() { @@ -1065,7 +1075,7 @@ fn test_purge_empty_accounts() { if pass == 0 { add_root_and_flush_write_cache(&bank0); - assert!(bank0.verify_accounts(VerifyAccountsHashConfig::default_for_test(), None)); + assert!(bank0.verify_accounts(None)); continue; } @@ -1074,14 +1084,14 @@ fn test_purge_empty_accounts() { bank0.squash(); add_root_and_flush_write_cache(&bank0); if pass == 1 { - assert!(bank0.verify_accounts(VerifyAccountsHashConfig::default_for_test(), None)); + assert!(bank0.verify_accounts(None)); continue; } bank1.freeze(); bank1.squash(); add_root_and_flush_write_cache(&bank1); - assert!(bank1.verify_accounts(VerifyAccountsHashConfig::default_for_test(), None)); + assert!(bank1.verify_accounts(None)); // keypair should have 0 tokens on both forks assert_eq!(bank0.get_account(&keypair.pubkey()), None); @@ -1089,7 +1099,7 @@ fn test_purge_empty_accounts() { bank1.clean_accounts_for_tests(); - assert!(bank1.verify_accounts(VerifyAccountsHashConfig::default_for_test(), None)); + assert!(bank1.verify_accounts(None)); } } @@ -2268,7 +2278,7 @@ fn test_bank_hash_internal_state() { bank2.transfer(amount, &mint_keypair, &pubkey2).unwrap(); bank2.squash(); bank2.force_flush_accounts_cache(); - assert!(bank2.verify_accounts(VerifyAccountsHashConfig::default_for_test(), None)); + assert!(bank2.verify_accounts(None)); } #[test] @@ -2302,7 +2312,7 @@ fn test_bank_hash_internal_state_verify() { // we later modify bank 2, so this flush is destructive to the test bank2.freeze(); add_root_and_flush_write_cache(&bank2); - assert!(bank2.verify_accounts(VerifyAccountsHashConfig::default_for_test(), None)); + assert!(bank2.verify_accounts(None)); } let bank3 = Bank::new_from_parent_with_bank_forks( &bank_forks, @@ -2313,7 +2323,7 @@ fn test_bank_hash_internal_state_verify() { assert_eq!(bank0_state, bank0.hash_internal_state()); if pass == 0 { // this relies on us having set bank2's accounts hash in the pass==0 if above - assert!(bank2.verify_accounts(VerifyAccountsHashConfig::default_for_test(), None)); + assert!(bank2.verify_accounts(None)); continue; } if pass == 1 { @@ -2322,7 +2332,7 @@ fn test_bank_hash_internal_state_verify() { // Doing so throws an assert. So, we can't flush 3 until 2 is flushed. bank3.freeze(); add_root_and_flush_write_cache(&bank3); - assert!(bank3.verify_accounts(VerifyAccountsHashConfig::default_for_test(), None)); + assert!(bank3.verify_accounts(None)); continue; } @@ -2331,7 +2341,7 @@ fn test_bank_hash_internal_state_verify() { bank2.freeze(); // <-- keep freeze() *outside* `if pass == 2 {}` if pass == 2 { add_root_and_flush_write_cache(&bank2); - assert!(bank2.verify_accounts(VerifyAccountsHashConfig::default_for_test(), None)); + assert!(bank2.verify_accounts(None)); // Verifying the accounts lt hash is only intended to be called at startup, and // normally in the background. Since here we're *not* at startup, and doing it @@ -2346,7 +2356,7 @@ fn test_bank_hash_internal_state_verify() { bank3.freeze(); add_root_and_flush_write_cache(&bank3); - assert!(bank3.verify_accounts(VerifyAccountsHashConfig::default_for_test(), None)); + assert!(bank3.verify_accounts(None)); } } @@ -2917,8 +2927,15 @@ fn test_bank_epoch_vote_accounts() { // epoch_stakes are a snapshot at the leader_schedule_slot_offset boundary // in the prior epoch (0 in this case) + let expected_stake = effective_stake( + &leader_stake, + 0, + &StakeHistory::default(), + None, + parent.use_fixed_point_stake_math(), + ); assert_eq!( - leader_stake.stake(0, &StakeHistory::default(), None), + expected_stake, vote_accounts.unwrap().get(&leader_vote_account).unwrap().0 ); @@ -2933,8 +2950,15 @@ fn test_bank_epoch_vote_accounts() { ); assert!(child.epoch_vote_accounts(epoch).is_some()); + let expected_stake = effective_stake( + &leader_stake, + child.epoch(), + &StakeHistory::default(), + None, + child.use_fixed_point_stake_math(), + ); assert_eq!( - leader_stake.stake(child.epoch(), &StakeHistory::default(), None), + expected_stake, child .epoch_vote_accounts(epoch) .unwrap() @@ -2951,8 +2975,15 @@ fn test_bank_epoch_vote_accounts() { SLOTS_PER_EPOCH - (LEADER_SCHEDULE_SLOT_OFFSET % SLOTS_PER_EPOCH) + 1, ); assert!(child.epoch_vote_accounts(epoch).is_some()); + let expected_stake = effective_stake( + &leader_stake, + child.epoch(), + &StakeHistory::default(), + None, + child.use_fixed_point_stake_math(), + ); assert_eq!( - leader_stake.stake(child.epoch(), &StakeHistory::default(), None), + expected_stake, child .epoch_vote_accounts(epoch) .unwrap() @@ -4573,6 +4604,7 @@ fn test_check_ro_durable_nonce_fails() { bank.check_nonce_transaction_validity( &new_sanitized_message(tx.message().clone()), &bank.next_durable_nonce(), + false, ), None ); @@ -5246,9 +5278,9 @@ fn test_bank_hash_consistency(deprecate_rent_exemption_threshold: bool) { assert_eq!( bank.hash().to_string(), if deprecate_rent_exemption_threshold { - "8xa8W2GxXP2fVXgY6EaTQFfWnL3jW9apWuu6FueGZPXs" + "7JJ15D1ce8yW5NceFtK3YN4JMeRV9epZRbpNjiw4Y2BW" } else { - "CeTjbRdS8Nt2Wb1QMuMf8rXQL3YGsUGLwsdT4Ahkvhut" + "GFDc9UWSp6E6sbjgk9EhZLBrX3bUyC8bkh9YmLUxsH23" }, ); } @@ -5257,9 +5289,9 @@ fn test_bank_hash_consistency(deprecate_rent_exemption_threshold: bool) { assert_eq!( bank.hash().to_string(), if deprecate_rent_exemption_threshold { - "748qUop2J7kyQjtYs9SDrxKRswjbeJPNT3mJqCJWmGfA" + "AxoMcCKN43V2JPtAc3ivoCFTfS2wmjB7xpZkn7u9KbLQ" } else { - "9Kr5dG4tSeS6gSboWMDHJ3GWs85uFXKh9yaHLHP73MJ4" + "79wpAVbcHWEJGLWtW4rDNjwdHnuRKbJheEstvsx9g3Xf" }, ); break; @@ -6285,6 +6317,324 @@ fn test_block_limits() { ); } +fn assert_slot_time_bank_state(bank: &Bank, params: SlotParams) { + assert_eq!(bank.ns_per_slot, params.ns_per_slot()); + assert_eq!(bank.hashes_per_tick(), params.hashes_per_tick()); + assert_eq!( + bank.slots_per_year.to_bits(), + params.slots_per_year().to_bits() + ); + assert_eq!( + bank.rent_collector.slots_per_year.to_bits(), + bank.slots_per_year.to_bits() + ); + assert_eq!( + bank.partitioned_rewards_stake_account_stores_per_block, + params.partitioned_epoch_rewards_stake_account_stores_per_block() + ); + assert_eq!(bank.vat_to_burn_per_epoch(), params.vat_to_burn_per_epoch()); + assert_eq!( + bank.entry_bytes_budget().slot_limit(), + params.max_entry_bytes_per_slot() + ); + let cost_tracker = bank.read_cost_tracker().unwrap(); + let cost_limits = params.cost_limits( + bank.feature_set + .is_active(&feature_set::raise_block_limits_to_100m::id()), + ); + assert_eq!(cost_tracker.get_limits(), cost_limits); +} + +#[test] +fn test_reduce_slot_time_features() { + let (genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + let feature_account_balance = genesis_config.rent.minimum_balance(Feature::size_of()); + + let (mut bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + assert!(!bank.slot_time_reduction_active()); + assert_eq!(bank.hashes_per_tick(), Some(LEGACY_HASHES_PER_TICK)); + + for (feature_id, params) in slot_time_feature_gates() { + let previous_params = bank.current_slot_params(); + bank.store_account( + &feature_id, + &feature::create_account(&Feature::default(), feature_account_balance), + ); + + let activation_slot = bank + .epoch_schedule() + .get_first_slot_in_epoch(bank.epoch().saturating_add(1)); + let activation_bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + bank, + SlotLeader::default(), + activation_slot, + ); + + assert!(activation_bank.feature_set.is_active(&feature_id)); + assert_eq!(activation_bank.current_slot_params(), previous_params); + assert_slot_time_bank_state(&activation_bank, previous_params); + + let effective_slot = activation_bank + .epoch_schedule() + .get_first_slot_in_epoch(activation_bank.epoch().saturating_add(1)); + bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + activation_bank, + SlotLeader::default(), + effective_slot, + ); + assert!(bank.slot_time_reduction_active()); + assert_slot_time_bank_state(&bank, params); + } +} + +#[test] +fn test_vat_burn_slot_params() { + let voting_keypair = ValidatorVoteKeypairs::new_rand(); + let validator_keypairs = [&voting_keypair]; + let vote_pubkey = voting_keypair.vote_keypair.pubkey(); + + // Loop through slot reduction features one at a time. + for (slot_time_feature_id, params) in std::iter::once((None, LEGACY_SLOT_PARAMS)) + .chain(slot_time_feature_gates().map(|(feature_id, params)| (Some(feature_id), params))) + { + // Genesis with slot reduction feature active (if applicable). + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_vote_accounts_and_cluster_type( + 1_000 * LAMPORTS_PER_SOL, + &validator_keypairs, + vec![minimum_vote_account_balance_for_vat(100)], + ClusterType::Development, + &FeatureSet::default(), + false, + ); + activate_feature(&mut genesis_config, feature_set::alpenglow::id()); + activate_feature( + &mut genesis_config, + feature_set::validator_admission_ticket::id(), + ); + if let Some(feature_id) = slot_time_feature_id { + activate_feature(&mut genesis_config, feature_id); + } + + // Advance forward such that feature goes effective. + let (parent_bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + let bank_slot = if slot_time_feature_id.is_some() { + parent_bank.epoch_schedule().get_first_slot_in_epoch(1) + } else { + parent_bank.slot().saturating_add(1) + }; + let mut bank = Bank::new_from_parent(parent_bank, SlotLeader::default(), bank_slot); + assert_eq!(bank.vat_to_burn_per_epoch(), params.vat_to_burn_per_epoch()); + + // Verify correct VAT amount is burned. + let vote_lamports_before = bank.get_balance(&vote_pubkey); + let incinerator_lamports_before = bank.get_balance(&incinerator::id()); + let stakes = SerdeStakesToStakeFormat::from(bank.get_top_epoch_stakes()); + let epoch_stakes = VersionedEpochStakes::new(stakes, bank.epoch()); + bank.maybe_burn_vat_from_staked_accounts(&epoch_stakes); + assert_eq!( + bank.get_balance(&vote_pubkey), + vote_lamports_before - params.vat_to_burn_per_epoch() + ); + assert_eq!( + bank.get_balance(&incinerator::id()), + incinerator_lamports_before + params.vat_to_burn_per_epoch() + ); + } +} + +#[test] +fn test_reduce_slot_time_features_active_at_genesis() { + let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + for feature_id in slot_time_feature_ids() { + activate_feature(&mut genesis_config, feature_id); + } + activate_feature( + &mut genesis_config, + feature_set::raise_block_limits_to_100m::id(), + ); + + let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + assert!(!bank.slot_time_reduction_active()); + assert_slot_time_bank_state(&bank, LEGACY_SLOT_PARAMS); + + let effective_slot = bank.epoch_schedule().get_first_slot_in_epoch(1); + let bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + bank, + SlotLeader::default(), + effective_slot, + ); + assert_slot_time_bank_state(&bank, SLOT_PARAMS_200MS); +} + +#[test] +fn test_reduce_slot_time_hashes_per_tick() { + let assert_reduced_slot_time_hashes_per_tick = + |genesis_config, expected_initial_hashes_per_tick, expected_effective_hashes_per_tick| { + let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + assert_eq!(bank.ns_per_slot, LEGACY_SLOT_PARAMS.ns_per_slot()); + assert_eq!(bank.hashes_per_tick(), expected_initial_hashes_per_tick); + + let effective_slot = bank.epoch_schedule().get_first_slot_in_epoch(1); + let bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + bank, + SlotLeader::default(), + effective_slot, + ); + assert_eq!(bank.ns_per_slot, SLOT_PARAMS_200MS.ns_per_slot()); + assert_eq!(bank.hashes_per_tick(), expected_effective_hashes_per_tick); + }; + + let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + activate_feature( + &mut genesis_config, + feature_set::reduce_slot_time_to_200ms::id(), + ); + assert_reduced_slot_time_hashes_per_tick( + genesis_config, + Some(LEGACY_HASHES_PER_TICK), + SLOT_PARAMS_200MS.hashes_per_tick(), + ); + + let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + genesis_utils::activate_all_features_alpenglow(&mut genesis_config); + assert_eq!(genesis_config.poh_config.hashes_per_tick, None); + assert_reduced_slot_time_hashes_per_tick(genesis_config, None, None); +} + +#[test] +fn test_reduce_slot_time_range_duration() { + const SLOTS_PER_EPOCH: Slot = 32; + let bank_for_activations = |activations: &[(Pubkey, Slot)]| { + let (mut genesis_config, _) = create_genesis_config_with_legacy_hashes(1_000_000); + genesis_config.epoch_schedule = + EpochSchedule::custom(SLOTS_PER_EPOCH, SLOTS_PER_EPOCH, false); + let mut bank = Bank::new_for_tests(&genesis_config); + let mut feature_set = FeatureSet::default(); + for (feature_id, activation_slot) in activations { + feature_set.activate(feature_id, *activation_slot); + } + bank.feature_set = Arc::new(feature_set); + bank.refresh_slot_params(); + bank + }; + + let ordered_activations = slot_time_feature_gates() + .into_iter() + .zip([1, 33, 65, 97]) + .map(|((feature_id, _), activation_slot)| (feature_id, activation_slot)) + .collect::>(); + let bank = bank_for_activations(&ordered_activations); + + let expected_duration = [ + (SLOTS_PER_EPOCH, LEGACY_SLOT_PARAMS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_350MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_300MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_250MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_200MS), + ] + .into_iter() + .map(|(slots, params)| slots as f64 / params.slots_per_year()) + .sum::(); + + assert_eq!( + bank.slot_range_duration_in_years(0, SLOTS_PER_EPOCH * 5) + .to_bits(), + expected_duration.to_bits() + ); + assert_eq!( + bank.slot_range_duration_nanos(0, SLOTS_PER_EPOCH * 5 - 1), + [ + (SLOTS_PER_EPOCH, LEGACY_SLOT_PARAMS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_350MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_300MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_250MS), + (SLOTS_PER_EPOCH, SLOT_PARAMS_200MS), + ] + .into_iter() + .map(|(slots, params)| u128::from(slots) * params.ns_per_slot()) + .sum::() + ); + + let [reduce_to_350ms, _, _, reduce_to_200ms] = slot_time_feature_ids(); + let bank = + bank_for_activations(&[(reduce_to_200ms, 1), (reduce_to_350ms, SLOTS_PER_EPOCH + 1)]); + assert_eq!( + bank.slot_params_at_slot(SLOTS_PER_EPOCH - 1), + LEGACY_SLOT_PARAMS + ); + assert_eq!(bank.slot_params_at_slot(SLOTS_PER_EPOCH), SLOT_PARAMS_200MS); + assert_eq!( + bank.slot_params_at_slot(SLOTS_PER_EPOCH * 2), + SLOT_PARAMS_200MS + ); + assert_eq!( + bank.slot_range_duration_in_years(0, SLOTS_PER_EPOCH * 3) + .to_bits(), + (SLOTS_PER_EPOCH as f64 / LEGACY_SLOT_PARAMS.slots_per_year() + + (SLOTS_PER_EPOCH * 2) as f64 / SLOT_PARAMS_200MS.slots_per_year()) + .to_bits() + ); + assert_eq!( + bank.slot_range_duration_nanos(0, SLOTS_PER_EPOCH * 3 - 1), + u128::from(SLOTS_PER_EPOCH) * LEGACY_SLOT_PARAMS.ns_per_slot() + + u128::from(SLOTS_PER_EPOCH * 2) * SLOT_PARAMS_200MS.ns_per_slot() + ); +} + +#[test] +fn test_update_clock_slot_range_duration() { + const SLOTS_PER_EPOCH: Slot = 32; + + let leader_pubkey = solana_pubkey::new_rand(); + let GenesisConfigInfo { + mut genesis_config, + voting_keypair, + .. + } = create_genesis_config_with_leader(5, &leader_pubkey, 3); + genesis_config.epoch_schedule = EpochSchedule::custom(SLOTS_PER_EPOCH, SLOTS_PER_EPOCH, false); + activate_feature( + &mut genesis_config, + feature_set::reduce_slot_time_to_200ms::id(), + ); + + let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + let vote_timestamp_slot = 4; + let vote_timestamp = bank.unix_timestamp_from_genesis(); + update_vote_account_timestamp( + BlockTimestamp { + slot: vote_timestamp_slot, + timestamp: vote_timestamp, + }, + &bank, + &voting_keypair.pubkey(), + ); + + let current_slot = SLOTS_PER_EPOCH + 3; + let bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + bank, + SlotLeader::default(), + current_slot, + ); + let expected_elapsed = Duration::from_nanos_u128( + bank.slot_range_duration_nanos(vote_timestamp_slot + 1, current_slot), + ); + let scalar_elapsed = + Duration::from_nanos(bank.ns_per_slot as u64) * (current_slot - vote_timestamp_slot) as u32; + + assert_ne!(expected_elapsed.as_secs(), scalar_elapsed.as_secs()); + assert_eq!( + bank.clock().unix_timestamp, + vote_timestamp + expected_elapsed.as_secs() as i64 + ); +} + #[test] fn test_rent_feature_gates_epoch_transition() { let (mut genesis_config, _mint_keypair) = create_genesis_config(1_000_000); @@ -6580,6 +6930,103 @@ fn poh_estimate_offset(bank: &Bank) -> Duration { * Duration::from_nanos(bank.ns_per_slot as u64) } +#[test] +fn test_ns_per_slot() { + let (genesis_config, _) = create_genesis_config(1); + let bank = Bank::new_for_tests(&genesis_config); + + assert_eq!(bank.ns_per_slot_at_slot(bank.slot()), bank.ns_per_slot); + assert_eq!( + bank.ns_per_slot_at_slot(bank.slot().saturating_add(1)), + bank.ns_per_slot + ); +} + +#[test] +fn test_slot_params_use_genesis_baseline_without_features() { + let (mut genesis_config, _) = create_genesis_config(1_000_000); + genesis_config.poh_config.target_tick_duration = Duration::from_millis(10); + let genesis_ns_per_slot = genesis_config.ns_per_slot(); + let genesis_slots_per_year = genesis_config.slots_per_year(); + let stake_account_stores_per_block = 17; + let mut accounts_db_config = ACCOUNTS_DB_CONFIG_FOR_TESTING; + accounts_db_config.partitioned_epoch_rewards_config = + PartitionedEpochRewardsConfig::new_for_test(stake_account_stores_per_block); + + let bank = Bank::new_from_genesis( + &genesis_config, + Arc::::default(), + Vec::new(), + None, + accounts_db_config, + None, + None, + Arc::default(), + None, + None, + ); + let baseline_params = bank.slot_params.baseline_params(); + + assert_ne!(baseline_params, LEGACY_SLOT_PARAMS); + assert_eq!(bank.ns_per_slot, genesis_ns_per_slot); + assert_eq!(baseline_params.ns_per_slot(), genesis_ns_per_slot); + assert_eq!(bank.ns_per_slot_at_slot(bank.slot()), genesis_ns_per_slot); + assert_eq!( + bank.ns_per_slot_at_slot(bank.slot().saturating_add(1)), + genesis_ns_per_slot + ); + assert_eq!( + bank.slots_per_year.to_bits(), + genesis_slots_per_year.to_bits() + ); + assert_eq!( + baseline_params.slots_per_year().to_bits(), + genesis_slots_per_year.to_bits() + ); + assert_eq!( + baseline_params.hashes_per_tick(), + genesis_config.hashes_per_tick() + ); + assert_eq!( + bank.epoch_duration_in_years(0).to_bits(), + (bank.get_slots_in_epoch(0) as f64 / genesis_slots_per_year).to_bits() + ); + assert_eq!( + bank.slot_range_duration_in_years(0, 64).to_bits(), + (64.0 / genesis_slots_per_year).to_bits() + ); + assert_eq!(bank.slot_range_duration_nanos(1, 0), 0); + assert_eq!(bank.slot_range_duration_nanos(0, 0), genesis_ns_per_slot); + assert_eq!( + bank.slot_range_duration_nanos(0, 63), + 64 * genesis_ns_per_slot + ); + assert_eq!( + bank.partitioned_rewards_stake_account_stores_per_block, + stake_account_stores_per_block + ); + assert_eq!( + baseline_params.partitioned_epoch_rewards_stake_account_stores_per_block(), + stake_account_stores_per_block + ); + assert_eq!( + baseline_params.vat_to_burn_per_epoch(), + DEFAULT_VAT_TO_BURN_PER_EPOCH + ); + assert_eq!( + bank.max_data_shreds_per_slot(), + DEFAULT_MAX_DATA_SHREDS_PER_SLOT + ); + assert_eq!( + bank.max_code_shreds_per_slot(), + DEFAULT_MAX_CODE_SHREDS_PER_SLOT + ); + assert_eq!( + bank.max_entry_bytes_per_slot(), + DEFAULT_MAX_ENTRY_BYTES_PER_SLOT + ); +} + #[test] fn test_timestamp_slow() { fn max_allowable_delta_since_epoch(bank: &Bank, max_allowable_drift: u32) -> i64 { @@ -6804,7 +7251,7 @@ fn test_store_scan_consistency( let (scan_finished_sender, scan_finished_receiver): ( crossbeam_channel::Sender, crossbeam_channel::Receiver, - ) = unbounded(); + ) = bounded(1024); let num_banks_scanned = Arc::new(AtomicU64::new(0)); let scan_thread = { let exit = exit.clone(); @@ -6823,7 +7270,15 @@ fn test_store_scan_consistency( { info!("scanning program accounts for slot {}", bank_to_scan.slot()); let accounts_result = bank_to_scan.get_program_accounts(&program_id); - let _ = scan_finished_sender.send(bank_to_scan.bank_id()); + match scan_finished_sender.try_send(bank_to_scan.bank_id()) { + // The receiver may have hung up during shutdown; that's fine. + Ok(()) | Err(TrySendError::Disconnected(_)) => {} + // A full channel means a caller isn't draining it, which would + // block this scan thread. Fail loudly instead of deadlocking. + Err(TrySendError::Full(_)) => { + panic!("scan_finished channel is full; caller must drain it") + } + } num_banks_scanned.fetch_add(1, Relaxed); match (&acceptable_scan_results, accounts_result.is_err()) { (AcceptableScanResults::DroppedSlotError, _) @@ -6913,14 +7368,14 @@ fn test_store_scan_consistency( #[test] fn test_store_scan_consistency_unrooted() { - let (pruned_banks_sender, pruned_banks_receiver) = unbounded(); + let (pruned_banks_sender, pruned_banks_receiver) = bounded(1024); let pruned_banks_request_handler = PrunedBanksRequestHandler { pruned_banks_receiver, }; test_store_scan_consistency( move |bank0, bank_to_scan_sender, - _scan_finished_receiver, + scan_finished_receiver, pubkeys_to_modify, program_id, starting_lamports| { @@ -6999,6 +7454,11 @@ fn test_store_scan_consistency_unrooted() { // Move purge here so that Bank::drop()->purge_slots() doesn't race // with clean. Simulates the call from AccountsBackgroundService pruned_banks_request_handler.handle_request(¤t_major_fork_bank); + + // Drain finished-scan notifications so the bounded channel can't fill + // up and block the scan thread. Non-blocking, to preserve the + // intentional overlap of clean/squash with the in-flight scan. + while scan_finished_receiver.try_recv().is_ok() {} } }, Some(Box::new(SendDroppedBankCallback::new(pruned_banks_sender))), @@ -7008,14 +7468,14 @@ fn test_store_scan_consistency_unrooted() { #[test] fn test_store_scan_consistency_root() { - let (pruned_banks_sender, pruned_banks_receiver) = unbounded(); + let (pruned_banks_sender, pruned_banks_receiver) = bounded(1024); let pruned_banks_request_handler = PrunedBanksRequestHandler { pruned_banks_receiver, }; test_store_scan_consistency( move |bank0, bank_to_scan_sender, - _scan_finished_receiver, + scan_finished_receiver, pubkeys_to_modify, program_id, starting_lamports| { @@ -7055,6 +7515,11 @@ fn test_store_scan_consistency_root() { // Move purge here so that Bank::drop()->purge_slots() doesn't race // with clean. Simulates the call from AccountsBackgroundService pruned_banks_request_handler.handle_request(¤t_bank); + + // Drain finished-scan notifications so the bounded channel can't fill + // up and block the scan thread. Non-blocking, to preserve the + // intentional overlap of clean/squash with the in-flight scan. + while scan_finished_receiver.try_recv().is_ok() {} } }, Some(Box::new(SendDroppedBankCallback::new(pruned_banks_sender))), @@ -7239,14 +7704,15 @@ fn test_remove_unrooted_scan_interleaved_with_remove_unrooted_slots() { starting_lamports| { loop { let step_size = 2; - let (bank_at_fork_tip, slots_on_fork, ancestors) = setup_banks_on_fork_to_remove( - bank0.clone(), - pubkeys_to_modify.clone(), - &program_id, - starting_lamports, - 10, - step_size, - ); + let (bank_at_fork_tip, mut slots_on_fork, ancestors) = + setup_banks_on_fork_to_remove( + bank0.clone(), + pubkeys_to_modify.clone(), + &program_id, + starting_lamports, + 10, + step_size, + ); // Although we dumped the slots last iteration via `remove_unrooted_slots()`, // we've recreated those slots this iteration, so they should be findable // again @@ -7264,7 +7730,7 @@ fn test_remove_unrooted_scan_interleaved_with_remove_unrooted_slots() { // Remove 1 < `step_size` of the *latest* slots while the scan is happening. // This should create inconsistency between the account balances of accounts // stored in that slot, and the accounts stored in earlier slots - let slot_to_remove = *slots_on_fork.last().unwrap(); + let slot_to_remove = slots_on_fork.pop().unwrap(); bank_at_fork_tip.remove_unrooted_slots(&[slot_to_remove]); // Wait for scan to finish before starting next iteration @@ -7274,7 +7740,8 @@ fn test_remove_unrooted_scan_interleaved_with_remove_unrooted_slots() { } assert_eq!(finished_scan_bank_id.unwrap(), bank_at_fork_tip.bank_id()); - // Remove the rest of the slots before the next iteration + // Remove the rest of the slots before the next iteration. The last slot + // was already popped and removed above. for (slot, bank_id) in slots_on_fork { bank_at_fork_tip.remove_unrooted_slots(&[(slot, bank_id)]); } @@ -10524,7 +10991,7 @@ fn test_verify_accounts() { bank.force_flush_accounts_cache(); // ensure the accounts verify successfully - assert!(bank.verify_accounts(VerifyAccountsHashConfig::default_for_test(), None)); + assert!(bank.verify_accounts(None)); } #[test] @@ -10534,14 +11001,12 @@ fn test_squash_timing_add_assign() { let t1 = SquashTiming { squash_accounts_ms: 1, squash_accounts_cache_ms: 2, - squash_accounts_index_ms: 3, squash_cache_ms: 5, }; let expected = SquashTiming { squash_accounts_ms: 2, squash_accounts_cache_ms: 2 * 2, - squash_accounts_index_ms: 3 * 2, squash_cache_ms: 5 * 2, }; @@ -11811,6 +12276,57 @@ fn test_new_from_snapshot_uses_rent_from_sysvar() { assert_ne!(new_bank.rent_collector.rent, wrong_rent); } +#[test] +fn test_new_from_snapshot_hashes_per_tick_changed() { + let GenesisConfigInfo { + mut genesis_config, .. + } = create_genesis_config_with_leader(100_000, &Pubkey::new_unique(), 3); + genesis_config.poh_config.hashes_per_tick = Some(12_500); + + let bank = Bank::new_for_tests(&genesis_config); + bank.set_hashes_per_tick(Some(LEGACY_HASHES_PER_TICK)); + bank.set_block_id(Some(Hash::default())); + + let snapshot_storages = bank.get_snapshot_storages(None); + let mut buf = vec![]; + crate::serde_snapshot::bank_to_stream( + &mut std::io::BufWriter::new(Cursor::new(&mut buf)), + &bank, + &snapshot_storages, + ) + .unwrap(); + + let mut reader = std::io::BufReader::new(Cursor::new(&buf)); + let (mut fields, _accounts_db_fields) = fields_from_stream(&mut reader).unwrap(); + let epoch_stakes = std::mem::take(&mut fields.versioned_epoch_stakes) + .into_iter() + .map(|(epoch, stakes)| (epoch, stakes.into())) + .collect(); + + let new_bank = Bank::new_from_snapshot( + BankRc { + accounts: Arc::clone(&bank.rc.accounts), + parent: RwLock::new(None), + bank_id_generator: Arc::clone(&bank.rc.bank_id_generator), + }, + &genesis_config, + Arc::new(RuntimeConfig::default()), + fields, + None, + None, + bank.load_accounts_data_size(), + epoch_stakes, + ); + + assert_eq!(genesis_config.hashes_per_tick(), Some(12_500)); + assert_eq!(new_bank.hashes_per_tick(), Some(LEGACY_HASHES_PER_TICK)); + assert_eq!( + new_bank.slot_params.baseline_params().hashes_per_tick(), + Some(LEGACY_HASHES_PER_TICK) + ); + new_bank.assert_bank_matches_slot_params(); +} + #[test] fn test_calculate_and_set_block_id_for_dcou() { // scenario 1: block id already set @@ -11937,7 +12453,7 @@ fn test_new_for_txn_tests_system_transfer() { ), ( solana_sdk_ids::sysvar::slot_hashes::id(), - make_sysvar(bincode::serialize(&solana_slot_hashes::SlotHashes::default()).unwrap()), + make_sysvar(wincode::serialize(&solana_slot_hashes::SlotHashes::default()).unwrap()), ), (system_program::id(), { let mut acct = AccountSharedData::new(1, 14, &native_loader::id()); @@ -12110,7 +12626,7 @@ fn test_new_for_block_tests_with_vote_account() { ), ( solana_sdk_ids::sysvar::slot_hashes::id(), - make_sysvar(bincode::serialize(&solana_slot_hashes::SlotHashes::default()).unwrap()), + make_sysvar(wincode::serialize(&solana_slot_hashes::SlotHashes::default()).unwrap()), ), (system_program::id(), { let mut acct = AccountSharedData::new(1, 14, &native_loader::id()); diff --git a/runtime/src/bank_client.rs b/runtime/src/bank_client.rs index 58781cf8c2c..785553c0337 100644 --- a/runtime/src/bank_client.rs +++ b/runtime/src/bank_client.rs @@ -191,10 +191,10 @@ impl SyncClient for BankClient { let now = Instant::now(); loop { let response = self.bank.get_signature_status(signature); - if let Some(res) = response { - if res.is_ok() { - break; - } + if let Some(res) = response + && res.is_ok() + { + break; } if now.elapsed().as_secs() > 15 { return Err(TransportError::IoError(io::Error::other(format!( diff --git a/runtime/src/bank_forks.rs b/runtime/src/bank_forks.rs index e44b31be1c5..2da03dede42 100644 --- a/runtime/src/bank_forks.rs +++ b/runtime/src/bank_forks.rs @@ -105,8 +105,7 @@ impl BankForks { BankWithScheduler::new_without_scheduler(root_bank.clone()), ); - let parents = root_bank.parents(); - for parent in parents { + for parent in root_bank.parents_iter() { if banks .insert( parent.slot(), @@ -488,7 +487,7 @@ impl BankForks { } } let root_tx_count = root_bank - .parents() + .parents_iter() .last() .map(|bank| bank.transaction_count()) .unwrap_or(0); @@ -540,7 +539,7 @@ impl BankForks { pub fn prune_program_cache(&self, root: Slot) { if let Some(root_bank) = self.banks.get(&root) { - root_bank.prune_program_cache(root, root_bank.epoch(), self); + root_bank.prune_program_cache(self); } } @@ -581,14 +580,6 @@ impl BankForks { .squash_accounts_ms, i64 ), - ( - "total_squash_accounts_index_ms", - set_root_metrics - .timings - .total_squash_time - .squash_accounts_index_ms, - i64 - ), ( "total_squash_accounts_cache_ms", set_root_metrics @@ -787,8 +778,10 @@ mod tests { }, agave_feature_set::FeatureSet, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType}, + certificate::{CertSignature, GenesisCert}, + consensus_message::Block, migration::{GENESIS_CERTIFICATE_ACCOUNT, MIGRATION_SLOT_OFFSET}, + wire::{WireBlockCertMessage, WireCertSignature}, }, assert_matches::assert_matches, crossbeam_channel::{Receiver, Sender, bounded}, @@ -985,15 +978,22 @@ mod tests { fn make_root_bank_for_migration_status_test( root_slot: Slot, ff_activation_slot: Option, - genesis_cert: Option, + genesis_cert: Option, ) -> Bank { let GenesisConfigInfo { mut genesis_config, .. } = create_genesis_config(10_000); genesis_config.epoch_schedule = EpochSchedule::new(32); - if let Some(genesis_cert) = genesis_cert.as_ref() { - let cert_data = wincode::serialize(genesis_cert).unwrap(); + if let Some(genesis_cert) = genesis_cert { + let cert = WireBlockCertMessage { + block: genesis_cert.block, + signature: WireCertSignature { + signature: genesis_cert.signature.signature, + bitmap: genesis_cert.signature.bitmap, + }, + }; + let cert_data = wincode::serialize(&cert).unwrap(); let lamports = Rent::default().minimum_balance(cert_data.len()); let mut cert_account = Account::new(lamports, cert_data.len(), &system_program::ID); cert_account.data = cert_data; @@ -1028,10 +1028,15 @@ mod tests { #[test] fn test_initialize_migration_status() { let ff_activation_slot = 5; - let genesis_cert = Certificate { - cert_type: CertificateType::Finalize(1), - signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), - bitmap: vec![], + let genesis_cert = GenesisCert { + block: Block { + slot: 1, + block_id: Hash::default(), + }, + signature: CertSignature { + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }, }; let root_bank = make_root_bank_for_migration_status_test(0, None, None); @@ -1052,8 +1057,8 @@ mod tests { Some(genesis_cert.clone()), ); assert_eq!( - root_bank.get_alpenglow_genesis_certificate(), - Some(genesis_cert.clone()) + root_bank.get_alpenglow_genesis_certificate().unwrap(), + genesis_cert.clone() ); let migration_status = BankForks::initialize_migration_status(&root_bank); assert!(migration_status.is_alpenglow_enabled()); @@ -1108,10 +1113,15 @@ mod tests { // Migration can still succeed let mut bank = Bank::new_from_parent(root_bank, SlotLeader::default(), 10); - let genesis_cert = Certificate { - cert_type: CertificateType::Finalize(1), - signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), - bitmap: vec![], + let genesis_cert = GenesisCert { + block: Block { + slot: 1, + block_id: Hash::new_unique(), + }, + signature: CertSignature { + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }, }; bank.activate_feature(&agave_feature_set::alpenglow::id()); bank.set_alpenglow_genesis_certificate(&genesis_cert); diff --git a/runtime/src/bank_forks_controller.rs b/runtime/src/bank_forks_controller.rs index 8f099f07b6d..d8495a09a16 100644 --- a/runtime/src/bank_forks_controller.rs +++ b/runtime/src/bank_forks_controller.rs @@ -4,9 +4,9 @@ use { log::warn, solana_clock::Slot, solana_metrics::datapoint_info, - solana_pubkey::Pubkey, std::{ fmt, + sync::{Arc, Mutex}, time::{Duration, Instant}, }, thiserror::Error, @@ -27,24 +27,22 @@ pub enum BankForksCommand { bank: Box, response_sender: Sender>, }, - SetRoot { - my_pubkey: Pubkey, - parent_slot: Slot, - new_root: Slot, - highest_super_majority_root: Option, - response_sender: Sender<()>, - }, ClearBank { slot: Slot, response_sender: Sender<()>, }, } +pub struct SetRootCommand { + pub parent_slot: Slot, + pub new_root: Slot, + pub highest_super_majority_root: Option, +} + impl BankForksCommand { fn metric_slot(&self) -> Slot { match self { Self::InsertBank { bank, .. } => bank.slot(), - Self::SetRoot { new_root, .. } => *new_root, Self::ClearBank { slot, .. } => *slot, } } @@ -54,7 +52,6 @@ impl fmt::Display for BankForksCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::InsertBank { .. } => write!(f, "insert_bank"), - Self::SetRoot { .. } => write!(f, "set_root"), Self::ClearBank { .. } => write!(f, "clear_bank"), } } @@ -63,13 +60,12 @@ impl fmt::Display for BankForksCommand { pub trait BankForksController: Send + Sync { fn insert_bank(&self, bank: Bank) -> Result; - fn set_root( + fn enqueue_set_root( &self, - my_pubkey: Pubkey, parent_slot: Slot, new_root: Slot, highest_super_majority_root: Option, - ) -> Result<(), BankForksControllerError>; + ); fn clear_bank(&self, slot: Slot) -> Result<(), BankForksControllerError>; } @@ -78,12 +74,27 @@ pub trait BankForksController: Send + Sync { #[derive(Clone)] pub struct BankForksControllerHandle { sender: Sender, + pending_set_root: Arc>>, + set_root_signal_sender: Sender<()>, } impl BankForksControllerHandle { pub fn new() -> (Self, BankForksCommandReceiver) { let (sender, receiver) = bounded(CHANNEL_SIZE); - (Self { sender }, BankForksCommandReceiver { receiver }) + let (set_root_signal_sender, set_root_signal_receiver) = bounded(1); + let pending_set_root = Arc::new(Mutex::new(None)); + ( + Self { + sender, + pending_set_root: pending_set_root.clone(), + set_root_signal_sender, + }, + BankForksCommandReceiver { + receiver, + pending_set_root, + set_root_signal_receiver, + }, + ) } fn send_command( @@ -146,24 +157,39 @@ impl BankForksController for BankForksControllerHandle { bank.ok_or(BankForksControllerError::UnableToInsertStaleBank(slot)) } - fn set_root( + fn enqueue_set_root( &self, - my_pubkey: Pubkey, parent_slot: Slot, new_root: Slot, highest_super_majority_root: Option, - ) -> Result<(), BankForksControllerError> { - let (response_sender, response_receiver) = bounded(1); - self.send_command( - BankForksCommand::SetRoot { - my_pubkey, - parent_slot, - new_root, - highest_super_majority_root, - response_sender, - }, - response_receiver, - ) + ) { + let total_start = Instant::now(); + let command = SetRootCommand { + parent_slot, + new_root, + highest_super_majority_root, + }; + + { + let mut pending_set_root = self.pending_set_root.lock().unwrap(); + // Replay only needs to process the highest pending root. + if pending_set_root + .as_ref() + .is_none_or(|pending| command.new_root > pending.new_root) + { + *pending_set_root = Some(command); + } + } + let _ = self.set_root_signal_sender.try_send(()); + + let total_us = total_start.elapsed().as_micros() as i64; + + datapoint_info!( + "bank_forks_controller-command", + ("command", "set_root", String), + ("slot", new_root as i64, i64), + ("total_us", total_us, i64), + ); } fn clear_bank(&self, slot: Slot) -> Result<(), BankForksControllerError> { @@ -180,12 +206,22 @@ impl BankForksController for BankForksControllerHandle { pub struct BankForksCommandReceiver { receiver: Receiver, + pending_set_root: Arc>>, + set_root_signal_receiver: Receiver<()>, } impl BankForksCommandReceiver { pub fn receiver(&self) -> &Receiver { &self.receiver } + + pub fn set_root_signal_receiver(&self) -> &Receiver<()> { + &self.set_root_signal_receiver + } + + pub fn take_set_root_command(&self) -> Option { + self.pending_set_root.lock().unwrap().take() + } } #[cfg(test)] @@ -193,19 +229,64 @@ mod tests { use { super::*, crate::{bank::SlotLeader, bank_forks::BankForks, genesis_utils::create_genesis_config}, - std::thread, + std::{thread, time::Duration}, }; + #[test] + fn test_bank_forks_controller_keeps_highest_pending_set_root() { + let (controller, receiver) = BankForksControllerHandle::new(); + + controller.enqueue_set_root(5, 5, Some(5)); + controller.enqueue_set_root(3, 3, Some(3)); + assert_eq!(receiver.take_set_root_command().unwrap().new_root, 5); + assert!(receiver.take_set_root_command().is_none()); + + controller.enqueue_set_root(3, 3, Some(3)); + controller.enqueue_set_root(5, 5, Some(5)); + assert_eq!(receiver.take_set_root_command().unwrap().new_root, 5); + } + + #[test] + fn test_bank_forks_controller_signals_pending_set_root() { + let (controller, receiver) = BankForksControllerHandle::new(); + + controller.enqueue_set_root(1, 1, Some(1)); + receiver + .set_root_signal_receiver() + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + assert_eq!(receiver.take_set_root_command().unwrap().new_root, 1); + + controller.enqueue_set_root(2, 2, Some(2)); + controller.enqueue_set_root(3, 3, Some(3)); + receiver + .set_root_signal_receiver() + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + assert!(receiver.set_root_signal_receiver().try_recv().is_err()); + assert_eq!(receiver.take_set_root_command().unwrap().new_root, 3); + } + #[test] fn test_bank_forks_controller_insert_and_set_root() { let genesis = create_genesis_config(10_000); let bank_forks = BankForks::new_rw_arc(Bank::new_for_tests(&genesis.genesis_config)); let (controller, receiver) = BankForksControllerHandle::new(); let replay_bank_forks = bank_forks.clone(); + let (root_sender, root_receiver) = bounded(1); let replay_thread = thread::spawn(move || { loop { - let Ok(command) = receiver.receiver().recv() else { - break; + if let Some(command) = receiver.take_set_root_command() { + { + let mut bank_forks = replay_bank_forks.write().unwrap(); + bank_forks.set_root(command.new_root, None, None); + } + root_sender.send(command.new_root).unwrap(); + } + let command = match receiver.receiver().recv_timeout(Duration::from_millis(10)) { + Ok(command) => command, + Err(RecvTimeoutError::Timeout) => continue, + Err(RecvTimeoutError::Disconnected) => break, }; match command { BankForksCommand::InsertBank { @@ -218,17 +299,6 @@ mod tests { }; response_sender.send(Some(bank)).unwrap(); } - BankForksCommand::SetRoot { - new_root, - response_sender, - .. - } => { - { - let mut bank_forks = replay_bank_forks.write().unwrap(); - bank_forks.set_root(new_root, None, None); - } - response_sender.send(()).unwrap(); - } BankForksCommand::ClearBank { slot, response_sender, @@ -256,7 +326,8 @@ mod tests { assert_eq!(inserted_bank.slot(), 1); assert!(bank_forks.read().unwrap().get(1).is_some()); - controller.set_root(Pubkey::default(), 1, 1, None).unwrap(); + controller.enqueue_set_root(1, 1, None); + assert_eq!(root_receiver.recv().unwrap(), 1); assert_eq!(bank_forks.read().unwrap().root(), 1); let parent_bank = bank_forks.read().unwrap().root_bank(); diff --git a/runtime/src/bank_utils.rs b/runtime/src/bank_utils.rs index 60a775c042c..458216267c9 100644 --- a/runtime/src/bank_utils.rs +++ b/runtime/src/bank_utils.rs @@ -51,26 +51,26 @@ pub fn find_and_send_votes( .iter() .zip(commit_results.iter()) .for_each(|(tx, commit_result)| { - if tx.is_simple_vote_transaction() && commit_result.was_executed_successfully() { - if let Some(parsed_vote) = vote_parser::parse_sanitized_vote_transaction(tx) { - if parsed_vote.1.last_voted_slot().is_some() { - let vote = match send_type { - ReplayVoteSendType::VerifiedExecuted => { - ReplayVoteMessage::VerifiedExecuted(parsed_vote) - } - ReplayVoteSendType::Executed { - replay_bank_id, - replay_slot, - } => ReplayVoteMessage::Executed { - replay_bank_id, - replay_slot, - message_hash: *tx.message_hash(), - parsed_vote, - }, - }; - let _ = vote_sender.send(vote); + if tx.is_simple_vote_transaction() + && commit_result.was_executed_successfully() + && let Some(parsed_vote) = vote_parser::parse_sanitized_vote_transaction(tx) + && parsed_vote.1.last_voted_slot().is_some() + { + let vote = match send_type { + ReplayVoteSendType::VerifiedExecuted => { + ReplayVoteMessage::VerifiedExecuted(parsed_vote) } - } + ReplayVoteSendType::Executed { + replay_bank_id, + replay_slot, + } => ReplayVoteMessage::Executed { + replay_bank_id, + replay_slot, + message_hash: *tx.message_hash(), + parsed_vote, + }, + }; + let _ = vote_sender.send(vote); } }); } diff --git a/runtime/src/block_component_processor.rs b/runtime/src/block_component_processor.rs index fb565e38570..fbd5c44e166 100644 --- a/runtime/src/block_component_processor.rs +++ b/runtime/src/block_component_processor.rs @@ -4,26 +4,28 @@ use { block_component_processor::vote_reward::{ CalcVoteRewardUpdateVoteStatesError, calc_vote_rewards_update_vote_states, }, + leader_schedule_utils::leader_slot_index, validated_block_finalization::{ BlockFinalizationCertError, ValidatedBlockFinalizationCert, }, validated_reward_certificate::{Error as ValidatedRewardCertError, ValidatedRewardCert}, }, agave_votor_messages::{ - consensus_message::{Certificate, ConsensusMessage}, - fraction::Fraction, - migration::{GENESIS_VOTE_THRESHOLD, MigrationStatus}, + certificate::{CertSignature, CertificateType, GenesisCert}, + consensus_message::{Block, ConsensusMessage}, + migration::MigrationStatus, + unverified_vote_message::UnverifiedCertificate, }, crossbeam_channel::Sender, log::*, solana_clock::Slot, solana_entry::block_component::{ - BlockFooterV1, BlockMarkerV1, GenesisCertificate, VersionedBlockFooter, + BlockFooterV1, BlockMarkerV1, GenesisCertBlockMarker, VersionedBlockFooter, VersionedBlockHeader, VersionedBlockMarker, VersionedUpdateParent, }, solana_hash::Hash, solana_pubkey::Pubkey, - std::{collections::HashSet, num::NonZeroU64, sync::Arc}, + std::{collections::HashSet, sync::Arc}, thiserror::Error, }; @@ -47,10 +49,14 @@ pub enum BlockComponentProcessorError { GenesisCertificateOnNonChild, #[error("GenesisCertificate was invalid and failed to verify")] GenesisCertificateFailedVerification, + #[error("GenesisCertificate marker must immediately follow the block header")] + GenesisCertificateOutOfOrder, #[error("FinalizationCertificate was invalid or failed to verify {0}")] InvalidFinalizationCertificate(#[from] BlockFinalizationCertError), #[error("Missing block footer")] MissingBlockFooter, + #[error("Missing genesis certificate marker")] + MissingGenesisCertificateMarker, #[error("Missing parent marker (neither a header nor an update parent was present)")] MissingParentMarker, #[error("Multiple block footers detected")] @@ -70,6 +76,8 @@ pub enum BlockComponentProcessorError { NanosecondClockOutOfBounds, #[error("Spurious update parent")] SpuriousUpdateParent, + #[error("UpdateParent marker is only valid in the first slot of a leader window: slot {0}")] + UpdateParentNotFirstInLeaderWindow(Slot), #[error( "UpdateParent cannot be the initial parent marker unless replay starts at UpdateParent" )] @@ -91,6 +99,7 @@ impl BlockComponentProcessorError { | BlockComponentProcessorError::HeaderParentSlotMismatch { .. } | BlockComponentProcessorError::NanosecondClockOutOfBounds | BlockComponentProcessorError::UnexpectedInitialUpdateParent + | BlockComponentProcessorError::GenesisCertificateOutOfOrder | BlockComponentProcessorError::AbandonedBank(_) | BlockComponentProcessorError::InvalidRewardCerts(_) | BlockComponentProcessorError::UpdateBankFooter(_) @@ -101,8 +110,10 @@ impl BlockComponentProcessorError { | BlockComponentProcessorError::GenesisCertificateOnNonChild | BlockComponentProcessorError::GenesisCertificateFailedVerification | BlockComponentProcessorError::MissingBlockFooter + | BlockComponentProcessorError::MissingGenesisCertificateMarker | BlockComponentProcessorError::MultipleUpdateParents - | BlockComponentProcessorError::SpuriousUpdateParent => false, + | BlockComponentProcessorError::SpuriousUpdateParent + | BlockComponentProcessorError::UpdateParentNotFirstInLeaderWindow(_) => false, } } } @@ -111,6 +122,8 @@ impl BlockComponentProcessorError { pub struct BlockComponentProcessor { has_header: bool, has_footer: bool, + has_entry_batch: bool, + has_genesis_certificate_marker: bool, update_parent: Option, } @@ -119,18 +132,19 @@ impl BlockComponentProcessor { &self, migration_status: &MigrationStatus, slot: Slot, + parent_slot: Slot, ) -> Result<(), BlockComponentProcessorError> { + if Self::requires_genesis_certificate_marker(migration_status, parent_slot) + && !self.has_genesis_certificate_marker + { + return Err(BlockComponentProcessorError::MissingGenesisCertificateMarker); + } + // Only require block markers (header/footer) for slots where they should be present if !migration_status.should_allow_block_markers(slot) { return Ok(()); } - // If we encounter an UpdateParent when fast leader handover is disabled, error. - if !migration_status.should_allow_fast_leader_handover(slot) && self.update_parent.is_some() - { - return Err(BlockComponentProcessorError::SpuriousUpdateParent); - } - // Post-migration: both header and footer are required if !self.has_footer { return Err(BlockComponentProcessorError::MissingBlockFooter); @@ -143,6 +157,22 @@ impl BlockComponentProcessor { Ok(()) } + /// Check if `parent_slot` is the alpenglow genesis block for use in enforcing + /// that the block has a genesis block marker + /// + /// Note: We have an exemption for Dev clusters that have alpenglow active at slot 0, + /// as these clusters do not need a genesis block marker + fn requires_genesis_certificate_marker( + migration_status: &MigrationStatus, + parent_slot: Slot, + ) -> bool { + migration_status + .genesis_block() + .is_some_and(|genesis_block| { + genesis_block.slot != 0 && parent_slot == genesis_block.slot + }) + } + /// Process an entry batch. /// /// Validates that a parent marker (header or update parent) has been @@ -153,6 +183,7 @@ impl BlockComponentProcessor { slot: Slot, ) -> Result<(), BlockComponentProcessorError> { if !migration_status.should_allow_block_markers(slot) { + self.has_entry_batch = true; return Ok(()); } @@ -161,6 +192,7 @@ impl BlockComponentProcessor { return Err(BlockComponentProcessorError::MissingParentMarker); } + self.has_entry_batch = true; Ok(()) } @@ -174,9 +206,10 @@ impl BlockComponentProcessor { &mut self, bank: Arc, parent_bank: Arc, + shred_version: u16, marker: VersionedBlockMarker, allow_initial_update_parent: bool, - finalization_cert_sender: Option<&Sender>>, + finalization_cert_sender: Option<&Sender>, migration_status: &MigrationStatus, ) -> Result<(), BlockComponentProcessorError> { let slot = bank.slot(); @@ -184,6 +217,8 @@ impl BlockComponentProcessor { let markers_fully_enabled = migration_status.should_allow_block_markers(slot); let in_migration = migration_status.is_in_migration(); + let fast_leader_handover_active = + bank.feature_set.snapshot().alpenglow_fast_leader_handover; match marker { // Header and genesis cert can be processed either: @@ -192,22 +227,32 @@ impl BlockComponentProcessor { BlockMarkerV1::BlockHeader(header) if markers_fully_enabled || in_migration => { self.on_header(header.inner(), bank.parent_slot()) } - BlockMarkerV1::GenesisCertificate(genesis_cert) + BlockMarkerV1::GenesisCertificate(genesis_cert_block_marker) if markers_fully_enabled || in_migration => { - self.on_genesis_certificate(bank, genesis_cert.into_inner(), migration_status) + self.on_genesis_cert_block_marker( + bank, + shred_version, + genesis_cert_block_marker.into_inner(), + migration_status, + ) } // Everything else is only valid once migration is complete BlockMarkerV1::BlockFooter(footer) if markers_fully_enabled => self.on_footer( bank, parent_bank, + shred_version, footer.into_inner(), finalization_cert_sender, ), BlockMarkerV1::UpdateParent(update_parent) if markers_fully_enabled => { - self.on_update_parent(update_parent.inner(), allow_initial_update_parent) + if fast_leader_handover_active { + self.on_update_parent(slot, update_parent.inner(), allow_initial_update_parent) + } else { + Err(BlockComponentProcessorError::SpuriousUpdateParent) + } } // Any other combination means we saw a marker too early @@ -215,11 +260,55 @@ impl BlockComponentProcessor { } } - pub fn on_genesis_certificate( + /// Processes the genesis block marker with full verification + pub fn on_genesis_cert_block_marker( + &mut self, + bank: Arc, + shred_version: u16, + genesis_block_marker: GenesisCertBlockMarker, + migration_status: &MigrationStatus, + ) -> Result<(), BlockComponentProcessorError> { + self.validate_genesis_cert_block_marker_position()?; + self.process_genesis_cert_block_marker( + bank, + genesis_block_marker, + migration_status, + Some(shred_version), + ) + } + + /// Processes a locally produced genesis certificate marker without + /// re-verifying the certificate signature. + pub fn on_genesis_cert_block_marker_leader( + &mut self, + bank: Arc, + genesis_block_marker: GenesisCertBlockMarker, + migration_status: &MigrationStatus, + ) -> Result<(), BlockComponentProcessorError> { + self.process_genesis_cert_block_marker(bank, genesis_block_marker, migration_status, None) + } + + fn validate_genesis_cert_block_marker_position( &self, + ) -> Result<(), BlockComponentProcessorError> { + if !self.has_header { + return Err(BlockComponentProcessorError::MissingParentMarker); + } + + if self.has_entry_batch || self.has_footer || self.update_parent.is_some() { + return Err(BlockComponentProcessorError::GenesisCertificateOutOfOrder); + } + + Ok(()) + } + + /// Performs verification if `shred_version` is specified + fn process_genesis_cert_block_marker( + &mut self, bank: Arc, - genesis_cert: GenesisCertificate, + genesis_block_marker: GenesisCertBlockMarker, migration_status: &MigrationStatus, + shred_version: Option, ) -> Result<(), BlockComponentProcessorError> { // Genesis Certificate is only allowed for direct child of genesis if bank.parent_slot() == 0 { @@ -229,7 +318,9 @@ impl BlockComponentProcessor { let parent_block_id = bank .parent_block_id() .expect("Block id is populated for all slots > 0"); - if (bank.parent_slot(), parent_block_id) != (genesis_cert.slot, genesis_cert.block_id) { + if (bank.parent_slot(), parent_block_id) + != (genesis_block_marker.slot, genesis_block_marker.block_id) + { return Err(BlockComponentProcessorError::GenesisCertificateOnNonChild); } @@ -237,9 +328,23 @@ impl BlockComponentProcessor { return Err(BlockComponentProcessorError::GenesisCertificateAlreadyPopulated); } - let genesis_cert = Certificate::from(genesis_cert); - Self::verify_genesis_certificate(&bank, &genesis_cert)?; + let genesis_cert = GenesisCert { + block: Block { + slot: genesis_block_marker.slot, + block_id: genesis_block_marker.block_id, + }, + signature: CertSignature { + signature: genesis_block_marker.bls_signature, + bitmap: genesis_block_marker.bitmap, + }, + }; + if let Some(shred_version) = shred_version { + Self::verify_genesis_certificate(&bank, &genesis_cert, shred_version)?; + } + bank.set_alpenglow_genesis_certificate(&genesis_cert); + bank.set_hashes_per_tick(None); + self.has_genesis_certificate_marker = true; if migration_status.is_alpenglow_enabled() { // We participated in the migration, nothing to do @@ -257,12 +362,7 @@ impl BlockComponentProcessor { migration_status.my_pubkey(), bank.slot() ); - migration_status.set_genesis_block( - genesis_cert - .cert_type - .to_block() - .expect("Genesis cert must correspond to a block"), - ); + migration_status.set_genesis_block(genesis_cert.block); migration_status.set_genesis_certificate(Arc::new(genesis_cert)); assert!(migration_status.is_ready_to_enable()); @@ -271,12 +371,17 @@ impl BlockComponentProcessor { fn verify_genesis_certificate( bank: &Bank, - cert: &Certificate, + cert: &GenesisCert, + shred_version: u16, ) -> Result<(), BlockComponentProcessorError> { - debug_assert!(cert.cert_type.is_genesis()); - - let cert_slot = cert.cert_type.slot(); - let (genesis_stake, total_stake) = bank.verify_certificate(cert).map_err(|_| { + let cert_slot = cert.block.slot; + let unverified_cert = UnverifiedCertificate { + cert_type: CertificateType::Genesis(cert.block), + signature: cert.signature.signature, + bitmap: cert.signature.bitmap.clone(), + shred_version, + }; + bank.verify_certificate(unverified_cert).map_err(|_| { warn!( "Failed to verify genesis certificate for slot {cert_slot} in bank slot {}", bank.slot() @@ -284,16 +389,6 @@ impl BlockComponentProcessor { BlockComponentProcessorError::GenesisCertificateFailedVerification })?; - let genesis_percent = Fraction::new(genesis_stake, NonZeroU64::new(total_stake).unwrap()); - if genesis_percent < GENESIS_VOTE_THRESHOLD { - warn!( - "Received a genesis certificate for slot {cert_slot} in bank slot {} with \ - {genesis_percent} stake < {GENESIS_VOTE_THRESHOLD}", - bank.slot() - ); - return Err(BlockComponentProcessorError::GenesisCertificateFailedVerification); - } - Ok(()) } @@ -301,8 +396,9 @@ impl BlockComponentProcessor { &mut self, bank: Arc, parent_bank: Arc, + shred_version: u16, footer: VersionedBlockFooter, - finalization_cert_sender: Option<&Sender>>, + finalization_cert_sender: Option<&Sender>, ) -> Result<(), BlockComponentProcessorError> { if !self.has_header && self.update_parent.is_none() { return Err(BlockComponentProcessorError::MissingParentMarker); @@ -320,18 +416,22 @@ impl BlockComponentProcessor { bank_hash, block_producer_time_nanos, block_user_agent: _, - final_cert, + block_final_cert, skip_reward_cert, notar_reward_cert, } = footer; - let reward_cert = - ValidatedRewardCert::try_new(&bank, &skip_reward_cert, ¬ar_reward_cert)?; + let reward_cert = ValidatedRewardCert::try_new( + &bank, + shred_version, + &skip_reward_cert, + ¬ar_reward_cert, + )?; let block_producer_time_nanos = Self::block_producer_time_nanos_as_i64(block_producer_time_nanos)?; - let final_cert = final_cert + let final_cert = block_final_cert .map(|final_cert| { - ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank) + ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank, shred_version) .map_err(BlockComponentProcessorError::InvalidFinalizationCertificate) }) .transpose()?; @@ -351,7 +451,7 @@ impl BlockComponentProcessor { Self::update_bank_with_footer_fields( &bank, block_producer_time_nanos, - bank_hash, + Some(bank_hash), reward_cert, footer_input .as_ref() @@ -359,19 +459,21 @@ impl BlockComponentProcessor { )?; // Send finalization cert(s) to consensus pool - if let Some((finalize_cert, notarize_cert)) = pool_input { - if let Some(sender) = finalization_cert_sender { - if let Some(notarize_cert) = notarize_cert { - // TODO blocking send. - let _ = sender - .send(vec![ConsensusMessage::from(notarize_cert)]) - .inspect_err(|_| info!("ConsensusMessage sender disconnected")); - } + if let Some((finalize_cert, notarize_cert)) = pool_input + && let Some(sender) = finalization_cert_sender + { + if let Some(notarize_cert) = notarize_cert { + let cert = ConsensusMessage::Certificate(notarize_cert); // TODO blocking send. let _ = sender - .send(vec![ConsensusMessage::from(finalize_cert)]) + .send(cert) .inspect_err(|_| info!("ConsensusMessage sender disconnected")); } + let cert = ConsensusMessage::Certificate(finalize_cert); + // TODO blocking send. + let _ = sender + .send(cert) + .inspect_err(|_| info!("ConsensusMessage sender disconnected")); } self.has_footer = true; @@ -405,6 +507,7 @@ impl BlockComponentProcessor { fn on_update_parent( &mut self, + slot: Slot, update_parent: &VersionedUpdateParent, allow_initial_update_parent: bool, ) -> Result<(), BlockComponentProcessorError> { @@ -412,6 +515,10 @@ impl BlockComponentProcessor { return Err(BlockComponentProcessorError::MultipleUpdateParents); } + if leader_slot_index(slot) != 0 { + return Err(BlockComponentProcessorError::UpdateParentNotFirstInLeaderWindow(slot)); + } + if !self.has_header && !allow_initial_update_parent { return Err(BlockComponentProcessorError::UnexpectedInitialUpdateParent); } @@ -435,21 +542,21 @@ impl BlockComponentProcessor { parent_bank: &Bank, footer: &BlockFooterV1, ) -> Result<(), BlockComponentProcessorError> { - // Get parent time from nanosecond clock account - // If nanosecond clock hasn't been populated, don't enforce the bounds; note that the - // nanosecond clock is populated as soon as Alpenglow migration is complete. - let Some(parent_time_nanos) = parent_bank.get_nanosecond_clock() else { - return Ok(()); - }; + // Get parent time from the nanosecond clock account, or from the Tower-based + // clock for the first Alpenglow block. + let parent_time_nanos = parent_bank + .get_nanosecond_clock() + .unwrap_or_else(|| bank.clock().unix_timestamp.saturating_mul(1_000_000_000)); let parent_slot = parent_bank.slot(); let current_time_nanos = Self::block_producer_time_nanos_as_i64(footer.block_producer_time_nanos)?; let current_slot = bank.slot(); - let ns_per_slot = bank.ns_per_slot.try_into().expect("ns_per_slot overflow"); + let elapsed_slot_duration_nanos = + bank.slot_range_duration_nanos(parent_slot.saturating_add(1), current_slot); let (lower_bound_nanos, upper_bound_nanos) = - Self::nanosecond_time_bounds(parent_slot, parent_time_nanos, current_slot, ns_per_slot); + Self::nanosecond_time_bounds(parent_time_nanos, elapsed_slot_duration_nanos); let is_valid = lower_bound_nanos <= current_time_nanos && current_time_nanos <= upper_bound_nanos; @@ -473,26 +580,24 @@ impl BlockComponentProcessor { .map_err(|_| BlockComponentProcessorError::NanosecondClockOutOfBounds) } - /// Given the parent slot, parent time, slot, and default nanoseconds per - /// slot, calculate the lower and upper bounds for the block producer time. - /// We return (lower_bound, upper_bound), where both bounds are inclusive. - /// I.e., the working bank time is valid if lower_bound <= working_bank_time - /// <= upper_bound. + /// Given a parent time and elapsed slot duration, calculates inclusive + /// block producer timestamp bounds. + /// + /// `parent_time_nanos` describes the parent bank's nanosecond clock. + /// `elapsed_slot_duration_nanos` is the summed duration for all skipped + /// and working slots after the parent. The returned `(lower_bound, + /// upper_bound)` accepts timestamps where + /// `lower_bound <= working_bank_time <= upper_bound`. /// /// Refer to /// https://github.com/solana-foundation/solana-improvement-documents/pull/363 /// for details on the bounds calculation. pub fn nanosecond_time_bounds( - parent_slot: Slot, parent_time_nanos: i64, - slot: Slot, - ns_per_slot: u64, + elapsed_slot_duration_nanos: u128, ) -> (i64, i64) { - let diff_slots = slot.saturating_sub(parent_slot); - let min_working_bank_time = parent_time_nanos.saturating_add(1); - let max_working_bank_time_offset = u128::from(diff_slots) - .saturating_mul(u128::from(ns_per_slot)) + let max_working_bank_time_offset = elapsed_slot_duration_nanos .saturating_mul(2) .min(i64::MAX as u128) as i64; let max_working_bank_time = parent_time_nanos.saturating_add(max_working_bank_time_offset); @@ -503,14 +608,22 @@ impl BlockComponentProcessor { pub fn update_bank_with_footer_fields( bank: &Bank, block_producer_time_nanos: i64, - bank_hash: Hash, + bank_hash: Option, reward_cert: Option, final_cert_input: Option<(&HashSet, Slot)>, ) -> Result<(), BankFooterError> { bank.update_clock_from_footer(block_producer_time_nanos); - calc_vote_rewards_update_vote_states(bank, reward_cert, final_cert_input)?; - // Record expected bank hash from footer for later verification when the bank is frozen. - bank.set_expected_bank_hash(bank_hash); + calc_vote_rewards_update_vote_states( + bank, + reward_cert, + final_cert_input, + block_producer_time_nanos, + )?; + + if let Some(hash) = bank_hash { + // Record expected bank hash from footer for later verification when the bank is frozen. + bank.set_expected_bank_hash(hash); + } Ok(()) } } @@ -524,6 +637,8 @@ mod tests { bank_forks::BankForks, genesis_utils::{activate_all_features_alpenglow, create_genesis_config}, }, + rand::Rng, + solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_clock::DEFAULT_MS_PER_SLOT, solana_entry::block_component::{ BlockFooterV1, BlockHeaderV1, UpdateParentV1, VersionedUpdateParent, @@ -546,7 +661,7 @@ mod tests { } fn create_child_bank( - bank_forks: &Arc>, + bank_forks: &RwLock, parent: &Arc, slot: u64, ) -> Arc { @@ -558,6 +673,38 @@ mod tests { ) } + fn test_genesis_cert_marker() -> GenesisCertBlockMarker { + GenesisCertBlockMarker { + slot: 0, + block_id: Hash::default(), + bls_signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + } + } + + fn post_migration_status_with_genesis_slot(genesis_slot: Slot) -> MigrationStatus { + let migration_status = MigrationStatus::default(); + let migration_slot = migration_status.record_feature_activation(0); + assert!(genesis_slot < migration_slot); + + let genesis_block = Block { + slot: genesis_slot, + block_id: Hash::default(), + }; + migration_status.set_genesis_block(genesis_block); + let cert = Arc::new(GenesisCert { + block: genesis_block, + signature: CertSignature { + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }, + }); + migration_status.set_genesis_certificate(cert); + migration_status.enable_alpenglow_during_startup(); + + migration_status + } + #[test] fn test_missing_header_error_on_entry_batch() { let migration_status = MigrationStatus::post_migration_status(); @@ -571,6 +718,117 @@ mod tests { )); } + #[test] + fn test_missing_header_error_on_genesis_certificate() { + let migration_status = MigrationStatus::post_migration_status(); + let mut processor = BlockComponentProcessor::default(); + let marker = + VersionedBlockMarker::from_genesis_cert_block_marker(test_genesis_cert_marker()); + + let (parent, bank_forks) = create_test_bank(); + let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); + + let result = processor.on_marker( + bank, + parent, + shred_version, + marker, + false, + None, + &migration_status, + ); + assert!(matches!( + result, + Err(BlockComponentProcessorError::MissingParentMarker) + )); + } + + #[test] + fn test_genesis_certificate_after_entry_batch_errors() { + let migration_status = MigrationStatus::post_migration_status(); + let mut processor = BlockComponentProcessor::default(); + let (parent, bank_forks) = create_test_bank(); + let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); + + let header = VersionedBlockMarker::from_block_header(BlockHeaderV1 { + parent_slot: 0, + parent_block_id: Hash::default(), + }); + processor + .on_marker( + bank.clone(), + parent.clone(), + shred_version, + header, + false, + None, + &migration_status, + ) + .unwrap(); + processor + .on_entry_batch(&migration_status, bank.slot()) + .unwrap(); + + let marker = + VersionedBlockMarker::from_genesis_cert_block_marker(test_genesis_cert_marker()); + let result = processor.on_marker( + bank, + parent, + shred_version, + marker, + false, + None, + &migration_status, + ); + assert!(matches!( + result, + Err(BlockComponentProcessorError::GenesisCertificateOutOfOrder) + )); + } + + #[test] + fn test_genesis_certificate_immediately_after_header_passes_order_check() { + let migration_status = MigrationStatus::post_migration_status(); + let mut processor = BlockComponentProcessor::default(); + let (parent, bank_forks) = create_test_bank(); + let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); + + let header = VersionedBlockMarker::from_block_header(BlockHeaderV1 { + parent_slot: 0, + parent_block_id: Hash::default(), + }); + processor + .on_marker( + bank.clone(), + parent.clone(), + shred_version, + header, + false, + None, + &migration_status, + ) + .unwrap(); + + let marker = + VersionedBlockMarker::from_genesis_cert_block_marker(test_genesis_cert_marker()); + let result = processor.on_marker( + bank, + parent, + shred_version, + marker, + false, + None, + &migration_status, + ); + assert!(matches!( + result, + Err(BlockComponentProcessorError::GenesisCertificateInAlpenglowCluster) + )); + } + #[test] fn test_missing_footer_error_on_slot_full() { let migration_status = MigrationStatus::post_migration_status(); @@ -580,13 +838,67 @@ mod tests { }; // Try to mark slot as full without footer - should fail - let result = processor.on_final(&migration_status, 1); + let result = processor.on_final(&migration_status, 1, 0); assert!(matches!( result, Err(BlockComponentProcessorError::MissingBlockFooter) )); } + #[test] + fn test_first_alpenglow_block_requires_genesis_certificate_marker() { + let migration_status = post_migration_status_with_genesis_slot(1); + let processor = BlockComponentProcessor { + has_header: true, + has_footer: true, + ..BlockComponentProcessor::default() + }; + + let result = processor.on_final(&migration_status, 2, 1); + assert!(matches!( + result, + Err(BlockComponentProcessorError::MissingGenesisCertificateMarker) + )); + } + + #[test] + fn test_first_alpenglow_block_with_genesis_certificate_marker_succeeds() { + let migration_status = post_migration_status_with_genesis_slot(1); + let (genesis_bank, bank_forks) = create_test_bank(); + let parent = create_child_bank(&bank_forks, &genesis_bank, 1); + let parent_block_id = Hash::new_unique(); + parent.set_block_id(Some(parent_block_id)); + let bank = create_child_bank(&bank_forks, &parent, 2); + let genesis_marker = GenesisCertBlockMarker { + slot: parent.slot(), + block_id: parent_block_id, + bls_signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }; + let mut processor = BlockComponentProcessor { + has_header: true, + has_footer: true, + ..BlockComponentProcessor::default() + }; + + processor + .on_genesis_cert_block_marker_leader(bank, genesis_marker, &migration_status) + .unwrap(); + assert!(processor.on_final(&migration_status, 2, 1).is_ok()); + } + + #[test] + fn test_first_alpenglow_block_genesis_slot_zero_skips_genesis_certificate_marker_check() { + let migration_status = MigrationStatus::post_migration_status(); + let processor = BlockComponentProcessor { + has_header: true, + has_footer: true, + ..BlockComponentProcessor::default() + }; + + assert!(processor.on_final(&migration_status, 1, 0).is_ok()); + } + #[test] fn test_multiple_headers_error() { let mut processor = BlockComponentProcessor::default(); @@ -615,6 +927,7 @@ mod tests { let (parent, bank_forks) = create_test_bank(); let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); // Calculate valid timestamp based on parent's time let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000); @@ -624,23 +937,29 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); // First footer should succeed - assert!( - processor - .on_footer(bank.clone(), parent.clone(), footer.clone(), None) - .is_ok() - ); + processor + .on_footer( + bank.clone(), + parent.clone(), + shred_version, + footer.clone(), + None, + ) + .unwrap(); // Second footer should fail - let result = processor.on_footer(bank, parent, footer, None); + let err = processor + .on_footer(bank, parent, shred_version, footer, None) + .unwrap_err(); assert!(matches!( - result, - Err(BlockComponentProcessorError::MultipleBlockFooters) + err, + BlockComponentProcessorError::MultipleBlockFooters )); } @@ -653,6 +972,7 @@ mod tests { let (parent, bank_forks) = create_test_bank(); let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); // Calculate valid timestamp based on parent's time let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000); @@ -663,13 +983,13 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); processor - .on_footer(bank.clone(), parent, footer, None) + .on_footer(bank.clone(), parent, shred_version, footer, None) .unwrap(); assert!(processor.has_footer); @@ -711,16 +1031,25 @@ mod tests { fn test_on_marker_processes_header() { let migration_status = MigrationStatus::post_migration_status(); let mut processor = BlockComponentProcessor::default(); - let marker = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let marker = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), }); let (parent, bank_forks) = create_test_bank(); let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); processor - .on_marker(bank, parent, marker, false, None, &migration_status) + .on_marker( + bank, + parent, + shred_version, + marker, + false, + None, + &migration_status, + ) .unwrap(); assert!(processor.has_header); } @@ -729,16 +1058,25 @@ mod tests { fn test_on_marker_rejects_header_parent_slot_mismatch() { let migration_status = MigrationStatus::post_migration_status(); let mut processor = BlockComponentProcessor::default(); - let marker = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let marker = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: 7, // mismatches bank.parent_slot() below (0) parent_block_id: Hash::default(), }); let (parent, bank_forks) = create_test_bank(); let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); assert!(matches!( - processor.on_marker(bank, parent, marker, false, None, &migration_status), + processor.on_marker( + bank, + parent, + shred_version, + marker, + false, + None, + &migration_status + ), Err(BlockComponentProcessorError::HeaderParentSlotMismatch { header_parent_slot: 7, bank_parent_slot: 0, @@ -756,23 +1094,32 @@ mod tests { let (parent, bank_forks) = create_test_bank(); let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); // Calculate valid timestamp based on parent's time let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000); let footer_time_nanos = parent_time_nanos + 300_000_000; // parent + 300ms let expected_time_secs = footer_time_nanos / 1_000_000_000; - let marker = VersionedBlockMarker::new_block_footer(BlockFooterV1 { + let marker = VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); processor - .on_marker(bank.clone(), parent, marker, false, None, &migration_status) + .on_marker( + bank.clone(), + parent, + shred_version, + marker, + false, + None, + &migration_status, + ) .unwrap(); assert!(processor.has_footer); @@ -786,6 +1133,7 @@ mod tests { let mut processor = BlockComponentProcessor::default(); let (parent, bank_forks) = create_test_bank(); let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); // Calculate valid timestamp based on parent's time let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000); @@ -807,12 +1155,12 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); processor - .on_footer(bank.clone(), parent.clone(), footer, None) + .on_footer(bank.clone(), parent.clone(), shred_version, footer, None) .unwrap(); // Verify clock sysvar was updated @@ -829,17 +1177,28 @@ mod tests { let mut processor = BlockComponentProcessor::default(); let (parent, bank_forks) = create_test_bank(); let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); // Try to process a block header marker pre-migration - should fail - let marker = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let marker = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), }); - let result = processor.on_marker(bank, parent, marker, false, None, &migration_status); + let err = processor + .on_marker( + bank, + parent, + shred_version, + marker, + false, + None, + &migration_status, + ) + .unwrap_err(); assert!(matches!( - result, - Err(BlockComponentProcessorError::BlockComponentPreMigration) + err, + BlockComponentProcessorError::BlockComponentPreMigration )); } @@ -848,46 +1207,53 @@ mod tests { let migration_status = MigrationStatus::default(); let (parent, bank_forks) = create_test_bank(); let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000); - let footer_marker = VersionedBlockMarker::new_block_footer(BlockFooterV1 { + let footer_marker = VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: (parent_time_nanos + 500_000_000) as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); let mut processor = BlockComponentProcessor::default(); assert!(matches!( - processor.on_marker( - bank.clone(), - parent.clone(), - footer_marker, - false, - None, - &migration_status - ), - Err(BlockComponentProcessorError::BlockComponentPreMigration) + processor + .on_marker( + bank.clone(), + parent.clone(), + shred_version, + footer_marker, + false, + None, + &migration_status + ) + .unwrap_err(), + BlockComponentProcessorError::BlockComponentPreMigration )); - let update_parent_marker = VersionedBlockMarker::new_update_parent(UpdateParentV1 { + let update_parent_marker = VersionedBlockMarker::from_update_parent(UpdateParentV1 { new_parent_slot: 0, new_parent_block_id: Hash::default(), }); let mut processor = BlockComponentProcessor::default(); assert!(matches!( - processor.on_marker( - bank, - parent, - update_parent_marker, - false, - None, - &migration_status - ), - Err(BlockComponentProcessorError::BlockComponentPreMigration) + processor + .on_marker( + bank, + parent, + shred_version, + update_parent_marker, + false, + None, + &migration_status + ) + .unwrap_err(), + BlockComponentProcessorError::BlockComponentPreMigration )); } @@ -911,9 +1277,10 @@ mod tests { let mut processor = BlockComponentProcessor::default(); let (parent, bank_forks) = create_test_bank(); let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); // Process header marker - let header_marker = VersionedBlockMarker::new_block_header(BlockHeaderV1 { + let header_marker = VersionedBlockMarker::from_block_header(BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), }); @@ -921,6 +1288,7 @@ mod tests { .on_marker( bank.clone(), parent.clone(), + shred_version, header_marker, false, None, @@ -937,11 +1305,11 @@ mod tests { let expected_time_secs = footer_time_nanos / 1_000_000_000; // Process footer marker - let footer_marker = VersionedBlockMarker::new_block_footer(BlockFooterV1 { + let footer_marker = VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); @@ -949,6 +1317,7 @@ mod tests { .on_marker( bank.clone(), parent, + shred_version, footer_marker, false, None, @@ -969,21 +1338,24 @@ mod tests { let mut processor = BlockComponentProcessor::default(); let (parent, bank_forks) = create_test_bank(); let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); let footer = VersionedBlockFooter::V1(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: 1_000_000_000, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); // Try to process footer without header - should fail - let result = processor.on_footer(bank, parent, footer, None); + let err = processor + .on_footer(bank, parent, shred_version, footer, None) + .unwrap_err(); assert!(matches!( - result, - Err(BlockComponentProcessorError::MissingParentMarker) + err, + BlockComponentProcessorError::MissingParentMarker )); } @@ -993,6 +1365,7 @@ mod tests { let mut processor = BlockComponentProcessor::default(); let (parent, bank_forks) = create_test_bank(); let bank = create_child_bank(&bank_forks, &parent, 1); + let shred_version = rand::rng().random(); // Process header first processor.has_header = true; @@ -1003,25 +1376,27 @@ mod tests { let expected_time_secs = footer_time_nanos / 1_000_000_000; // Process footer marker - let footer_marker = VersionedBlockMarker::new_block_footer(BlockFooterV1 { + let footer_marker = VersionedBlockMarker::from_block_footer(BlockFooterV1 { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); // Should succeed - footer is processed - let result = processor.on_marker( - bank.clone(), - parent, - footer_marker, - false, - None, - &migration_status, - ); - assert!(result.is_ok()); + processor + .on_marker( + bank.clone(), + parent, + shred_version, + footer_marker, + false, + None, + &migration_status, + ) + .unwrap(); assert!(processor.has_footer); // Verify clock sysvar was updated @@ -1047,6 +1422,7 @@ mod tests { has_header: true, ..Default::default() }; + let shred_version = rand::rng().random(); // Create genesis bank let genesis_config_info = create_genesis_config(10_000); @@ -1073,14 +1449,13 @@ mod tests { let parent_slot = parent.slot(); let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000); let current_slot = bank.slot(); - let ns_per_slot = bank.ns_per_slot.try_into().expect("ns_per_slot overflow"); + let elapsed_slot_duration_nanos = + bank.slot_range_duration_nanos(parent_slot.saturating_add(1), current_slot); // Use a timestamp in the middle of the valid range let (lower_bound, upper_bound) = BlockComponentProcessor::nanosecond_time_bounds( - parent_slot, parent_time_nanos, - current_slot, - ns_per_slot, + elapsed_slot_duration_nanos, ); let footer_time_nanos = (lower_bound + upper_bound) / 2; let expected_time_secs = footer_time_nanos / 1_000_000_000; @@ -1089,13 +1464,13 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); processor - .on_footer(bank.clone(), parent, footer, None) + .on_footer(bank.clone(), parent, shred_version, footer, None) .unwrap(); // Verify clock sysvar was updated @@ -1115,6 +1490,7 @@ mod tests { has_header: true, ..Default::default() }; + let shred_version = rand::rng().random(); let (parent, bank_forks) = create_test_bank_alpenglow(); let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000); @@ -1123,13 +1499,11 @@ mod tests { parent.update_clock_from_footer(parent_time_nanos); let bank: Arc = create_child_bank(&bank_forks, &parent, slot_gap); - let ns_per_slot = bank.ns_per_slot.try_into().expect("ns_per_slot overflow"); + let elapsed_slot_duration_nanos = bank.slot_range_duration_nanos(1, slot_gap); let (lower_bound, upper_bound) = BlockComponentProcessor::nanosecond_time_bounds( - 0, parent_time_nanos, - slot_gap, - ns_per_slot, + elapsed_slot_duration_nanos, ); let footer_time_nanos = timestamp_fn(parent_time_nanos, lower_bound, upper_bound); @@ -1138,18 +1512,18 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: footer_time_nanos as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); - let result = processor.on_footer(bank, parent, footer, None); + let result = processor.on_footer(bank, parent, shred_version, footer, None); if should_pass { - assert!(result.is_ok()); + result.unwrap(); } else { assert!(matches!( - result, - Err(BlockComponentProcessorError::NanosecondClockOutOfBounds) + result.unwrap_err(), + BlockComponentProcessorError::NanosecondClockOutOfBounds )); } } @@ -1193,12 +1567,50 @@ mod tests { test_clock_bounds_helper(1, |parent_time, _, _| parent_time, false); } + #[test] + fn test_clock_bounds_without_parent_nanosecond_clock_rejects_out_of_bounds() { + let mut processor = BlockComponentProcessor { + has_header: true, + ..Default::default() + }; + let shred_version = rand::rng().random(); + + let (parent, bank_forks) = create_test_bank_alpenglow(); + assert_eq!(parent.get_nanosecond_clock(), None); + + let bank = create_child_bank(&bank_forks, &parent, 1); + let parent_time_nanos = bank.clock().unix_timestamp.saturating_mul(1_000_000_000); + let elapsed_slot_duration_nanos = + bank.slot_range_duration_nanos(parent.slot().saturating_add(1), bank.slot()); + let (_, upper_bound) = BlockComponentProcessor::nanosecond_time_bounds( + parent_time_nanos, + elapsed_slot_duration_nanos, + ); + + let footer = VersionedBlockFooter::V1(BlockFooterV1 { + bank_hash: Hash::new_unique(), + block_producer_time_nanos: u64::try_from(upper_bound.saturating_add(1)).unwrap(), + block_user_agent: vec![], + block_final_cert: None, + skip_reward_cert: None, + notar_reward_cert: None, + }); + + assert!(matches!( + processor + .on_footer(bank, parent, shred_version, footer, None) + .unwrap_err(), + BlockComponentProcessorError::NanosecondClockOutOfBounds + )); + } + #[test] fn test_clock_bounds_rejects_timestamp_above_i64() { let mut processor = BlockComponentProcessor { has_header: true, ..Default::default() }; + let shred_version = rand::rng().random(); let (parent, bank_forks) = create_test_bank_alpenglow(); let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000); @@ -1209,31 +1621,29 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: u64::MAX, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); assert!(matches!( - processor.on_footer(bank, parent, footer, None), - Err(BlockComponentProcessorError::NanosecondClockOutOfBounds) + processor + .on_footer(bank, parent, shred_version, footer, None) + .unwrap_err(), + BlockComponentProcessorError::NanosecondClockOutOfBounds )); } // Helper function to test nanosecond_time_bounds calculation fn test_nanosecond_time_bounds_helper( - parent_slot: u64, parent_time_nanos: i64, - working_slot: u64, - ns_per_slot: u64, + elapsed_slot_duration_nanos: u128, expected_lower: i64, expected_upper: i64, ) { let (lower, upper) = BlockComponentProcessor::nanosecond_time_bounds( - parent_slot, parent_time_nanos, - working_slot, - ns_per_slot, + elapsed_slot_duration_nanos, ); assert_eq!(lower, expected_lower); @@ -1251,10 +1661,8 @@ mod tests { let working_slot = 15; let slot_delta = working_slot - parent_slot; test_nanosecond_time_bounds_helper( - parent_slot, parent_time, - working_slot, - DEFAULT_NS_PER_SLOT, + u128::from(slot_delta).saturating_mul(u128::from(DEFAULT_NS_PER_SLOT)), parent_time + 1, parent_time + (2 * DEFAULT_NS_PER_SLOT * slot_delta) as i64, ); @@ -1269,21 +1677,14 @@ mod tests { // Note: In this case, lower > upper, so no timestamp would be valid // This is expected since we shouldn't have the same slot for parent and working bank let parent_time = 1_000_000_000_000; - test_nanosecond_time_bounds_helper( - 10, - parent_time, - 10, - DEFAULT_NS_PER_SLOT, - parent_time + 1, - parent_time, - ); + test_nanosecond_time_bounds_helper(parent_time, 0, parent_time + 1, parent_time); } #[test] fn test_nanosecond_time_bounds_saturates_upper_bound() { let parent_time = i64::MAX - 5; let (lower, upper) = - BlockComponentProcessor::nanosecond_time_bounds(0, parent_time, u64::MAX, u64::MAX); + BlockComponentProcessor::nanosecond_time_bounds(parent_time, u128::MAX); assert_eq!(lower, parent_time + 1); assert_eq!(upper, i64::MAX); @@ -1298,12 +1699,27 @@ mod tests { }); assert!(matches!( - processor.on_update_parent(&update_parent, false), + processor.on_update_parent(4, &update_parent, false), Err(BlockComponentProcessorError::UnexpectedInitialUpdateParent) )); assert!(processor.update_parent.is_none()); } + #[test] + fn test_update_parent_rejects_non_first_leader_window_slot() { + let mut processor = BlockComponentProcessor::default(); + let update_parent = VersionedUpdateParent::V1(UpdateParentV1 { + new_parent_slot: 0, + new_parent_block_id: Hash::default(), + }); + + assert!(matches!( + processor.on_update_parent(5, &update_parent, true), + Err(BlockComponentProcessorError::UpdateParentNotFirstInLeaderWindow(5)) + )); + assert!(processor.update_parent.is_none()); + } + #[test] fn test_initial_up_ok() { let mut processor = BlockComponentProcessor::default(); @@ -1312,7 +1728,7 @@ mod tests { new_parent_block_id: Hash::default(), }); - processor.on_update_parent(&update_parent, true).unwrap(); + processor.on_update_parent(4, &update_parent, true).unwrap(); assert!(processor.update_parent.is_some()); } @@ -1335,7 +1751,7 @@ mod tests { }); assert!(matches!( - processor.on_update_parent(&update_parent, false), + processor.on_update_parent(4, &update_parent, false), Err(BlockComponentProcessorError::AbandonedBank(_)) )); } @@ -1349,11 +1765,11 @@ mod tests { }); // First should succeed - processor.on_update_parent(&update_parent, true).unwrap(); + processor.on_update_parent(4, &update_parent, true).unwrap(); // Second should fail assert!(matches!( - processor.on_update_parent(&update_parent, true), + processor.on_update_parent(4, &update_parent, true), Err(BlockComponentProcessorError::MultipleUpdateParents) )); } @@ -1363,6 +1779,7 @@ mod tests { let mut processor = BlockComponentProcessor::default(); processor .on_update_parent( + 4, &VersionedUpdateParent::V1(UpdateParentV1 { new_parent_slot: 0, new_parent_block_id: Hash::default(), @@ -1383,15 +1800,16 @@ mod tests { } #[test] - #[ignore] // TODO(ksn): Enable when fast leader handover is enabled in MigrationPhase::should_allow_fast_leader_handover fn test_workflow_with_update_parent() { let migration_status = MigrationStatus::post_migration_status(); let mut processor = BlockComponentProcessor::default(); let (parent, bank_forks) = create_test_bank(); - let bank = create_child_bank(&bank_forks, &parent, 1); + let bank = create_child_bank(&bank_forks, &parent, 4); + let shred_version = rand::rng().random(); processor .on_update_parent( + bank.slot(), &VersionedUpdateParent::V1(UpdateParentV1 { new_parent_slot: 0, new_parent_block_id: Hash::default(), @@ -1407,12 +1825,14 @@ mod tests { bank_hash: Hash::new_unique(), block_producer_time_nanos: (parent_time_nanos + 100_000_000) as u64, block_user_agent: vec![], - final_cert: None, + block_final_cert: None, skip_reward_cert: None, notar_reward_cert: None, }); - processor.on_footer(bank, parent, footer, None).unwrap(); + processor + .on_footer(bank, parent, shred_version, footer, None) + .unwrap(); - assert!(processor.on_final(&migration_status, 1).is_ok()); + assert!(processor.on_final(&migration_status, 1, 0).is_ok()); } } diff --git a/runtime/src/block_component_processor/vote_reward.rs b/runtime/src/block_component_processor/vote_reward.rs index 8e31f9f0dc4..c77ab43ad07 100644 --- a/runtime/src/block_component_processor/vote_reward.rs +++ b/runtime/src/block_component_processor/vote_reward.rs @@ -1,18 +1,25 @@ use { crate::{bank::Bank, validated_reward_certificate::ValidatedRewardCert}, + agave_votor_messages::migration::AG_MIGRATION_EPOCH_CREDIT, epoch_inflation_account_state::{EpochInflationAccountState, EpochInflationState}, log::info, solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, solana_clock::{Epoch, Slot}, solana_pubkey::Pubkey, solana_vote::vote_account::VoteAccount, - solana_vote_interface::state::{LandedVote, Lockout}, + solana_vote_interface::state::{ + BlockTimestamp, LandedVote, Lockout, MAX_EPOCH_CREDITS_HISTORY, + }, solana_vote_program::vote_state::handler::VoteStateHandler, - std::collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, + std::{ + collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, + num::NonZero, + }, thiserror::Error, }; -pub mod epoch_inflation_account_state; +pub(crate) mod epoch_inflation_account_state; +mod migration_test; /// Different types of errors that can happen when calculating and paying voting reward. /// @@ -30,16 +37,10 @@ pub enum CalcVoteRewardUpdateVoteStatesError { /// These errors should cause the processing of the bank to fail. #[derive(Debug, Error)] pub enum AllocateAccountsError { - #[error("did not find rank map for final_slot={final_slot} in current_slot={current_slot}")] - FinalCert { - current_slot: Slot, - final_slot: Slot, - }, - #[error("did not find rank map for reward_slot={reward_slot} in current_slot={current_slot}")] - RewardCert { - current_slot: Slot, - reward_slot: Slot, - }, + #[error("did not find rank map for final_slot={final_slot} in bank_slot={bank_slot}")] + FinalCert { bank_slot: Slot, final_slot: Slot }, + #[error("did not find rank map for reward_slot={reward_slot} in bank_slot={bank_slot}")] + RewardCert { bank_slot: Slot, reward_slot: Slot }, } /// Different types of error that happen when looking up state to process the reward cert. @@ -47,32 +48,29 @@ pub enum AllocateAccountsError { /// These errors should cause the processing of the bank to fail. #[derive(Debug, Error)] pub enum RewardStateError { - #[error("missing epoch stakes for reward_slot {reward_slot} in current_slot {current_slot}")] - MissingEpochStakes { - reward_slot: Slot, - current_slot: Slot, - }, - #[error("missing EpochInflationAccountState for current slot {current_slot}")] - MissingEpochInflationAccountState { current_slot: Slot }, + #[error("missing epoch stakes for reward_slot {reward_slot} in bank_slot {bank_slot}")] + MissingEpochStakes { reward_slot: Slot, bank_slot: Slot }, + #[error("missing EpochInflationAccountState for bank_slot {bank_slot}")] + MissingEpochInflationAccountState { bank_slot: Slot }, #[error( - "missing validator stake info for reward epoch {reward_epoch} in current_slot \ - {current_slot}" + "missing validator stake info for reward epoch {reward_epoch} in bank_slot {bank_slot}" )] NoEpochValidatorStake { reward_epoch: Epoch, - current_slot: Slot, + bank_slot: Slot, }, - #[error( - "validator {pubkey} missing in current slot {current_slot} for reward slot {reward_slot}" - )] + #[error("validator {pubkey} missing in bank_slot {bank_slot} for reward slot {reward_slot}")] MissingRewardSlotValidator { pubkey: Pubkey, reward_slot: Slot, - current_slot: Slot, + bank_slot: Slot, }, + #[error("genesis cert not found. reward_slot={reward_slot}; bank_slot={bank_slot}")] + GenesisCertNotFound { reward_slot: Slot, bank_slot: Slot }, } /// Data needed to operate on `VoteStateHandler`. +#[derive(Debug)] struct VoteState { /// The pubkey of the vote account. vote_pubkey: Pubkey, @@ -135,9 +133,8 @@ impl VoteState { } } - /// Sets the slot in `votes` in the vote state to the max of `slot`, vote_state.root_slot, and - /// vote_state.last_voted_slot. - fn maybe_update_votes(&mut self, slot: Slot) { + /// Updates `votes` and `last_timestamp` in the vote state. + fn maybe_update_votes(&mut self, slot: Slot, slot_timestamp_ns: i64) { let latest_voted_slot = slot .max(self.handler.last_voted_slot().unwrap_or(0)) .max(self.handler.root_slot().unwrap_or(0)); @@ -145,6 +142,12 @@ impl VoteState { lockout: Lockout::new(latest_voted_slot), latency: 0, }])); + + let timestamp = slot_timestamp_ns / 1_000_000_000; + if timestamp > self.handler.last_timestamp().timestamp { + self.handler + .set_last_timestamp(BlockTimestamp { slot, timestamp }); + } } /// If `slot` is bigger than vote_state.root_slot, then updates vote_state.root_slot. @@ -157,6 +160,7 @@ impl VoteState { /// Common state required to pay rewards. #[derive(Debug)] struct RewardState<'a> { + reward_slot_timestamp_ns: i64, /// The epoch in which the reward was paid into the vote account. current_epoch: Epoch, /// The slot in which the reward was earned. @@ -164,7 +168,7 @@ struct RewardState<'a> { /// Validators present in the reward cert. reward_validators: &'a HashSet, /// The slot in which the reward is being paid into the vote account. - current_slot: Slot, + bank_slot: Slot, /// The pubkey of the validator that will receive the leader rewards. leader_vote_pubkey: Pubkey, /// Vote accounts at reward slot. @@ -173,6 +177,7 @@ struct RewardState<'a> { total_stake: u64, /// inflation state at `reward_slot`. epoch_inflation_state: EpochInflationState, + migration_epoch: Epoch, } impl<'a> RewardState<'a> { @@ -180,12 +185,13 @@ impl<'a> RewardState<'a> { bank: &'a Bank, reward_slot: Slot, reward_validators: &'a HashSet, + block_producer_time_nanos: i64, ) -> Result { - let current_slot = bank.slot(); + let bank_slot = bank.slot(); let epoch_stakes = bank.epoch_stakes_from_slot(reward_slot).ok_or( RewardStateError::MissingEpochStakes { reward_slot, - current_slot, + bank_slot, }, )?; let accounts = epoch_stakes.stakes().vote_accounts().as_ref(); @@ -199,22 +205,31 @@ impl<'a> RewardState<'a> { // that activated Alpenglow should have created the account. debug_assert!(epoch_inflation_account_state.is_some()); epoch_inflation_account_state - .ok_or(RewardStateError::MissingEpochInflationAccountState { current_slot })? + .ok_or(RewardStateError::MissingEpochInflationAccountState { bank_slot })? .get_epoch_state(reward_epoch) .ok_or(RewardStateError::NoEpochValidatorStake { reward_epoch, - current_slot, + bank_slot, })? }; + let migration_epoch = + get_migration_epoch(bank).ok_or(RewardStateError::GenesisCertNotFound { + reward_slot, + bank_slot, + })?; + let reward_slot_timestamp_ns = + calc_slot_timestamp(bank, reward_slot, block_producer_time_nanos); Ok(Self { + reward_slot_timestamp_ns, current_epoch: bank.epoch(), reward_slot, reward_validators, - current_slot, + bank_slot, leader_vote_pubkey: bank.leader().vote_address, accounts, total_stake, epoch_inflation_state, + migration_epoch, }) } @@ -232,7 +247,7 @@ impl<'a> RewardState<'a> { .ok_or(RewardStateError::MissingRewardSlotValidator { pubkey: validator, reward_slot: self.reward_slot, - current_slot: self.current_slot, + bank_slot: self.bank_slot, })?; let (validator_reward, leader_reward) = calculate_reward( &self.epoch_inflation_state, @@ -245,7 +260,7 @@ impl<'a> RewardState<'a> { fn update_votes(&self, vote_state: &mut VoteState) { debug_assert!(self.reward_validators.contains(&vote_state.vote_pubkey)); - vote_state.maybe_update_votes(self.reward_slot); + vote_state.maybe_update_votes(self.reward_slot, self.reward_slot_timestamp_ns); } fn update_account( @@ -257,11 +272,14 @@ impl<'a> RewardState<'a> { self.update_votes(vote_state); let reward = self.calculate_reward(vote_state.vote_pubkey, accumulating_leader_reward)?; - if reward != 0 { - vote_state - .handler - .increment_credits(self.current_epoch, reward); - } + if let Some(reward) = NonZero::new(reward) { + increment_credits( + vote_state.handler.epoch_credits_mut(), + self.migration_epoch, + self.current_epoch, + reward, + ); + }; Ok(true) } else { Ok(false) @@ -270,17 +288,14 @@ impl<'a> RewardState<'a> { /// Assumes that `Self::update_account` was already called and pays the additional /// `leader_rewards` to the account. - #[must_use] - fn update_leader(&self, vote_state: &mut VoteState, leader_reward: u64) -> bool { + fn update_leader(&self, vote_state: &mut VoteState, leader_reward: NonZero) { debug_assert_eq!(vote_state.vote_pubkey, self.leader_vote_pubkey); - if leader_reward != 0 { - vote_state - .handler - .increment_credits(self.current_epoch, leader_reward); - true - } else { - false - } + increment_credits( + vote_state.handler.epoch_credits_mut(), + self.migration_epoch, + self.current_epoch, + leader_reward, + ); } } @@ -289,9 +304,25 @@ impl<'a> RewardState<'a> { struct FinalCertState<'a> { signers: &'a HashSet, final_slot: Slot, + final_slot_timestamp_ns: i64, } impl<'a> FinalCertState<'a> { + fn new( + bank: &Bank, + signers: &'a HashSet, + final_slot: Slot, + block_producer_time_nanos: i64, + ) -> Self { + let final_slot_timestamp_ns = + calc_slot_timestamp(bank, final_slot, block_producer_time_nanos); + Self { + signers, + final_slot, + final_slot_timestamp_ns, + } + } + /// Updates the `root_slot` and the `votes` fields in the `VoteStateHandler`. #[must_use] fn update_account(&self, vote_state: &mut VoteState) -> bool { @@ -299,7 +330,7 @@ impl<'a> FinalCertState<'a> { vote_state.maybe_update_root(self.final_slot); // If a validator is included in the finalization cert, it must have voted for it. // So even if the reward cert is absent, we can still update votes. - vote_state.maybe_update_votes(self.final_slot); + vote_state.maybe_update_votes(self.final_slot, self.final_slot_timestamp_ns); true } else { false @@ -326,14 +357,14 @@ fn allocate_updated_accounts( let final_cert_slot_max_validators = bank .get_rank_map(*slot) .ok_or(AllocateAccountsError::FinalCert { - current_slot: bank.slot(), + bank_slot: bank.slot(), final_slot: *slot, })? .len(); let reward_cert_slot_max_validators = bank .get_rank_map(reward_cert.slot()) .ok_or(AllocateAccountsError::RewardCert { - current_slot: bank.slot(), + bank_slot: bank.slot(), reward_slot: reward_cert.slot(), })? .len(); @@ -372,16 +403,18 @@ fn update_accounts( } // all validators have been processed, can pay leader rewards now. - if let Some(state) = &reward_state { + if let Some(state) = &reward_state + && let Some(leader_reward) = NonZero::new(leader_reward) + { match updated_accounts.entry(state.leader_vote_pubkey) { Entry::Occupied(e) => { - let _ = state.update_leader(e.into_mut(), leader_reward); + state.update_leader(e.into_mut(), leader_reward); } Entry::Vacant(e) => { if let Some(mut vote_state) = VoteState::try_new(vote_accounts, state.leader_vote_pubkey) - && state.update_leader(&mut vote_state, leader_reward) { + state.update_leader(&mut vote_state, leader_reward); e.insert(vote_state); } } @@ -400,18 +433,23 @@ pub(super) fn calc_vote_rewards_update_vote_states( bank: &Bank, reward_cert: Option, final_cert_input: Option<(&HashSet, Slot)>, + block_producer_time_nanos: i64, ) -> Result<(), CalcVoteRewardUpdateVoteStatesError> { let Some(updated_accounts) = allocate_updated_accounts(bank, &reward_cert, &final_cert_input)? else { return Ok(()); }; let reward_state = match &reward_cert { - Some(c) => Some(RewardState::try_new(bank, c.slot(), c.validators())?), + Some(c) => Some(RewardState::try_new( + bank, + c.slot(), + c.validators(), + block_producer_time_nanos, + )?), None => None, }; - let final_cert_state = final_cert_input.map(|(signers, final_slot)| FinalCertState { - signers, - final_slot, + let final_cert_state = final_cert_input.map(|(signers, final_slot)| { + FinalCertState::new(bank, signers, final_slot, block_producer_time_nanos) }); let vote_accounts = bank.vote_accounts(); @@ -472,6 +510,93 @@ fn calculate_reward( (validator_reward_lamports, leader_reward_lamports) } +fn calc_slot_timestamp(bank: &Bank, slot: Slot, block_producer_time_nanos: i64) -> i64 { + block_producer_time_nanos.saturating_sub( + i64::try_from(bank.slot_range_duration_nanos(slot + 1, bank.slot())).unwrap(), + ) +} + +fn get_migration_epoch(bank: &Bank) -> Option { + let migration_slot = bank.get_alpenglow_migration_slot()?; + let migration_epoch = bank.epoch_schedule.get_epoch(migration_slot); + Some(migration_epoch) +} + +fn ensure_marker(epoch_credits: &mut Vec<(Epoch, u64, u64)>) { + for elem in epoch_credits.iter().rev() { + if elem == &AG_MIGRATION_EPOCH_CREDIT { + return; + } + } + epoch_credits.push(AG_MIGRATION_EPOCH_CREDIT); +} + +fn increment_credits( + epoch_credits: &mut Vec<(Epoch, u64, u64)>, + migration_epoch: Epoch, + epoch: Epoch, + new_credits: NonZero, +) { + if epoch == migration_epoch { + ensure_marker(epoch_credits); + } + + let Some(entry) = epoch_credits.last_mut() else { + // no entries, insert a new entry and we are done. + epoch_credits.push((epoch, new_credits.get(), 0)); + return; + }; + + // Latest element is the marker, start a new entry. + if *entry == AG_MIGRATION_EPOCH_CREDIT { + // If there was a tower entry before, its final credits forms this entry's initial credits. + let len = epoch_credits.len(); + let final_tower_credits = if len >= 2 { + assert_ne!(epoch_credits[len - 2], AG_MIGRATION_EPOCH_CREDIT); + epoch_credits[len - 2].1 + } else { + 0 + }; + epoch_credits.push(( + epoch, + new_credits.get().saturating_add(final_tower_credits), + final_tower_credits, + )); + while epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY { + epoch_credits.remove(0); + } + return; + } + + let (entry_epoch, final_credits, initial_credits) = entry; + + // Latest element is the same epoch, simply increment final credits. + if *entry_epoch == epoch { + *final_credits = final_credits.saturating_add(new_credits.get()); + return; + } + + // Different epochs but the latest epoch didn't earn any credits, reuse the entry. + if final_credits == initial_credits { + *entry_epoch = epoch; + *final_credits = final_credits.saturating_add(new_credits.get()); + return; + } + + // Different epochs and the latest epoch earned credits, insert a new entry. + let entry = ( + epoch, + new_credits.get().saturating_add(*final_credits), + *final_credits, + ); + epoch_credits.push(entry); + + // maybe included a marker and a new entry above. So might have multiple entries to remove here. + while epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY { + epoch_credits.remove(0); + } +} + #[cfg(test)] mod tests { use { @@ -479,19 +604,27 @@ mod tests { crate::{ bank_forks::BankForks, genesis_utils::{ - ValidatorVoteKeypairs, create_genesis_config_with_alpenglow_vote_accounts, + ValidatorVoteKeypairs, activate_all_features_alpenglow, + create_genesis_config_with_alpenglow_vote_accounts, + create_genesis_config_with_leader_ex, create_validator, }, + inflation_rewards::commission_split_preserve_lamports, + stake_utils, validated_block_finalization::ValidatedBlockFinalizationCert, }, + agave_feature_set::FeatureSet, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType}, + certificate::{CertSignature, FastFinalizeCert}, + consensus_message::Block, reward_certificate::NUM_SLOTS_FOR_REWARD, }, bitvec::prelude::*, rand::seq::IndexedRandom, - solana_account::ReadableAccount, + solana_account::{Account, ReadableAccount, WritableAccount}, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, + solana_cluster_type::ClusterType, solana_epoch_schedule::EpochSchedule, + solana_fee_calculator::FeeRateGovernor, solana_genesis_config::GenesisConfig, solana_hash::Hash, solana_keypair::Keypair, @@ -500,7 +633,14 @@ mod tests { solana_rent::Rent, solana_signer::Signer, solana_signer_store::encode_base2, - std::sync::{Arc, RwLock}, + solana_stake_interface::state::StakeStateV2, + solana_vote_interface::state::{VoteStateV4, VoteStateVersions}, + std::{ + collections::HashMap, + sync::{Arc, RwLock}, + time::{SystemTime, UNIX_EPOCH}, + }, + test_case::test_matrix, }; fn new_bank_for_tests( @@ -512,7 +652,7 @@ mod tests { bank.wrap_with_bank_forks_for_tests() } - fn new_bank_from_parent(parent_bank: Arc, slot: Slot) -> Arc { + pub fn new_bank_from_parent(parent_bank: Arc, slot: Slot) -> Arc { let leader = *parent_bank.leader(); Arc::new(Bank::new_from_parent(parent_bank, leader, slot)) } @@ -532,20 +672,22 @@ mod tests { bank: &Bank, signing_ranks: &[usize], ) -> ValidatedBlockFinalizationCert { - let slot = bank.slot(); - let block_id = Hash::new_unique(); - let cert_type = CertificateType::FinalizeFast(slot, block_id); + let block = Block { + slot: bank.slot(), + block_id: Hash::new_unique(), + }; let max_rank = signing_ranks.iter().copied().max().unwrap_or(0); let mut bitvec = BitVec::::repeat(false, max_rank.saturating_add(1)); for &rank in signing_ranks { bitvec.set(rank, true); } let bitmap = encode_base2(&bitvec).unwrap(); - - let cert = Certificate { - cert_type, - signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), - bitmap, + let cert = FastFinalizeCert { + block, + signature: CertSignature { + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap, + }, }; ValidatedBlockFinalizationCert::from_validated_fast(cert, bank) } @@ -584,17 +726,20 @@ mod tests { } fn calc_reward_for_test( - prev_bank: &Bank, bank: &Bank, + epoch_start_capitalization: u64, + reward_epoch: Epoch, total_stake: u64, stake_voted: u64, - ) -> u64 { + ) -> (u64, u64) { let epoch_inflation = - bank.calculate_epoch_inflation_rewards(prev_bank.capitalization(), prev_bank.epoch()); + bank.calculate_epoch_inflation_rewards(epoch_start_capitalization, reward_epoch); let numerator = epoch_inflation as u128 * stake_voted as u128; let denominator = bank.epoch_schedule.slots_per_epoch as u128 * total_stake as u128; let reward: u64 = (numerator / denominator).try_into().unwrap(); - reward / 2 + let validator_reward = reward / 2; + let leader_reward = reward - validator_reward; + (validator_reward, leader_reward) } #[test] @@ -638,6 +783,10 @@ mod tests { let bank = new_bank_from_parent(prev_bank.clone(), current_slot); let reward_slot = current_slot - NUM_SLOTS_FOR_REWARD; + let block_producer_time_nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as i64; calc_vote_rewards_update_vote_states( &bank, Some(ValidatedRewardCert::new_for_tests( @@ -645,8 +794,14 @@ mod tests { validator_pubkeys_to_reward.clone(), )), None, + block_producer_time_nanos, ) .unwrap(); + let slot_timestamp = BlockTimestamp { + slot: reward_slot, + timestamp: calc_slot_timestamp(&bank, reward_slot, block_producer_time_nanos) + / 1_000_000_000, + }; let vote_accounts = bank.vote_accounts(); let rewards = validator_pubkeys_to_reward @@ -660,18 +815,33 @@ mod tests { .epoch_stakes_from_slot(reward_slot) .unwrap() .total_stake(); - let expected_validator_reward = - calc_reward_for_test(&prev_bank, &bank, total_stake, per_validator_stake); + let reward_epoch = bank.epoch_schedule.get_epoch(reward_slot); + assert_eq!(vote_state.last_timestamp(), &slot_timestamp); + let (expected_validator_reward, expected_leader_reward_per_validator) = + calc_reward_for_test( + &bank, + prev_bank.capitalization(), + reward_epoch, + total_stake, + per_validator_stake, + ); if *validator != leader_vote_pubkey { assert_eq!(got_reward, expected_validator_reward); } - got_reward + ( + got_reward, + expected_validator_reward, + expected_leader_reward_per_validator, + ) }) .collect::>(); - let expected_leader_reward = rewards.last().unwrap() - * validator_pubkeys_to_reward.len() as u64 - + rewards.last().unwrap(); - assert_eq!(expected_leader_reward, rewards[0]); + let (leader_reward, expected_validator_reward, expected_leader_reward_per_validator) = + rewards[0]; + assert_eq!( + expected_validator_reward + + expected_leader_reward_per_validator * validator_pubkeys_to_reward.len() as u64, + leader_reward + ); } #[test] @@ -729,15 +899,19 @@ mod tests { vec![target_vote_pubkey], )), final_cert_input, + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as i64, ) .unwrap(); let handle = vote_state_from_bank(&bank, &target_vote_pubkey); - assert_eq!(handle.root_slot(), Some(final_cert.slot())); + assert_eq!(handle.root_slot(), Some(final_cert.slot().slot())); assert_eq!(handle.votes().len(), 1); assert_eq!( handle.votes().front().unwrap().lockout.slot(), - final_cert.slot().max(reward_slot) + final_cert.slot().slot().max(reward_slot) ); } @@ -797,6 +971,10 @@ mod tests { vec![target_vote_pubkey], )), final_cert_input, + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as i64, ) .unwrap(); @@ -856,6 +1034,10 @@ mod tests { vec![vote_pubkey], )), None, + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as i64, ) .unwrap(); let vote_accounts = bank.vote_accounts(); @@ -868,4 +1050,402 @@ mod tests { } } } + + pub fn set_commission( + genesis_config: &mut GenesisConfig, + validators: &[ValidatorVoteKeypairs], + commission_bps: u16, + ) { + for validator in validators { + let vote_pubkey = validator.vote_keypair.pubkey(); + let account = genesis_config.accounts.get_mut(&vote_pubkey).unwrap(); + let vote_state_versions = bincode::deserialize(&account.data).unwrap(); + let VoteStateVersions::V4(mut vote_state) = vote_state_versions else { + panic!(); + }; + vote_state.inflation_rewards_commission_bps = commission_bps; + VoteStateV4::serialize( + &VoteStateVersions::V4(vote_state), + account.data_as_mut_slice(), + ) + .unwrap(); + } + } + + struct State { + commission_bps: u16, + _bank_forks: Arc>, + validators: Vec, + /// key is validator pubkey, value is list of stakers. + stakers: HashMap>, + } + + impl State { + fn new( + num_validators: u64, + num_add_stakers: u64, + pay_leader: bool, + commission_bps: u16, + ) -> (Self, Arc) { + let lamports = LAMPORTS_PER_SOL * 20; + let mint_keypair = Keypair::new(); + let validators = (0..num_validators) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + let leader = if pay_leader { + let vote_pubkey = validators[0].vote_keypair.pubkey(); + let node_pubkey = validators[0].node_keypair.pubkey(); + SlotLeader { + id: node_pubkey, + vote_address: vote_pubkey, + } + } else { + SlotLeader::new_unique() + }; + let mut genesis_config = create_genesis_config_with_leader_ex( + lamports, + &mint_keypair.pubkey(), + &validators[0].node_keypair.pubkey(), + &validators[0].vote_keypair.pubkey(), + &validators[0].stake_keypair.pubkey(), + Some(validators[0].bls_keypair.public.to_bytes_compressed()), + lamports, + lamports, + FeeRateGovernor::new(0, 0), + Rent::default(), + ClusterType::Development, + &FeatureSet::all_enabled(), + vec![], + ); + genesis_config.epoch_schedule = EpochSchedule::without_warmup(); + activate_all_features_alpenglow(&mut genesis_config); + for (ind, keypair) in validators.iter().enumerate().skip(1) { + let node_pubkey = keypair.node_keypair.pubkey(); + let vote_pubkey = keypair.vote_keypair.pubkey(); + let stake_pubkey = keypair.stake_keypair.pubkey(); + let bls_pubkey = Some(keypair.bls_keypair.public.to_bytes_compressed()); + let lamports = lamports + ind as u64 * LAMPORTS_PER_SOL; + let accounts = create_validator( + &genesis_config.rent, + node_pubkey, + lamports, + vote_pubkey, + lamports, + stake_pubkey, + lamports, + bls_pubkey, + ) + .into_iter() + .map(|(pubkey, account)| (pubkey, Account::from(account))); + genesis_config.accounts.extend(accounts); + } + set_commission(&mut genesis_config, &validators, commission_bps); + + let vote_account = genesis_config + .accounts + .get(&validators[0].vote_keypair.pubkey()) + .unwrap() + .clone() + .into(); + + let staker_keypairs = (0..num_add_stakers) + .map(|_| Keypair::new()) + .collect::>(); + for (ind, keypair) in staker_keypairs.iter().enumerate() { + let stake_pubkey = keypair.pubkey(); + let account = Account::from(stake_utils::create_stake_account( + &stake_pubkey, + &validators[0].vote_keypair.pubkey(), + &vote_account, + &genesis_config.rent, + lamports + (ind as u64 + 1) * lamports, + )); + genesis_config.accounts.insert(stake_pubkey, account); + } + + let staker_pubkeys = { + let mut staker_pubkeys = validators + .iter() + .map(|keypair| { + ( + keypair.vote_keypair.pubkey(), + vec![keypair.stake_keypair.pubkey()], + ) + }) + .collect::>(); + for staker_keypair in &staker_keypairs { + let staker_pubkey = staker_keypair.pubkey(); + staker_pubkeys + .get_mut(&validators[0].vote_keypair.pubkey()) + .unwrap() + .push(staker_pubkey); + } + staker_pubkeys + }; + + let (bank_epoch0, bank_forks) = new_bank_for_tests(leader, &genesis_config); + assert_eq!(bank_epoch0.epoch(), 0); + + // need to bump epoch by 1 as epoch 0 is migration epoch + let first_slot_in_epoch1 = bank_epoch0 + .epoch_schedule + .get_first_slot_in_epoch(bank_epoch0.epoch() + 1); + let bank_epoch1 = new_bank_from_parent(bank_epoch0, first_slot_in_epoch1); + ( + Self { + commission_bps, + _bank_forks: bank_forks, + validators, + stakers: staker_pubkeys, + }, + bank_epoch1, + ) + } + + fn get_validator_stake(&self, bank: &Bank, pubkey: &Pubkey) -> u64 { + let rent_exempt_reserve = bank + .rent_collector() + .rent + .minimum_balance(StakeStateV2::size_of()); + self.stakers + .get(pubkey) + .unwrap() + .iter() + .map(|pubkey| { + let lamports = bank.get_account(pubkey).unwrap().lamports(); + lamports - rent_exempt_reserve + }) + .sum() + } + + /// returns (validator_rewards, leader_rewards) + fn get_rewards(&self, bank: &Bank, num_reward_slots: u64, pubkey: &Pubkey) -> (u64, u64) { + let epoch_state = EpochInflationAccountState::new_from_bank(bank) + .unwrap() + .get_epoch_state(bank.epoch()) + .unwrap(); + let total_stake = bank.epoch_stakes(bank.epoch()).unwrap().total_stake(); + let (validator_reward, leader_reward) = calculate_reward( + &epoch_state, + total_stake, + self.get_validator_stake(bank, pubkey), + ); + let vote_rewards = validator_reward * num_reward_slots; + let leader_reward = leader_reward * num_reward_slots; + (vote_rewards, leader_reward) + } + + fn get_initial_and_final_lamports( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + pubkey: &Pubkey, + ) -> (u64, u64) { + assert!(payout_bank.slot() > reward_bank.slot()); + assert!(payout_bank.epoch() > reward_bank.epoch()); + let initial_lamports = reward_bank.get_account(pubkey).unwrap().lamports(); + let final_lamports = payout_bank.get_account(pubkey).unwrap().lamports(); + (initial_lamports, final_lamports) + } + + fn validate_stakers( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + voter_pubkey: &Pubkey, + validator_reward: u64, + ) -> u64 { + assert!(payout_bank.slot() > reward_bank.slot()); + assert!(payout_bank.epoch() > reward_bank.epoch()); + let rent_exempt_reserve = reward_bank + .rent_collector() + .rent + .minimum_balance(StakeStateV2::size_of()); + let validator_stake = self.get_validator_stake(reward_bank, voter_pubkey); + let mut expected_validator_reward = 0; + for staker_pubkey in self.stakers.get(voter_pubkey).unwrap().iter() { + let (initial_lamports, final_lamports) = + self.get_initial_and_final_lamports(reward_bank, payout_bank, staker_pubkey); + if initial_lamports <= LAMPORTS_PER_SOL + rent_exempt_reserve { + continue; + } + let stake = initial_lamports - rent_exempt_reserve; + let stake_weighted_reward = validator_reward * stake / validator_stake; + let (voter_reward, staker_reward, is_split) = + commission_split_preserve_lamports(self.commission_bps, stake_weighted_reward); + assert!(is_split); + assert_eq!( + staker_reward, + final_lamports - initial_lamports, + "final={final_lamports}; initial={initial_lamports}" + ); + expected_validator_reward += voter_reward; + } + expected_validator_reward + } + + /// Returns leader_rewards + fn validate_voter_reward( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + num_reward_slots: u64, + voter_pubkey: &Pubkey, + ) -> u64 { + assert!(payout_bank.slot() > reward_bank.slot()); + assert!(payout_bank.epoch() > reward_bank.epoch()); + + let (validator_reward, leader_reward) = + self.get_rewards(reward_bank, num_reward_slots, voter_pubkey); + + let expected_validator_reward = + self.validate_stakers(reward_bank, payout_bank, voter_pubkey, validator_reward); + + let (initial_validator_lamports, final_validator_lamports) = + self.get_initial_and_final_lamports(reward_bank, payout_bank, voter_pubkey); + let vat_burn = + payout_bank.vat_to_burn_per_epoch() * (payout_bank.epoch() - reward_bank.epoch()); + assert_eq!( + expected_validator_reward, + final_validator_lamports + vat_burn - initial_validator_lamports + ); + leader_reward + } + + fn validate_leader_reward( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + num_reward_slots: u64, + leader: Pubkey, + add_leader_reward: u64, + ) { + if !reward_bank.vote_accounts().contains_key(&leader) { + return; + } + assert!(payout_bank.slot() > reward_bank.slot()); + assert!(payout_bank.epoch() > reward_bank.epoch()); + + let (validator_reward, leader_reward) = + self.get_rewards(reward_bank, num_reward_slots, &leader); + let validator_reward = validator_reward + leader_reward + add_leader_reward; + + let expected_validator_reward = + self.validate_stakers(reward_bank, payout_bank, &leader, validator_reward); + + let (initial_validator_lamports, final_validator_lamports) = + self.get_initial_and_final_lamports(reward_bank, payout_bank, &leader); + let vat_burn = + payout_bank.vat_to_burn_per_epoch() * (payout_bank.epoch() - reward_bank.epoch()); + assert_eq!( + expected_validator_reward, + final_validator_lamports + vat_burn - initial_validator_lamports + ); + } + + fn validate_rewards(&self, reward_bank: &Bank, payout_bank: &Bank, num_reward_slots: u64) { + assert_eq!(reward_bank.leader(), payout_bank.leader()); + let leader = reward_bank.leader().vote_address; + + let mut add_leader_reward = 0; + for voter_pubkey in self.stakers.keys() { + if voter_pubkey == &leader { + continue; + } + let leader_reward = self.validate_voter_reward( + reward_bank, + payout_bank, + num_reward_slots, + voter_pubkey, + ); + add_leader_reward += leader_reward; + } + self.validate_leader_reward( + reward_bank, + payout_bank, + num_reward_slots, + leader, + add_leader_reward, + ); + } + } + + fn reward_validators( + bank: Arc, + validators: &[ValidatorVoteKeypairs], + num_reward_slots: u64, + ) -> Arc { + let validators_to_reward = validators + .iter() + .map(|k| k.vote_keypair.pubkey()) + .collect::>() + .into_iter() + .collect::>(); + + let mut looping_bank = bank; + for _ in 0..num_reward_slots { + let reward_cert = ValidatedRewardCert::new_for_tests( + looping_bank.slot() - 100, + validators_to_reward.clone(), + ); + calc_vote_rewards_update_vote_states( + &looping_bank, + Some(reward_cert), + None, + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as i64, + ) + .unwrap(); + + let slot = looping_bank.slot() + 1; + looping_bank = new_bank_from_parent(looping_bank, slot); + } + looping_bank + } + + /// Progresses the bank a few times to pay out the rewards. + fn progress_bank_for_payout(mut bank: Arc) -> Arc { + for _ in 0..10 { + let slot = bank.slot() + 1; + bank = new_bank_from_parent(bank, slot); + } + bank + } + + fn test_vote_reward_payout_impl( + validators: &[ValidatorVoteKeypairs], + initial_bank: Arc, + num_reward_slots: u64, + ) -> Arc { + // bump slots a bit so that reward slots always land in the same epoch and after AG is activated if in migration epoch. + let slot = initial_bank.slot() + 100_000; + let initial_bank = new_bank_from_parent(initial_bank, slot); + let initial_bank_epoch = initial_bank.epoch(); + let rewarded_bank = reward_validators(initial_bank, validators, num_reward_slots); + assert_eq!(rewarded_bank.epoch(), initial_bank_epoch); + + let payout_epoch = rewarded_bank.epoch() + 1; + let payout_epoch_slot = rewarded_bank + .epoch_schedule + .get_first_slot_in_epoch(payout_epoch); + let payout_bank = new_bank_from_parent(rewarded_bank, payout_epoch_slot); + assert_eq!(payout_bank.epoch(), payout_epoch); + + let bank = progress_bank_for_payout(payout_bank); + assert_eq!(bank.epoch(), initial_bank_epoch + 1); + bank + } + + #[test_matrix([true, false], [1_000, 5_000], [0, 10])] + fn test_vote_reward_payout(pay_leader: bool, commission_bps: u16, num_add_stakers: u64) { + let num_validators = 2; + let num_reward_slots = 10; + let (state, initial_bank) = + State::new(num_validators, num_add_stakers, pay_leader, commission_bps); + let final_bank = + test_vote_reward_payout_impl(&state.validators, initial_bank.clone(), num_reward_slots); + state.validate_rewards(&initial_bank, &final_bank, num_reward_slots); + } } diff --git a/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs b/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs index ebe78660437..0a4fab1e4ee 100644 --- a/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs +++ b/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs @@ -21,6 +21,16 @@ static VOTE_REWARD_ACCOUNT_ADDR: LazyLock = LazyLock::new(|| { pubkey }); +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + digest = "DwwQZJF7Epufk6MN9W6bfJ1z1DkjGfMZUsXDMWNv86jb", + abi_digest = "CrSvqX8ZAYxjZ6XoTp9Z1ED6McdnqhXq8zYFWBDkCwJs", + abi_serializer = "wincode", + test_roundtrip = "eq_and_wire", + ) +)] /// The per epoch info stored in the off curve account. #[derive(Debug, Clone, PartialEq, Eq, SchemaRead, SchemaWrite, Deserialize, Serialize)] pub(super) struct EpochInflationState { @@ -37,13 +47,12 @@ pub(super) struct EpochInflationState { impl EpochInflationState { fn new_from_bank( bank: &Bank, - prev_epoch: Epoch, - prev_epoch_capitalization: u64, - additional_validator_rewards: u64, + epoch_start_capitalization: u64, + additional_rewards: u64, ) -> Self { let max_possible_validator_reward = bank.calculate_epoch_inflation_rewards( - prev_epoch_capitalization + additional_validator_rewards, - prev_epoch, + epoch_start_capitalization + additional_rewards, + bank.epoch(), ); EpochInflationState { max_possible_validator_reward, @@ -53,6 +62,16 @@ impl EpochInflationState { } } +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + digest = "HR1JbQp4gVU7fcsG4ji1fe28j8uNJRmuptdQz4PDkKoC", + abi_digest = "FeEFnXTk7DxHkCamcHDpRRjRDfSyMh3DGnefbwSvA8Kc", + abi_serializer = "wincode", + test_roundtrip = "eq_and_wire", + ) +)] /// The state stored in the off curve account used to store metadata for calculating and paying /// voting rewards. /// @@ -116,21 +135,19 @@ impl EpochInflationAccountState { /// capitalization keeps increasing in the first slots of the epoch. Vote rewards are /// calculated as a function of the capitalization and we do not want voting in the initial /// slots to earn less rewards than voting in the later rewards. As such this function is - /// called with [`additional_validator_rewards`] which should be the total rewards that will + /// called with [`additional_rewards`] which should be the total rewards that will /// be paid by PER and we use the capitalization from the previous epoch plus this value to /// compute the vote rewards. pub(crate) fn new_epoch_update_account( bank: &Bank, - prev_epoch: Epoch, - prev_epoch_capitalization: u64, - additional_validator_rewards: u64, + epoch_start_capitalization: u64, + additional_rewards: u64, ) { let prev = Self::new_from_bank(bank).map(|s| s.current); let current = EpochInflationState::new_from_bank( bank, - prev_epoch, - prev_epoch_capitalization, - additional_validator_rewards, + epoch_start_capitalization, + additional_rewards, ); let state = Self { prev, current }; state.set_state(bank); @@ -141,10 +158,10 @@ impl EpochInflationAccountState { if self.current.epoch == epoch { return Some(self.current); } - if let Some(prev) = self.prev { - if prev.epoch == epoch { - return Some(prev); - } + if let Some(prev) = self.prev + && prev.epoch == epoch + { + return Some(prev); } None } @@ -158,11 +175,15 @@ mod tests { bank_forks::BankForks, genesis_utils::{ GenesisConfigInfo, ValidatorVoteKeypairs, create_genesis_config, - create_genesis_config_with_alpenglow_vote_accounts, + create_genesis_config_with_alpenglow_vote_accounts, deactivate_features, }, + slot_params::slot_time_feature_ids, }, + agave_feature_set as feature_set, rand::Rng, + solana_epoch_schedule::EpochSchedule, solana_genesis_config::GenesisConfig, + solana_inflation::Inflation, solana_leader_schedule::SlotLeader, std::sync::Arc, }; @@ -221,24 +242,92 @@ mod tests { assert_eq!(bank_epoch_1.epoch(), 1); assert_eq!(bank_epoch_2.epoch(), 2); - let expected_prev = EpochInflationState::new_from_bank( - &bank_epoch_1, - bank_epoch_0.epoch(), - bank_epoch_0.capitalization(), - 0, - ); - let expected_current = EpochInflationState::new_from_bank( - &bank_epoch_2, - bank_epoch_1.epoch(), - bank_epoch_1.capitalization(), - 0, - ); + let expected_prev = + EpochInflationState::new_from_bank(&bank_epoch_1, bank_epoch_0.capitalization(), 0); + let expected_current = + EpochInflationState::new_from_bank(&bank_epoch_2, bank_epoch_1.capitalization(), 0); let EpochInflationAccountState { current, prev } = EpochInflationAccountState::new_from_bank(&bank_epoch_2).unwrap(); assert_eq!(current, expected_current); assert_eq!(prev.unwrap(), expected_prev); } + #[test] + fn new_epoch_update_account_uses_current_epoch_rewards() { + let GenesisConfigInfo { + mut genesis_config, .. + } = create_genesis_config(1_000_000_000); + genesis_config.inflation = Inflation::full(); + // Warmup is necessary here to give epoch 0 and epoch 1 different reward + // budgets. + genesis_config.epoch_schedule = EpochSchedule::custom(64, 64, true); + deactivate_features(&mut genesis_config, &slot_time_feature_ids().to_vec()); + + for (case, genesis_config, activate_slot_time_feature) in [ + ("current reward budget", genesis_config, false), + ( + "current epoch slot params", + GenesisConfig { + inflation: Inflation::full(), + ..GenesisConfig::default() + }, + true, + ), + ] { + let mut bank_epoch_0 = Bank::new_for_tests(&genesis_config); + if activate_slot_time_feature { + bank_epoch_0.activate_feature(&feature_set::reduce_slot_time_to_350ms::id()); + } + let bank_forks = BankForks::new_rw_arc(bank_epoch_0); + let bank_epoch_0 = bank_forks.read().unwrap().root_bank(); + let epoch_1_first_slot = bank_epoch_0.epoch_schedule().get_first_slot_in_epoch(1); + let bank_epoch_1 = Bank::new_from_parent_with_bank_forks( + bank_forks.as_ref(), + bank_epoch_0.clone(), + SlotLeader::new_unique(), + epoch_1_first_slot, + ); + + assert_eq!(bank_epoch_0.epoch(), 0, "{case}"); + assert_eq!(bank_epoch_1.epoch(), 1, "{case}"); + if activate_slot_time_feature { + assert_ne!(bank_epoch_0.ns_per_slot, bank_epoch_1.ns_per_slot, "{case}"); + } else { + assert_ne!( + bank_epoch_1.epoch_schedule().get_slots_in_epoch(0), + bank_epoch_1.epoch_schedule().get_slots_in_epoch(1), + "{case}", + ); + } + + let epoch_start_capitalization = bank_epoch_0.capitalization(); + let expected_current_epoch_rewards = bank_epoch_1.calculate_epoch_inflation_rewards( + epoch_start_capitalization, + bank_epoch_1.epoch(), + ); + assert_ne!( + expected_current_epoch_rewards, + bank_epoch_1.calculate_epoch_inflation_rewards( + epoch_start_capitalization, + bank_epoch_0.epoch() + ), + "{case}", + ); + + EpochInflationAccountState::new_epoch_update_account( + &bank_epoch_1, + epoch_start_capitalization, + 0, + ); + let state = EpochInflationAccountState::new_from_bank(&bank_epoch_1).unwrap(); + assert_eq!(state.current.epoch, bank_epoch_1.epoch(), "{case}"); + assert_eq!( + state.current.max_possible_validator_reward, expected_current_epoch_rewards, + "{case}", + ); + } + } + #[test] fn handles_prefunded_account() { let GenesisConfigInfo { diff --git a/runtime/src/block_component_processor/vote_reward/migration_test.rs b/runtime/src/block_component_processor/vote_reward/migration_test.rs new file mode 100644 index 00000000000..bada7e11ccd --- /dev/null +++ b/runtime/src/block_component_processor/vote_reward/migration_test.rs @@ -0,0 +1,466 @@ +#![cfg(test)] +mod tests { + + use { + crate::{ + bank::Bank, + block_component_processor::vote_reward::{ + VoteState, increment_credits, + tests::{new_bank_from_parent, set_commission}, + }, + genesis_utils::{ + ValidatorVoteKeypairs, activate_all_features, create_genesis_config_with_leader_ex, + create_validator, + }, + inflation_rewards::commission_split_preserve_lamports, + stake_utils, + }, + agave_feature_set::FeatureSet, + agave_votor_messages::{ + certificate::{CertSignature, GenesisCert}, + consensus_message::Block, + }, + solana_account::{Account, ReadableAccount, from_account}, + solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, + solana_cluster_type::ClusterType, + solana_epoch_schedule::EpochSchedule, + solana_fee_calculator::FeeRateGovernor, + solana_genesis_config::GenesisConfig, + solana_hash::Hash, + solana_keypair::Keypair, + solana_leader_schedule::SlotLeader, + solana_native_token::LAMPORTS_PER_SOL, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_signer::Signer, + solana_stake_interface::state::StakeStateV2, + solana_sysvar::{self as sysvar, epoch_rewards::EpochRewards}, + std::{collections::HashMap, num::NonZero, sync::Arc}, + test_case::test_matrix, + }; + + fn new_bank_for_tests(leader: SlotLeader, genesis_config: &GenesisConfig) -> Bank { + let bank = Bank::new_with_paths_for_tests(genesis_config, None, vec![], Some(leader)); + assert_eq!(*bank.leader(), leader); + bank + } + + fn install_add_stakers( + genesis_config: &mut GenesisConfig, + validators: &[ValidatorVoteKeypairs], + num_add_stakers: u64, + lamports: u64, + ) -> HashMap> { + let mut ret = validators + .iter() + .map(|keypair| { + ( + keypair.vote_keypair.pubkey(), + vec![keypair.stake_keypair.pubkey()], + ) + }) + .collect::>(); + for validator in validators { + let voter_pubkey = validator.vote_keypair.pubkey(); + let vote_account = genesis_config + .accounts + .get(&voter_pubkey) + .unwrap() + .clone() + .into(); + + for i in 0..num_add_stakers { + let staker_keypair = Keypair::new(); + let staker_pubkey = staker_keypair.pubkey(); + let account = Account::from(stake_utils::create_stake_account( + &staker_pubkey, + &voter_pubkey, + &vote_account, + &genesis_config.rent, + lamports + (i + 1) * lamports, + )); + genesis_config.accounts.insert(staker_pubkey, account); + ret.get_mut(&voter_pubkey).unwrap().push(staker_pubkey); + } + } + ret + } + + enum PayType { + Both { + ag_credits: NonZero, + tower_credits: u64, + }, + Tower(u64), + None, + } + + impl PayType { + fn tower(&self) -> u64 { + match self { + Self::Both { tower_credits, .. } => *tower_credits, + Self::Tower(t) => *t, + Self::None => 0, + } + } + + fn ag(&self) -> Option> { + match self { + Self::Both { ag_credits, .. } => Some(*ag_credits), + Self::Tower(_) | Self::None => None, + } + } + } + + struct State { + pay_type: PayType, + commission_bps: u16, + validators: Vec, + /// key is validator pubkey, value is list of stakers. + stakers: HashMap>, + } + + impl State { + fn new( + pay_leader: bool, + num_validators: u64, + num_add_stakers: u64, + commission_bps: u16, + pay_type: PayType, + ) -> (Self, Bank) { + let lamports = LAMPORTS_PER_SOL * 20; + let mint_keypair = Keypair::new(); + let validators = (0..num_validators) + .map(|_| ValidatorVoteKeypairs::new_rand()) + .collect::>(); + let leader = if pay_leader { + let vote_pubkey = validators[0].vote_keypair.pubkey(); + let node_pubkey = validators[0].node_keypair.pubkey(); + SlotLeader { + id: node_pubkey, + vote_address: vote_pubkey, + } + } else { + SlotLeader::new_unique() + }; + let mut genesis_config = create_genesis_config_with_leader_ex( + lamports, + &mint_keypair.pubkey(), + &validators[0].node_keypair.pubkey(), + &validators[0].vote_keypair.pubkey(), + &validators[0].stake_keypair.pubkey(), + Some(validators[0].bls_keypair.public.to_bytes_compressed()), + lamports, + lamports, + FeeRateGovernor::new(0, 0), + Rent::default(), + ClusterType::Development, + &FeatureSet::all_enabled(), + vec![], + ); + genesis_config.epoch_schedule = EpochSchedule::without_warmup(); + activate_all_features(&mut genesis_config); + for (ind, keypair) in validators.iter().enumerate().skip(1) { + let node_pubkey = keypair.node_keypair.pubkey(); + let vote_pubkey = keypair.vote_keypair.pubkey(); + let stake_pubkey = keypair.stake_keypair.pubkey(); + let bls_pubkey = Some(keypair.bls_keypair.public.to_bytes_compressed()); + let lamports = lamports + ind as u64 * LAMPORTS_PER_SOL; + let accounts = create_validator( + &genesis_config.rent, + node_pubkey, + lamports, + vote_pubkey, + lamports, + stake_pubkey, + lamports, + bls_pubkey, + ) + .into_iter() + .map(|(pubkey, account)| (pubkey, Account::from(account))); + genesis_config.accounts.extend(accounts); + } + set_commission(&mut genesis_config, &validators, commission_bps); + + let stakers = + install_add_stakers(&mut genesis_config, &validators, num_add_stakers, lamports); + let bank_epoch0 = new_bank_for_tests(leader, &genesis_config); + assert_eq!(bank_epoch0.epoch(), 0); + + ( + Self { + pay_type, + commission_bps, + validators, + stakers, + }, + bank_epoch0, + ) + } + + fn add_tower_rewards(&self, bank: Arc) -> Arc { + let vote_accounts = bank.vote_accounts(); + let updated_accounts = self + .validators + .iter() + .map(|validator| { + let vote_pubkey = validator.vote_keypair.pubkey(); + let mut vote_state = VoteState::try_new(&vote_accounts, vote_pubkey).unwrap(); + vote_state + .handler + .increment_credits(bank.epoch(), self.pay_type.tower()); + vote_state.serialize().unwrap() + }) + .collect::>(); + bank.store_accounts((bank.slot(), updated_accounts.as_slice())); + let slot = bank.slot() + 10; + new_bank_from_parent(bank, slot) + } + + fn add_ag_rewards(&self, bank: Arc) -> Arc { + let vote_accounts = bank.vote_accounts(); + let updated_accounts = self + .validators + .iter() + .map(|validator| { + let vote_pubkey = validator.vote_keypair.pubkey(); + let mut vote_state = VoteState::try_new(&vote_accounts, vote_pubkey).unwrap(); + if let Some(ag_credits) = self.pay_type.ag() { + increment_credits( + vote_state.handler.epoch_credits_mut(), + bank.epoch(), + bank.epoch(), + ag_credits, + ); + } + vote_state.serialize().unwrap() + }) + .collect::>(); + bank.store_accounts((bank.slot(), updated_accounts.as_slice())); + let slot = bank.slot() + 10; + new_bank_from_parent(bank, slot) + } + + fn get_validator_stake(&self, bank: &Bank, pubkey: &Pubkey) -> u64 { + let rent_exempt_reserve = bank + .rent_collector() + .rent + .minimum_balance(StakeStateV2::size_of()); + self.stakers + .get(pubkey) + .unwrap() + .iter() + .map(|pubkey| { + let lamports = bank.get_account(pubkey).unwrap().lamports(); + lamports - rent_exempt_reserve + }) + .sum() + } + + fn calculate_tower_rewards( + &self, + payout_bank: &Bank, + reward_bank: &Bank, + ) -> HashMap { + if self.pay_type.tower() == 0 { + let mut ret = HashMap::new(); + for stakers in self.stakers.values() { + for staker in stakers { + ret.insert(*staker, 0); + } + } + return ret; + } + + let epoch_rewards: EpochRewards = payout_bank + .get_account(&sysvar::epoch_rewards::id()) + .and_then(|account| from_account(&account)) + .unwrap(); + let epoch_inflation = epoch_rewards.total_rewards; + let total_points = epoch_rewards.total_points; + let genesis_cert = payout_bank.get_alpenglow_genesis_certificate().unwrap(); + let first_slot_in_reward_epoch = payout_bank + .epoch_schedule + .get_first_slot_in_epoch(reward_bank.epoch()); + let num_tower_slots = genesis_cert.block.slot - first_slot_in_reward_epoch + 1; + let total_slots = reward_bank.epoch_schedule.slots_per_epoch; + + let rent_exempt_reserve = reward_bank + .rent_collector() + .rent + .minimum_balance(StakeStateV2::size_of()); + let mut ret = HashMap::new(); + for stakers in self.stakers.values() { + for staker in stakers { + let (initial_lamports, _final_lamports) = + self.get_initial_and_final_lamports(reward_bank, payout_bank, staker); + if initial_lamports <= LAMPORTS_PER_SOL + rent_exempt_reserve { + continue; + } + let stake = initial_lamports - rent_exempt_reserve; + let points = (stake * self.pay_type.tower()) as u128; + let reward = points + .checked_mul(u128::from(epoch_inflation)) + .unwrap() + .checked_div(total_points) + .unwrap() + .checked_mul(num_tower_slots as u128) + .unwrap() + .checked_div(total_slots as u128) + .unwrap() + .try_into() + .unwrap(); + ret.insert(*staker, reward); + } + } + ret + } + + fn get_initial_and_final_lamports( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + pubkey: &Pubkey, + ) -> (u64, u64) { + assert!(payout_bank.slot() > reward_bank.slot()); + assert!(payout_bank.epoch() > reward_bank.epoch()); + let initial_lamports = reward_bank.get_account(pubkey).unwrap().lamports(); + let final_lamports = payout_bank.get_account(pubkey).unwrap().lamports(); + (initial_lamports, final_lamports) + } + + fn validate_stakers( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + voter_pubkey: &Pubkey, + tower_rewards: &HashMap, + ) -> u64 { + assert!(payout_bank.slot() > reward_bank.slot()); + assert!(payout_bank.epoch() > reward_bank.epoch()); + let rent_exempt_reserve = reward_bank + .rent_collector() + .rent + .minimum_balance(StakeStateV2::size_of()); + let validator_stake = self.get_validator_stake(reward_bank, voter_pubkey); + let mut expected_validator_reward = 0; + for staker_pubkey in self.stakers.get(voter_pubkey).unwrap().iter() { + let (initial_lamports, final_lamports) = + self.get_initial_and_final_lamports(reward_bank, payout_bank, staker_pubkey); + if initial_lamports <= LAMPORTS_PER_SOL + rent_exempt_reserve { + continue; + } + let stake = initial_lamports - rent_exempt_reserve; + let stake_weighted_tower = *tower_rewards.get(staker_pubkey).unwrap(); + let stake_weighted_ag = + self.pay_type.ag().map(NonZero::get).unwrap_or(0) * stake / validator_stake; + let stake_weighted_reward = stake_weighted_tower + stake_weighted_ag; + let (voter_reward, staker_reward, is_split) = + commission_split_preserve_lamports(self.commission_bps, stake_weighted_reward); + assert!(is_split); + assert_eq!( + staker_reward, + final_lamports - initial_lamports, + "final={final_lamports}; initial={initial_lamports}" + ); + expected_validator_reward += voter_reward; + } + expected_validator_reward + } + + fn validate_validator_reward( + &self, + reward_bank: &Bank, + payout_bank: &Bank, + tower_rewards: &HashMap, + vote_pubkey: Pubkey, + ) { + let expected_validator_reward = + self.validate_stakers(reward_bank, payout_bank, &vote_pubkey, tower_rewards); + + let (initial_lamports, final_lamports) = + self.get_initial_and_final_lamports(reward_bank, payout_bank, &vote_pubkey); + let vat_burn = + payout_bank.vat_to_burn_per_epoch() * (payout_bank.epoch() - reward_bank.epoch()); + assert_eq!( + expected_validator_reward, + final_lamports + vat_burn - initial_lamports + ); + } + + fn validate_rewards(&self, reward_bank: &Bank, payout_bank: &Bank) { + let tower_rewards = self.calculate_tower_rewards(payout_bank, reward_bank); + for validator in self.validators.iter() { + let vote_pubkey = validator.vote_keypair.pubkey(); + self.validate_validator_reward( + reward_bank, + payout_bank, + &tower_rewards, + vote_pubkey, + ); + } + } + } + + /// Progresses the bank a few times to pay out the rewards. + fn progress_bank_for_payout(mut bank: Arc) -> Arc { + for _ in 0..10 { + let slot = bank.slot() + 1; + bank = new_bank_from_parent(bank, slot); + } + bank + } + + #[test_matrix([true, false], [1_000, 5_000], [0, 10], [PayType::Both{ag_credits: NonZero::new(1023).unwrap(), tower_credits:532}, PayType::Tower(383), PayType::None])] + fn test_migration_epoch( + pay_leader: bool, + commission_bps: u16, + num_add_stakers: u64, + pay_type: PayType, + ) { + let num_validators = 5; + + let (state, mut bank_at_tower) = State::new( + pay_leader, + num_validators, + num_add_stakers, + commission_bps, + pay_type, + ); + bank_at_tower.activate_feature(&agave_feature_set::alpenglow::id()); + let (bank_at_tower, _bank_forks) = bank_at_tower.wrap_with_bank_forks_for_tests(); + + let first_slot_in_migration_epoch = bank_at_tower + .epoch_schedule + .get_first_slot_in_epoch(bank_at_tower.epoch() + 1); + let bank_at_migration0 = new_bank_from_parent(bank_at_tower, first_slot_in_migration_epoch); + + let bank_with_tower_rewards = state.add_tower_rewards(bank_at_migration0); + + let genesis_cert = GenesisCert { + block: Block { + slot: bank_with_tower_rewards.slot(), + block_id: Hash::default(), + }, + signature: CertSignature { + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }, + }; + bank_with_tower_rewards.set_alpenglow_genesis_certificate(&genesis_cert); + let bank_with_genesis_cert_slot = bank_with_tower_rewards.slot() + 10_000; + let bank_with_genesis_cert = + new_bank_from_parent(bank_with_tower_rewards, bank_with_genesis_cert_slot); + + let bank_with_ag_rewards = state.add_ag_rewards(bank_with_genesis_cert); + let reward_epoch = bank_with_ag_rewards.epoch(); + + let first_slot_in_ag_epoch = bank_with_ag_rewards + .epoch_schedule + .get_first_slot_in_epoch(reward_epoch + 1); + let bank_at_ag = new_bank_from_parent(bank_with_ag_rewards.clone(), first_slot_in_ag_epoch); + let progressed_bank = progress_bank_for_payout(bank_at_ag); + + state.validate_rewards(&bank_with_ag_rewards, &progressed_bank); + } +} diff --git a/runtime/src/commitment.rs b/runtime/src/commitment.rs index 42cbc32b880..781bb3d7220 100644 --- a/runtime/src/commitment.rs +++ b/runtime/src/commitment.rs @@ -124,10 +124,10 @@ impl BlockCommitmentCache { fn highest_slot_with_confirmation_count(&self, confirmation_count: usize) -> Slot { assert!(confirmation_count > 0 && confirmation_count <= MAX_LOCKOUT_HISTORY); for slot in (self.root()..self.slot()).rev() { - if let Some(count) = self.get_confirmation_count(slot) { - if count >= confirmation_count { - return slot; - } + if let Some(count) = self.get_confirmation_count(slot) + && count >= confirmation_count + { + return slot; } } self.commitment_slots.root diff --git a/runtime/src/epoch_stakes.rs b/runtime/src/epoch_stakes.rs index 980f89c6527..a617b8ae2c5 100644 --- a/runtime/src/epoch_stakes.rs +++ b/runtime/src/epoch_stakes.rs @@ -17,6 +17,7 @@ use { std::{ collections::HashMap, fmt, + num::NonZero, sync::{Arc, OnceLock}, }, }; @@ -36,7 +37,7 @@ pub struct BLSPubkeyStakeEntry { /// The bls pubkey of the validator specified in the vote account pub bls_pubkey: PopVerified, /// The stake of the validator - pub stake: u64, + pub stake: NonZero, } /// Container to store a mapping from validator [`BLSPubkeyAffine`] to rank. @@ -49,6 +50,7 @@ pub struct BLSPubkeyToRankMap { rank_map: HashMap, vote_pubkey_to_rank: HashMap, sorted_pubkeys: Vec, + total_stake: NonZero, } // We cannot auto derive `AbiExample` for `BLSPubkeyToRankMap` because @@ -60,6 +62,7 @@ impl solana_frozen_abi::abi_example::AbiExample for BLSPubkeyToRankMap { rank_map: HashMap::new(), vote_pubkey_to_rank: HashMap::new(), sorted_pubkeys: Vec::new(), + total_stake: NonZero::new(1).unwrap(), } } } @@ -68,7 +71,7 @@ pub(crate) fn bls_pubkey_compressed_bytes_to_bls_pubkey( bls_pubkey_compressed_bytes: [u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE], ) -> Option<(BLSPubkeyCompressed, PopVerified)> { let bls_pubkey_compressed: BLSPubkeyCompressed = - bincode::deserialize(&bls_pubkey_compressed_bytes).ok()?; + wincode::deserialize(&bls_pubkey_compressed_bytes).ok()?; let bls_pubkey_affine = BLSPubkeyAffine::try_from(bls_pubkey_compressed).ok()?; // It is safe to use `new_unchecked` here because data coming from the vote // state has already had its PoP verified. @@ -78,32 +81,46 @@ pub(crate) fn bls_pubkey_compressed_bytes_to_bls_pubkey( impl BLSPubkeyToRankMap { pub fn new(epoch_vote_accounts_hash_map: &VoteAccountsHashMap) -> Self { + let mut candidates = Vec::with_capacity(epoch_vote_accounts_hash_map.len()); + let mut bls_pubkey_counts = HashMap::new(); + let mut node_pubkey_counts = HashMap::new(); + for (&vote_account_pubkey, (stake, account)) in epoch_vote_accounts_hash_map { + let Some(stake) = NonZero::new(*stake) else { + continue; + }; + let node_pubkey = *account.vote_state_view().node_pubkey(); + let Some((bls_pubkey_compressed, bls_pubkey)) = account + .vote_state_view() + .bls_pubkey_compressed() + .and_then(bls_pubkey_compressed_bytes_to_bls_pubkey) + else { + continue; + }; + let entry = BLSPubkeyStakeEntry { + vote_account_pubkey, + node_pubkey, + bls_pubkey, + stake, + }; + *bls_pubkey_counts.entry(bls_pubkey_compressed).or_insert(0) += 1; + *node_pubkey_counts.entry(node_pubkey).or_insert(0) += 1; + candidates.push((entry, bls_pubkey_compressed)); + } let mut keys_stake_entry_with_compressed: Vec<(BLSPubkeyStakeEntry, BLSPubkeyCompressed)> = - epoch_vote_accounts_hash_map - .iter() - .filter_map(|(&vote_account_pubkey, (stake, account))| { - if *stake > 0 { - let node_pubkey = *account.vote_state_view().node_pubkey(); - account - .vote_state_view() - .bls_pubkey_compressed() - .and_then(bls_pubkey_compressed_bytes_to_bls_pubkey) - .map(|(bls_pubkey_compressed, bls_pubkey)| { - ( - BLSPubkeyStakeEntry { - vote_account_pubkey, - node_pubkey, - bls_pubkey, - stake: *stake, - }, - bls_pubkey_compressed, - ) - }) - } else { - None - } + candidates + .into_iter() + .filter_map(|(entry, bls_pubkey_compressed)| { + (bls_pubkey_counts[&bls_pubkey_compressed] == 1 + && node_pubkey_counts[&entry.node_pubkey] == 1) + .then_some((entry, bls_pubkey_compressed)) }) .collect(); + let total_stake = keys_stake_entry_with_compressed + .iter() + .fold(0u64, |stake, (entry, _)| { + stake.saturating_add(entry.stake.get()) + }); + let total_stake = NonZero::new(total_stake).expect("total stakes should not be 0"); keys_stake_entry_with_compressed.sort_by( |(a_entry, a_pubkey_compressed), (b_entry, b_pubkey_compressed)| { b_entry @@ -128,6 +145,7 @@ impl BLSPubkeyToRankMap { rank_map: bls_pubkey_to_rank_map, vote_pubkey_to_rank: vote_pubkey_to_rank_map, sorted_pubkeys, + total_stake, } } @@ -136,7 +154,11 @@ impl BLSPubkeyToRankMap { } pub fn len(&self) -> usize { - self.rank_map.len() + self.sorted_pubkeys.len() + } + + pub fn total_stake(&self) -> NonZero { + self.total_stake } pub fn get_rank(&self, bls_pubkey: &PopVerified) -> Option<&u16> { @@ -153,7 +175,7 @@ impl BLSPubkeyToRankMap { } } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Clone, Serialize, Debug, Deserialize, Default, PartialEq, Eq)] pub struct NodeVoteAccounts { pub vote_accounts: Vec, @@ -176,7 +198,10 @@ pub(crate) enum DeserializableVersionedEpochStakes { } #[derive(Clone, Debug, Serialize)] -#[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, AbiEnumVisitor, StableAbi, StableAbiSample) +)] #[cfg_attr(feature = "dev-context-only-utils", derive(PartialEq))] pub enum VersionedEpochStakes { Current { @@ -185,6 +210,7 @@ pub enum VersionedEpochStakes { total_stake: u64, node_id_to_vote_accounts: Arc, epoch_authorized_voters: Arc, + #[cfg_attr(feature = "frozen-abi", stable_abi_sample(with = "Default::default()"))] #[serde(skip)] bls_pubkey_to_rank_map: OnceLock>, }, @@ -347,7 +373,7 @@ impl VersionedEpochStakes { /// The current version of epoch stakes #[derive(Clone, Debug, Default)] -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[cfg_attr(feature = "dev-context-only-utils", derive(PartialEq))] pub struct EpochStakes { epoch: Epoch, @@ -503,7 +529,7 @@ pub(crate) mod tests { let bls_pubkey_compressed: BLSPubkeyCompressed = (*BLSKeypair::new().public).into(); let bls_pubkey_compressed_serialized = - bincode::serialize(&bls_pubkey_compressed) + wincode::serialize(&bls_pubkey_compressed) .unwrap() .try_into() .unwrap(); @@ -644,14 +670,13 @@ pub(crate) mod tests { } } - #[test_case(1; "single_vote_account")] - #[test_case(2; "multiple_vote_accounts")] - fn test_bls_pubkey_rank_map(num_vote_accounts_per_node: usize) { + #[test] + fn test_bls_pubkey_rank_map() { agave_logger::setup(); let num_nodes = 10; - let num_vote_accounts = num_nodes * num_vote_accounts_per_node; + let num_vote_accounts = num_nodes; - let vote_accounts_map = new_vote_accounts(num_nodes, num_vote_accounts_per_node, true); + let vote_accounts_map = new_vote_accounts(num_nodes, 1, true); let node_id_to_stake_map = vote_accounts_map .keys() .enumerate() @@ -662,7 +687,13 @@ pub(crate) mod tests { }); let epoch_stakes = VersionedEpochStakes::new_for_tests(epoch_vote_accounts.clone(), 0); let bls_pubkey_to_rank_map = epoch_stakes.bls_pubkey_to_rank_map(); - assert_eq!(bls_pubkey_to_rank_map.len(), num_vote_accounts); + let expected_num_vote_accounts = num_vote_accounts; + assert_eq!(bls_pubkey_to_rank_map.len(), expected_num_vote_accounts); + let expected_total_stake = epoch_stakes.total_stake(); + assert_eq!( + bls_pubkey_to_rank_map.total_stake().get(), + expected_total_stake + ); for (vote_account_pubkey, (stake, vote_account)) in epoch_vote_accounts { let vote_state_view = vote_account.vote_state_view(); let (_comp, bls_pubkey) = bls_pubkey_compressed_bytes_to_bls_pubkey( @@ -671,14 +702,14 @@ pub(crate) mod tests { .unwrap(); let node_pubkey = *vote_state_view.node_pubkey(); let index = bls_pubkey_to_rank_map.get_rank(&bls_pubkey).unwrap(); - assert!(index >= &0 && index < &(num_vote_accounts as u16)); + assert!(index >= &0 && index < &(expected_num_vote_accounts as u16)); assert_eq!( bls_pubkey_to_rank_map.get_pubkey_stake_entry(*index as usize), Some(&BLSPubkeyStakeEntry { vote_account_pubkey, node_pubkey, bls_pubkey, - stake, + stake: NonZero::new(stake).unwrap(), }) ); } @@ -693,6 +724,221 @@ pub(crate) mod tests { assert_eq!(bls_pubkey_to_rank_map2, bls_pubkey_to_rank_map); } + #[test] + #[should_panic(expected = "total stakes should not be 0")] + fn test_multiple_vote_accounts_panics() { + agave_logger::setup(); + let num_nodes = 10; + + let vote_accounts_map = new_vote_accounts(num_nodes, 2, true); + let node_id_to_stake_map = vote_accounts_map + .keys() + .enumerate() + .map(|(index, node_id)| (*node_id, ((index + 1) * 100) as u64)) + .collect::>(); + let epoch_vote_accounts = new_epoch_vote_accounts(&vote_accounts_map, |node_id| { + *node_id_to_stake_map.get(node_id).unwrap() + }); + let epoch_stakes = VersionedEpochStakes::new_for_tests(epoch_vote_accounts.clone(), 0); + epoch_stakes.bls_pubkey_to_rank_map(); + } + + #[test] + fn test_bls_pubkey_rank_map_excludes_duplicate_bls_and_identity() { + let new_bls_pubkey = || { + let bls_pubkey_compressed: BLSPubkeyCompressed = (*BLSKeypair::new().public).into(); + let bls_pubkey_compressed_serialized = wincode::serialize(&bls_pubkey_compressed) + .unwrap() + .try_into() + .unwrap(); + let (_bls_pubkey_compressed, bls_pubkey) = + bls_pubkey_compressed_bytes_to_bls_pubkey(bls_pubkey_compressed_serialized) + .unwrap(); + (bls_pubkey_compressed_serialized, bls_pubkey) + }; + + let (duplicate_bls_pubkey_serialized, duplicate_bls_pubkey) = new_bls_pubkey(); + let (duplicate_node_bls_pubkey_serialized, duplicate_node_bls_pubkey) = new_bls_pubkey(); + let (duplicate_node_bls_pubkey_serialized_2, duplicate_node_bls_pubkey_2) = + new_bls_pubkey(); + let (shared_voter_bls_pubkey_serialized, shared_voter_bls_pubkey) = new_bls_pubkey(); + let (shared_voter_bls_pubkey_serialized_2, shared_voter_bls_pubkey_2) = new_bls_pubkey(); + let (unique_bls_pubkey_serialized, unique_bls_pubkey) = new_bls_pubkey(); + + let duplicate_bls_vote_pubkey = Pubkey::new_unique(); + let duplicate_bls_vote_pubkey_2 = Pubkey::new_unique(); + let duplicate_node_vote_pubkey = Pubkey::new_unique(); + let duplicate_node_vote_pubkey_2 = Pubkey::new_unique(); + let shared_voter_vote_pubkey = Pubkey::new_unique(); + let shared_voter_vote_pubkey_2 = Pubkey::new_unique(); + let unique_vote_pubkey = Pubkey::new_unique(); + + let duplicate_node_pubkey = Pubkey::new_unique(); + let shared_authorized_voter = Pubkey::new_unique(); + let shared_voter_node_pubkey = Pubkey::new_unique(); + let shared_voter_node_pubkey_2 = Pubkey::new_unique(); + let unique_node_pubkey = Pubkey::new_unique(); + let unique_voter = Pubkey::new_unique(); + + let account = |node_pubkey, authorized_voter, bls_pubkey| { + VoteAccount::try_from(create_v4_account_with_authorized( + &node_pubkey, + &authorized_voter, + bls_pubkey, + &node_pubkey, + 0, + &node_pubkey, + 0, + &node_pubkey, + 100, + )) + .unwrap() + }; + let epoch_vote_accounts = VoteAccountsHashMap::from([ + ( + duplicate_bls_vote_pubkey, + ( + 100, + account( + Pubkey::new_unique(), + Pubkey::new_unique(), + duplicate_bls_pubkey_serialized, + ), + ), + ), + ( + duplicate_bls_vote_pubkey_2, + ( + 100, + account( + Pubkey::new_unique(), + Pubkey::new_unique(), + duplicate_bls_pubkey_serialized, + ), + ), + ), + ( + duplicate_node_vote_pubkey, + ( + 100, + account( + duplicate_node_pubkey, + Pubkey::new_unique(), + duplicate_node_bls_pubkey_serialized, + ), + ), + ), + ( + duplicate_node_vote_pubkey_2, + ( + 100, + account( + duplicate_node_pubkey, + Pubkey::new_unique(), + duplicate_node_bls_pubkey_serialized_2, + ), + ), + ), + ( + shared_voter_vote_pubkey, + ( + 100, + account( + shared_voter_node_pubkey, + shared_authorized_voter, + shared_voter_bls_pubkey_serialized, + ), + ), + ), + ( + shared_voter_vote_pubkey_2, + ( + 100, + account( + shared_voter_node_pubkey_2, + shared_authorized_voter, + shared_voter_bls_pubkey_serialized_2, + ), + ), + ), + ( + unique_vote_pubkey, + ( + 50, + account( + unique_node_pubkey, + unique_voter, + unique_bls_pubkey_serialized, + ), + ), + ), + ]); + + let rank_map = BLSPubkeyToRankMap::new(&epoch_vote_accounts); + + assert_eq!(rank_map.len(), 3); + assert_eq!(rank_map.total_stake().get(), 250); + for bls_pubkey in [ + duplicate_bls_pubkey, + duplicate_node_bls_pubkey, + duplicate_node_bls_pubkey_2, + ] { + assert!(rank_map.get_rank(&bls_pubkey).is_none()); + } + for vote_pubkey in [ + duplicate_bls_vote_pubkey, + duplicate_bls_vote_pubkey_2, + duplicate_node_vote_pubkey, + duplicate_node_vote_pubkey_2, + ] { + assert!(rank_map.get_rank_for_vote_pubkey(&vote_pubkey).is_none()); + } + + for (vote_account_pubkey, node_pubkey, bls_pubkey) in [ + ( + shared_voter_vote_pubkey, + shared_voter_node_pubkey, + shared_voter_bls_pubkey, + ), + ( + shared_voter_vote_pubkey_2, + shared_voter_node_pubkey_2, + shared_voter_bls_pubkey_2, + ), + ] { + let rank = *rank_map.get_rank(&bls_pubkey).unwrap(); + assert_eq!( + rank_map.get_rank_for_vote_pubkey(&vote_account_pubkey), + Some(&rank) + ); + assert_eq!( + rank_map.get_pubkey_stake_entry(rank as usize), + Some(&BLSPubkeyStakeEntry { + vote_account_pubkey, + node_pubkey, + bls_pubkey, + stake: NonZero::new(100).unwrap(), + }) + ); + } + + let unique_rank = *rank_map.get_rank(&unique_bls_pubkey).unwrap(); + assert_eq!( + rank_map.get_rank_for_vote_pubkey(&unique_vote_pubkey), + Some(&unique_rank) + ); + assert_eq!( + rank_map.get_pubkey_stake_entry(unique_rank as usize), + Some(&BLSPubkeyStakeEntry { + vote_account_pubkey: unique_vote_pubkey, + node_pubkey: unique_node_pubkey, + bls_pubkey: unique_bls_pubkey, + stake: NonZero::new(50).unwrap(), + }) + ); + assert!(rank_map.get_pubkey_stake_entry(rank_map.len()).is_none()); + } + #[test] fn test_versioned_epoch_stakes_does_not_serialize_delegations() { // test-only types to get the serialized EpochStakes that still have stake delegations diff --git a/runtime/src/genesis_utils.rs b/runtime/src/genesis_utils.rs index 57cf44f1eb5..2d01fbfe8f0 100644 --- a/runtime/src/genesis_utils.rs +++ b/runtime/src/genesis_utils.rs @@ -2,17 +2,19 @@ use solana_stake_interface::config::Config as StakeConfig; use { crate::{ - bank::VAT_TO_BURN_PER_EPOCH, + bank::DEFAULT_VAT_TO_BURN_PER_EPOCH, block_component_processor::vote_reward::epoch_inflation_account_state::EpochInflationAccountState, stake_utils, }, agave_feature_set::{FEATURE_NAMES, FeatureSet}, agave_votor_messages::{ self, - consensus_message::{BLS_KEYPAIR_DERIVE_SEED, Certificate, CertificateType}, + consensus_message::{BLS_KEYPAIR_DERIVE_SEED, Block}, migration::GENESIS_CERTIFICATE_ACCOUNT, + wire::{WireBlockCertMessage, WireCertSignature}, }, bincode::serialize, + bitvec::vec::BitVec, log::*, solana_account::{Account, AccountSharedData, ReadableAccount, state_traits::StateMut}, solana_bls_signatures::{ @@ -33,6 +35,7 @@ use { solana_sdk_ids::{stake as stake_program, sysvar}, solana_seed_derivable::SeedDerivable, solana_signer::Signer, + solana_signer_store::encode_base2, solana_stake_interface::state::{Authorized, Lockup, Meta, StakeStateV2}, solana_system_interface::program as system_program, solana_sysvar::{ @@ -47,10 +50,11 @@ use { // Default amount received by the validator const VALIDATOR_LAMPORTS: u64 = 890_880; -// Minimum vote account balance required for VAT (SIMD-0357). -// Vote accounts need this minimum to pass VAT filtering. +// Default minimum vote account balance used by tests/genesis helpers. This is +// conservative once shorter slot-time regimes lower the live bank VAT burn. pub fn minimum_vote_account_balance_for_vat(num_epochs: Epoch) -> u64 { - VAT_TO_BURN_PER_EPOCH * num_epochs + Rent::default().minimum_balance(VoteStateV4::size_of()) + DEFAULT_VAT_TO_BURN_PER_EPOCH * num_epochs + + Rent::default().minimum_balance(VoteStateV4::size_of()) } // Minimum stake lamports required for a valid stake account with non-zero stake. @@ -84,6 +88,7 @@ pub const fn genesis_sysvar_and_builtin_program_lamports() -> u64 { + NUM_PRECOMPILES } +#[derive(Debug)] pub struct ValidatorVoteKeypairs { pub node_keypair: Keypair, pub vote_keypair: Keypair, @@ -317,13 +322,29 @@ pub fn create_genesis_config_with_leader_with_mint_keypair( pub fn activate_all_features_alpenglow(genesis_config: &mut GenesisConfig) { do_activate_all_features::(genesis_config); + configure_alpenglow_at_genesis(genesis_config); +} + +pub fn activate_alpenglow_at_genesis(genesis_config: &mut GenesisConfig) { + activate_feature(genesis_config, agave_feature_set::alpenglow::id()); + configure_alpenglow_at_genesis(genesis_config); +} + +fn configure_alpenglow_at_genesis(genesis_config: &mut GenesisConfig) { + // PoH is in low power mode + genesis_config.poh_config.hashes_per_tick = None; // This is a dev cluster with alpenglow enabled at genesis. We don't want to test the migration pathway // so we add a fake genesis certificate. - let cert = Certificate { - cert_type: CertificateType::Genesis(0, Hash::default()), - signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), - bitmap: Vec::default(), + let cert = WireBlockCertMessage { + block: Block { + slot: 0, + block_id: Hash::default(), + }, + signature: WireCertSignature { + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: encode_base2(&BitVec::new()).unwrap(), + }, }; let cert_size = bincode::serialized_size(&cert).unwrap(); let lamports = Rent::default().minimum_balance(cert_size as usize); @@ -384,7 +405,7 @@ pub fn bls_pubkey_to_compressed_bytes( bincode::serialize(&key).unwrap().try_into().unwrap() } -fn create_validator( +pub(crate) fn create_validator( rent: &Rent, node_pubkey: Pubkey, node_lamports: u64, @@ -439,7 +460,9 @@ pub fn create_genesis_config_with_leader_ex_no_features( mut initial_accounts: Vec<(Pubkey, AccountSharedData)>, ) -> GenesisConfig { // Ensure minimum lamports for VAT filtering, but only when stake > 0. - // VAT requires: non-zero stake, BLS pubkey, and lamports >= VAT_TO_BURN_PER_EPOCH + rent_exempt_minimum. + // VAT requires non-zero stake, a BLS pubkey, and lamports >= the bank's + // current VAT burn plus rent-exempt minimum. This helper funds with the + // conservative default burn amount. let (vote_account_lamports, stake_lamports) = if validator_stake_lamports > 0 { ( validator_stake_lamports.max(minimum_vote_account_balance_for_vat(100)), diff --git a/runtime/src/inflation_rewards/mod.rs b/runtime/src/inflation_rewards/mod.rs index 4e90506421e..4f1996e6426 100644 --- a/runtime/src/inflation_rewards/mod.rs +++ b/runtime/src/inflation_rewards/mod.rs @@ -1,16 +1,14 @@ //! Information about stake and voter rewards based on stake state. - +#[cfg(feature = "dev-context-only-utils")] +use qualifier_attr::qualifiers; use { self::points::{ CalculatedStakePoints, CalculationEnvironment, DelegatedVoteState, InflationPointCalculationEvent, SkippedReason, calculate_stake_points_and_credits, }, - solana_clock::Epoch, + crate::{alpenglow_epoch_type::AlpenglowEpochType, stake_delegation::effective_stake}, solana_instruction::error::InstructionError, - solana_stake_interface::{ - error::StakeError, - state::{Delegation, Stake}, - }, + solana_stake_interface::{error::StakeError, state::Stake}, }; pub mod points; @@ -34,6 +32,7 @@ pub(crate) fn redeem_rewards<'a>( vote_state: DelegatedVoteState, calculation_environment: CalculationEnvironment<'a>, inflation_point_calc_tracer: Option, + ag_epoch_type: &AlpenglowEpochType, current_lamports: u64, minimum_lamports: u64, ) -> Result<(u64, u64, Stake), InstructionError> { @@ -43,14 +42,20 @@ pub(crate) fn redeem_rewards<'a>( stake_history, new_rate_activation_epoch, commission_rate_in_basis_points, + use_fixed_point_stake_math, .. } = calculation_environment; + let effective_stake_at_rewarded_epoch = effective_stake( + &stake, + rewarded_epoch, + stake_history, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); inflation_point_calc_tracer( - &InflationPointCalculationEvent::EffectiveStakeAtRewardedEpoch(stake.stake( - rewarded_epoch, - stake_history, - new_rate_activation_epoch, - )), + &InflationPointCalculationEvent::EffectiveStakeAtRewardedEpoch( + effective_stake_at_rewarded_epoch, + ), ); inflation_point_calc_tracer(&InflationPointCalculationEvent::PriorTotalLamports( current_lamports, @@ -73,6 +78,7 @@ pub(crate) fn redeem_rewards<'a>( vote_state, calculation_environment, inflation_point_calc_tracer, + ag_epoch_type, current_lamports, minimum_lamports, ) { @@ -88,6 +94,7 @@ fn redeem_stake_rewards<'a>( vote_state: DelegatedVoteState, calculation_environment: CalculationEnvironment<'a>, inflation_point_calc_tracer: Option, + ag_epoch_type: &AlpenglowEpochType, current_lamports: u64, minimum_lamports: u64, ) -> Option<(u64, u64)> { @@ -98,7 +105,6 @@ fn redeem_stake_rewards<'a>( )); } - let rewarded_epoch = calculation_environment.rewarded_epoch; let adjust_delegations_for_rent = calculation_environment.adjust_delegations_for_rent; let maybe_rewards = calculate_stake_rewards( stake, @@ -106,6 +112,7 @@ fn redeem_stake_rewards<'a>( vote_state, calculation_environment, inflation_point_calc_tracer.as_ref(), + ag_epoch_type, ) .map(|calculated_stake_rewards| { if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer { @@ -124,16 +131,16 @@ fn redeem_stake_rewards<'a>( let staker_rewards = maybe_rewards.map(|x| x.0).unwrap_or(0); if adjust_delegations_for_rent { let new_delegation_with_rewards = stake.delegation.stake.saturating_add(staker_rewards); - let stake_was_adjusted = adjust_delegation_for_rent( - &mut stake.delegation, - rewarded_epoch, + let needs_adjustment = delegation_may_need_adjustment( + stake.delegation.stake, new_delegation_with_rewards, current_lamports.saturating_add(staker_rewards), minimum_lamports, ); // If `maybe_rewards.is_some()`, need to drive forward credits, even // if rewards are zero - if stake_was_adjusted || maybe_rewards.is_some() { + if needs_adjustment || maybe_rewards.is_some() { + stake.delegation.stake = new_delegation_with_rewards; let voter_rewards = maybe_rewards.map(|x| x.1).unwrap_or(0); Some((staker_rewards, voter_rewards)) } else { @@ -145,14 +152,14 @@ fn redeem_stake_rewards<'a>( } } -/// Adjusts stake delegation based on Rent sysvar parameters at epoch boundary +/// Returns `true` if stake delegation needs to be adjusted during distribution +/// based on Rent sysvar parameters at epoch boundary /// -/// As part of SIMD-0392, if Rent is ever increased, we need to make sure that -/// lamports are not double-counted for the rent-exempt minimum and the stake -/// delegation. This function adjusts the delegation in a Stake if needed. -pub(crate) fn adjust_delegation_for_rent( - delegation: &mut Delegation, - rewarded_epoch: Epoch, +/// The actual adjustment happens at distribution, to account for any lamports +/// credited to the account during partitioned epoch rewards, before the +/// distribution has occurred. +pub(crate) fn delegation_may_need_adjustment( + current_delegation: u64, new_delegation_with_rewards: u64, lamports_with_rewards: u64, minimum_lamports: u64, @@ -162,18 +169,7 @@ pub(crate) fn adjust_delegation_for_rent( lamports_with_rewards.saturating_sub(minimum_lamports), ); - if new_delegation != delegation.stake { - delegation.stake = new_delegation; - // Deactivate stake if needed. This deactivation is immediate, - // unlike a requested deactivation which happens at the next epoch - // boundary - if new_delegation == 0 { - delegation.deactivation_epoch = rewarded_epoch; - } - true - } else { - false - } + new_delegation != current_delegation } /// for a given stake and vote_state, calculate what distributions and what updates should be made @@ -189,17 +185,21 @@ fn calculate_stake_rewards<'a>( vote_state: DelegatedVoteState, calculation_environment: CalculationEnvironment<'a>, inflation_point_calc_tracer: Option, + ag_epoch_type: &AlpenglowEpochType, ) -> Option { let CalculationEnvironment { stake_history, new_rate_activation_epoch, point_value, rewarded_epoch, + use_fixed_point_stake_math, .. } = calculation_environment; + // ensure to run to trigger (optional) inflation_point_calc_tracer let CalculatedStakePoints { - points, + tower_points, + ag_points, new_credits_observed, mut force_credits_update_with_skipped_reward, } = calculate_stake_points_and_credits( @@ -208,6 +208,8 @@ fn calculate_stake_rewards<'a>( stake_history, inflation_point_calc_tracer.as_ref(), new_rate_activation_epoch, + ag_epoch_type, + use_fixed_point_stake_math, ); // Drive credits_observed forward unconditionally when rewards are disabled @@ -225,44 +227,99 @@ fn calculate_stake_rewards<'a>( force_credits_update_with_skipped_reward = true; } - if force_credits_update_with_skipped_reward { - return Some(CalculatedStakeRewards { + // Once alpenglow is active we no longer allow for epochs where rewards are not redeemed. + let is_tower_epoch = matches!(ag_epoch_type, AlpenglowEpochType::Tower); + let advance_credits_for_skipped_reward = + !is_tower_epoch && new_credits_observed != stake.credits_observed; + let skipped_reward = || { + Some(CalculatedStakeRewards { staker_rewards: 0, voter_rewards: 0, new_credits_observed, - }); - } + }) + }; - if points == 0 { + let skip_reward = |reason: SkippedReason| { if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { - inflation_point_calc_tracer(&SkippedReason::ZeroPoints.into()); + inflation_point_calc_tracer(&reason.into()); } - return None; - } - if point_value.points == 0 { - if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { - inflation_point_calc_tracer(&SkippedReason::ZeroPointValue.into()); + if advance_credits_for_skipped_reward { + skipped_reward() + } else { + None } - return None; + }; + + if force_credits_update_with_skipped_reward { + return skipped_reward(); } - // The final unwrap is safe, as points_value.points is guaranteed to be non zero above. - let rewards = points - .checked_mul(u128::from(point_value.rewards)) - .expect("Rewards intermediate calculation should fit within u128") - .checked_div(point_value.points) - .unwrap(); + let rewards = match ag_epoch_type { + AlpenglowEpochType::Alpenglow { .. } => { + if ag_points == 0 { + return skip_reward(SkippedReason::ZeroPoints); + } + // In alpenglow, `points` represents the actual reward that this `stake` earned. + ag_points + } + AlpenglowEpochType::Tower => { + if tower_points == 0 { + if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { + inflation_point_calc_tracer(&SkippedReason::ZeroPoints.into()); + } + return None; + } + if point_value.points == 0 { + if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { + inflation_point_calc_tracer(&SkippedReason::ZeroPointValue.into()); + } + return None; + } + // In tower, `points` still needs to be scaled by `point_value` to calculate this + // `vote_state` earned. + // The final unwrap is safe, as points_value.points is guaranteed to be non zero above. + tower_points + .checked_mul(u128::from(point_value.rewards)) + .expect("Rewards intermediate calculation should fit within u128") + .checked_div(point_value.points) + .unwrap() + } + AlpenglowEpochType::MigrationEpoch { + num_tower_slots, + num_ag_slots, + .. + } => { + if tower_points == 0 && ag_points == 0 { + return skip_reward(SkippedReason::ZeroPoints); + } + if ag_points == 0 && point_value.points == 0 { + return skip_reward(SkippedReason::ZeroPointValue); + } + let total_slots = (num_tower_slots + num_ag_slots) as u128; + let tower_points = tower_points + .checked_mul(u128::from(point_value.rewards)) + .expect("Rewards intermediate calculation should fit within u128") + .checked_div(point_value.points) + .unwrap() + .checked_mul(*num_tower_slots as u128) + .unwrap() + .checked_div(total_slots) + .unwrap(); + tower_points + ag_points + } + }; let rewards = u64::try_from(rewards).expect("Rewards should fit within u64"); // don't bother trying to split if fractional lamports got truncated if rewards == 0 { - if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { - inflation_point_calc_tracer(&SkippedReason::ZeroReward.into()); - } - return None; + return skip_reward(SkippedReason::ZeroReward); } - let (voter_rewards, staker_rewards, is_split) = commission_split(voter_commission_bps, rewards); + let (voter_rewards, staker_rewards, is_split) = if is_tower_epoch { + commission_split(voter_commission_bps, rewards) + } else { + commission_split_preserve_lamports(voter_commission_bps, rewards) + }; if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { inflation_point_calc_tracer(&InflationPointCalculationEvent::SplitRewards( rewards, @@ -272,10 +329,10 @@ fn calculate_stake_rewards<'a>( )); } - if (voter_rewards == 0 || staker_rewards == 0) && is_split { - // don't collect if we lose a whole lamport somewhere - // is_split means there should be tokens on both sides, - // uncool to move credits_observed if one side didn't get paid + if (voter_rewards == 0 || staker_rewards == 0) && is_split && is_tower_epoch { + // In Tower, don't collect if we lose a whole lamport somewhere. + // is_split means there should be tokens on both sides; don't move + // credits_observed if one side didn't get paid. if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { inflation_point_calc_tracer(&SkippedReason::TooEarlyUnfairSplit.into()); } @@ -295,6 +352,7 @@ fn calculate_stake_rewards<'a>( /// indicate with false for was_split /// /// DEVELOPER NOTE: This function used to be a method on VoteState, but was moved here +#[cfg_attr(any(test, feature = "dev-context-only-utils"), qualifiers(pub(crate)))] fn commission_split(commission_bps: u16, on: u64) -> (u64, u64, bool) { const MAX_BPS: u16 = 10_000; const MAX_BPS_U128: u128 = MAX_BPS as u128; @@ -305,7 +363,7 @@ fn commission_split(commission_bps: u16, on: u64) -> (u64, u64, bool) { let on = u128::from(on); // Calculate mine and theirs independently and symmetrically instead of // using the remainder of the other to treat them strictly equally. - // This is also to cancel the rewarding if either of the parties + // In Tower, this is also to cancel the rewarding if either of the parties // should receive only fractional lamports, resulting in not being rewarded at all. // Thus, note that we intentionally discard any residual fractional lamports. let mine = on @@ -326,11 +384,42 @@ fn commission_split(commission_bps: u16, on: u64) -> (u64, u64, bool) { } } +/// returns commission split as (voter_portion, staker_portion, was_split) tuple, +/// assigning any fractional-lamport remainder to the voter so no lamports are lost. +/// +/// This is used only for non-Tower epochs, where small unfair splits no longer defer redemption. +#[cfg_attr(any(test, feature = "dev-context-only-utils"), qualifiers(pub(crate)))] +fn commission_split_preserve_lamports(commission_bps: u16, on: u64) -> (u64, u64, bool) { + const MAX_BPS: u16 = 10_000; + const MAX_BPS_U128: u128 = MAX_BPS as u128; + match commission_bps.min(MAX_BPS) { + 0 => (0, on, false), + MAX_BPS => (on, 0, false), + split => { + let staker_bps = MAX_BPS + .checked_sub(split) + .expect("commission cannot be greater than MAX_BPS"); + let staker_rewards = u128::from(on) + .checked_mul(u128::from(staker_bps)) + .expect("multiplication of a u64 and u16 should not overflow") + / MAX_BPS_U128; + let staker_rewards = staker_rewards as u64; + let voter_rewards = on + .checked_sub(staker_rewards) + .expect("staker rewards cannot exceed total rewards"); + + (voter_rewards, staker_rewards, true) + } + } +} + #[cfg(test)] mod tests { use { self::points::{PointValue, null_tracer}, super::*, + crate::alpenglow_epoch_type::RewardEpochDelegatedStakes, + agave_votor_messages::migration::AG_MIGRATION_EPOCH_CREDIT, proptest::prelude::*, solana_clock::Epoch, solana_native_token::LAMPORTS_PER_SOL, @@ -341,7 +430,7 @@ mod tests { state::{Delegation, StakeStateV2}, }, solana_vote_program::vote_state::{VoteStateV4, handler::VoteStateHandler}, - test_case::test_case, + test_case::{test_case, test_matrix}, }; fn new_stake( @@ -356,9 +445,50 @@ mod tests { } } - #[test_case(true; "adjust_delegations_for_rent")] - #[test_case(false; "no_adjust_delegations_for_rent")] - fn test_stake_state_redeem_rewards(adjust_delegations_for_rent: bool) { + /// Returns an instance of `AlpenglowEpochType`, total stake, and first AG epoch. + fn get_ag_epoch_type() -> (AlpenglowEpochType, u64, Epoch) { + let total_stake = 1_000; + let migration_epoch = 0; + let first_ag_epoch = migration_epoch + 1; + ( + AlpenglowEpochType::Alpenglow { + migration_epoch, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes { + epoch: first_ag_epoch, + delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(), + }, + }, + total_stake, + first_ag_epoch, + ) + } + + fn make_ag_epoch_type_for_test( + ag_enabled: bool, + vote_state: &VoteStateV4, + ag_total_stake_multiplier: u64, + ) -> AlpenglowEpochType { + if ag_enabled { + AlpenglowEpochType::Alpenglow { + migration_epoch: 0, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes { + epoch: vote_state + .epoch_credits + .last() + .map(|(epoch, _final_epoch_credits, _initial_epoch_credits)| *epoch) + .unwrap_or(1), + delegated_stakes: [(Pubkey::default(), ag_total_stake_multiplier)] + .into_iter() + .collect(), + }, + } + } else { + AlpenglowEpochType::Tower + } + } + + #[test_matrix([true, false], [true, false])] + fn test_stake_state_redeem_rewards(adjust_delegations_for_rent: bool, ag_enabled: bool) { let mut vote_state = VoteStateHandler::new_v4(VoteStateV4::default()); // assume stake.stake() is right // bootstrap means fully-vested stake at epoch 0 @@ -373,6 +503,23 @@ mod tests { let new_rate_activation_epoch = None; let commission_rate_in_basis_points = true; + // epoch credits work differently in AG, so we need a multiplier to account for that. + let ag_total_stake_multiplier = if ag_enabled { + let (_, ag_total_stake_multiplier, _) = get_ag_epoch_type(); + ag_total_stake_multiplier + } else { + 1 + }; + + let inc_credits = |handler: &mut VoteStateHandler, epoch: Epoch, credits: u64| { + if ag_enabled { + let (_, _, first_ag_epoch) = get_ag_epoch_type(); + handler + .increment_credits(epoch + first_ag_epoch, credits * ag_total_stake_multiplier); + } else { + handler.increment_credits(epoch, credits); + } + }; let mut rent = Rent::default(); let minimum_balance = rent.minimum_balance(StakeStateV2::size_of()); @@ -399,16 +546,21 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier, + ), stake_lamports + minimum_balance, new_minimum_balance, ) ); // put 2 credits in at epoch 0 - vote_state.increment_credits(0, 1); - vote_state.increment_credits(0, 1); + inc_credits(&mut vote_state, 0, 2); // this one should be able to collect exactly 2 assert_eq!( @@ -427,8 +579,14 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier, + ), stake_lamports + minimum_balance, new_minimum_balance, ) @@ -438,11 +596,11 @@ mod tests { stake.delegation.stake, stake_lamports + (stake_lamports * 2) ); - assert_eq!(stake.credits_observed, 2); + assert_eq!(stake.credits_observed, 2 * ag_total_stake_multiplier); } - #[test] - fn test_stake_state_calculate_rewards() { + #[test_matrix([true, false])] + fn test_stake_state_calculate_rewards(ag_enabled: bool) { let mut vote_state = VoteStateHandler::new_v4(VoteStateV4::default()); // assume stake.stake() is right // bootstrap means fully-vested stake at epoch 0 @@ -453,6 +611,23 @@ mod tests { let commission_rate_in_basis_points = true; let adjust_delegations_for_rent = true; + // epoch credits work differently in AG, so we need a multiplier to account for that. + let ag_total_stake_multiplier = if ag_enabled { + let (_, ag_total_stake_multiplier, _) = get_ag_epoch_type(); + ag_total_stake_multiplier + } else { + 1 + }; + + let inc_credits = |handler: &mut VoteStateHandler, epoch: Epoch, credits: u64| { + if ag_enabled { + let (_, _, first_ag_epoch) = get_ag_epoch_type(); + handler + .increment_credits(epoch + first_ag_epoch, credits * ag_total_stake_multiplier); + } else { + handler.increment_credits(epoch, credits); + } + }; // this one can't collect now, credits_observed == vote_state.credits() assert_eq!( None, @@ -470,21 +645,26 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), ) ); // put 2 credits in at epoch 0 - vote_state.increment_credits(0, 1); - vote_state.increment_credits(0, 1); + inc_credits(&mut vote_state, 0, 2); // this one should be able to collect exactly 2 assert_eq!( Some(CalculatedStakeRewards { staker_rewards: stake.delegation.stake * 2, voter_rewards: 0, - new_credits_observed: 2, + new_credits_observed: 2 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -500,18 +680,24 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), ) ); - stake.credits_observed = 1; + stake.credits_observed = ag_total_stake_multiplier; // this one should be able to collect exactly 1 (already observed one) assert_eq!( Some(CalculatedStakeRewards { staker_rewards: stake.delegation.stake, voter_rewards: 0, - new_credits_observed: 2, + new_credits_observed: 2 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -527,21 +713,27 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), ) ); // put 1 credit in epoch 1 - vote_state.increment_credits(1, 1); + inc_credits(&mut vote_state, 1, 1); - stake.credits_observed = 2; + stake.credits_observed = 2 * ag_total_stake_multiplier; // this one should be able to collect the one just added assert_eq!( Some(CalculatedStakeRewards { staker_rewards: stake.delegation.stake, voter_rewards: 0, - new_credits_observed: 3, + new_credits_observed: 3 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -557,19 +749,31 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), ) ); // put 1 credit in epoch 2 - vote_state.increment_credits(2, 1); - // this one should be able to collect 2 now + inc_credits(&mut vote_state, 2, 1); + // Tower redeems all unobserved epoch credits. Full AG only considers + // the latest epoch credit entry. + let expected_staker_rewards = if ag_enabled { + stake.delegation.stake + } else { + stake.delegation.stake * 2 + }; assert_eq!( Some(CalculatedStakeRewards { - staker_rewards: stake.delegation.stake * 2, + staker_rewards: expected_staker_rewards, voter_rewards: 0, - new_credits_observed: 4, + new_credits_observed: 4 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -585,21 +789,32 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), ) ); stake.credits_observed = 0; - // this one should be able to collect everything from t=0 a warmed up stake of 2 - // (2 credits at stake of 1) + (1 credit at a stake of 2) + // Tower collects everything from t=0. Full AG only considers the + // latest epoch credit entry. + let expected_staker_rewards = if ag_enabled { + stake.delegation.stake + } else { + stake.delegation.stake * 2 // epoch 0 + + stake.delegation.stake // epoch 1 + + stake.delegation.stake // epoch 2 + }; assert_eq!( Some(CalculatedStakeRewards { - staker_rewards: stake.delegation.stake * 2 // epoch 0 - + stake.delegation.stake // epoch 1 - + stake.delegation.stake, // epoch 2 + staker_rewards: expected_staker_rewards, voter_rewards: 0, - new_credits_observed: 4, + new_credits_observed: 4 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -615,16 +830,31 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), ) ); - // same as above, but is a really small commission out of 32 bits, - // verify that None comes back on small redemptions where no one gets paid + let small_redemption_result = || { + ag_enabled.then_some(CalculatedStakeRewards { + staker_rewards: 0, + voter_rewards: 1, + new_credits_observed: 4 * ag_total_stake_multiplier, + }) + }; + + // same as above, but is a small enough reward that both sides round to + // zero after the Tower commission split. Tower defers; AG assigns the + // remainder to the voter and advances credits. vote_state.set_inflation_rewards_commission_bps(100); assert_eq!( - None, // would be Some((0, 2 * 1 + 1 * 2, 4)), + small_redemption_result(), calculate_stake_rewards( &stake, vote_state.as_ref_v4().inflation_rewards_commission_bps, @@ -639,13 +869,19 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), ) ); vote_state.set_inflation_rewards_commission_bps(9900); assert_eq!( - None, // would be Some((0, 2 * 1 + 1 * 2, 4)), + small_redemption_result(), calculate_stake_rewards( &stake, vote_state.as_ref_v4().inflation_rewards_commission_bps, @@ -660,8 +896,14 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), ) ); @@ -672,7 +914,7 @@ mod tests { Some(CalculatedStakeRewards { staker_rewards: 0, voter_rewards: 0, - new_credits_observed: 4, + new_credits_observed: 4 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -688,19 +930,25 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), ) ); // credits_observed remains at previous level when vote_state credits are // not advancing and inflation is disabled - stake.credits_observed = 4; + stake.credits_observed = 4 * ag_total_stake_multiplier; assert_eq!( Some(CalculatedStakeRewards { staker_rewards: 0, voter_rewards: 0, - new_credits_observed: 4, + new_credits_observed: 4 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -716,15 +964,22 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), ) ); assert_eq!( CalculatedStakePoints { - points: 0, - new_credits_observed: 4, + tower_points: 0, + ag_points: 0, + new_credits_observed: 4 * ag_total_stake_multiplier, force_credits_update_with_skipped_reward: false, }, calculate_stake_points_and_credits( @@ -732,18 +987,25 @@ mod tests { DelegatedVoteState::from(vote_state.as_ref_v4()), &StakeHistory::default(), null_tracer(), - None + None, + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), + true, ) ); // credits_observed is auto-rewound when vote_state credits are assumed to have been // recreated - stake.credits_observed = 1000; + stake.credits_observed = 1000 * ag_total_stake_multiplier; // this is new behavior 1; return the post-recreation rewound credits from the vote account assert_eq!( CalculatedStakePoints { - points: 0, - new_credits_observed: 4, + tower_points: 0, + ag_points: 0, + new_credits_observed: 4 * ag_total_stake_multiplier, force_credits_update_with_skipped_reward: true, }, calculate_stake_points_and_credits( @@ -751,15 +1013,22 @@ mod tests { DelegatedVoteState::from(vote_state.as_ref_v4()), &StakeHistory::default(), null_tracer(), - None + None, + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), + true, ) ); // this is new behavior 2; don't hint when credits both from stake and vote are identical - stake.credits_observed = 4; + stake.credits_observed = 4 * ag_total_stake_multiplier; assert_eq!( CalculatedStakePoints { - points: 0, - new_credits_observed: 4, + tower_points: 0, + ag_points: 0, + new_credits_observed: 4 * ag_total_stake_multiplier, force_credits_update_with_skipped_reward: false, }, calculate_stake_points_and_credits( @@ -767,19 +1036,25 @@ mod tests { DelegatedVoteState::from(vote_state.as_ref_v4()), &StakeHistory::default(), null_tracer(), - None + None, + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), + true, ) ); // get rewards and credits observed when not the activation epoch vote_state.set_inflation_rewards_commission_bps(0); - stake.credits_observed = 3; + stake.credits_observed = 3 * ag_total_stake_multiplier; stake.delegation.activation_epoch = 1; assert_eq!( Some(CalculatedStakeRewards { staker_rewards: stake.delegation.stake, // epoch 2 voter_rewards: 0, - new_credits_observed: 4, + new_credits_observed: 4 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -795,20 +1070,26 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), ) ); // credits_observed is moved forward for the stake's activation epoch, // and no rewards are perceived stake.delegation.activation_epoch = 2; - stake.credits_observed = 3; + stake.credits_observed = 3 * ag_total_stake_multiplier; assert_eq!( Some(CalculatedStakeRewards { staker_rewards: 0, voter_rewards: 0, - new_credits_observed: 4, + new_credits_observed: 4 * ag_total_stake_multiplier, }), calculate_stake_rewards( &stake, @@ -824,276 +1105,18 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &make_ag_epoch_type_for_test( + ag_enabled, + vote_state.as_ref_v4(), + ag_total_stake_multiplier + ), ) ); } - fn check_rent_adjusted_stake_delegation( - rewarded_epoch: u64, - mut pre_stake: Stake, - pre_lamports: u64, - new_minimum_balance: u64, - total_rewards: u64, - post_stake: Stake, - staker_rewards: Option, - ) { - let mut vote_state = VoteStateHandler::new_v4(VoteStateV4::default()); - // put 1 credit to create rewards - vote_state.increment_credits(rewarded_epoch, 1); - let stake_history: &StakeHistory = &StakeHistory::default(); - let new_rate_activation_epoch = None; - let commission_rate_in_basis_points = true; - let adjust_delegations_for_rent = true; - - let maybe_rewards = redeem_stake_rewards( - &mut pre_stake, - vote_state.as_ref_v4().inflation_rewards_commission_bps, - DelegatedVoteState::from(vote_state.as_ref_v4()), - CalculationEnvironment { - rewarded_epoch, - point_value: &PointValue { - rewards: total_rewards, - points: 1, - }, - stake_history, - new_rate_activation_epoch, - commission_rate_in_basis_points, - adjust_delegations_for_rent, - }, - null_tracer(), - pre_lamports, - new_minimum_balance, - ); - assert_eq!(pre_stake, post_stake); - assert_eq!(maybe_rewards.map(|x| x.0), staker_rewards) - } - - #[test] - fn rent_adjusted_stake_delegation_calculations() { - let old_minimum_balance = 8; - let new_minimum_balance = 9; - let rewarded_epoch = 1; - - // No rewards at all -> updated (all stakes get driven forward if - // inflation is disabled) - check_rent_adjusted_stake_delegation( - rewarded_epoch, - Stake { - delegation: Delegation { - stake: 1, - ..Default::default() - }, - credits_observed: 1, - }, - new_minimum_balance + 1, - new_minimum_balance, - 0, - Stake { - delegation: Delegation { - stake: 1, - ..Default::default() - }, - credits_observed: 1, - }, - Some(0), - ); - - // Stake receives no rewards or delegation adjustment -> no update - check_rent_adjusted_stake_delegation( - rewarded_epoch, - Stake { - delegation: Delegation { - stake: 1, - ..Default::default() - }, - credits_observed: 1, - }, - new_minimum_balance + 1, - new_minimum_balance, - 1, - Stake { - delegation: Delegation { - stake: 1, - ..Default::default() - }, - credits_observed: 1, - }, - None, - ); - - // Already destaked -> no update - check_rent_adjusted_stake_delegation( - rewarded_epoch, - Stake { - delegation: Delegation { - stake: 0, - deactivation_epoch: 0, - ..Default::default() - }, - credits_observed: 0, - }, - old_minimum_balance - 1, - new_minimum_balance, - 1, - Stake { - delegation: Delegation { - stake: 0, - deactivation_epoch: 0, - ..Default::default() - }, - credits_observed: 0, - }, - None, - ); - - // Staked, already below minimum, go further below minimum - // -> destaked, still one lamport of rewards though - check_rent_adjusted_stake_delegation( - rewarded_epoch, - Stake { - delegation: Delegation { - stake: 1, - ..Default::default() - }, - credits_observed: 0, - }, - old_minimum_balance - 1, - new_minimum_balance, - 1, - Stake { - delegation: Delegation { - stake: 0, - deactivation_epoch: rewarded_epoch, - ..Default::default() - }, - credits_observed: 1, - }, - Some(1), - ); - - // Delegation hits exactly 0 -> destaked, no rewards - check_rent_adjusted_stake_delegation( - rewarded_epoch, - Stake { - delegation: Delegation { - stake: 1, - ..Default::default() - }, - credits_observed: 0, - }, - new_minimum_balance, - new_minimum_balance, - 0, - Stake { - delegation: Delegation { - stake: 0, - deactivation_epoch: rewarded_epoch, - ..Default::default() - }, - credits_observed: 1, - }, - Some(0), - ); - - // Delegation decreases to 1 -> still staked - check_rent_adjusted_stake_delegation( - rewarded_epoch, - Stake { - delegation: Delegation { - stake: 2, - ..Default::default() - }, - credits_observed: 0, - }, - new_minimum_balance + 1, - new_minimum_balance, - 0, - Stake { - delegation: Delegation { - stake: 1, - ..Default::default() - }, - credits_observed: 1, - }, - Some(0), - ); - - // Rewards partially cover minimum balance change - // -> decrease stake - // This case is confusing because it pays out 2 lamports in rewards, - // so we adjust minimum up so that even with 2 lamports in rewards, the - // delegation goes down. - check_rent_adjusted_stake_delegation( - rewarded_epoch, - Stake { - delegation: Delegation { - stake: 2, - ..Default::default() - }, - credits_observed: 0, - }, - new_minimum_balance, - new_minimum_balance + 1, - 1, - Stake { - delegation: Delegation { - stake: 1, - ..Default::default() - }, - credits_observed: 1, - }, - Some(2), - ); - - // Rewards cover minimum balance change -> no change in stake - check_rent_adjusted_stake_delegation( - rewarded_epoch, - Stake { - delegation: Delegation { - stake: 1, - ..Default::default() - }, - credits_observed: 0, - }, - new_minimum_balance, - new_minimum_balance, - 1, - Stake { - delegation: Delegation { - stake: 1, - ..Default::default() - }, - credits_observed: 1, - }, - Some(1), - ); - - // Well above new minimum balance -> delegation change capped to rewards - check_rent_adjusted_stake_delegation( - rewarded_epoch, - Stake { - delegation: Delegation { - stake: 1, - ..Default::default() - }, - credits_observed: 0, - }, - new_minimum_balance + 2, - new_minimum_balance, - 1, - Stake { - delegation: Delegation { - stake: 2, - ..Default::default() - }, - credits_observed: 1, - }, - Some(1), - ); - } - #[test_case(u64::MAX, 1_000, u64::MAX => panics "Rewards intermediate calculation should fit within u128")] #[test_case(1, u64::MAX, u64::MAX => panics "Rewards should fit within u64")] fn calculate_rewards_tests(stake: u64, rewards: u64, credits: u64) { @@ -1119,13 +1142,15 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &AlpenglowEpochType::Tower, ); } - #[test] - fn test_stake_state_calculate_points_with_typical_values() { + #[test_matrix([true, false])] + fn test_stake_state_calculate_points_with_typical_values(ag_enabled: bool) { let vote_state = VoteStateHandler::new_v4(VoteStateV4::default()); // bootstrap means fully-vested stake at epoch 0 with @@ -1141,6 +1166,13 @@ mod tests { let commission_rate_in_basis_points = true; let adjust_delegations_for_rent = true; + let ag_stake_state = if ag_enabled { + let (state, _, _) = get_ag_epoch_type(); + state + } else { + AlpenglowEpochType::Tower + }; + // this one can't collect now, credits_observed == vote_state.credits() assert_eq!( None, @@ -1158,8 +1190,85 @@ mod tests { new_rate_activation_epoch, commission_rate_in_basis_points, adjust_delegations_for_rent, + use_fixed_point_stake_math: true, }, null_tracer(), + &ag_stake_state, + ) + ); + } + + #[test] + fn test_migration_epoch_dust_split_advances_credits() { + let (_, ag_total_stake_multiplier, _) = get_ag_epoch_type(); + let mut vote_state = VoteStateV4 { + inflation_rewards_commission_bps: 100, + epoch_credits: vec![ + AG_MIGRATION_EPOCH_CREDIT, + (0, 4 * ag_total_stake_multiplier, 0), + ], + ..VoteStateV4::default() + }; + let stake = Stake { + delegation: Delegation::new(&Pubkey::default(), 1, u64::MAX), + credits_observed: 0, + }; + let point_value = PointValue { + rewards: 4, + points: 1, + }; + let stake_history = StakeHistory::default(); + let calculation_environment = || CalculationEnvironment { + rewarded_epoch: 0, + point_value: &point_value, + stake_history: &stake_history, + new_rate_activation_epoch: None, + commission_rate_in_basis_points: true, + adjust_delegations_for_rent: true, + use_fixed_point_stake_math: true, + }; + let migration_epoch_type = AlpenglowEpochType::MigrationEpoch { + num_tower_slots: 0, + num_ag_slots: 1, + migration_epoch: 0, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes { + epoch: 0, + delegated_stakes: [(Pubkey::default(), ag_total_stake_multiplier)] + .into_iter() + .collect(), + }, + }; + + assert_eq!( + Some(CalculatedStakeRewards { + staker_rewards: 3, + voter_rewards: 1, + new_credits_observed: 4 * ag_total_stake_multiplier, + }), + calculate_stake_rewards( + &stake, + vote_state.inflation_rewards_commission_bps, + DelegatedVoteState::from(&vote_state), + calculation_environment(), + null_tracer(), + &migration_epoch_type, + ) + ); + + vote_state.inflation_rewards_commission_bps = 9_900; + assert_eq!( + Some(CalculatedStakeRewards { + staker_rewards: 0, + voter_rewards: 4, + new_credits_observed: 4 * ag_total_stake_multiplier, + }), + calculate_stake_rewards( + &stake, + vote_state.inflation_rewards_commission_bps, + DelegatedVoteState::from(&vote_state), + calculation_environment(), + null_tracer(), + &migration_epoch_type, ) ); } @@ -1272,6 +1381,92 @@ mod tests { ); // 1-lamport truncation } + #[test] + fn test_commission_split_preserve_lamports_bps() { + // 0% commission + assert_eq!(commission_split_preserve_lamports(0, 1), (0, 1, false)); + assert_eq!(commission_split_preserve_lamports(0, 10), (0, 10, false)); + assert_eq!(commission_split_preserve_lamports(0, 100), (0, 100, false)); + assert_eq!( + commission_split_preserve_lamports(0, u64::MAX), + (0, u64::MAX, false) + ); + + // 100% commission (10,000 bps) + assert_eq!(commission_split_preserve_lamports(10_000, 1), (1, 0, false)); + assert_eq!( + commission_split_preserve_lamports(10_000, 10), + (10, 0, false) + ); + assert_eq!( + commission_split_preserve_lamports(10_000, 100), + (100, 0, false) + ); + assert_eq!( + commission_split_preserve_lamports(10_000, u64::MAX), + (u64::MAX, 0, false) + ); + + // Values > 10,000 bps are capped at 100% + assert_eq!( + commission_split_preserve_lamports(u16::MAX, 1), + (1, 0, false) + ); + assert_eq!( + commission_split_preserve_lamports(u16::MAX, u64::MAX), + (u64::MAX, 0, false) + ); + + // Remainder lamports go to the voter. + assert_eq!(commission_split_preserve_lamports(9_900, 1), (1, 0, true)); + assert_eq!(commission_split_preserve_lamports(9_900, 10), (10, 0, true)); + assert_eq!( + commission_split_preserve_lamports(9_900, 100), + (99, 1, true) + ); + assert_eq!( + commission_split_preserve_lamports(9_900, 1_000), + (990, 10, true) + ); + + assert_eq!(commission_split_preserve_lamports(100, 1), (1, 0, true)); + assert_eq!(commission_split_preserve_lamports(100, 10), (1, 9, true)); + assert_eq!(commission_split_preserve_lamports(100, 100), (1, 99, true)); + assert_eq!( + commission_split_preserve_lamports(100, 1_000), + (10, 990, true) + ); + + assert_eq!(commission_split_preserve_lamports(5_000, 1), (1, 0, true)); + assert_eq!(commission_split_preserve_lamports(5_000, 10), (5, 5, true)); + assert_eq!( + commission_split_preserve_lamports(5_000, 100), + (50, 50, true) + ); + + assert_eq!(commission_split_preserve_lamports(1_234, 1), (1, 0, true)); + assert_eq!(commission_split_preserve_lamports(1_234, 10), (2, 8, true)); + assert_eq!( + commission_split_preserve_lamports(1_234, 1_000), + (124, 876, true) + ); + assert_eq!( + commission_split_preserve_lamports(1_234, 10_000), + (1_234, 8_766, true) + ); + + assert_eq!(commission_split_preserve_lamports(3_333, 1), (1, 0, true)); + assert_eq!(commission_split_preserve_lamports(3_333, 10), (4, 6, true)); + assert_eq!( + commission_split_preserve_lamports(3_333, 1_000), + (334, 666, true) + ); + assert_eq!( + commission_split_preserve_lamports(3_333, 10_000), + (3_333, 6_667, true) + ); + } + proptest! { #[test] fn test_commission_split_properties( @@ -1332,5 +1527,61 @@ mod tests { prop_assert_eq!(voter, expected_voter); prop_assert_eq!(staker, expected_staker); } + + #[test] + fn test_commission_split_preserve_lamports_properties( + commission_bps in 0..=u16::MAX, + rewards in 0..=u64::MAX, + ) { + let (voter, staker, was_split) = + commission_split_preserve_lamports(commission_bps, rewards); + + // Invariant 1: The full reward amount is assigned. + prop_assert_eq!(voter + staker, rewards); + + // Invariant 2: was_split is false only at the 0% and 100% boundaries. + let effective_bps = commission_bps.min(10_000); + if effective_bps == 0 || effective_bps == 10_000 { + prop_assert!(!was_split); + } else { + prop_assert!(was_split); + } + + // Invariant 3: Boundary - 0% commission gives everything to staker. + if effective_bps == 0 { + prop_assert_eq!(voter, 0); + prop_assert_eq!(staker, rewards); + } + + // Invariant 4: Boundary - 100% commission gives everything to voter. + if effective_bps == 10_000 { + prop_assert_eq!(voter, rewards); + prop_assert_eq!(staker, 0); + } + + // Invariant 5: Clamping - values above 10,000 bps behave as 10,000. + if commission_bps > 10_000 { + let (clamped_voter, clamped_staker, clamped_ws) = + commission_split_preserve_lamports(10_000, rewards); + prop_assert_eq!(voter, clamped_voter); + prop_assert_eq!(staker, clamped_staker); + prop_assert_eq!(was_split, clamped_ws); + } + + // Invariant 6: Higher commission does not decrease the voter amount. + if commission_bps > 0 { + let lower_bps = commission_bps - 1; + let (lower_voter, _, _) = commission_split_preserve_lamports(lower_bps, rewards); + prop_assert!(voter >= lower_voter); + } + + // Invariant 7: The staker side is floored, and the voter gets the remainder. + let staker_bps = 10_000 - effective_bps; + let expected_staker = + (u128::from(rewards) * u128::from(staker_bps) / 10_000) as u64; + let expected_voter = rewards - expected_staker; + prop_assert_eq!(voter, expected_voter); + prop_assert_eq!(staker, expected_staker); + } } } diff --git a/runtime/src/inflation_rewards/points.rs b/runtime/src/inflation_rewards/points.rs index 8b26ebe170b..24667e0ad6d 100644 --- a/runtime/src/inflation_rewards/points.rs +++ b/runtime/src/inflation_rewards/points.rs @@ -1,6 +1,12 @@ //! Information about points calculation based on stake state. use { + crate::{ + alpenglow_epoch_type::{AlpenglowEpochType, RewardEpochDelegatedStakes}, + stake_delegation::delegation_effective_stake, + }, + agave_votor_messages::migration::AG_MIGRATION_EPOCH_CREDIT, + log::error, solana_clock::Epoch, solana_instruction::error::InstructionError, solana_pubkey::Pubkey, @@ -24,7 +30,8 @@ pub struct PointValue { #[derive(Debug, PartialEq, Eq)] pub(crate) struct CalculatedStakePoints { - pub(crate) points: u128, + pub(crate) tower_points: u128, + pub(crate) ag_points: u128, pub(crate) new_credits_observed: u64, pub(crate) force_credits_update_with_skipped_reward: bool, } @@ -37,6 +44,7 @@ pub(crate) struct CalculationEnvironment<'a> { pub(crate) new_rate_activation_epoch: Option, pub(crate) commission_rate_in_basis_points: bool, pub(crate) adjust_delegations_for_rent: bool, + pub(crate) use_fixed_point_stake_math: bool, } #[derive(Debug)] @@ -94,31 +102,36 @@ impl<'a> From<&'a VoteStateView> for DelegatedVoteState<'a> { } } -pub(crate) fn calculate_points( +pub(crate) fn calculate_points_for_tower( stake_state: &StakeStateV2, vote_state: DelegatedVoteState, stake_history: &StakeHistory, new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, ) -> Result { if let StakeStateV2::Stake(_meta, stake, _stake_flags) = stake_state { - Ok(calculate_stake_points( + Ok(calculate_stake_points_for_tower( stake, vote_state, stake_history, null_tracer(), new_rate_activation_epoch, + use_fixed_point_stake_math, )) } else { Err(InstructionError::InvalidAccountData) } } -fn calculate_stake_points( +/// This function is used to calculate `PointValue::points` for tower epoch and tower portion of +/// the migration epoch. +fn calculate_stake_points_for_tower( stake: &Stake, vote_state: DelegatedVoteState, stake_history: &StakeHistory, inflation_point_calc_tracer: Option, new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, ) -> u128 { calculate_stake_points_and_credits( stake, @@ -126,8 +139,218 @@ fn calculate_stake_points( stake_history, inflation_point_calc_tracer, new_rate_activation_epoch, + &AlpenglowEpochType::Tower, + use_fixed_point_stake_math, ) - .points + .tower_points +} + +fn record_error(msg: String) { + error!("{msg}"); + datapoint_error!( + "PER-total-stake-calculation-failure", + ("error", msg, String) + ); +} + +/// Returns how many credits the stake actually earned based on what it has already observed and +/// the vote account has recorded. +/// +/// Also updates the `new_credits_observed` as a side effect. +fn calc_earned_credits( + stake: &Stake, + final_epoch_credits: u64, + initial_epoch_credits: u64, + new_credits_observed: &mut u64, +) -> u128 { + let credits_in_stake = stake.credits_observed; + + // figure out how much this stake has seen that + // for which the vote account has a record + let earned_credits = if credits_in_stake < initial_epoch_credits { + // the staker observed the entire epoch + final_epoch_credits - initial_epoch_credits + } else if credits_in_stake < final_epoch_credits { + // the staker registered sometime during the epoch, partial credit + final_epoch_credits - *new_credits_observed + } else { + // the staker has already observed or been redeemed this epoch + // or was activated after this epoch + 0 + }; + *new_credits_observed = (*new_credits_observed).max(final_epoch_credits); + u128::from(earned_credits) +} + +/// Calculates tower points. +/// +/// Returns (points calculated, new_credits_observed, +/// true if AG_MIGRATION_EPOCH_CREDIT was seen else false) +fn tower_epoch_credits_iter( + stake: &Stake, + epoch_credits_iter: impl Iterator, + stake_history: &StakeHistory, + inflation_point_calc_tracer: Option, + new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, +) -> (u128, u64, bool) { + let mut points = 0; + let credits_in_stake = stake.credits_observed; + let mut new_credits_observed = credits_in_stake; + let mut saw_marker = false; + + for entry in epoch_credits_iter { + if entry == AG_MIGRATION_EPOCH_CREDIT { + saw_marker = true; + break; + } + let (epoch, final_epoch_credits, initial_epoch_credits) = entry; + let earned_credits = calc_earned_credits( + stake, + final_epoch_credits, + initial_epoch_credits, + &mut new_credits_observed, + ); + let stake_amount = u128::from(delegation_effective_stake( + &stake.delegation, + epoch, + stake_history, + new_rate_activation_epoch, + use_fixed_point_stake_math, + )); + + // finally calculate points for this epoch + let earned_points = stake_amount * earned_credits; + points += earned_points; + + if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { + inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints( + epoch, + stake_amount, + earned_credits, + earned_points, + )); + } + } + (points, new_credits_observed, saw_marker) +} + +/// Calculate alpenglow points for `stake` based on the vote account's `reward_epoch_credits` +/// +/// This value is the lamports paid to the vote account * `stake_amount` / `vote_account_stake` +/// `vote_account_stake` is fetched from the precomputed `reward_epoch_delegated_stakes` for the +/// reward epoch +/// +/// Returns (alpenglow points, new_credits_observed) +fn calculate_alpenglow_points( + stake: &Stake, + reward_epoch_credits: Option<(Epoch, u64, u64)>, + stake_history: &StakeHistory, + inflation_point_calc_tracer: Option, + new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, + reward_epoch_delegated_stakes: &RewardEpochDelegatedStakes, +) -> Result<(u128, u64), CalculatedStakePoints> { + let Some((epoch, final_epoch_credits, initial_epoch_credits)) = reward_epoch_credits else { + return Ok((0, stake.credits_observed)); + }; + if epoch != reward_epoch_delegated_stakes.epoch { + // In this case, the vote account did not record any credits in this epoch + // The latest entry is from a prior epoch - thus the delegation gets 0 rewards + return Ok((0, stake.credits_observed)); + } + + let (earned_credits, new_credits_observed) = { + let mut new_credits_observed = stake.credits_observed; + let earned_credits = calc_earned_credits( + stake, + final_epoch_credits, + initial_epoch_credits, + &mut new_credits_observed, + ); + (earned_credits, new_credits_observed) + }; + + let stake_amount = u128::from(delegation_effective_stake( + &stake.delegation, + epoch, + stake_history, + new_rate_activation_epoch, + use_fixed_point_stake_math, + )); + + let earned_points = if earned_credits == 0 || stake_amount == 0 { + 0 + } else { + let Some(total_stake) = reward_epoch_delegated_stakes + .delegated_stakes + .get(&stake.delegation.voter_pubkey) + .copied() + .filter(|stake| *stake != 0) + else { + record_error(format!( + "AG delegated stake denominator for vote_pubkey={} in epoch={} failed", + stake.delegation.voter_pubkey, reward_epoch_delegated_stakes.epoch + )); + return Err(CalculatedStakePoints { + tower_points: 0, + ag_points: 0, + new_credits_observed, + force_credits_update_with_skipped_reward: true, + }); + }; + earned_credits * stake_amount / total_stake as u128 + }; + + if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { + inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints( + epoch, + stake_amount, + earned_credits, + earned_points, + )); + } + Ok((earned_points, new_credits_observed)) +} + +/// Calculates the tower and alpenglow points for `stake` based on the vote account's `reward_epoch_credits` +/// for the alpenglow migration epoch +/// +/// Expects the epoch_credits_iter is sorted in ascending epoch order (excluding the migration marker) +/// Returns (tower_points, alpenglow points, new_credits_observed) +fn calculate_migration_points( + stake: &Stake, + mut epoch_credits_iter: impl Iterator, + stake_history: &StakeHistory, + inflation_point_calc_tracer: Option, + new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, + reward_epoch_delegated_stakes: &RewardEpochDelegatedStakes, +) -> Result<(u128, u128, u64), CalculatedStakePoints> { + let (tower_points, tower_new_credits_observed, saw_marker) = tower_epoch_credits_iter( + stake, + epoch_credits_iter.by_ref(), + stake_history, + inflation_point_calc_tracer.as_ref(), + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); + let (ag_points, ag_new_credits_observed) = if saw_marker { + calculate_alpenglow_points( + stake, + epoch_credits_iter.next(), + stake_history, + inflation_point_calc_tracer, + new_rate_activation_epoch, + use_fixed_point_stake_math, + reward_epoch_delegated_stakes, + )? + } else { + (0, stake.credits_observed) + }; + + let new_credits_observed = tower_new_credits_observed.max(ag_new_credits_observed); + Ok((tower_points, ag_points, new_credits_observed)) } /// for a given stake and vote_state, calculate how many @@ -139,6 +362,8 @@ pub(crate) fn calculate_stake_points_and_credits( stake_history: &StakeHistory, inflation_point_calc_tracer: Option, new_rate_activation_epoch: Option, + ag_epoch_type: &AlpenglowEpochType, + use_fixed_point_stake_math: bool, ) -> CalculatedStakePoints { let credits_in_stake = stake.credits_observed; let credits_in_vote = vote_state.credits; @@ -167,7 +392,8 @@ pub(crate) fn calculate_stake_points_and_credits( // hint with true to indicate some exceptional credits handling is needed return CalculatedStakePoints { - points: 0, + tower_points: 0, + ag_points: 0, new_credits_observed: credits_in_vote, force_credits_update_with_skipped_reward: true, }; @@ -178,7 +404,8 @@ pub(crate) fn calculate_stake_points_and_credits( } // don't hint caller and return current value if credits remain unchanged (= delinquent) return CalculatedStakePoints { - points: 0, + tower_points: 0, + ag_points: 0, new_credits_observed: credits_in_stake, force_credits_update_with_skipped_reward: false, }; @@ -186,51 +413,60 @@ pub(crate) fn calculate_stake_points_and_credits( Ordering::Greater => {} } - let mut points = 0; - let mut new_credits_observed = credits_in_stake; - - for epoch_credits_item in vote_state.epoch_credits_iter { - let (epoch, final_epoch_credits, initial_epoch_credits) = epoch_credits_item; - let stake_amount = u128::from(stake.delegation.stake( - epoch, - stake_history, - new_rate_activation_epoch, - )); - - // figure out how much this stake has seen that - // for which the vote account has a record - let earned_credits = if credits_in_stake < initial_epoch_credits { - // the staker observed the entire epoch - final_epoch_credits - initial_epoch_credits - } else if credits_in_stake < final_epoch_credits { - // the staker registered sometime during the epoch, partial credit - final_epoch_credits - new_credits_observed - } else { - // the staker has already observed or been redeemed this epoch - // or was activated after this epoch - 0 - }; - let earned_credits = u128::from(earned_credits); - - // don't want to assume anything about order of the iterator... - new_credits_observed = new_credits_observed.max(final_epoch_credits); - - // finally calculate points for this epoch - let earned_points = stake_amount * earned_credits; - points += earned_points; - - if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() { - inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints( - epoch, - stake_amount, - earned_credits, - earned_points, - )); + let (tower_points, ag_points, new_credits_observed) = match ag_epoch_type { + AlpenglowEpochType::Tower => { + let (points, credits, _) = tower_epoch_credits_iter( + stake, + vote_state.epoch_credits_iter, + stake_history, + inflation_point_calc_tracer, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); + (points, 0, credits) } - } - + AlpenglowEpochType::MigrationEpoch { + migration_epoch, + reward_epoch_delegated_stakes, + .. + } => { + debug_assert_eq!(reward_epoch_delegated_stakes.epoch, *migration_epoch); + match calculate_migration_points( + stake, + vote_state.epoch_credits_iter, + stake_history, + inflation_point_calc_tracer, + new_rate_activation_epoch, + use_fixed_point_stake_math, + reward_epoch_delegated_stakes, + ) { + Ok(r) => r, + Err(e) => return e, + } + } + AlpenglowEpochType::Alpenglow { + migration_epoch, + reward_epoch_delegated_stakes, + } => { + debug_assert!(reward_epoch_delegated_stakes.epoch > *migration_epoch); + let (ag_points, credits) = match calculate_alpenglow_points( + stake, + vote_state.epoch_credits_iter.last(), + stake_history, + inflation_point_calc_tracer, + new_rate_activation_epoch, + use_fixed_point_stake_math, + reward_epoch_delegated_stakes, + ) { + Ok(result) => result, + Err(e) => return e, + }; + (0, ag_points, credits) + } + }; CalculatedStakePoints { - points, + tower_points, + ag_points, new_credits_observed, force_credits_update_with_skipped_reward: false, } @@ -242,6 +478,7 @@ mod tests { super::*, solana_native_token::LAMPORTS_PER_SOL, solana_vote_program::vote_state::{VoteStateV4, handler::VoteStateHandler}, + std::cell::Cell, }; impl<'a> From<&'a VoteStateV4> for DelegatedVoteState<'a> { @@ -288,13 +525,564 @@ mod tests { // no overflow on points assert_eq!( u128::from(stake.delegation.stake) * epoch_slots, - calculate_stake_points( + calculate_stake_points_for_tower( &stake, DelegatedVoteState::from(vote_state.as_ref_v4()), &StakeHistory::default(), null_tracer(), - None + None, + true, ) ); } + + #[test] + fn test_tower_epoch_credits_iter() { + let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL; + let credits = 1235; + + let stake = new_stake( + stake_lamports, + &Pubkey::default(), + VoteStateHandler::new_v4(VoteStateV4::default()).as_ref_v4(), + u64::MAX, + ); + + let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)]; + let mut epoch_credits_iter = epoch_credits.into_iter(); + let (points, new_credits, saw_marker) = tower_epoch_credits_iter( + &stake, + epoch_credits_iter.by_ref(), + &StakeHistory::default(), + null_tracer(), + None, + true, + ); + assert_eq!(points, credits as u128 * stake_lamports as u128 * 2); + assert_eq!(new_credits, credits * 2); + assert_eq!(epoch_credits_iter.next(), None); + assert!(!saw_marker); + + let epoch_credits = vec![ + (0, credits, 0), + (1, credits * 2, credits), + AG_MIGRATION_EPOCH_CREDIT, + ]; + let mut epoch_credits_iter = epoch_credits.into_iter(); + let (points, new_credits, saw_marker) = tower_epoch_credits_iter( + &stake, + epoch_credits_iter.by_ref(), + &StakeHistory::default(), + null_tracer(), + None, + true, + ); + assert_eq!(points, credits as u128 * stake_lamports as u128 * 2); + assert_eq!(new_credits, credits * 2); + assert_eq!(epoch_credits_iter.next(), None); + assert!(saw_marker); + + let epoch_credits = vec![ + (0, credits, 0), + (1, credits * 2, credits), + AG_MIGRATION_EPOCH_CREDIT, + (1, credits * 3, credits * 2), + ]; + let mut epoch_credits_iter = epoch_credits.into_iter(); + let (points, new_credits, saw_marker) = tower_epoch_credits_iter( + &stake, + epoch_credits_iter.by_ref(), + &StakeHistory::default(), + null_tracer(), + None, + true, + ); + assert_eq!(points, credits as u128 * stake_lamports as u128 * 2); + assert_eq!(new_credits, credits * 2); + assert_eq!( + epoch_credits_iter.next().unwrap(), + (1, credits * 3, credits * 2) + ); + assert!(saw_marker); + } + + #[test] + fn test_calculate_alpenglow_points() { + let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL; + let total_stake = stake_lamports * 2; + let credits = 1235; + + let stake = new_stake( + stake_lamports, + &Pubkey::default(), + VoteStateHandler::new_v4(VoteStateV4::default()).as_ref_v4(), + u64::MAX, + ); + + let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes { + epoch: 1, + delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(), + }; + + let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)]; + let (points, new_credits) = calculate_alpenglow_points( + &stake, + epoch_credits.into_iter().last(), + &StakeHistory::default(), + null_tracer(), + None, + true, + &reward_epoch_delegated_stakes, + ) + .unwrap(); + assert_eq!( + points, + credits as u128 * stake_lamports as u128 / total_stake as u128 + ); + assert_eq!(new_credits, credits * 2); + + let epoch_credits = vec![ + (0, credits, 0), + AG_MIGRATION_EPOCH_CREDIT, + (0, credits * 2, credits), + (1, credits * 3, credits * 2), + ]; + let (points, new_credits) = calculate_alpenglow_points( + &stake, + epoch_credits.into_iter().last(), + &StakeHistory::default(), + null_tracer(), + None, + true, + &reward_epoch_delegated_stakes, + ) + .unwrap(); + assert_eq!( + points, + credits as u128 * stake_lamports as u128 / total_stake as u128 + ); + assert_eq!(new_credits, credits * 3); + + let missing_reward_epoch_delegated_stakes = RewardEpochDelegatedStakes { + epoch: 2, + delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(), + }; + let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)]; + let (points, new_credits) = calculate_alpenglow_points( + &stake, + epoch_credits.into_iter().last(), + &StakeHistory::default(), + null_tracer(), + None, + true, + &missing_reward_epoch_delegated_stakes, + ) + .unwrap(); + assert_eq!(points, 0); + assert_eq!(new_credits, 0); + } + + #[test] + fn test_calculate_alpenglow_points_uses_current_delegated_stake_denominator() { + let stake_lamports = 200; + let current_delegated_total = 200; + let credits = 10; + + let stake = Stake { + delegation: Delegation { + voter_pubkey: Pubkey::default(), + stake: stake_lamports, + activation_epoch: u64::MAX, + deactivation_epoch: u64::MAX, + ..Default::default() + }, + credits_observed: 0, + }; + + let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes { + epoch: 1, + delegated_stakes: [(Pubkey::default(), current_delegated_total)] + .into_iter() + .collect(), + }; + let epoch_credits = vec![ + (0, credits, 0), + AG_MIGRATION_EPOCH_CREDIT, + (0, credits * 2, credits), + (1, credits * 3, credits * 2), + ]; + + let (points, new_credits) = calculate_alpenglow_points( + &stake, + epoch_credits.into_iter().last(), + &StakeHistory::default(), + null_tracer(), + None, + true, + &reward_epoch_delegated_stakes, + ) + .unwrap(); + + assert_eq!(points, credits as u128); + assert_eq!(new_credits, credits * 3); + } + + #[test] + fn test_alpenglow_uses_rewarded_epoch_credits_only() { + let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL; + let total_stake = stake_lamports * 2; + let credits = 1235; + + let stake = Stake { + delegation: Delegation { + voter_pubkey: Pubkey::default(), + stake: stake_lamports, + activation_epoch: u64::MAX, + deactivation_epoch: u64::MAX, + ..Default::default() + }, + credits_observed: 0, + }; + let vote_state = VoteStateV4 { + epoch_credits: (1..=64) + .map(|epoch| { + let initial_credits = (epoch - 1) * credits; + (epoch, initial_credits + credits, initial_credits) + }) + .collect(), + ..VoteStateV4::default() + }; + + let ag_epoch_type = AlpenglowEpochType::Alpenglow { + migration_epoch: 0, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes { + epoch: 64, + delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(), + }, + }; + + let calculated_points_events = Cell::new(0); + let tracer = |event: &InflationPointCalculationEvent| { + if matches!(event, InflationPointCalculationEvent::CalculatedPoints(..)) { + calculated_points_events.set(calculated_points_events.get() + 1); + } + }; + + let calculated_stake_points = calculate_stake_points_and_credits( + &stake, + DelegatedVoteState::from(&vote_state), + &StakeHistory::default(), + Some(tracer), + None, + &ag_epoch_type, + true, + ); + + assert_eq!(calculated_points_events.get(), 1); + assert_eq!( + calculated_stake_points, + CalculatedStakePoints { + tower_points: 0, + ag_points: credits as u128 * stake_lamports as u128 / total_stake as u128, + new_credits_observed: credits * 64, + force_credits_update_with_skipped_reward: false, + } + ); + } + + #[test] + fn test_alpenglow_rewarded_epoch_uses_delegated_stake_denominator() { + let stake_lamports = 200; + let current_delegated_total = 200; + let credits = 10; + + let stake = Stake { + delegation: Delegation { + voter_pubkey: Pubkey::default(), + stake: stake_lamports, + activation_epoch: u64::MAX, + deactivation_epoch: u64::MAX, + ..Default::default() + }, + credits_observed: 0, + }; + let vote_state = VoteStateV4 { + epoch_credits: vec![(1, credits, 0)], + ..VoteStateV4::default() + }; + + let ag_epoch_type = AlpenglowEpochType::Alpenglow { + migration_epoch: 0, + reward_epoch_delegated_stakes: RewardEpochDelegatedStakes { + epoch: 1, + delegated_stakes: [(Pubkey::default(), current_delegated_total)] + .into_iter() + .collect(), + }, + }; + + let calculated_stake_points = calculate_stake_points_and_credits( + &stake, + DelegatedVoteState::from(&vote_state), + &StakeHistory::default(), + null_tracer(), + None, + &ag_epoch_type, + true, + ); + + assert_eq!( + calculated_stake_points, + CalculatedStakePoints { + tower_points: 0, + ag_points: credits as u128, + new_credits_observed: credits, + force_credits_update_with_skipped_reward: false, + } + ); + } + + #[test] + fn test_calculate_migration_points() { + let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL; + let total_stake = stake_lamports * 2; + let credits = 1235; + + let stake = new_stake( + stake_lamports, + &Pubkey::default(), + VoteStateHandler::new_v4(VoteStateV4::default()).as_ref_v4(), + u64::MAX, + ); + + let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)]; + let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes { + epoch: 2, + delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(), + }; + let (tower_points, ag_points, new_credits) = calculate_migration_points( + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + true, + &reward_epoch_delegated_stakes, + ) + .unwrap(); + assert_eq!(tower_points, credits as u128 * stake_lamports as u128 * 2); + assert_eq!(ag_points, 0); + assert_eq!(new_credits, credits * 2); + + let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes { + epoch: 0, + delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(), + }; + let epoch_credits = vec![ + (0, credits, 0), + AG_MIGRATION_EPOCH_CREDIT, + (0, credits * 2, credits), + ]; + let (tower_points, ag_points, new_credits) = calculate_migration_points( + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + true, + &reward_epoch_delegated_stakes, + ) + .unwrap(); + assert_eq!(tower_points, credits as u128 * stake_lamports as u128); + assert_eq!( + ag_points, + credits as u128 * stake_lamports as u128 / total_stake as u128 + ); + assert_eq!(new_credits, credits * 2); + + let epoch_credits = vec![AG_MIGRATION_EPOCH_CREDIT, (0, credits * 2, credits)]; + let (tower_points, ag_points, new_credits) = calculate_migration_points( + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + true, + &reward_epoch_delegated_stakes, + ) + .unwrap(); + assert_eq!(tower_points, 0); + assert_eq!( + ag_points, + credits as u128 * stake_lamports as u128 / total_stake as u128 + ); + assert_eq!(new_credits, credits * 2); + } + + #[test] + fn test_changing_total_stake() { + let pubkey = Pubkey::new_unique(); + let staker_delegation = LAMPORTS_PER_SOL; + let reward_epoch_validator_stake = staker_delegation * 5; + let stake = Stake { + delegation: Delegation { + voter_pubkey: pubkey, + stake: staker_delegation, + activation_epoch: u64::MAX, + deactivation_epoch: u64::MAX, + ..Default::default() + }, + credits_observed: 0, + }; + + let credits = 1235; + let epoch_credits = vec![ + (0, credits, 0), + AG_MIGRATION_EPOCH_CREDIT, + (0, credits * 2, credits), + (1, credits * 3, credits * 2), + (2, credits * 4, credits * 3), + ]; + let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes { + epoch: 2, + delegated_stakes: [(pubkey, reward_epoch_validator_stake)] + .into_iter() + .collect(), + }; + let (points, new_credits) = calculate_alpenglow_points( + &stake, + epoch_credits.into_iter().last(), + &StakeHistory::default(), + null_tracer(), + None, + true, + &reward_epoch_delegated_stakes, + ) + .unwrap(); + assert_eq!(new_credits, credits * 4); + let expected_points = credits * staker_delegation / reward_epoch_validator_stake; + assert_eq!(points, expected_points as u128); + } + + #[test] + fn test_stake_activating_deactivating() { + let stake_lamports = 10_000_000 * LAMPORTS_PER_SOL; + let credits = 1235; + + for (activation_epoch, deactivation_epoch) in [(0, u64::MAX), (u64::MAX, 0)] { + let stake = Stake { + delegation: Delegation { + voter_pubkey: Pubkey::default(), + stake: stake_lamports, + activation_epoch, + deactivation_epoch, + ..Default::default() + }, + credits_observed: 0, + }; + let epoch_credits = vec![(0, credits, 0), (1, credits * 2, credits)]; + let mut epoch_credits_iter = epoch_credits.into_iter(); + let (points, new_credits, saw_marker) = tower_epoch_credits_iter( + &stake, + epoch_credits_iter.by_ref(), + &StakeHistory::default(), + null_tracer(), + None, + true, + ); + assert_eq!(points, credits as u128 * stake_lamports as u128); + assert_eq!(new_credits, credits * 2); + assert_eq!(epoch_credits_iter.next(), None); + assert!(!saw_marker); + } + + for (activation_epoch, deactivation_epoch) in [(0, u64::MAX), (u64::MAX, 0)] { + let stake = Stake { + delegation: Delegation { + voter_pubkey: Pubkey::default(), + stake: stake_lamports, + activation_epoch, + deactivation_epoch, + ..Default::default() + }, + credits_observed: 0, + }; + + let total_stake = stake_lamports * 2; + let epoch_credits = vec![ + (0, credits, 0), + AG_MIGRATION_EPOCH_CREDIT, + (0, credits * 2, credits), + (1, credits * 3, credits * 2), + ]; + let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes { + epoch: 1, + delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(), + }; + let (points, new_credits) = calculate_alpenglow_points( + &stake, + epoch_credits.into_iter().last(), + &StakeHistory::default(), + null_tracer(), + None, + true, + &reward_epoch_delegated_stakes, + ) + .unwrap(); + let expected_points = if activation_epoch == 0 { + credits as u128 * stake_lamports as u128 / total_stake as u128 + } else { + 0 + }; + assert_eq!(points, expected_points); + assert_eq!(new_credits, credits * 3); + } + + for (activation_epoch, deactivation_epoch) in [(0, u64::MAX), (u64::MAX, 0)] { + let stake = Stake { + delegation: Delegation { + voter_pubkey: Pubkey::default(), + stake: stake_lamports, + activation_epoch, + deactivation_epoch, + ..Default::default() + }, + credits_observed: 0, + }; + + let total_stake = stake_lamports * 2; + let epoch_credits = vec![ + (0, credits, 0), + AG_MIGRATION_EPOCH_CREDIT, + (0, credits * 2, credits), + (1, credits * 3, credits * 2), + ]; + let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes { + epoch: 0, + delegated_stakes: [(Pubkey::default(), total_stake)].into_iter().collect(), + }; + let (tower_points, ag_points, new_credits) = calculate_migration_points( + &stake, + epoch_credits.into_iter(), + &StakeHistory::default(), + null_tracer(), + None, + true, + &reward_epoch_delegated_stakes, + ) + .unwrap(); + if activation_epoch == 0 { + assert_eq!(tower_points, 0); + } else { + assert_eq!(tower_points, credits as u128 * stake_lamports as u128); + } + let expected_ag_points = if activation_epoch == 0 { + 0 + } else { + credits as u128 * stake_lamports as u128 / total_stake as u128 + }; + assert_eq!(ag_points, expected_ag_points); + assert_eq!(new_credits, credits * 2); + } + } } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 73c247dd727..d1d02ed6258 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -7,6 +7,7 @@ mod account_saver; #[cfg(feature = "dev-context-only-utils")] pub mod account_saver; pub mod accounts_background_service; +mod alpenglow_epoch_type; pub mod bank; pub mod bank_client; pub mod bank_forks; @@ -29,6 +30,7 @@ pub mod rent_collector; mod reward_info; pub mod runtime_config; pub mod serde_snapshot; +pub mod slot_params; pub mod snapshot_bank_utils; pub mod snapshot_controller; pub mod snapshot_minimizer; @@ -38,6 +40,7 @@ pub mod snapshot_utils; mod stake_account; #[cfg(feature = "dev-context-only-utils")] pub mod stake_account; +mod stake_delegation; pub mod stake_history; pub mod stake_utils; pub mod stake_weighted_timestamp; diff --git a/runtime/src/non_circulating_supply.rs b/runtime/src/non_circulating_supply.rs index 3b167e90ad6..2eee3768250 100644 --- a/runtime/src/non_circulating_supply.rs +++ b/runtime/src/non_circulating_supply.rs @@ -191,6 +191,110 @@ pub fn non_circulating_accounts() -> Vec { solana_pubkey::pubkey!("CY7X5o3Wi2eQhTocLmUS6JSWyx1NinBfW7AXRrkRCpi8"), solana_pubkey::pubkey!("HQJtLqvEGGxgNYfRXUurfxV8E1swvCnsbC3456ik27HY"), solana_pubkey::pubkey!("9xbcBZoGYFnfJZe81EDuDYKUm8xGkjzW8z4EgnVhNvsv"), + solana_pubkey::pubkey!("4sNBQyPbJCQyUimBueZkGWnLVqds4rWkm7eXyi9WskGU"), + solana_pubkey::pubkey!("AZWdNvnZxJnbcT8ZzonpN19AZJadxPdUxSiCEDTJzu8L"), + solana_pubkey::pubkey!("5kFTzLuM2VgFdb6x16smnY3JWoVdPxNZVFAqeVgjSTUP"), + solana_pubkey::pubkey!("Gbz6wkNFus8SNEkWGNNENLv9NFwVvF1pWVDpaVKUWcMh"), + solana_pubkey::pubkey!("AksPzoA9DKCipgdhHjhUzQJe4iEniCvBoEfvayuFA3BN"), + solana_pubkey::pubkey!("DVhs8YHWrvhhGxoefDNY9KotqtEEnjnSAK8MYGL2Q7X"), + solana_pubkey::pubkey!("2bvGnYAPSV8pa1H3vRYr5tPAXktP4DkFACHfAgqyyfhd"), + solana_pubkey::pubkey!("2xP8YQ3sVmfNPtGM17tZi7Lr4vsPUiN6mHLx42roazG5"), + solana_pubkey::pubkey!("3p9ZxnrSFTkXVrT3KnYg2tT6asnysDApEFB5DRkdeAhB"), + solana_pubkey::pubkey!("wRVP5MYuqP8HJ1Q8RCJ5NzUraL3DxCKPGMKSBd5iQH1"), + solana_pubkey::pubkey!("513qFSVgmAQFBDsnyFCM1MrVKBrWiDgb4nXrdGsqa7Z4"), + solana_pubkey::pubkey!("619qLS85ieR4qh7MGNLLZyLefN7hMi2FDVb5cmX1nisb"), + solana_pubkey::pubkey!("768NcPfBJpFBtjDAbYZLFkej6ca1W5jeArsW5MtdF8S9"), + solana_pubkey::pubkey!("EWAjC8a9VPbALSM3D6tGsbRfgDV48kRuHZPu8qtYSNDv"), + solana_pubkey::pubkey!("H8wurbnaaXsgtrjqkNH1HhncUPUhTLAmKUHkwMeyqmfN"), + solana_pubkey::pubkey!("HBfi37TwD4kMa1WrAWwXp3ZaFbZQ1g3XxWwNZs8QsCpY"), + solana_pubkey::pubkey!("6B7mXMM6BixHvDpPAPLSKweLyCXcbtprkfsw3HfMUSjZ"), + solana_pubkey::pubkey!("5TuV9WpmESXNfTNqasXVehoXpQy65WUHBbzgKPXPLwWx"), + solana_pubkey::pubkey!("FB6VmiYFnVGp1uKXA3WbNsqg9neGYVpwYYiB9q8bFRrQ"), + solana_pubkey::pubkey!("7Anoa4ZRiq8qaaiEnhmdpXyTEmBjZASXGfnQqVDynNie"), + solana_pubkey::pubkey!("6dXeE5hS8bQKeqZsc18ewCyqHimnhCBAvVJBusnRqa2F"), + solana_pubkey::pubkey!("Ds69ZQPb3D3aVPXdN5REyzALBrzJLdruJZ1cwyfyoEEx"), + solana_pubkey::pubkey!("5HA8QV7tp59iNpfjs8f84LGUGX4imaynMFSKWHUcTrMT"), + solana_pubkey::pubkey!("EPfiDzgbdgXdyqqwbYFMqU6Qvfx2J9Zf2J9noXBYYbbx"), + solana_pubkey::pubkey!("DEGyTHFXmYuyANRDYRoEcShBXLonWNdBRpNzbFZBuzhY"), + solana_pubkey::pubkey!("9xz1vZSWgY6TFPLZgPM2WJjDM1KiPXcALnjYFHRTkYiK"), + solana_pubkey::pubkey!("6n1mSmsdGFCkEyyHe4wtgEigbwhiwYRszrerMW9YRyYF"), + solana_pubkey::pubkey!("8rp9vcJG1Uwk25g9XfQNHysfGBYcHizVEJvavqiffB6P"), + solana_pubkey::pubkey!("BVsJuJbhXmxPB9XPZrYMXs1SfuZuo998u1jQ9emVp8YD"), + solana_pubkey::pubkey!("Fas9FrnngyVjdMZ6r8p8SsfyFDtHLMmrw1dyBxRWWVTF"), + solana_pubkey::pubkey!("6Lfk4e6yxXGcFNMHue5u8KQM9hWYKjbdMFZ2n7SHHd6L"), + solana_pubkey::pubkey!("6WuU6DWoM7ZpgUVL5TR97Fgose7vLgrv4Qvg1Yhqv8Xw"), + solana_pubkey::pubkey!("6mXiGJbQbX726ZhrMSmbLCF81E8rkSfo1pvBWyn1asG5"), + solana_pubkey::pubkey!("2UdDthRdtZKMjEx9oQYnzuLdWy3sa4XKeaqihdXkXFza"), + solana_pubkey::pubkey!("33XwSCq2Ft5tTCsGK7kTyMCJ4hqufDZT2REFAWXmsia8"), + solana_pubkey::pubkey!("B5tRZWRCTCLANkY5zLnxEXAn8S2exJD9EkH3cacEYqmE"), + solana_pubkey::pubkey!("CfCvwXZk1sLSqzuoFx1K7KG1puMofgwHA3tSpcArVysX"), + solana_pubkey::pubkey!("C4fATCN93YL5iYS3c2MrHeDmkMD6waHuBXLqsri5q5AF"), + solana_pubkey::pubkey!("HbJUXNHXLzpucDr9wgjuoqAwK6XNKEcqi5h29MkkuSBQ"), + solana_pubkey::pubkey!("G8EBx1Qo5W8731nmaBSGYyvU96onpEP5adrVSjt4vnLF"), + solana_pubkey::pubkey!("4dc5ty7TNod2EQfx4bPnDofPbQk5cELcn3it47qnLizL"), + solana_pubkey::pubkey!("4VCET97mE8ixxAPRYabM7S7dvkiAFZ8ZzEoH6kbmfYus"), + solana_pubkey::pubkey!("DwqBsPgtAHqQUmRN3WF5YTuLKhK7dqzrb9eRJ4UVRNLf"), + solana_pubkey::pubkey!("26FqcAbTLvr6cVXk2ktPu9hpeMYWukY7uUwNCtTwu7Wb"), + solana_pubkey::pubkey!("B22zqCsZL6PDsi4fftmcjj5k7hFjhH1BM8Dcz4fXfFXK"), + solana_pubkey::pubkey!("9ahLw5LFpeTuBykK3A5aiFZ1CidTY7z4YxG2kmQdSGon"), + solana_pubkey::pubkey!("8M731ZthMdeVoMFGRaPnUBzR7Ze6qY4zEcQ7WALEAn5i"), + solana_pubkey::pubkey!("A3aaVsuX7BfNCUQbuepQSaLs9KNB7c6djG6u4UuJzwwP"), + solana_pubkey::pubkey!("DeXE7LTUqsC3B9kSACfFdW5TPG1eT9n1Z6gVB9dJNxqz"), + solana_pubkey::pubkey!("BtvSeDfZNfA3VEH6baFprfx6JcS6JX8jdt6NffCxoJGV"), + solana_pubkey::pubkey!("8o8trB6XdG8o4qo29De2mcooY5E3pW2rvMMkeBwJHkUD"), + solana_pubkey::pubkey!("CSSxN8vQYqSjg5GV6ZUGSM65dXqhytLzANLpMrBZjLKP"), + solana_pubkey::pubkey!("5A2yoxdGdC3B3QC39pkXpjx2ZPH4WGgie9exHQnhNz4J"), + solana_pubkey::pubkey!("DFaHYDcuLmxx3vrJGCgw8sPehzBNjvbvZTjz7PgvYLcb"), + solana_pubkey::pubkey!("43Cwv866ActL11JqVQXoFPiytUYA3tFXUaNg1zPsarn9"), + solana_pubkey::pubkey!("3wzw3L4wd3B7DHocbQxHosQXEfqEz5AnkQHKBisGKLzY"), + solana_pubkey::pubkey!("EnX9MvycxMGKFpMPJHgNo6kD2proVaorKQcSaMmguMEZ"), + solana_pubkey::pubkey!("GLz4zxemfHJy3BnK7PjZ4uUgJoHtycpT3JrmTDYduVpg"), + solana_pubkey::pubkey!("362rDzQwChHJ9ToZgLdv8wzFHTTsxsxzFLTZPwKY3CYg"), + solana_pubkey::pubkey!("3FKwgoQWCz4i43Pbo3jdfsjzkgrkjnk44rDWh7A9J9Ee"), + solana_pubkey::pubkey!("Cj53e3n1WX9dAUNX5PZpsc9xG9e7hXGnuJkKLJkNaf6v"), + solana_pubkey::pubkey!("Amn9KQZY5kJj932b49pxwqZuuKfNWewrmNQtdMrRfdvf"), + solana_pubkey::pubkey!("6spvvQJzFCcw5fVVdF1zKxCeq8Gj2yRFGgFuG16fYSMo"), + solana_pubkey::pubkey!("H5PutDz8EgAoznaX7Bc8qSB6sPxD2Xho145H6MH6AcbV"), + solana_pubkey::pubkey!("HNCooAiRiVeqGK9VSjxsvRsfr4weTcx14XhkvM7zDMgx"), + solana_pubkey::pubkey!("AUqLkd6bR54NtXY9Zt4J2zZ7CJbhVHuDR95YFk5qahaX"), + solana_pubkey::pubkey!("H35YRuMGE6WDPBLu3fDYLxV7tEcL2yJ7C97jDkw6NrBf"), + solana_pubkey::pubkey!("EhN8bv8LCXnNAVju9ERGsCccmdBNmNTAESAHXE8PyBLR"), + solana_pubkey::pubkey!("Ht5roME7Fjzt3dcEyf7UxYpPPAAdyTrnJ6rgEwQrNAFg"), + solana_pubkey::pubkey!("5zHRch6mowPiobRQHYXZ1siRcthw4dYkFPuaNDQkK3EE"), + solana_pubkey::pubkey!("AZkdkkHzSEbaZXpr7xBmt71AHj9c8iWE2oWYELm3nns5"), + solana_pubkey::pubkey!("FThHNjNQYi7FLvP4KjeYf3LXjcN2LhyYHmza35ucqtDT"), + solana_pubkey::pubkey!("6vHscAGjgbTfi1ooRVWGjSiUhHcUnLBceJyuKjpKrCYy"), + solana_pubkey::pubkey!("AnbszMCtjEVZTquG3FQ68x1f1WK5B1Z6E7gtN2J6vD5a"), + solana_pubkey::pubkey!("5aUknZidNKmAPxLMHoUeWsEdmXX8sGKvj8s71pHTDC7H"), + solana_pubkey::pubkey!("7Xgby7EgwoRkhbQSdNJL9sXwpo3jddUhqB4zv3WuJB4m"), + solana_pubkey::pubkey!("43KdfhYK6LUXX4LmyxfZ1KLaQrfpRe8vNwopNS7wXsUC"), + solana_pubkey::pubkey!("FG6UGgT4VpCQxjwQsVy6woHeemwb5jP5u9kR5ry3twcK"), + solana_pubkey::pubkey!("BDvLyqobZFubd7ESEJde6KqjbNoChkdj5aEiBdCqcN5r"), + solana_pubkey::pubkey!("BtF4msVoQYJPrQUMwNtEXw58LVjux18ddVhC4XjbsdwX"), + solana_pubkey::pubkey!("GbPK6cgUKso2ZGNV6GRwbp3hkdUzTVjsybYQMGDBvMFa"), + solana_pubkey::pubkey!("4bWjzNek2m6P3Q8rKkq11C47TaoAwpj3ykk5yDCYLWr1"), + solana_pubkey::pubkey!("FPzPT7YV5BHSTjYjqycXbwBsD4E5WsrZNPcCgcHhPkUx"), + solana_pubkey::pubkey!("EbitXxcwjC1dzrGJ7sa3Y6RBzSM3wxvrxMaBncNBKEf2"), + solana_pubkey::pubkey!("HMmHHmCmC3oo4G4QCjKvNxctSBQzRFDp2WQHZoXyrW6Q"), + solana_pubkey::pubkey!("145FLxWrDG9gKifyJKSnkKNAUPhG6hxUMEiZWw6XeSMW"), + solana_pubkey::pubkey!("FnXgwJZT7RMhpYEJFCXGLHp63bhVLfVn5d4aSVQDe19n"), + solana_pubkey::pubkey!("5tDptzMGG79DLp9XV4fdcmiRNPQGCkfBJeo8EVCSYKw7"), + solana_pubkey::pubkey!("9oBNYSARUMuH2swnxzbxCi827YfUnYWyWSAkrHz93h5y"), + solana_pubkey::pubkey!("DWfXRxmUXKdSTji4S1nXncviTmrhWQwxHAQCcvjRLeWm"), + solana_pubkey::pubkey!("EHUv6Hc9XGAb57C8iNCUuoE9xnJnRTFr9LaP7zbu8LNB"), + solana_pubkey::pubkey!("CTqnGTKHrcg8AVK5pSwVzpA3mmSxz7Js9y7rEqCbjryH"), + solana_pubkey::pubkey!("7PzsZ3ApLcfQosSr2B1C9YV1GNNgriZVFyqzDpeHoAD"), + solana_pubkey::pubkey!("AKXpgmVhedTT49sekGbh6MXfx2kJKBcaFQLpc83NCp6N"), + solana_pubkey::pubkey!("3MP45RTcFdLX3HpPD18asXCCxFjsgrZM9V6tEeQxFXd4"), + solana_pubkey::pubkey!("88YC29WwpugANLcN7DYkuL3benW79WsdxkQ3KX8zER9s"), + solana_pubkey::pubkey!("98j2yMfxrLzQ5YQJU1FMW3CTkoYgZStJirpm4HLAkA3o"), + solana_pubkey::pubkey!("56AidDLicyQaLMqbMQeZ7itUmJyd7TAvehVDi8MXDYbx"), + solana_pubkey::pubkey!("3yeCpRbBU5gzBaweVRqUXFcZUp7gSXBeknKcbGmjwmjJ"), + solana_pubkey::pubkey!("5McU7LcsRw3RHyrjj98qHefx6ehy7MdjF1uKKi6Kp8hV"), + solana_pubkey::pubkey!("3CYxk8uTXtANvbU1HW3NHNhKcBEfYesi2BdBFizy3f6s"), + solana_pubkey::pubkey!("4bAowZuPRG8jzNwMceqjYjZnrGXQNR5ZCgQJM8s4hF4i"), + solana_pubkey::pubkey!("ATWFW8xBGPe6xB8JnjyjZqG5FuEXnfyuhEocmqAqfeJG"), + solana_pubkey::pubkey!("73Aa7yCd6AMPqsaLisfgrpjESVFtDrLnHEjfDsXBK3HV"), ] .into() } @@ -208,6 +312,110 @@ pub fn withdraw_authority() -> Vec { solana_pubkey::pubkey!("Bj3aQ2oFnZYfNR1njzRjmWizzuhvfcYLckh76cqsbuBM"), solana_pubkey::pubkey!("4ZJhPQAgUseCsWhKvJLTmmRRUV74fdoTpQLNfKoekbPY"), solana_pubkey::pubkey!("HXdYQ5gixrY2H6Y9gqsD8kPM2JQKSaRiohDQtLbZkRWE"), + solana_pubkey::pubkey!("4sNBQyPbJCQyUimBueZkGWnLVqds4rWkm7eXyi9WskGU"), + solana_pubkey::pubkey!("AZWdNvnZxJnbcT8ZzonpN19AZJadxPdUxSiCEDTJzu8L"), + solana_pubkey::pubkey!("5kFTzLuM2VgFdb6x16smnY3JWoVdPxNZVFAqeVgjSTUP"), + solana_pubkey::pubkey!("Gbz6wkNFus8SNEkWGNNENLv9NFwVvF1pWVDpaVKUWcMh"), + solana_pubkey::pubkey!("AksPzoA9DKCipgdhHjhUzQJe4iEniCvBoEfvayuFA3BN"), + solana_pubkey::pubkey!("DVhs8YHWrvhhGxoefDNY9KotqtEEnjnSAK8MYGL2Q7X"), + solana_pubkey::pubkey!("2bvGnYAPSV8pa1H3vRYr5tPAXktP4DkFACHfAgqyyfhd"), + solana_pubkey::pubkey!("2xP8YQ3sVmfNPtGM17tZi7Lr4vsPUiN6mHLx42roazG5"), + solana_pubkey::pubkey!("3p9ZxnrSFTkXVrT3KnYg2tT6asnysDApEFB5DRkdeAhB"), + solana_pubkey::pubkey!("wRVP5MYuqP8HJ1Q8RCJ5NzUraL3DxCKPGMKSBd5iQH1"), + solana_pubkey::pubkey!("513qFSVgmAQFBDsnyFCM1MrVKBrWiDgb4nXrdGsqa7Z4"), + solana_pubkey::pubkey!("619qLS85ieR4qh7MGNLLZyLefN7hMi2FDVb5cmX1nisb"), + solana_pubkey::pubkey!("768NcPfBJpFBtjDAbYZLFkej6ca1W5jeArsW5MtdF8S9"), + solana_pubkey::pubkey!("EWAjC8a9VPbALSM3D6tGsbRfgDV48kRuHZPu8qtYSNDv"), + solana_pubkey::pubkey!("H8wurbnaaXsgtrjqkNH1HhncUPUhTLAmKUHkwMeyqmfN"), + solana_pubkey::pubkey!("HBfi37TwD4kMa1WrAWwXp3ZaFbZQ1g3XxWwNZs8QsCpY"), + solana_pubkey::pubkey!("6B7mXMM6BixHvDpPAPLSKweLyCXcbtprkfsw3HfMUSjZ"), + solana_pubkey::pubkey!("5TuV9WpmESXNfTNqasXVehoXpQy65WUHBbzgKPXPLwWx"), + solana_pubkey::pubkey!("FB6VmiYFnVGp1uKXA3WbNsqg9neGYVpwYYiB9q8bFRrQ"), + solana_pubkey::pubkey!("7Anoa4ZRiq8qaaiEnhmdpXyTEmBjZASXGfnQqVDynNie"), + solana_pubkey::pubkey!("6dXeE5hS8bQKeqZsc18ewCyqHimnhCBAvVJBusnRqa2F"), + solana_pubkey::pubkey!("Ds69ZQPb3D3aVPXdN5REyzALBrzJLdruJZ1cwyfyoEEx"), + solana_pubkey::pubkey!("5HA8QV7tp59iNpfjs8f84LGUGX4imaynMFSKWHUcTrMT"), + solana_pubkey::pubkey!("EPfiDzgbdgXdyqqwbYFMqU6Qvfx2J9Zf2J9noXBYYbbx"), + solana_pubkey::pubkey!("DEGyTHFXmYuyANRDYRoEcShBXLonWNdBRpNzbFZBuzhY"), + solana_pubkey::pubkey!("9xz1vZSWgY6TFPLZgPM2WJjDM1KiPXcALnjYFHRTkYiK"), + solana_pubkey::pubkey!("6n1mSmsdGFCkEyyHe4wtgEigbwhiwYRszrerMW9YRyYF"), + solana_pubkey::pubkey!("8rp9vcJG1Uwk25g9XfQNHysfGBYcHizVEJvavqiffB6P"), + solana_pubkey::pubkey!("BVsJuJbhXmxPB9XPZrYMXs1SfuZuo998u1jQ9emVp8YD"), + solana_pubkey::pubkey!("Fas9FrnngyVjdMZ6r8p8SsfyFDtHLMmrw1dyBxRWWVTF"), + solana_pubkey::pubkey!("6Lfk4e6yxXGcFNMHue5u8KQM9hWYKjbdMFZ2n7SHHd6L"), + solana_pubkey::pubkey!("6WuU6DWoM7ZpgUVL5TR97Fgose7vLgrv4Qvg1Yhqv8Xw"), + solana_pubkey::pubkey!("6mXiGJbQbX726ZhrMSmbLCF81E8rkSfo1pvBWyn1asG5"), + solana_pubkey::pubkey!("2UdDthRdtZKMjEx9oQYnzuLdWy3sa4XKeaqihdXkXFza"), + solana_pubkey::pubkey!("33XwSCq2Ft5tTCsGK7kTyMCJ4hqufDZT2REFAWXmsia8"), + solana_pubkey::pubkey!("B5tRZWRCTCLANkY5zLnxEXAn8S2exJD9EkH3cacEYqmE"), + solana_pubkey::pubkey!("CfCvwXZk1sLSqzuoFx1K7KG1puMofgwHA3tSpcArVysX"), + solana_pubkey::pubkey!("C4fATCN93YL5iYS3c2MrHeDmkMD6waHuBXLqsri5q5AF"), + solana_pubkey::pubkey!("HbJUXNHXLzpucDr9wgjuoqAwK6XNKEcqi5h29MkkuSBQ"), + solana_pubkey::pubkey!("G8EBx1Qo5W8731nmaBSGYyvU96onpEP5adrVSjt4vnLF"), + solana_pubkey::pubkey!("4dc5ty7TNod2EQfx4bPnDofPbQk5cELcn3it47qnLizL"), + solana_pubkey::pubkey!("4VCET97mE8ixxAPRYabM7S7dvkiAFZ8ZzEoH6kbmfYus"), + solana_pubkey::pubkey!("DwqBsPgtAHqQUmRN3WF5YTuLKhK7dqzrb9eRJ4UVRNLf"), + solana_pubkey::pubkey!("26FqcAbTLvr6cVXk2ktPu9hpeMYWukY7uUwNCtTwu7Wb"), + solana_pubkey::pubkey!("B22zqCsZL6PDsi4fftmcjj5k7hFjhH1BM8Dcz4fXfFXK"), + solana_pubkey::pubkey!("9ahLw5LFpeTuBykK3A5aiFZ1CidTY7z4YxG2kmQdSGon"), + solana_pubkey::pubkey!("8M731ZthMdeVoMFGRaPnUBzR7Ze6qY4zEcQ7WALEAn5i"), + solana_pubkey::pubkey!("A3aaVsuX7BfNCUQbuepQSaLs9KNB7c6djG6u4UuJzwwP"), + solana_pubkey::pubkey!("DeXE7LTUqsC3B9kSACfFdW5TPG1eT9n1Z6gVB9dJNxqz"), + solana_pubkey::pubkey!("BtvSeDfZNfA3VEH6baFprfx6JcS6JX8jdt6NffCxoJGV"), + solana_pubkey::pubkey!("8o8trB6XdG8o4qo29De2mcooY5E3pW2rvMMkeBwJHkUD"), + solana_pubkey::pubkey!("CSSxN8vQYqSjg5GV6ZUGSM65dXqhytLzANLpMrBZjLKP"), + solana_pubkey::pubkey!("5A2yoxdGdC3B3QC39pkXpjx2ZPH4WGgie9exHQnhNz4J"), + solana_pubkey::pubkey!("DFaHYDcuLmxx3vrJGCgw8sPehzBNjvbvZTjz7PgvYLcb"), + solana_pubkey::pubkey!("43Cwv866ActL11JqVQXoFPiytUYA3tFXUaNg1zPsarn9"), + solana_pubkey::pubkey!("3wzw3L4wd3B7DHocbQxHosQXEfqEz5AnkQHKBisGKLzY"), + solana_pubkey::pubkey!("EnX9MvycxMGKFpMPJHgNo6kD2proVaorKQcSaMmguMEZ"), + solana_pubkey::pubkey!("GLz4zxemfHJy3BnK7PjZ4uUgJoHtycpT3JrmTDYduVpg"), + solana_pubkey::pubkey!("362rDzQwChHJ9ToZgLdv8wzFHTTsxsxzFLTZPwKY3CYg"), + solana_pubkey::pubkey!("3FKwgoQWCz4i43Pbo3jdfsjzkgrkjnk44rDWh7A9J9Ee"), + solana_pubkey::pubkey!("Cj53e3n1WX9dAUNX5PZpsc9xG9e7hXGnuJkKLJkNaf6v"), + solana_pubkey::pubkey!("Amn9KQZY5kJj932b49pxwqZuuKfNWewrmNQtdMrRfdvf"), + solana_pubkey::pubkey!("6spvvQJzFCcw5fVVdF1zKxCeq8Gj2yRFGgFuG16fYSMo"), + solana_pubkey::pubkey!("H5PutDz8EgAoznaX7Bc8qSB6sPxD2Xho145H6MH6AcbV"), + solana_pubkey::pubkey!("HNCooAiRiVeqGK9VSjxsvRsfr4weTcx14XhkvM7zDMgx"), + solana_pubkey::pubkey!("AUqLkd6bR54NtXY9Zt4J2zZ7CJbhVHuDR95YFk5qahaX"), + solana_pubkey::pubkey!("H35YRuMGE6WDPBLu3fDYLxV7tEcL2yJ7C97jDkw6NrBf"), + solana_pubkey::pubkey!("EhN8bv8LCXnNAVju9ERGsCccmdBNmNTAESAHXE8PyBLR"), + solana_pubkey::pubkey!("Ht5roME7Fjzt3dcEyf7UxYpPPAAdyTrnJ6rgEwQrNAFg"), + solana_pubkey::pubkey!("5zHRch6mowPiobRQHYXZ1siRcthw4dYkFPuaNDQkK3EE"), + solana_pubkey::pubkey!("AZkdkkHzSEbaZXpr7xBmt71AHj9c8iWE2oWYELm3nns5"), + solana_pubkey::pubkey!("FThHNjNQYi7FLvP4KjeYf3LXjcN2LhyYHmza35ucqtDT"), + solana_pubkey::pubkey!("6vHscAGjgbTfi1ooRVWGjSiUhHcUnLBceJyuKjpKrCYy"), + solana_pubkey::pubkey!("AnbszMCtjEVZTquG3FQ68x1f1WK5B1Z6E7gtN2J6vD5a"), + solana_pubkey::pubkey!("5aUknZidNKmAPxLMHoUeWsEdmXX8sGKvj8s71pHTDC7H"), + solana_pubkey::pubkey!("7Xgby7EgwoRkhbQSdNJL9sXwpo3jddUhqB4zv3WuJB4m"), + solana_pubkey::pubkey!("43KdfhYK6LUXX4LmyxfZ1KLaQrfpRe8vNwopNS7wXsUC"), + solana_pubkey::pubkey!("FG6UGgT4VpCQxjwQsVy6woHeemwb5jP5u9kR5ry3twcK"), + solana_pubkey::pubkey!("BDvLyqobZFubd7ESEJde6KqjbNoChkdj5aEiBdCqcN5r"), + solana_pubkey::pubkey!("BtF4msVoQYJPrQUMwNtEXw58LVjux18ddVhC4XjbsdwX"), + solana_pubkey::pubkey!("GbPK6cgUKso2ZGNV6GRwbp3hkdUzTVjsybYQMGDBvMFa"), + solana_pubkey::pubkey!("4bWjzNek2m6P3Q8rKkq11C47TaoAwpj3ykk5yDCYLWr1"), + solana_pubkey::pubkey!("FPzPT7YV5BHSTjYjqycXbwBsD4E5WsrZNPcCgcHhPkUx"), + solana_pubkey::pubkey!("EbitXxcwjC1dzrGJ7sa3Y6RBzSM3wxvrxMaBncNBKEf2"), + solana_pubkey::pubkey!("HMmHHmCmC3oo4G4QCjKvNxctSBQzRFDp2WQHZoXyrW6Q"), + solana_pubkey::pubkey!("145FLxWrDG9gKifyJKSnkKNAUPhG6hxUMEiZWw6XeSMW"), + solana_pubkey::pubkey!("FnXgwJZT7RMhpYEJFCXGLHp63bhVLfVn5d4aSVQDe19n"), + solana_pubkey::pubkey!("5tDptzMGG79DLp9XV4fdcmiRNPQGCkfBJeo8EVCSYKw7"), + solana_pubkey::pubkey!("9oBNYSARUMuH2swnxzbxCi827YfUnYWyWSAkrHz93h5y"), + solana_pubkey::pubkey!("DWfXRxmUXKdSTji4S1nXncviTmrhWQwxHAQCcvjRLeWm"), + solana_pubkey::pubkey!("EHUv6Hc9XGAb57C8iNCUuoE9xnJnRTFr9LaP7zbu8LNB"), + solana_pubkey::pubkey!("CTqnGTKHrcg8AVK5pSwVzpA3mmSxz7Js9y7rEqCbjryH"), + solana_pubkey::pubkey!("7PzsZ3ApLcfQosSr2B1C9YV1GNNgriZVFyqzDpeHoAD"), + solana_pubkey::pubkey!("AKXpgmVhedTT49sekGbh6MXfx2kJKBcaFQLpc83NCp6N"), + solana_pubkey::pubkey!("3MP45RTcFdLX3HpPD18asXCCxFjsgrZM9V6tEeQxFXd4"), + solana_pubkey::pubkey!("88YC29WwpugANLcN7DYkuL3benW79WsdxkQ3KX8zER9s"), + solana_pubkey::pubkey!("98j2yMfxrLzQ5YQJU1FMW3CTkoYgZStJirpm4HLAkA3o"), + solana_pubkey::pubkey!("56AidDLicyQaLMqbMQeZ7itUmJyd7TAvehVDi8MXDYbx"), + solana_pubkey::pubkey!("3yeCpRbBU5gzBaweVRqUXFcZUp7gSXBeknKcbGmjwmjJ"), + solana_pubkey::pubkey!("5McU7LcsRw3RHyrjj98qHefx6ehy7MdjF1uKKi6Kp8hV"), + solana_pubkey::pubkey!("3CYxk8uTXtANvbU1HW3NHNhKcBEfYesi2BdBFizy3f6s"), + solana_pubkey::pubkey!("4bAowZuPRG8jzNwMceqjYjZnrGXQNR5ZCgQJM8s4hF4i"), + solana_pubkey::pubkey!("ATWFW8xBGPe6xB8JnjyjZqG5FuEXnfyuhEocmqAqfeJG"), + solana_pubkey::pubkey!("73Aa7yCd6AMPqsaLisfgrpjESVFtDrLnHEjfDsXBK3HV"), ] .into() } diff --git a/runtime/src/prioritization_fee_cache.rs b/runtime/src/prioritization_fee_cache.rs index 3ed4237b9e1..267e7500345 100644 --- a/runtime/src/prioritization_fee_cache.rs +++ b/runtime/src/prioritization_fee_cache.rs @@ -522,10 +522,10 @@ mod tests { // wait till finalization is done loop { let cache = prioritization_fee_cache.cache.read().unwrap(); - if let Some(slot_cache) = cache.get(&slot) { - if slot_cache.is_finalized() { - return; - } + if let Some(slot_cache) = cache.get(&slot) + && slot_cache.is_finalized() + { + return; } drop(cache); diff --git a/runtime/src/serde_snapshot.rs b/runtime/src/serde_snapshot.rs index 6b0af01e8a0..30fb4d9e863 100644 --- a/runtime/src/serde_snapshot.rs +++ b/runtime/src/serde_snapshot.rs @@ -17,7 +17,7 @@ use { bincode::{self, Error, config::Options}, log::*, serde::{Deserialize, Serialize, de::DeserializeOwned}, - smallvec::SmallVec, + smallvec::{SmallVec, smallvec}, solana_accounts_db::{ ObsoleteAccounts, account_storage_entry::AccountStorageEntry, @@ -39,7 +39,6 @@ use { solana_inflation::Inflation, solana_lattice_hash::lt_hash::LtHash, solana_leader_schedule::SlotLeader, - solana_measure::measure::Measure, solana_pubkey::Pubkey, solana_serde::default_on_eof, solana_stake_interface::state::Delegation, @@ -50,12 +49,11 @@ use { result::Result, sync::{ Arc, - atomic::{AtomicBool, AtomicUsize, Ordering}, + atomic::{AtomicBool, Ordering}, }, thread, time::Instant, }, - storage::SerializableStorage, types::{SerdeAccountsLtHash, UnusedRentCollector}, wincode::{ SchemaReadOwned, SchemaWrite, @@ -66,14 +64,15 @@ use { mod obsolete_accounts; mod status_cache; mod storage; +mod storages_list; mod tests; mod types; -mod utils; pub(crate) use { obsolete_accounts::{SerdeObsoleteAccounts, SerdeObsoleteAccountsMap}, status_cache::{deserialize_status_cache, serialize_status_cache}, storage::{SerializableAccountStorageEntry, SerializedAccountsFileId}, + storages_list::{StorageListItem, StoragesList}, }; const MAX_STREAM_SIZE: usize = 32 * 1024 * 1024 * 1024; @@ -94,39 +93,7 @@ pub(crate) struct AccountsDbFields( Vec<(Slot, Hash)>, ); -impl AccountsDbFields { - /// Get snapshot storage lengths filtering to slots above base slot (if provided). - /// - /// Returns an error if storage slots exceed snapshot slot indicating inconsistency of data. - pub(crate) fn get_storage_lengths_for_snapshot_slots( - &self, - base_slot: Option, - ) -> Result, SnapshotError> { - let AccountsDbFields(snapshot_storage, _, snapshot_slot, ..) = self; - let filtered_min_slot = base_slot.map(|slot| slot + 1).unwrap_or(Slot::MIN); - let mut lengths = HashMap::with_capacity(snapshot_storage.len()); - - for (slot, slot_storage) in snapshot_storage { - if slot > snapshot_slot { - return Err(SnapshotError::MismatchedSnapshotStorageSlot( - *slot, - *snapshot_slot, - )); - } - if *slot < filtered_min_slot { - // Serialized bank includes storage mapping for all slots, but it might be used for - // rebuilding storages only up from `base_slot`, so this case is not an error. - continue; - } - assert_eq!(slot_storage.len(), 1, "invalid storage count (slot={slot})"); - let storage_entry = &slot_storage[0]; - lengths.insert(*slot, storage_entry.current_len()); - } - Ok(lengths) - } -} - -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[cfg_attr(feature = "dev-context-only-utils", derive(Default, PartialEq))] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct UnusedIncrementalSnapshotPersistence { @@ -137,7 +104,14 @@ pub struct UnusedIncrementalSnapshotPersistence { pub incremental_capitalization: u64, } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "EcPdH21GSyYYTiSZbAN157YfrT3G8rKvDiNh7q1fw8Bc", + test_roundtrip = "eq_and_wire" + ) +)] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)] struct BankHashInfo { unused_accounts_delta_hash: [u8; 32], @@ -145,7 +119,7 @@ struct BankHashInfo { stats: BankHashStats, } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Default, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] struct UnusedAccounts { unused1: HashSet, @@ -232,6 +206,16 @@ impl From for BankFieldsToDeserialize { // Serializable version of Bank, not Deserializable to avoid cloning by using refs. // Sync fields with DeserializableVersionedBank! +#[cfg_attr( + feature = "frozen-abi", + derive(StableAbi, StableAbiSample), + // Write-only type (its deserialize counterpart is `DeserializableVersionedBank`), so the abi + // digest only verifies the serialized wire format; there is no roundtrip. + frozen_abi( + abi_digest = "6sm6hSNiTsNBAbSAiNe2BSQgnum3UdeNBpZnZiX7aM9r", + test_roundtrip = "no" + ) +)] #[derive(Serialize)] struct SerializableVersionedBank { blockhash_queue: BlockhashQueue, @@ -308,9 +292,6 @@ impl From for SerializableVersionedBank { } } -#[cfg(feature = "frozen-abi")] -impl solana_frozen_abi::abi_example::TransparentAsHelper for SerializableVersionedBank {} - /// Helper type to wrap BufReader streams when deserializing and reconstructing from either just a /// full snapshot, or both a full and incremental snapshot pub struct SnapshotStreams<'a, R> { @@ -444,7 +425,16 @@ struct ExtraFieldsToDeserialize { /// ExtraFieldsToDeserialize with the exception that new "extra fields" should /// be added to the deserialize struct a minor release before they are added to /// this one. -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + // Write-only type (its deserialize counterpart is `ExtraFieldsToDeserialize`), so the abi digest + // only verifies the serialized wire format; there is no roundtrip. + frozen_abi( + abi_digest = "726M1TRfibJsSAGcFqan4TSC8qKkJhDZfiK2P3h71eoo", + test_roundtrip = "no" + ) +)] #[cfg_attr(feature = "dev-context-only-utils", derive(Default, PartialEq))] #[derive(Debug, Serialize)] pub struct ExtraFieldsToSerialize { @@ -632,6 +622,29 @@ pub fn serialize_bank_snapshot_into( ) } +// The full serialized form of a bank snapshot: the bank fields, the accounts db fields, and the +// extra fields, in wire order. Generic over the account-storage-entries serializer `E` so the +// runtime can stream the storage entries lazily (see `SerializableAccountsDb`). +#[cfg_attr(feature = "frozen-abi", derive(StableAbi, StableAbiSample))] +#[derive(Serialize)] +struct SerializableBankSnapshot { + bank: SerializableVersionedBank, + accounts_db: SerializableAccountsDb, + extra_fields: ExtraFieldsToSerialize, +} + +// Concrete instantiation of `SerializableBankSnapshot` used only to pin the wire ABI; see +// `SerializableAccountsDbForAbi`. Write-only type (its deserialize counterparts are +// `DeserializableVersionedBank`, `AccountsDbFields` and `ExtraFieldsToDeserialize`), so there is no +// roundtrip. +#[cfg(all(test, feature = "frozen-abi"))] +#[frozen_abi( + abi_digest = "E4waD1iVxUYi3x9xA5xQHhCEbhhGtV9bqmo2TJ9ZsqTw", + test_roundtrip = "no" +)] +type SerializableBankSnapshotForAbi = + SerializableBankSnapshot)>>; + /// Serializes bank snapshot with `serializer` pub fn serialize_bank_snapshot_with( serializer: S, @@ -644,13 +657,14 @@ where S: serde::Serializer, { let slot = bank_fields.slot; - let serializable_bank = SerializableVersionedBank::from(bank_fields); - let serializable_accounts_db = SerializableAccountsDb::<'_> { - slot, - account_storage_entries, - bank_hash_stats, + let snapshot = SerializableBankSnapshot { + bank: SerializableVersionedBank::from(bank_fields), + accounts_db: SerializableAccountsDb::new(slot, account_storage_entries, bank_hash_stats), + extra_fields, }; - (serializable_bank, serializable_accounts_db, extra_fields).serialize(serializer) + // Note: the time spent here is reported by the caller (e.g. as `bank_serialize_us` in the + // `snapshot_bank` datapoint). + snapshot.serialize(serializer) } #[cfg(test)] @@ -672,14 +686,10 @@ impl Serialize for SerializableBankAndStorage<'_> { let versioned_epoch_stakes = std::mem::take(&mut bank_fields.versioned_epoch_stakes); let accounts_lt_hash = Some(bank_fields.accounts_lt_hash.clone().into()); let block_id = Some(bank_fields.block_id); - let bank_fields_to_serialize = ( - SerializableVersionedBank::from(bank_fields), - SerializableAccountsDb::<'_> { - slot, - account_storage_entries: self.snapshot_storages, - bank_hash_stats, - }, - ExtraFieldsToSerialize { + let bank_fields_to_serialize = SerializableBankSnapshot { + bank: SerializableVersionedBank::from(bank_fields), + accounts_db: SerializableAccountsDb::new(slot, self.snapshot_storages, bank_hash_stats), + extra_fields: ExtraFieldsToSerialize { lamports_per_signature, unused_incremental_snapshot_persistence: None, unused_epoch_accounts_hash: None, @@ -687,7 +697,7 @@ impl Serialize for SerializableBankAndStorage<'_> { accounts_lt_hash, block_id, }, - ); + }; bank_fields_to_serialize.serialize(serializer) } } @@ -709,11 +719,7 @@ impl Serialize for SerializableBankAndStorageNoExtra<'_> { let bank_hash_stats = self.bank.get_bank_hash_stats(); ( SerializableVersionedBank::from(bank_fields), - SerializableAccountsDb::<'_> { - slot, - account_storage_entries: self.snapshot_storages, - bank_hash_stats, - }, + SerializableAccountsDb::new(slot, self.snapshot_storages, bank_hash_stats), ) .serialize(serializer) } @@ -733,58 +739,93 @@ impl<'a> From> for SerializableBankAndStor } } -struct SerializableAccountsDb<'a> { +// Serializable counterpart of `AccountsDbFields`, generic over the type used to serialize the +// account storage entries so that the runtime can stream them via a lazy map-serializing iterator +// (see `SerializableAccountsDb::new`) without materializing a collection. Sync fields with +// `AccountsDbFields`! +#[cfg_attr(feature = "frozen-abi", derive(StableAbi, StableAbiSample))] +#[derive(Serialize)] +struct SerializableAccountsDb { + /// account storage entries, serialized as a map of slot to its storage entries + accounts_storage_entries: E, + unused_write_version: u64, // unused, formerly write_version slot: Slot, - account_storage_entries: &'a [Arc], - bank_hash_stats: BankHashStats, + bank_hash_info: BankHashInfo, + /// all slots that were roots within the last epoch + historical_roots: Vec, + /// slots that were roots within the last epoch for which we care about the hash value + historical_roots_with_hash: Vec<(Slot, Hash)>, } -impl Serialize for SerializableAccountsDb<'_> { +/// Adapts a cloneable, exact-size iterator into a value that serializes as a length-prefixed +/// sequence, re-creating the iterator via `Clone` on each serialization so a locally built iterator +/// can be written without first materializing it into a collection. +struct SerializableExactIteratorView(I); + +impl Serialize for SerializableExactIteratorView +where + I: ExactSizeIterator + Clone, + I::Item: Serialize, +{ fn serialize(&self, serializer: S) -> std::result::Result where - S: serde::ser::Serializer, + S: serde::Serializer, { - // (1st of 3 elements) write the list of account storage entry lists out as a map - let entries = utils::serialize_iter_as_map(self.account_storage_entries.iter().map(|x| { - ( - x.slot(), - utils::serialize_iter_as_seq( - [x].into_iter() - .map(|x| SerializableAccountStorageEntry::new(x, self.slot)), - ), - ) - })); + serializer.collect_seq(self.0.clone()) + } +} + +impl SerializableAccountsDb<()> { + fn new( + slot: Slot, + account_storage_entries: &[Arc], + bank_hash_stats: BankHashStats, + ) -> SerializableAccountsDb< + SerializableExactIteratorView< + impl ExactSizeIterator)> + + Clone + + '_, + >, + > { + // Stream the storage entries as a `slot -> [entry]` map, each slot's single entry kept + // inline in a `SmallVec`. `SerializableExactIteratorView` re-creates the iterator on each + // serialization, so nothing is materialized. + let accounts_storage_entries = + SerializableExactIteratorView(account_storage_entries.iter().map( + move |entry| -> (Slot, SmallVec<[SerializableAccountStorageEntry; 1]>) { + ( + entry.slot(), + smallvec![SerializableAccountStorageEntry::new(entry, slot)], + ) + }, + )); let bank_hash_info = BankHashInfo { unused_accounts_delta_hash: [0; 32], unused_accounts_hash: [0; 32], - stats: self.bank_hash_stats.clone(), + stats: bank_hash_stats, }; - - let historical_roots = Vec::::default(); - let historical_roots_with_hash = Vec::<(Slot, Hash)>::default(); - - let mut serialize_account_storage_timer = Measure::start("serialize_account_storage_ms"); - let result = ( - entries, - 0u64, // unused, formerly write_version - self.slot, + SerializableAccountsDb { + accounts_storage_entries, + unused_write_version: 0, + slot, bank_hash_info, - historical_roots, - historical_roots_with_hash, - ) - .serialize(serializer); - serialize_account_storage_timer.stop(); - datapoint_info!( - "serialize_account_storage_ms", - ("duration", serialize_account_storage_timer.as_ms(), i64), - ("num_entries", self.account_storage_entries.len(), i64), - ); - result + historical_roots: Vec::default(), + historical_roots_with_hash: Vec::default(), + } } } -#[cfg(feature = "frozen-abi")] -impl solana_frozen_abi::abi_example::TransparentAsHelper for SerializableAccountsDb<'_> {} +// Concrete instantiation of `SerializableAccountsDb` used only to pin the wire ABI. The runtime +// serializes the storage entries with a lazy map-serializing iterator; this alias uses a `Vec` of +// the same `(slot, entries)` shape, which serializes to identical bytes. Write-only type (its +// deserialize counterpart is `AccountsDbFields`), so there is no roundtrip. +#[cfg(all(test, feature = "frozen-abi"))] +#[frozen_abi( + abi_digest = "2TpwtsyrverM4ius4ykX3RRCGqeLfXJQbAvYHkxdUVrs", + test_roundtrip = "no" +)] +type SerializableAccountsDbForAbi = + SerializableAccountsDb)>>; /// This struct contains side-info while reconstructing the bank from fields #[derive(Debug)] @@ -811,10 +852,7 @@ pub(crate) fn reconstruct_bank_from_fields( accounts_db_config: AccountsDbConfig, accounts_update_notifier: Option, exit: Arc, -) -> Result<(Bank, ReconstructedBankInfo), Error> -where - E: SerializableStorage + std::marker::Sync, -{ +) -> Result<(Bank, ReconstructedBankInfo), Error> { let mut bank_fields = bank_fields.collapse_into(); // Epoch stakes take several seconds to reconstruct, do it in parallel with loading accountsdb let deserializable_epoch_stakes = std::mem::take(&mut bank_fields.versioned_epoch_stakes); @@ -865,28 +903,31 @@ where pub(crate) fn reconstruct_single_storage( slot: &Slot, append_vec_file_info: FileInfo, - current_len: usize, id: AccountsFileId, obsolete_accounts: Option<(ObsoleteAccounts, AccountsFileId, usize)>, ) -> Result, SnapshotError> { - // When restoring from an archive, obsolete accounts will always be `None` + // The storage length is taken directly from the on-disk file size (see + // `AccountsFile::new_for_startup`). When restoring from an archive the obsolete accounts have + // been physically removed during serialization, and when restoring from a snapshot directory + // they are still present in the file. In both cases the file size already reflects the exact + // number of bytes the storage spans, so there is no need to carry the length separately in the + // snapshot fields. + // + // When restoring from an archive, obsolete accounts will always be `None`. // When restoring from fastboot, obsolete accounts will be 'Some' if the storage contained // accounts marked obsolete at the time the snapshot was taken. - let (current_len, obsolete_accounts) = if let Some(obsolete_accounts) = obsolete_accounts { - let updated_len = current_len + obsolete_accounts.2; - if obsolete_accounts.1 != id { - return Err(SnapshotError::MismatchedAccountsFileId( - id, - obsolete_accounts.1, - )); - } + let obsolete_accounts = + if let Some((obsolete_accounts, obsolete_id, _obsolete_bytes)) = obsolete_accounts { + if obsolete_id != id { + return Err(SnapshotError::MismatchedAccountsFileId(id, obsolete_id)); + } - (updated_len, obsolete_accounts.0) - } else { - (current_len, ObsoleteAccounts::default()) - }; + obsolete_accounts + } else { + ObsoleteAccounts::default() + }; - let accounts_file = AccountsFile::new_for_startup(append_vec_file_info, current_len)?; + let accounts_file = AccountsFile::new_for_startup(append_vec_file_info)?; Ok(Arc::new(AccountStorageEntry::new_existing( *slot, id, @@ -903,7 +944,7 @@ pub(crate) fn remap_append_vec_file( old_append_vec_id: SerializedAccountsFileId, append_vec_file_info: FileInfo, next_append_vec_id: &AtomicAccountsFileId, - num_collisions: &AtomicUsize, + num_collisions: &mut usize, ) -> io::Result<(AccountsFileId, FileInfo)> { #[cfg(all(target_os = "linux", target_env = "gnu"))] let append_vec_path_cstr = cstring_from_path(&append_vec_file_info.path)?; @@ -955,7 +996,7 @@ pub(crate) fn remap_append_vec_file( // If we made it this far, a file exists at the new path. Record the collision // and try again. - num_collisions.fetch_add(1, Ordering::Relaxed); + *num_collisions += 1; }; // Only rename the file if the new ID is actually different from the original. In the target_os @@ -980,10 +1021,9 @@ pub(crate) fn remap_append_vec_file( pub(crate) fn remap_and_reconstruct_single_storage( slot: Slot, old_append_vec_id: SerializedAccountsFileId, - current_len: usize, append_vec_file_info: FileInfo, next_append_vec_id: &AtomicAccountsFileId, - num_collisions: &AtomicUsize, + num_collisions: &mut usize, ) -> Result, SnapshotError> { let (remapped_append_vec_id, remapped_append_vec_file_info) = remap_append_vec_file( slot, @@ -995,7 +1035,6 @@ pub(crate) fn remap_and_reconstruct_single_storage( let storage = reconstruct_single_storage( &slot, remapped_append_vec_file_info, - current_len, remapped_append_vec_id, None, )?; @@ -1023,10 +1062,7 @@ fn reconstruct_accountsdb_from_fields( accounts_db_config: AccountsDbConfig, accounts_update_notifier: Option, exit: Arc, -) -> Result<(AccountsDb, ReconstructedAccountsDbInfo), Error> -where - E: SerializableStorage + std::marker::Sync, -{ +) -> Result<(AccountsDb, ReconstructedAccountsDbInfo), Error> { let mut accounts_db = AccountsDb::new_with_config( account_paths.to_vec(), accounts_db_config, diff --git a/runtime/src/serde_snapshot/obsolete_accounts.rs b/runtime/src/serde_snapshot/obsolete_accounts.rs index 39faab8f052..4a6e34855f7 100644 --- a/runtime/src/serde_snapshot/obsolete_accounts.rs +++ b/runtime/src/serde_snapshot/obsolete_accounts.rs @@ -1,6 +1,5 @@ use { crate::serde_snapshot::SerializedAccountsFileId, - dashmap::DashMap, rayon::iter::{IntoParallelIterator, ParallelIterator}, serde::Serialize, solana_accounts_db::{ @@ -8,12 +7,12 @@ use { account_storage_entry::AccountStorageEntry, accounts_db::AccountsFileId, }, solana_clock::Slot, - std::sync::Arc, + std::{collections::HashMap, sync::Arc}, wincode::{SchemaRead, SchemaWrite}, }; #[repr(C)] -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Debug, SchemaRead, SchemaWrite, Serialize)] pub struct SerdeObsoleteAccountItem { /// Offset of the account in the account storage entry @@ -24,7 +23,7 @@ pub struct SerdeObsoleteAccountItem { pub slot: Slot, } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Debug, Default, Serialize, SchemaRead, SchemaWrite)] pub(crate) struct SerdeObsoleteAccounts { /// The ID of the associated account file. Used for verification to ensure the restored @@ -90,8 +89,12 @@ impl SerdeObsoleteAccounts { /// to capture and restore obsolete accounts information for account storages. #[cfg_attr( feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "FbAP5pg2FLgSkMWiv3eMwEh433qMoNRbaJij9aLtG2NH") + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + api_digest = "FbAP5pg2FLgSkMWiv3eMwEh433qMoNRbaJij9aLtG2NH", + abi_digest = "5AMjJ751HYUj4rCtZgAsjVi9q5NgdYYZ11YEp9W7cB86", + abi_serializer = "wincode" + ) )] #[derive(Serialize, Debug, SchemaRead, SchemaWrite)] pub(crate) struct SerdeObsoleteAccountsMap { @@ -116,7 +119,7 @@ impl SerdeObsoleteAccountsMap { SerdeObsoleteAccountsMap { map } } - pub(crate) fn into_dashmap(self) -> DashMap { + pub(crate) fn into_hashmap(self) -> HashMap { self.map.into_iter().collect() } } @@ -139,7 +142,7 @@ mod test { num_obsolete_accounts_per_storage: usize, ) { // Create a set of obsolete accounts - let obsolete_accounts = DashMap::::new(); + let mut obsolete_accounts = HashMap::::new(); for slot in 1..=num_storages { let obsolete_accounts_list = ObsoleteAccounts { accounts: (0..num_obsolete_accounts_per_storage) @@ -157,15 +160,13 @@ mod test { // Convert the obsolete accounts into a SerdeObsoleteAccountsMap let map = obsolete_accounts .iter() - .map(|entry| { + .map(|(slot, accounts)| { let serde_obsolete_accounts = SerdeObsoleteAccounts { - id: *entry.key() as SerializedAccountsFileId, + id: *slot as SerializedAccountsFileId, bytes: num_obsolete_accounts_per_storage as u64 * 1000, - accounts: SerdeObsoleteAccounts::items_from_obsolete_accounts( - entry.value().clone(), - ), + accounts: SerdeObsoleteAccounts::items_from_obsolete_accounts(accounts.clone()), }; - (*entry.key(), serde_obsolete_accounts) + (*slot, serde_obsolete_accounts) }) .collect(); let obsolete_accounts_map = SerdeObsoleteAccountsMap { map }; @@ -178,7 +179,7 @@ mod test { let cursor = Cursor::new(buf.as_slice()); let deserialized_obsolete_accounts: SerdeObsoleteAccountsMap = deserialize_wincode_from(cursor).unwrap(); - let map = deserialized_obsolete_accounts.into_dashmap(); + let mut map = deserialized_obsolete_accounts.into_hashmap(); // Verify the deserialized data matches the original obsolete accounts assert_eq!(map.len(), obsolete_accounts.len()); @@ -186,7 +187,7 @@ mod test { let deserialized_obsolete_accounts = map.remove(&slot).unwrap(); assert_eq!( obsolete_accounts, - deserialized_obsolete_accounts.1.into_tuple().0 + deserialized_obsolete_accounts.into_tuple().0 ); } } diff --git a/runtime/src/serde_snapshot/status_cache.rs b/runtime/src/serde_snapshot/status_cache.rs index 00642f6fe9f..c2001c901e2 100644 --- a/runtime/src/serde_snapshot/status_cache.rs +++ b/runtime/src/serde_snapshot/status_cache.rs @@ -5,20 +5,25 @@ use shuttle::sync::Mutex; #[cfg(not(feature = "shuttle-test"))] use std::sync::Mutex; use { - crate::{bank::BankSlotDelta, snapshot_utils, status_cache::KeySlice}, + crate::{bank::BankSlotDelta, serde_snapshot, snapshot_utils, status_cache::KeySlice}, agave_fs::io_setup::IoSetupState, - bincode::{self, Options as _}, - serde::{Deserialize, Serialize}, + serde::Serialize, solana_clock::Slot, solana_hash::Hash, solana_instruction::error::InstructionError, solana_transaction_error::TransactionError, std::{collections::HashMap, path::Path, sync::Arc}, + wincode::{SchemaRead, SchemaWrite}, }; #[cfg_attr( feature = "frozen-abi", - frozen_abi(digest = "AardUUq1At4qq6oNNp9V2JZFsMR5k54RZmBmZkxUfk7m") + frozen_abi( + api_digest = "AardUUq1At4qq6oNNp9V2JZFsMR5k54RZmBmZkxUfk7m", + abi_digest = "AGXdE33medQcQ5Bzq7Mppz3cQ9TNPxBaaX2iUM68pnmC", + abi_serializer = "wincode", + test_roundtrip = "eq_and_wire" + ) )] type SerdeBankSlotDelta = SerdeSlotDelta>; type SerdeSlotDelta = (Slot, bool, SerdeStatus); @@ -30,43 +35,43 @@ type SerdeStatus = ahash::HashMap)>; pub fn serialize_status_cache( slot_deltas: &[BankSlotDelta], status_cache_path: &Path, + io_setup: &IoSetupState, ) -> agave_snapshots::Result { - snapshot_utils::serialize_snapshot_data_file( - status_cache_path, - &IoSetupState::default(), - |stream| { - let snapshot_slot_deltas = slot_deltas - .iter() - .map(|slot_delta| { - let status_map = slot_delta.2.lock().unwrap(); - let snapshot_status_map = status_map - .iter() - .map(|(key, value)| { + snapshot_utils::serialize_snapshot_data_file(status_cache_path, io_setup, |stream| { + let snapshot_slot_deltas = slot_deltas + .iter() + .map(|slot_delta| { + let status_map = slot_delta.2.lock().unwrap(); + let snapshot_status_map = status_map + .iter() + .map(|(key, value)| { + ( + *key, ( - *key, - ( - value.0, - value - .1 - .iter() - .map(|(key_slice, result)| { - ( - *key_slice, - result.clone().map_err(SerdeTransactionError::from), - ) - }) - .collect::>(), - ), - ) - }) - .collect::>(); - (slot_delta.0, slot_delta.1, snapshot_status_map) - }) - .collect::>(); - bincode::serialize_into(stream, &snapshot_slot_deltas)?; - Ok(()) - }, - ) + value.0, + value + .1 + .iter() + .map(|(key_slice, result)| { + ( + *key_slice, + result + .as_ref() + .map(|_| ()) + .map_err(SerdeTransactionError::from), + ) + }) + .collect::>(), + ), + ) + }) + .collect::>(); + (slot_delta.0, slot_delta.1, snapshot_status_map) + }) + .collect::>(); + serde_snapshot::serialize_into(stream, &snapshot_slot_deltas)?; + Ok(()) + }) } /// Deserializes the status cache from file at `status_cache_path` @@ -76,28 +81,25 @@ pub fn deserialize_status_cache( status_cache_path: &Path, ) -> agave_snapshots::Result> { snapshot_utils::deserialize_snapshot_data_file(status_cache_path, |stream| { - let snapshot_slot_deltas: Vec = bincode::options() - .with_limit(snapshot_utils::MAX_SNAPSHOT_DATA_FILE_SIZE) - .with_fixint_encoding() - .allow_trailing_bytes() - .deserialize_from(stream)?; + let snapshot_slot_deltas: Vec = + serde_snapshot::deserialize_wincode_from(stream)?; let slot_deltas = snapshot_slot_deltas - .iter() + .into_iter() .map(|slot_delta| { let status_map = slot_delta .2 - .iter() + .into_iter() .map(|(key, value)| { ( - *key, + key, ( value.0, value .1 - .iter() + .into_iter() .map(|(key_slice, result)| { - (*key_slice, result.clone().map_err(TransactionError::from)) + (key_slice, result.map_err(TransactionError::from)) }) .collect::>(), ), @@ -115,10 +117,10 @@ pub fn deserialize_status_cache( /// contain a string in the BorshIoError variant. #[cfg_attr( feature = "frozen-abi", - frozen_abi(digest = "5pMgydVNgsYbg64Trhjxbftsug5La7fRDmooyrsHd4wy"), - derive(AbiExample, AbiEnumVisitor) + frozen_abi(api_digest = "5pMgydVNgsYbg64Trhjxbftsug5La7fRDmooyrsHd4wy"), + derive(AbiExample, AbiEnumVisitor, StableAbi, StableAbiSample) )] -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, SchemaRead, SchemaWrite)] enum SerdeTransactionError { AccountInUse, AccountLoadedTwice, @@ -161,8 +163,8 @@ enum SerdeTransactionError { CommitCancelled, } -impl From for SerdeTransactionError { - fn from(err: TransactionError) -> Self { +impl From<&TransactionError> for SerdeTransactionError { + fn from(err: &TransactionError) -> Self { match err { TransactionError::AccountInUse => Self::AccountInUse, TransactionError::AccountLoadedTwice => Self::AccountLoadedTwice, @@ -172,7 +174,9 @@ impl From for SerdeTransactionError { TransactionError::InvalidAccountForFee => Self::InvalidAccountForFee, TransactionError::AlreadyProcessed => Self::AlreadyProcessed, TransactionError::BlockhashNotFound => Self::BlockhashNotFound, - TransactionError::InstructionError(i, inner) => Self::InstructionError(i, inner.into()), + TransactionError::InstructionError(i, inner) => { + Self::InstructionError(*i, inner.into()) + } TransactionError::CallChainTooDeep => Self::CallChainTooDeep, TransactionError::MissingSignatureForFee => Self::MissingSignatureForFee, TransactionError::InvalidAccountIndex => Self::InvalidAccountIndex, @@ -204,9 +208,11 @@ impl From for SerdeTransactionError { TransactionError::WouldExceedAccountDataTotalLimit => { Self::WouldExceedAccountDataTotalLimit } - TransactionError::DuplicateInstruction(i) => Self::DuplicateInstruction(i), + TransactionError::DuplicateInstruction(i) => Self::DuplicateInstruction(*i), TransactionError::InsufficientFundsForRent { account_index } => { - Self::InsufficientFundsForRent { account_index } + Self::InsufficientFundsForRent { + account_index: *account_index, + } } TransactionError::MaxLoadedAccountsDataSizeExceeded => { Self::MaxLoadedAccountsDataSizeExceeded @@ -216,7 +222,9 @@ impl From for SerdeTransactionError { } TransactionError::ResanitizationNeeded => Self::ResanitizationNeeded, TransactionError::ProgramExecutionTemporarilyRestricted { account_index } => { - Self::ProgramExecutionTemporarilyRestricted { account_index } + Self::ProgramExecutionTemporarilyRestricted { + account_index: *account_index, + } } TransactionError::UnbalancedTransaction => Self::UnbalancedTransaction, TransactionError::ProgramCacheHitMaxLimit => Self::ProgramCacheHitMaxLimit, @@ -298,8 +306,11 @@ impl From for TransactionError { /// Copy of `InstructionError` type in which the `BorshIoError` variant /// contains a string. #[cfg_attr(test, derive(strum_macros::FromRepr, strum_macros::EnumIter))] -#[cfg_attr(feature = "frozen-abi", derive(AbiExample, AbiEnumVisitor))] -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, AbiEnumVisitor, StableAbi, StableAbiSample) +)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, SchemaRead, SchemaWrite)] enum SerdeInstructionError { GenericError, InvalidArgument, @@ -429,8 +440,8 @@ impl From for InstructionError { } } -impl From for SerdeInstructionError { - fn from(err: InstructionError) -> Self { +impl From<&InstructionError> for SerdeInstructionError { + fn from(err: &InstructionError) -> Self { match err { InstructionError::GenericError => Self::GenericError, InstructionError::InvalidArgument => Self::InvalidArgument, @@ -458,7 +469,7 @@ impl From for SerdeInstructionError { InstructionError::AccountBorrowFailed => Self::AccountBorrowFailed, InstructionError::AccountBorrowOutstanding => Self::AccountBorrowOutstanding, InstructionError::DuplicateAccountOutOfSync => Self::DuplicateAccountOutOfSync, - InstructionError::Custom(n) => Self::Custom(n), + InstructionError::Custom(n) => Self::Custom(*n), InstructionError::InvalidError => Self::InvalidError, InstructionError::ExecutableDataModified => Self::ExecutableDataModified, InstructionError::ExecutableLamportChange => Self::ExecutableLamportChange, diff --git a/runtime/src/serde_snapshot/storage.rs b/runtime/src/serde_snapshot/storage.rs index 714c2f65a33..44699ed497e 100644 --- a/runtime/src/serde_snapshot/storage.rs +++ b/runtime/src/serde_snapshot/storage.rs @@ -8,6 +8,14 @@ use { pub(crate) type SerializedAccountsFileId = usize; // Serializable version of AccountStorageEntry for snapshot format +#[cfg_attr( + feature = "frozen-abi", + derive(StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "CMckX3HiC6K5FSmFo4tH44wU1mvGfabNtYAs65uaGvGU", + test_roundtrip = "eq_and_wire" + ) +)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] pub struct SerializableAccountStorageEntry { id: SerializedAccountsFileId, @@ -32,16 +40,3 @@ impl SerializableAccountStorageEntry { } } } - -pub(crate) trait SerializableStorage { - fn current_len(&self) -> usize; -} - -impl SerializableStorage for SerializableAccountStorageEntry { - fn current_len(&self) -> usize { - self.accounts_current_len - } -} - -#[cfg(feature = "frozen-abi")] -impl solana_frozen_abi::abi_example::TransparentAsHelper for SerializableAccountStorageEntry {} diff --git a/runtime/src/serde_snapshot/storages_list.rs b/runtime/src/serde_snapshot/storages_list.rs new file mode 100644 index 00000000000..75aebdbb448 --- /dev/null +++ b/runtime/src/serde_snapshot/storages_list.rs @@ -0,0 +1,84 @@ +use { + solana_accounts_db::{account_storage_entry::AccountStorageEntry, accounts_db::AccountsFileId}, + solana_clock::Slot, + std::{collections::HashSet, sync::Arc}, + wincode::{SchemaRead, SchemaWrite}, +}; + +/// Identifies a storage file belonging to a bank snapshot, by `(slot, id)`. +/// +/// The file is local-only (never archived) and gated behind `SNAPSHOT_FASTBOOT_VERSION`, so the +/// on-disk encoding can use `AccountsFileId` (`u32`) directly without worrying about +/// forward-compat with potential future widening. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, SchemaRead, SchemaWrite)] +pub struct StorageListItem { + pub slot: Slot, + pub id: AccountsFileId, +} + +/// On-disk format of the storages list file. +/// +/// Written next to the bank snapshot on graceful exit to record which storages in the account +/// run dirs make up the snapshot; read on startup to prune anything else before fastboot. +#[derive(Debug, SchemaRead, SchemaWrite)] +pub struct StoragesList { + list: Vec, +} + +impl StoragesList { + pub fn new_from_storages(snapshot_storages: &[Arc]) -> Self { + Self::from_items( + snapshot_storages + .iter() + .map(|storage| StorageListItem { + slot: storage.slot(), + id: storage.id(), + }) + .collect(), + ) + } + + /// Build a `StoragesList` from an existing `Vec` of items. + pub fn from_items(list: Vec) -> Self { + Self { list } + } + + pub fn into_slot_file_id_set(self) -> HashSet<(Slot, AccountsFileId)> { + self.list + .into_iter() + .map(|item| (item.slot, item.id)) + .collect() + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::serde_snapshot::{deserialize_wincode_from, serialize_into}, + std::io::Cursor, + }; + + #[test] + fn test_roundtrip_storages_list() { + let list = StoragesList { + list: vec![ + StorageListItem { slot: 7, id: 42 }, + StorageListItem { slot: 11, id: 13 }, + StorageListItem { + slot: Slot::MAX, + id: AccountsFileId::MAX, + }, + ], + }; + let expected_set: HashSet<(Slot, AccountsFileId)> = + HashSet::from([(7, 42), (11, 13), (Slot::MAX, AccountsFileId::MAX)]); + + let mut buf = Vec::new(); + serialize_into(Cursor::new(&mut buf), &list).unwrap(); + + let decoded: StoragesList = deserialize_wincode_from(Cursor::new(&buf)).unwrap(); + assert_eq!(decoded.into_slot_file_id_set(), expected_set); + } +} diff --git a/runtime/src/serde_snapshot/tests.rs b/runtime/src/serde_snapshot/tests.rs index 25dc6938868..94be607b4ec 100644 --- a/runtime/src/serde_snapshot/tests.rs +++ b/runtime/src/serde_snapshot/tests.rs @@ -36,10 +36,7 @@ mod serde_snapshot_tests { io::{self, BufReader, Cursor, Read, Write}, ops::RangeFull, path::{Path, PathBuf}, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, + sync::{Arc, atomic::Ordering}, }, tempfile::TempDir, test_case::test_case, @@ -109,11 +106,7 @@ mod serde_snapshot_tests { let bank_hash_stats = BankHashStats::default(); serialize_into( stream, - &SerializableAccountsDb { - slot, - account_storage_entries, - bank_hash_stats, - }, + &SerializableAccountsDb::new(slot, account_storage_entries, bank_hash_stats), ) } @@ -126,7 +119,8 @@ mod serde_snapshot_tests { let storage: AccountStorageMap = AccountStorageMap::with_capacity(storage_entries.len()); let mut next_append_vec_id = 0; const MAX_BUFFER_SIZE: usize = 2 * 1024 * 1024; - let storage_files = open_storage_files(storage_entries.iter().map(|s| s.as_ref()), false)?; + let storage_files = open_storage_files(storage_entries.iter().map(|s| s.as_ref()), false) + .collect::>>()?; let mut buf_reader = storage_file_buf_reader(MAX_BUFFER_SIZE, false, &IoSetupState::default())?; for (storage_entry, file) in storage_entries.iter().zip(storage_files.iter()) { @@ -674,23 +668,6 @@ mod serde_snapshot_tests { assert_eq!(accounts.accounts_index.ref_count_from_storage(&pubkey1), 1); accounts.store_for_tests((current_slot, [(&pubkey1, &zero_lamport_account)].as_slice())); accounts.add_root_and_flush_write_cache(current_slot); - // had to be a root to flush, but clean won't work as this test expects if it is a root - // so, remove the root from alive_roots, then restore it after clean - accounts - .accounts_index - .roots_tracker - .write() - .unwrap() - .alive_roots - .remove(¤t_slot); - accounts.clean_accounts_for_tests(); - accounts - .accounts_index - .roots_tracker - .write() - .unwrap() - .alive_roots - .insert(current_slot); // Ref count is 1 as the older versions were marked obsolete assert_eq!(accounts.accounts_index.ref_count_from_storage(&pubkey1), 1); @@ -850,18 +827,18 @@ mod serde_snapshot_tests { become_ungovernable(tmp.path()); let next_append_vec_id = AtomicAccountsFileId::new(next_id as u32); - let num_collisions = AtomicUsize::new(0); + let mut num_collisions = 0; let (remapped_id, remapped_file_info) = remap_append_vec_file( 123, old_id, old_file_info, &next_append_vec_id, - &num_collisions, + &mut num_collisions, ) .unwrap(); assert_eq!(remapped_id as usize, expected_remapped_id); assert_eq!(&remapped_file_info.path, &expected_remapped_path); - assert_eq!(num_collisions.load(Ordering::Relaxed), expected_collisions); + assert_eq!(num_collisions, expected_collisions); } #[test] @@ -879,13 +856,13 @@ mod serde_snapshot_tests { // In remap_append_vec_file() we want to handle EEXIST (collisions), but we want to return all // other errors let next_append_vec_id = AtomicAccountsFileId::new(457); - let num_collisions = AtomicUsize::new(0); + let mut num_collisions = 0; remap_append_vec_file( 123, 456, original_file_info, &next_append_vec_id, - &num_collisions, + &mut num_collisions, ) .unwrap(); } diff --git a/runtime/src/serde_snapshot/types.rs b/runtime/src/serde_snapshot/types.rs index 25d9eb52fb5..f8865d872f3 100644 --- a/runtime/src/serde_snapshot/types.rs +++ b/runtime/src/serde_snapshot/types.rs @@ -4,7 +4,7 @@ use { }; /// Snapshot serde-safe AccountsLtHash -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[serde_with::serde_as] #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)] pub struct SerdeAccountsLtHash( @@ -26,7 +26,7 @@ impl From for SerdeAccountsLtHash { /// Snapshot serde-safe RentCollector, which is now unused #[repr(C)] -#[cfg_attr(feature = "frozen-abi", derive(solana_frozen_abi_macro::AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct UnusedRentCollector { epoch: Epoch, diff --git a/runtime/src/serde_snapshot/utils.rs b/runtime/src/serde_snapshot/utils.rs deleted file mode 100644 index a9615f67dca..00000000000 --- a/runtime/src/serde_snapshot/utils.rs +++ /dev/null @@ -1,116 +0,0 @@ -use serde::{ - Serialize, Serializer, - ser::{SerializeSeq, SerializeTuple}, -}; -#[cfg(all(test, feature = "frozen-abi"))] -use solana_frozen_abi::abi_example::TransparentAsHelper; - -// consumes an iterator and returns an object that will serialize as a serde seq -pub fn serialize_iter_as_seq(iter: I) -> impl Serialize -where - I: IntoIterator, - ::Item: Serialize, - ::IntoIter: ExactSizeIterator, -{ - struct SerializableSequencedIterator { - iter: std::cell::RefCell>, - } - - #[cfg(all(test, feature = "frozen-abi"))] - impl TransparentAsHelper for SerializableSequencedIterator {} - - impl Serialize for SerializableSequencedIterator - where - I: IntoIterator, - ::Item: Serialize, - ::IntoIter: ExactSizeIterator, - { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let iter = self.iter.borrow_mut().take().unwrap().into_iter(); - let mut seq = serializer.serialize_seq(Some(iter.len()))?; - for item in iter { - seq.serialize_element(&item)?; - } - seq.end() - } - } - - SerializableSequencedIterator { - iter: std::cell::RefCell::new(Some(iter)), - } -} - -// consumes an iterator and returns an object that will serialize as a serde tuple -#[expect(dead_code)] -pub fn serialize_iter_as_tuple(iter: I) -> impl Serialize -where - I: IntoIterator, - ::Item: Serialize, - ::IntoIter: ExactSizeIterator, -{ - struct SerializableSequencedIterator { - iter: std::cell::RefCell>, - } - - #[cfg(all(test, feature = "frozen-abi"))] - impl TransparentAsHelper for SerializableSequencedIterator {} - - impl Serialize for SerializableSequencedIterator - where - I: IntoIterator, - ::Item: Serialize, - ::IntoIter: ExactSizeIterator, - { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let iter = self.iter.borrow_mut().take().unwrap().into_iter(); - let mut tup = serializer.serialize_tuple(iter.len())?; - for item in iter { - tup.serialize_element(&item)?; - } - tup.end() - } - } - - SerializableSequencedIterator { - iter: std::cell::RefCell::new(Some(iter)), - } -} - -// consumes a 2-tuple iterator and returns an object that will serialize as a serde map -pub fn serialize_iter_as_map(iter: I) -> impl Serialize -where - K: Serialize, - V: Serialize, - I: IntoIterator, -{ - struct SerializableMappedIterator { - iter: std::cell::RefCell>, - } - - #[cfg(all(test, feature = "frozen-abi"))] - impl TransparentAsHelper for SerializableMappedIterator {} - - impl Serialize for SerializableMappedIterator - where - K: Serialize, - V: Serialize, - I: IntoIterator, - { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_map(self.iter.borrow_mut().take().unwrap()) - } - } - - SerializableMappedIterator { - iter: std::cell::RefCell::new(Some(iter)), - } -} diff --git a/runtime/src/slot_params.rs b/runtime/src/slot_params.rs new file mode 100644 index 00000000000..7333e44d69d --- /dev/null +++ b/runtime/src/slot_params.rs @@ -0,0 +1,376 @@ +use { + agave_feature_set::{self as feature_set, FeatureSet}, + solana_clock::Slot, + solana_cost_model::cost_tracker::CostTrackerLimits, + solana_epoch_schedule::EpochSchedule, + solana_pubkey::Pubkey, + std::{ + collections::BTreeMap, + ops::Bound::{Excluded, Included}, + }, +}; + +pub const DEFAULT_MAX_ENTRY_BYTES_PER_SLOT: u64 = 20 * 1024 * 1024; // 20 MiB + +/// Runtime parameters tied to a slot-time regime. +/// +/// Slot-time reduction features select explicit table values instead of +/// deriving ratios at runtime, which avoids rounding drift in consensus paths. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SlotParams { + pub(crate) ns_per_slot: u128, + pub(crate) slots_per_year: f64, + pub(crate) hashes_per_tick: Option, + pub(crate) cost_tracker_limits: CostTrackerLimits, + pub(crate) max_data_shreds_per_slot: u32, + pub(crate) max_code_shreds_per_slot: u32, + pub(crate) max_entry_bytes_per_slot: u64, + pub(crate) partitioned_epoch_rewards_stake_account_stores_per_block: u64, + pub(crate) vat_to_burn_per_epoch: u64, +} + +impl SlotParams { + /// Builds the slot-0 params baseline from genesis-derived Bank fields. + /// + /// Genesis can customize timing values. Bank construction can also + /// customize the partitioned-rewards write budget. Values without an + /// explicit construction-time knob retain the legacy baseline. + pub(crate) fn genesis_baseline( + ns_per_slot: u128, + slots_per_year: f64, + hashes_per_tick: Option, + partitioned_epoch_rewards_stake_account_stores_per_block: u64, + ) -> Self { + Self { + ns_per_slot, + slots_per_year, + hashes_per_tick, + partitioned_epoch_rewards_stake_account_stores_per_block, + ..LEGACY_SLOT_PARAMS + } + } + + /// Nanoseconds per slot for these params. + pub const fn ns_per_slot(&self) -> u128 { + self.ns_per_slot + } + + /// Slots per year for these params. + pub const fn slots_per_year(&self) -> f64 { + self.slots_per_year + } + + /// PoH hashes per tick for these params. + pub const fn hashes_per_tick(&self) -> Option { + self.hashes_per_tick + } + + /// Maximum data shred index capacity for these params. + pub const fn max_data_shreds_per_slot(&self) -> u32 { + self.max_data_shreds_per_slot + } + + /// Maximum coding shred index capacity for these params. + pub const fn max_code_shreds_per_slot(&self) -> u32 { + self.max_code_shreds_per_slot + } + + /// Maximum bytes that can be reserved for entries in one slot. + pub const fn max_entry_bytes_per_slot(&self) -> u64 { + self.max_entry_bytes_per_slot + } + + /// Number of stake account reward stores allowed in one block. + pub const fn partitioned_epoch_rewards_stake_account_stores_per_block(&self) -> u64 { + self.partitioned_epoch_rewards_stake_account_stores_per_block + } + + /// Validator Admission Ticket burn amount for one epoch. + pub const fn vat_to_burn_per_epoch(&self) -> u64 { + self.vat_to_burn_per_epoch + } + + /// Returns the per-bank cost limits for these params. + pub(crate) const fn cost_limits(self, raise_block_limits_to_100m: bool) -> CostTrackerLimits { + let cost_tracker_limits = self.cost_tracker_limits; + let (account_cost, block_cost) = if raise_block_limits_to_100m { + ( + cost_tracker_limits + .account_cost + .saturating_mul(100) + .saturating_div(60), + cost_tracker_limits + .block_cost + .saturating_mul(100) + .saturating_div(60), + ) + } else { + ( + cost_tracker_limits.account_cost, + cost_tracker_limits.block_cost, + ) + }; + + CostTrackerLimits::new( + account_cost, + block_cost, + cost_tracker_limits.allocated_data_size, + ) + } +} + +pub const LEGACY_HASHES_PER_TICK: u64 = 62_500; +pub(crate) const LEGACY_SLOT_PARAMS: SlotParams = SlotParams { + ns_per_slot: 400_000_000, + slots_per_year: 78_892_314.984, + hashes_per_tick: Some(LEGACY_HASHES_PER_TICK), + cost_tracker_limits: CostTrackerLimits::new(24_000_000, 60_000_000, 100_000_000), + max_data_shreds_per_slot: 32_768, + max_code_shreds_per_slot: 32_768, + max_entry_bytes_per_slot: 20 * 1024 * 1024, + partitioned_epoch_rewards_stake_account_stores_per_block: 4096, + vat_to_burn_per_epoch: 1_600_000_000, +}; + +pub(crate) const SLOT_PARAMS_350MS: SlotParams = SlotParams { + ns_per_slot: 350_000_000, + slots_per_year: 90_162_645.696, + hashes_per_tick: Some(54_687), + cost_tracker_limits: CostTrackerLimits::new(21_000_000, 52_500_000, 87_500_000), + max_data_shreds_per_slot: 28_672, + max_code_shreds_per_slot: 28_672, + max_entry_bytes_per_slot: 18_350_080, + partitioned_epoch_rewards_stake_account_stores_per_block: 3_584, + vat_to_burn_per_epoch: 1_400_000_000, +}; + +pub(crate) const SLOT_PARAMS_300MS: SlotParams = SlotParams { + ns_per_slot: 300_000_000, + slots_per_year: 105_189_753.312, + hashes_per_tick: Some(46_875), + cost_tracker_limits: CostTrackerLimits::new(18_000_000, 45_000_000, 75_000_000), + max_data_shreds_per_slot: 24_576, + max_code_shreds_per_slot: 24_576, + max_entry_bytes_per_slot: 15_728_640, + partitioned_epoch_rewards_stake_account_stores_per_block: 3_072, + vat_to_burn_per_epoch: 1_200_000_000, +}; + +pub(crate) const SLOT_PARAMS_250MS: SlotParams = SlotParams { + ns_per_slot: 250_000_000, + slots_per_year: 126_227_703.974, + hashes_per_tick: Some(39_062), + cost_tracker_limits: CostTrackerLimits::new(15_000_000, 37_500_000, 62_500_000), + max_data_shreds_per_slot: 20_480, + max_code_shreds_per_slot: 20_480, + max_entry_bytes_per_slot: 13_107_200, + partitioned_epoch_rewards_stake_account_stores_per_block: 2_560, + vat_to_burn_per_epoch: 1_000_000_000, +}; + +pub(crate) const SLOT_PARAMS_200MS: SlotParams = SlotParams { + ns_per_slot: 200_000_000, + slots_per_year: 157_784_629.968, + hashes_per_tick: Some(31_250), + cost_tracker_limits: CostTrackerLimits::new(12_000_000, 30_000_000, 50_000_000), + max_data_shreds_per_slot: 16_384, + max_code_shreds_per_slot: 16_384, + max_entry_bytes_per_slot: 10_485_760, + partitioned_epoch_rewards_stake_account_stores_per_block: 2_048, + vat_to_burn_per_epoch: 800_000_000, +}; + +/// Slot-time reduction gates in the intended activation order. +const SLOT_TIME_REDUCTION_PARAMS: [(Pubkey, SlotParams); 4] = [ + ( + feature_set::reduce_slot_time_to_350ms::ID, + SLOT_PARAMS_350MS, + ), + ( + feature_set::reduce_slot_time_to_300ms::ID, + SLOT_PARAMS_300MS, + ), + ( + feature_set::reduce_slot_time_to_250ms::ID, + SLOT_PARAMS_250MS, + ), + ( + feature_set::reduce_slot_time_to_200ms::ID, + SLOT_PARAMS_200MS, + ), +]; + +/// Returns slot-time feature gates mapped to runtime slot parameters. +pub fn slot_time_feature_gates() -> [(Pubkey, SlotParams); 4] { + SLOT_TIME_REDUCTION_PARAMS +} + +/// Returns all slot-time reduction feature IDs in activation order. +pub fn slot_time_feature_ids() -> [Pubkey; 4] { + SLOT_TIME_REDUCTION_PARAMS.map(|(feature_id, _)| feature_id) +} + +/// Bank-local archive of slot-parameter transitions. +/// +/// This is rebuilt from feature accounts on startup instead of being serialized +/// into snapshots. The source of truth remains the feature set; this cache only +/// avoids repeatedly scanning, collecting, and sorting slot-time gates once they +/// are added. +#[derive(Clone, Debug)] +pub(crate) struct SlotParamsArchive { + /// Sorted `(effective_slot, params)` transitions, including baseline at slot 0. + /// + /// This is used when a bank, usually the root bank, must answer "which + /// parameters apply to some other slot?" Examples include shred filtering + /// for incoming shreds and inflation calculations that span historical slot + /// ranges. The transitions are normalized so slot duration never increases + /// as slots advance, even if slot-time features activate out of order. + param_transitions: BTreeMap, +} + +impl Default for SlotParamsArchive { + fn default() -> Self { + Self::new( + &FeatureSet::default(), + &EpochSchedule::default(), + LEGACY_SLOT_PARAMS, + ) + } +} + +impl SlotParamsArchive { + /// Rebuilds slot-parameter transitions from the active feature set. + pub(crate) fn new( + feature_set: &FeatureSet, + epoch_schedule: &EpochSchedule, + baseline_params: SlotParams, + ) -> Self { + let mut param_transitions = BTreeMap::from([(0, baseline_params)]); + let mut earliest_same_or_shorter_slot = Slot::MAX; + + // The feature table is ordered longest-to-shortest. Walk it in reverse + // so once a same-or-shorter target is effective at slot S, any longer + // target effective at S or later is known to be redundant. + for (feature_id, params) in slot_time_feature_gates().into_iter().rev() { + if params.ns_per_slot > baseline_params.ns_per_slot { + continue; + } + let Some(activation_slot) = feature_set.activated_slot(&feature_id) else { + continue; + }; + let effective_slot = Self::feature_effective_slot(epoch_schedule, activation_slot); + if effective_slot < earliest_same_or_shorter_slot { + param_transitions.insert(effective_slot, params); + earliest_same_or_shorter_slot = effective_slot; + } + } + + Self { param_transitions } + } + + /// Returns the baseline params supplied at genesis or snapshot restore. + pub(crate) fn baseline_params(&self) -> SlotParams { + self.param_transitions + .first_key_value() + .map(|(_, params)| *params) + .unwrap_or(LEGACY_SLOT_PARAMS) + } + + /// Returns the slot params effective at `slot`. + pub(crate) fn params_at_slot(&self, slot: Slot) -> SlotParams { + self.param_transitions + .range(..=slot) + .next_back() + .map(|(_, params)| *params) + .unwrap_or(LEGACY_SLOT_PARAMS) + } + + /// Returns sorted slot-parameter transitions. + pub(crate) fn param_transitions(&self) -> impl Iterator + '_ { + self.param_transitions + .iter() + .map(|(slot, params)| (*slot, *params)) + } + + /// Returns the exact wall-clock duration in nanoseconds for + /// `start_slot..=end_slot`. + pub(crate) fn slot_range_duration_nanos(&self, start_slot: Slot, end_slot: Slot) -> u128 { + if start_slot > end_slot { + return 0; + } + + let mut cursor = start_slot; + let mut params = self.params_at_slot(start_slot); + let mut duration = 0u128; + + for (&effective_slot, &effective_params) in self + .param_transitions + .range((Excluded(start_slot), Included(end_slot))) + { + duration = duration.saturating_add( + u128::from(effective_slot.saturating_sub(cursor)) + .saturating_mul(params.ns_per_slot()), + ); + cursor = effective_slot; + params = effective_params; + } + + let remaining_slots = u128::from(end_slot.saturating_sub(cursor)) + 1; + duration.saturating_add(remaining_slots.saturating_mul(params.ns_per_slot())) + } + + /// Returns the first slot where a slot-time feature may affect bank state. + /// + /// A gate that activates in epoch E is effective starting at the first slot + /// of epoch E + 1, giving shred filters a full epoch of advance notice + /// before enforcing lower shred limits. + fn feature_effective_slot(epoch_schedule: &EpochSchedule, activation_slot: Slot) -> Slot { + let activation_epoch = epoch_schedule.get_epoch(activation_slot); + epoch_schedule.get_first_slot_in_epoch(activation_epoch.saturating_add(1)) + } + + /// Returns true if any slot-time reduction has taken effect by this bank. + /// + /// Feature activation happens in one epoch, but slot params become effective + /// at the start of the following epoch. + pub(crate) fn any_slot_time_reduction_effective( + epoch_schedule: &EpochSchedule, + slot: Slot, + feature_set: &FeatureSet, + ns_per_slot: u128, + ) -> bool { + slot_time_feature_gates() + .into_iter() + .filter(|(_, params)| params.ns_per_slot() <= ns_per_slot) + .filter_map(|(feature_id, _)| feature_set.activated_slot(&feature_id)) + .any(|activation_slot| { + Self::feature_effective_slot(epoch_schedule, activation_slot) <= slot + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cost_limits_scaling_matches_simd_0525() { + for (params, expected_account_limit, expected_block_limit) in [ + (LEGACY_SLOT_PARAMS, 40_000_000, 100_000_000), + (SLOT_PARAMS_350MS, 35_000_000, 87_500_000), + (SLOT_PARAMS_300MS, 30_000_000, 75_000_000), + (SLOT_PARAMS_250MS, 25_000_000, 62_500_000), + (SLOT_PARAMS_200MS, 20_000_000, 50_000_000), + ] { + let data_size_limit = params.cost_limits(false).allocated_data_size; + assert_eq!( + params.cost_limits(true), + CostTrackerLimits::new( + expected_account_limit, + expected_block_limit, + data_size_limit + ) + ); + } + } +} diff --git a/runtime/src/snapshot_bank_utils.rs b/runtime/src/snapshot_bank_utils.rs index 859304dab22..0f21833b661 100644 --- a/runtime/src/snapshot_bank_utils.rs +++ b/runtime/src/snapshot_bank_utils.rs @@ -25,7 +25,7 @@ use { rebuild_storages_from_snapshot_dir, verify_and_unarchive_snapshots, }, }, - agave_fs::{dirs, io_setup::IoSetupState}, + agave_fs::io_setup::IoSetupState, agave_snapshots::{ SnapshotArchiveKind, SnapshotKind, error::{ @@ -376,11 +376,9 @@ pub fn bank_from_snapshot_dir( bank_snapshot.snapshot_dir.display() ); - // Clear the contents of the account paths run directories. When constructing the bank, the appendvec - // files will be extracted from the snapshot hardlink directories into these run/ directories. - for path in account_paths { - dirs::remove_dir_contents(path); - } + // Storages from this snapshot already live under `account_paths`; the storages list + // inside the bank snapshot dir tells us which ones belong to it, and stale files + // (e.g. from later slots) are pruned by `rebuild_storages_from_snapshot_dir`. let next_append_vec_id = Arc::new(AtomicAccountsFileId::new(0)); @@ -527,12 +525,10 @@ fn verify_slot_deltas( ) -> std::result::Result<(), VerifySlotDeltasError> { let max_root_entries = bank.status_cache.read().unwrap().max_root_entries(); let info = verify_slot_deltas_structural(slot_deltas, bank.slot(), max_root_entries)?; - verify_slot_deltas_with_history( - &info.slots, - &bank.get_slot_history(), - bank.slot(), - max_root_entries, - ) + let slot_history = bank + .get_slot_history() + .expect("snapshot bank must have slot history"); + verify_slot_deltas_with_history(&info.slots, &slot_history, bank.slot(), max_root_entries) } /// Verify that the snapshot's slot deltas are not corrupt/invalid @@ -839,9 +835,8 @@ mod tests { genesis_utils::{GenesisConfigInfo, create_genesis_config_with_leader}, snapshot_package::BankSnapshotPackage, snapshot_utils::{ - clean_orphaned_account_snapshot_dirs, create_tmp_accounts_dir_for_tests, - get_bank_snapshots, get_highest_bank_snapshot, get_highest_loadable_bank_snapshot, - purge_all_bank_snapshots, purge_bank_snapshot, + create_tmp_accounts_dir_for_tests, get_bank_snapshots, get_highest_bank_snapshot, + get_highest_loadable_bank_snapshot, purge_all_bank_snapshots, purge_bank_snapshot, purge_bank_snapshots_older_than_slot, purge_incomplete_bank_snapshots, purge_old_bank_snapshots, purge_old_bank_snapshots_at_startup, snapshot_storage_rebuilder::get_slot_and_append_vec_id, @@ -852,7 +847,10 @@ mod tests { SnapshotVersion, error::VerifySlotDeltasError, paths::get_bank_snapshot_dir, }, semver::Version, - solana_accounts_db::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING, + solana_accounts_db::{ + accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsFileId}, + accounts_file::AccountsFile, + }, solana_hash::Hash, solana_keypair::Keypair, solana_native_token::LAMPORTS_PER_SOL, @@ -888,7 +886,7 @@ mod tests { genesis_config: &GenesisConfig, bank_snapshots_dir: impl AsRef, num_total: usize, - should_flush_and_hard_link_storages: bool, + should_finalize: bool, ) -> Arc { let (bank0, bank_forks) = Bank::new_for_tests(genesis_config).wrap_with_bank_forks_for_tests(); @@ -908,7 +906,7 @@ mod tests { &bank_snapshots_dir, &bank, SnapshotVersion::default(), - should_flush_and_hard_link_storages, + should_finalize, ) .unwrap(); } @@ -917,7 +915,7 @@ mod tests { } /// Creates a bank snapshot from a bank, regardless of state. The Bank will be frozen during - /// the process. Passing in should_flush_and_hard_link_storages as true will create a + /// the process. Passing in should_finalize as true will create a /// a fastbootable bank snapshot /// /// Requires: @@ -927,7 +925,7 @@ mod tests { bank_snapshots_dir: impl AsRef, bank: &Bank, snapshot_version: SnapshotVersion, - should_flush_and_hard_link_storages: bool, + should_finalize: bool, ) -> agave_snapshots::Result<()> { assert!(bank.is_complete()); assert!(bank.block_id().is_some()); @@ -950,7 +948,7 @@ mod tests { snapshot_version, bank_snapshot_package, snapshot_storages.as_slice(), - should_flush_and_hard_link_storages, + should_finalize, &IoSetupState::default(), )?; @@ -1675,7 +1673,7 @@ mod tests { } #[test] - fn test_bank_snapshot_dir_accounts_hardlinks() { + fn test_bank_snapshot_dir_storages_list() { let bank = Bank::new_for_tests(&GenesisConfig::default()); bank.fill_bank_with_ticks_for_tests(); bank.set_block_id(Some(Hash::default())); @@ -1689,25 +1687,13 @@ mod tests { ) .unwrap(); - let accounts_hardlinks_dir = get_bank_snapshot_dir(&bank_snapshots_dir, bank.slot()) - .join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - assert!(fs::metadata(&accounts_hardlinks_dir).is_ok()); - - let mut hardlink_dirs = Vec::new(); - // This directory contain symlinks to all accounts snapshot directories. - for entry in fs::read_dir(accounts_hardlinks_dir).unwrap() { - let entry = entry.unwrap(); - let symlink = entry.path(); - let dst_path = fs::read_link(symlink).unwrap(); - assert!(fs::metadata(&dst_path).is_ok()); - hardlink_dirs.push(dst_path); - } - let bank_snapshot_dir = get_bank_snapshot_dir(&bank_snapshots_dir, bank.slot()); - assert!(purge_bank_snapshot(bank_snapshot_dir).is_ok()); + let storages_list_file = + bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_STORAGES_LIST_FILENAME); + assert!(fs::metadata(&storages_list_file).is_ok()); - // When the bank snapshot is removed, all the snapshot hardlink directories should be removed. - assert!(hardlink_dirs.iter().all(|dir| fs::metadata(dir).is_err())); + assert!(purge_bank_snapshot(&bank_snapshot_dir).is_ok()); + assert!(fs::metadata(&bank_snapshot_dir).is_err()); } /// Test versioning when fastbooting @@ -1767,14 +1753,14 @@ mod tests { #[test_case(false)] #[test_case(true)] - fn test_get_highest_bank_snapshot(should_flush_and_hard_link_storages: bool) { + fn test_get_highest_bank_snapshot(should_finalize: bool) { let genesis_config = GenesisConfig::default(); let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); let _bank = create_snapshot_dirs_for_tests( &genesis_config, &bank_snapshots_dir, 4, - should_flush_and_hard_link_storages, + should_finalize, ); let snapshot = get_highest_bank_snapshot(&bank_snapshots_dir).unwrap(); @@ -1805,83 +1791,16 @@ mod tests { assert_eq!(snapshot.slot, 1); } - #[test] - fn test_clean_orphaned_account_snapshot_dirs() { - let genesis_config = GenesisConfig::default(); - let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); - let _bank = create_snapshot_dirs_for_tests(&genesis_config, &bank_snapshots_dir, 2, true); - - let snapshot_dir_slot_2 = bank_snapshots_dir.path().join("2"); - let accounts_link_dir_slot_2 = - snapshot_dir_slot_2.join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - - // the symlinks point to the account snapshot hardlink directories /snapshot// for slot 2 - // get them via read_link - let hardlink_dirs_slot_2: Vec = fs::read_dir(accounts_link_dir_slot_2) - .unwrap() - .map(|entry| { - let symlink = entry.unwrap().path(); - fs::read_link(symlink).unwrap() - }) - .collect(); - - // remove the bank snapshot directory for slot 2, so the account snapshot slot 2 directories become orphaned - fs::remove_dir_all(snapshot_dir_slot_2).unwrap(); - - // verify the orphaned account snapshot hardlink directories are still there - assert!( - hardlink_dirs_slot_2 - .iter() - .all(|dir| fs::metadata(dir).is_ok()) - ); - - let account_snapshot_paths: Vec = hardlink_dirs_slot_2 - .iter() - .map(|dir| dir.parent().unwrap().parent().unwrap().to_path_buf()) - .collect(); - // clean the orphaned hardlink directories - clean_orphaned_account_snapshot_dirs(&bank_snapshots_dir, &account_snapshot_paths).unwrap(); - - // verify the hardlink directories are gone - assert!( - hardlink_dirs_slot_2 - .iter() - .all(|dir| fs::metadata(dir).is_err()) - ); - } - - // Ensure that `clean_orphaned_account_snapshot_dirs()` works correctly for bank snapshots - // that *do not* hard link the storages into their staging dir. - #[test] - fn test_clean_orphaned_account_snapshot_dirs_no_hard_link() { - let genesis_config = GenesisConfig::default(); - let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); - let _bank = create_snapshot_dirs_for_tests(&genesis_config, &bank_snapshots_dir, 2, false); - - // Ensure the bank snapshot dir does exist. - let bank_snapshot_dir = get_bank_snapshot_dir(&bank_snapshots_dir, 2); - assert!(fs::exists(&bank_snapshot_dir).unwrap()); - - // Ensure the accounts hard links dir does *not* exist for this bank snapshot - // (since we asked create_snapshot_dirs_for_tests() to *not* hard link). - let bank_snapshot_accounts_hard_link_dir = - bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - assert!(!fs::exists(&bank_snapshot_accounts_hard_link_dir).unwrap()); - - // Now make sure clean_orphaned_account_snapshot_dirs() doesn't error. - clean_orphaned_account_snapshot_dirs(&bank_snapshots_dir, &[]).unwrap(); - } - #[test_case(false)] #[test_case(true)] - fn test_purge_incomplete_bank_snapshots(should_flush_and_hard_link_storages: bool) { + fn test_purge_incomplete_bank_snapshots(should_finalize: bool) { let genesis_config = GenesisConfig::default(); let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); let _bank = create_snapshot_dirs_for_tests( &genesis_config, &bank_snapshots_dir, 2, - should_flush_and_hard_link_storages, + should_finalize, ); // remove the "version" files so the snapshots will be purged @@ -2099,7 +2018,7 @@ mod tests { bank2.set_block_id(Some(Hash::default())); assert_eq!(bank2.get_balance(&key2.pubkey()), 0); - // Take a bank snapshot, passing `true` for `should_flush_and_hard_link_storages`. + // Take a bank snapshot, passing `true` for `should_finalize`. // This ensures that `serialize_snapshot` performs all necessary steps to create // a snapshot that supports fastbooting. create_bank_snapshot_from_bank( @@ -2182,7 +2101,7 @@ mod tests { bank.fill_bank_with_ticks_for_tests(); bank.set_block_id(Some(Hash::default())); - // Take a bank snapshot, passing `true` for `should_flush_and_hard_link_storages`. + // Take a bank snapshot, passing `true` for `should_finalize`. // This ensures that `serialize_snapshot` performs all necessary steps to create // a snapshot that supports fastbooting. create_bank_snapshot_from_bank( @@ -2227,6 +2146,92 @@ mod tests { assert_eq!(max_id, next_id - 1); } + /// Drop a stale `.` file into the account_paths run dir before calling + /// `bank_from_snapshot_dir`, and verify that the fastboot rebuild path removes it (because + /// the `(slot, id)` pair isn't in the storages list) while keeping the snapshot's own + /// storage files in place and producing a working bank. + #[test] + fn test_bank_from_snapshot_dir_prunes_stale_storage() { + let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_leader( + 1_000_000 * LAMPORTS_PER_SOL, + &Pubkey::new_unique(), + 1_000_000 * LAMPORTS_PER_SOL, + ); + let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); + let bank = Bank::new_for_tests(&genesis_config); + bank.fill_bank_with_ticks_for_tests(); + bank.set_block_id(Some(Hash::default())); + + create_bank_snapshot_from_bank( + &bank_snapshots_dir, + &bank, + SnapshotVersion::default(), + true, + ) + .unwrap(); + let bank_snapshot = get_highest_bank_snapshot(&bank_snapshots_dir).unwrap(); + let account_paths = &bank.rc.accounts.accounts_db.paths; + + // Collect the legit paths (created by `create_bank_snapshot_from_bank` above) before + // we drop any stale files, so we can assert they survive the prune. + let pre_load_legit_files: Vec<_> = account_paths + .iter() + .flat_map(|account_path| { + fs::read_dir(account_path) + .unwrap() + .map(|e| e.unwrap().path()) + }) + .collect(); + assert!( + !pre_load_legit_files.is_empty(), + "expected at least one accounts storage file" + ); + + // Pick a `(slot, id)` that the snapshot doesn't reference. The snapshot only covers + // storages up to `bank.slot()`, so a far-future slot is guaranteed to be stale. + let stale_slot = bank.slot() + 1_000; + let stale_id = AccountsFileId::MAX; + let stale_files: Vec<_> = account_paths + .iter() + .map(|account_path| { + let stale = account_path.join(AccountsFile::file_name(stale_slot, stale_id)); + fs::write(&stale, b"junk").unwrap(); + stale + }) + .collect(); + + let bank_constructed = bank_from_snapshot_dir( + account_paths, + &bank_snapshot, + &genesis_config, + &RuntimeConfig::default(), + None, + None, + None, + false, + ACCOUNTS_DB_CONFIG_FOR_TESTING, + None, + Arc::default(), + ) + .unwrap(); + assert_eq!(bank_constructed, bank); + + for stale in stale_files { + assert!( + !stale.exists(), + "stale storage file '{}' should have been pruned", + stale.display(), + ); + } + for legit in pre_load_legit_files { + assert!( + legit.exists(), + "snapshot storage file '{}' should have survived the prune", + legit.display(), + ); + } + } + /// Ensure bank_from_snapshot_dir() catches a snapshot with incorrect capitalization. #[test] fn test_bank_from_snapshot_dir_bad_capitalization() { @@ -2282,14 +2287,14 @@ mod tests { #[test_case(false)] #[test_case(true)] - fn test_purge_all_bank_snapshots(should_flush_and_hard_link_storages: bool) { + fn test_purge_all_bank_snapshots(should_finalize: bool) { let genesis_config = GenesisConfig::default(); let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); let _bank = create_snapshot_dirs_for_tests( &genesis_config, &bank_snapshots_dir, 10, - should_flush_and_hard_link_storages, + should_finalize, ); // Keep bank in this scope so that its account_paths tmp dirs are not released, and purge_all_bank_snapshots // can clear the account hardlinks correctly. @@ -2301,14 +2306,14 @@ mod tests { #[test_case(false)] #[test_case(true)] - fn test_purge_old_bank_snapshots(should_flush_and_hard_link_storages: bool) { + fn test_purge_old_bank_snapshots(should_finalize: bool) { let genesis_config = GenesisConfig::default(); let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); let _bank = create_snapshot_dirs_for_tests( &genesis_config, &bank_snapshots_dir, 10, - should_flush_and_hard_link_storages, + should_finalize, ); // Keep bank in this scope so that its account_paths tmp dirs are not released, and purge_old_bank_snapshots // can clear the account hardlinks correctly. @@ -2328,7 +2333,7 @@ mod tests { #[test_case(false)] #[test_case(true)] - fn test_purge_bank_snapshots_older_than_slot(should_flush_and_hard_link_storages: bool) { + fn test_purge_bank_snapshots_older_than_slot(should_finalize: bool) { let genesis_config = GenesisConfig::default(); let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); @@ -2337,7 +2342,7 @@ mod tests { &genesis_config, &bank_snapshots_dir, 9, - should_flush_and_hard_link_storages, + should_finalize, ); let bank_snapshots_before = get_bank_snapshots(&bank_snapshots_dir); @@ -2361,7 +2366,7 @@ mod tests { #[test_case(false)] #[test_case(true)] - fn test_purge_old_bank_snapshots_at_startup(should_flush_and_hard_link_storages: bool) { + fn test_purge_old_bank_snapshots_at_startup(should_finalize: bool) { let genesis_config = GenesisConfig::default(); let bank_snapshots_dir = tempfile::TempDir::new().unwrap(); @@ -2370,7 +2375,7 @@ mod tests { &genesis_config, &bank_snapshots_dir, 9, - should_flush_and_hard_link_storages, + should_finalize, ); purge_old_bank_snapshots_at_startup(&bank_snapshots_dir); diff --git a/runtime/src/snapshot_controller.rs b/runtime/src/snapshot_controller.rs index 671ffacab1c..88750d38663 100644 --- a/runtime/src/snapshot_controller.rs +++ b/runtime/src/snapshot_controller.rs @@ -172,7 +172,7 @@ impl SnapshotController { mod tests { use { super::*, crate::accounts_background_service::SnapshotRequestKind, - agave_snapshots::snapshot_config::SnapshotConfig, crossbeam_channel::unbounded, + agave_snapshots::snapshot_config::SnapshotConfig, crossbeam_channel::bounded, solana_genesis_config::create_genesis_config, solana_leader_schedule::SlotLeader, std::sync::Arc, test_case::test_case, }; @@ -227,7 +227,7 @@ mod tests { ..Default::default() }; - let (snapshot_request_sender, snapshot_request_receiver) = unbounded(); + let (snapshot_request_sender, snapshot_request_receiver) = bounded(1024); let snapshot_controller = SnapshotController::new(snapshot_request_sender, snapshot_config, 0); @@ -287,7 +287,7 @@ mod tests { incremental_snapshot_archive_interval, ..Default::default() }; - let (snapshot_request_sender, _snapshot_request_receiver) = unbounded(); + let (snapshot_request_sender, _snapshot_request_receiver) = bounded(1024); let snapshot_controller = SnapshotController::new(snapshot_request_sender, snapshot_config, 0); assert_eq!(snapshot_controller.is_generating_snapshots(), expected); @@ -318,7 +318,7 @@ mod tests { ..Default::default() }; - let (snapshot_request_sender, snapshot_request_receiver) = unbounded(); + let (snapshot_request_sender, snapshot_request_receiver) = bounded(1024); let snapshot_controller = SnapshotController::new(snapshot_request_sender, snapshot_config, 0); diff --git a/runtime/src/snapshot_minimizer.rs b/runtime/src/snapshot_minimizer.rs index 9a52baa3579..888783bc055 100644 --- a/runtime/src/snapshot_minimizer.rs +++ b/runtime/src/snapshot_minimizer.rs @@ -317,7 +317,7 @@ impl<'a> SnapshotMinimizer<'a> { let storable_accounts = StorableAccountsBySlot::new(slot, &accounts, self.accounts_db()); - self.accounts_db().store_accounts_frozen( + self.accounts_db().store_accounts_for_shrink( storable_accounts, new_storage, UpdateIndexThreadSelection::Inline, diff --git a/runtime/src/snapshot_utils.rs b/runtime/src/snapshot_utils.rs index e5268c7c6f7..6192272e1c0 100644 --- a/runtime/src/snapshot_utils.rs +++ b/runtime/src/snapshot_utils.rs @@ -6,7 +6,7 @@ use { serde_snapshot::{ self, AccountsDbFields, ExtraFieldsToSerialize, SerdeObsoleteAccountsMap, SerializableAccountStorageEntry, SnapshotAccountsDbFields, SnapshotBankFields, - SnapshotStreams, + SnapshotStreams, StorageListItem, StoragesList, }, snapshot_package::BankSnapshotPackage, snapshot_utils::snapshot_storage_rebuilder::{ @@ -14,7 +14,7 @@ use { }, }, agave_fs::{ - FileInfo, + FileInfo, FileSize, buffered_reader::large_file_buf_reader, buffered_writer::{SizeLimitedWriter, large_file_buf_writer}, io_setup::IoSetupState, @@ -22,9 +22,7 @@ use { agave_snapshots::{ ArchiveFormat, Result, SnapshotArchiveKind, SnapshotVersion, archive_snapshot, error::{ - AddBankSnapshotError, GetSnapshotAccountsHardLinkDirError, - HardLinkStoragesToSnapshotError, SnapshotError, SnapshotFastbootError, - SnapshotNewFromDirError, + AddBankSnapshotError, SnapshotError, SnapshotFastbootError, SnapshotNewFromDirError, }, paths::{self as snapshot_paths, incremental_snapshot_archives_iter}, snapshot_archive_info::{ @@ -42,9 +40,11 @@ use { solana_accounts_db::{ account_storage::AccountStorageMap, account_storage_entry::AccountStorageEntry, - accounts_db::AtomicAccountsFileId, - accounts_file::AccountsFile, - utils::{ACCOUNTS_RUN_DIR, ACCOUNTS_SNAPSHOT_DIR, move_and_async_delete_path}, + accounts_db::{AccountsFileId, AtomicAccountsFileId}, + utils::{ + ACCOUNTS_RUN_DIR, ACCOUNTS_SNAPSHOT_DIR, move_and_async_delete_path, + move_and_async_delete_path_contents, + }, }, solana_clock::Slot, solana_measure::{measure::Measure, measure_time, measure_us}, @@ -71,10 +71,15 @@ pub mod snapshot_storage_rebuilder; /// Limit is set assuming 24 bytes per entry, 5% of 10 billion accounts /// = 500 million entries * 24 bytes = 12 GB pub const MAX_OBSOLETE_ACCOUNTS_FILE_SIZE: u64 = 1024 * 1024 * 1024 * 12; // 12 GB +/// Limit the size of the storages list file. +/// Each `(slot, id)` entry encodes to 12 bytes; 100 MiB covers ~8.7 million entries, well past +/// any realistic storage count. +pub const MAX_STORAGES_LIST_FILE_SIZE: u64 = 100 * 1024 * 1024; // 100 MiB pub const MAX_SNAPSHOT_DATA_FILE_SIZE: u64 = 32 * 1024 * 1024 * 1024; // 32 GiB const MAX_SNAPSHOT_VERSION_FILE_SIZE: u64 = 8; // byte -/// Buffer size that allows several concurrent reads using default io-uring reader read size (1MiB) -const OBSOLETE_ACCOUNTS_READ_BUF_SIZE: usize = 4 * 1024 * 1024; +/// Buffer size for reading auxiliary per-snapshot files (obsolete accounts, storages list). +/// Sized to allow several concurrent reads at the default io-uring reader read size (1MiB). +const AUX_SNAPSHOT_FILE_READ_BUF_SIZE: usize = 4 * 1024 * 1024; // Snapshot Fastboot Version History // Legacy - No fastboot version file, storages flushed file presence determines if snapshot is loadable @@ -82,7 +87,13 @@ const OBSOLETE_ACCOUNTS_READ_BUF_SIZE: usize = 4 * 1024 * 1024; // 2.0.0 - Obsolete Accounts File added, storages flushed file not written anymore // Snapshots created with version 2.0.0 will not fastboot to older versions // Snapshots created with versions <2.0.0 will fastboot to version 2.0.0 -const SNAPSHOT_FASTBOOT_VERSION: Version = Version::new(2, 0, 0); +// 3.0.0 - Storages List file added, replaces the per-storage hardlink dirs. +// 3.0.0 validators can still fastboot from 2.0.0 snapshots: the legacy hardlinks are +// migrated back into the account run dirs at load time (see `migrate_legacy_hardlinks`), +// and the next teardown writes the new-format storages list. +// Note: 2.0.0 validators cannot fastboot from 3.0.0 snapshots because the per-storage +// hardlink dirs they rely on are no longer written; they must fall back to archive. +const SNAPSHOT_FASTBOOT_VERSION: Version = Version::new(3, 0, 0); /// Information about a bank snapshot. Namely the slot of the bank, the path to the snapshot, and /// the kind of the snapshot. @@ -252,71 +263,6 @@ pub(crate) struct StorageAndNextAccountsFileId { pub next_append_vec_id: AtomicAccountsFileId, } -/// The account snapshot directories under /snapshot/ contain account files hardlinked -/// from /run taken at snapshot time. They are referenced by the symlinks from the -/// bank snapshot dir snapshot//accounts_hardlinks/. We observed that sometimes the bank snapshot dir -/// could be deleted but the account snapshot directories were left behind, possibly by some manual operations -/// or some legacy code not using the symlinks to clean up the account snapshot hardlink directories. -/// This function cleans up any account snapshot directories that are no longer referenced by the bank -/// snapshot dirs, to ensure proper snapshot operations. -pub fn clean_orphaned_account_snapshot_dirs( - bank_snapshots_dir: impl AsRef, - account_snapshot_paths: &[PathBuf], -) -> io::Result<()> { - // Create the HashSet of the account snapshot hardlink directories referenced by the snapshot dirs. - // This is used to clean up any hardlinks that are no longer referenced by the snapshot dirs. - let mut account_snapshot_dirs_referenced = HashSet::new(); - let snapshots = get_bank_snapshots(bank_snapshots_dir); - for snapshot in snapshots { - let account_hardlinks_dir = snapshot - .snapshot_dir - .join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - // loop through entries in the snapshot_hardlink_dir, read the symlinks, add the target to the HashSet - let Ok(read_dir) = fs::read_dir(&account_hardlinks_dir) else { - // The bank snapshot may not have a hard links dir with the storages. - // This is fine, and happens for bank snapshots we do *not* fastboot from. - // In this case, log it and go to the next bank snapshot. - debug!( - "failed to read account hardlinks dir '{}'", - account_hardlinks_dir.display(), - ); - continue; - }; - for entry in read_dir { - let path = entry?.path(); - let target = fs::read_link(&path).map_err(|err| { - IoError::other(format!( - "failed to read symlink '{}': {err}", - path.display(), - )) - })?; - account_snapshot_dirs_referenced.insert(target); - } - } - - // loop through the account snapshot hardlink directories, if the directory is not in the account_snapshot_dirs_referenced set, delete it - for account_snapshot_path in account_snapshot_paths { - let read_dir = fs::read_dir(account_snapshot_path).map_err(|err| { - IoError::other(format!( - "failed to read account snapshot dir '{}': {err}", - account_snapshot_path.display(), - )) - })?; - for entry in read_dir { - let path = entry?.path(); - if !account_snapshot_dirs_referenced.contains(&path) { - info!( - "Removing orphaned account snapshot hardlink directory '{}'...", - path.display() - ); - move_and_async_delete_path(&path); - } - } - } - - Ok(()) -} - /// Purges incomplete bank snapshots pub fn purge_incomplete_bank_snapshots(bank_snapshots_dir: impl AsRef) { let Ok(read_dir_iter) = std::fs::read_dir(&bank_snapshots_dir) else { @@ -423,10 +369,17 @@ fn is_bank_snapshot_loadable( fn is_snapshot_fastboot_compatible( version: &Version, ) -> std::result::Result { - if version.major <= SNAPSHOT_FASTBOOT_VERSION.major { - Ok(true) - } else { - Err(SnapshotFastbootError::IncompatibleVersion(version.clone())) + match version.major { + // Current format: storages list lives next to the bank snapshot file. + 3 => Ok(true), + // Legacy format: per-storage hardlink dirs. `rebuild_storages_from_snapshot_dir` + // migrates them to the new format at load time. + 2 => Ok(true), + v if v > SNAPSHOT_FASTBOOT_VERSION.major => { + Err(SnapshotFastbootError::IncompatibleVersion(version.clone())) + } + // Older format we no longer know how to load — fall back to archive. + _ => Ok(false), } } @@ -533,7 +486,7 @@ pub fn serialize_snapshot( snapshot_version: SnapshotVersion, bank_snapshot_package: BankSnapshotPackage, snapshot_storages: &[Arc], - should_flush_and_hard_link_storages: bool, + should_finalize: bool, io_setup: &IoSetupState, ) -> Result { let BankSnapshotPackage { @@ -594,8 +547,12 @@ pub fn serialize_snapshot( let status_cache_path = bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_STATUS_CACHE_FILENAME); let (status_cache_consumed_size, status_cache_serialize_us) = measure_us!( - serde_snapshot::serialize_status_cache(status_cache_slot_deltas, &status_cache_path) - .map_err(|err| AddBankSnapshotError::SerializeStatusCache(Box::new(err)))? + serde_snapshot::serialize_status_cache( + status_cache_slot_deltas, + &status_cache_path, + io_setup, + ) + .map_err(|err| AddBankSnapshotError::SerializeStatusCache(Box::new(err)))? ); let version_path = bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_VERSION_FILENAME); @@ -604,19 +561,18 @@ pub fn serialize_snapshot( .map_err(|err| AddBankSnapshotError::WriteSnapshotVersionFile(err, version_path))? ); - let (flush_storages_us, hard_link_storages_us, serialize_obsolete_accounts_us) = - if should_flush_and_hard_link_storages { + let (flush_storages_us, serialize_obsolete_accounts_us, write_storages_list_us) = + if should_finalize { let flush_measure = Measure::start(""); for storage in snapshot_storages { storage.flush().map_err(|err| { AddBankSnapshotError::FlushStorage(err, storage.path().to_path_buf()) })?; + // We're about to mark this snapshot fastboot-loadable. Pin the storage + // file so it outlives the validator-exit Drop chain. + storage.disable_remove_on_drop(); } let flush_us = flush_measure.end_as_us(); - let (_, hard_link_us) = measure_us!( - hard_link_storages_to_snapshot(&bank_snapshot_dir, slot, snapshot_storages) - .map_err(AddBankSnapshotError::HardLinkStorages)? - ); let (_, serialize_obsolete_accounts_us) = measure_us!({ write_obsolete_accounts_to_snapshot( @@ -628,13 +584,22 @@ pub fn serialize_snapshot( .map_err(|err| AddBankSnapshotError::SerializeObsoleteAccounts(Box::new(err)))? }); + let (_, write_storages_list_us) = measure_us!( + write_storages_list_to_snapshot( + &bank_snapshot_dir, + snapshot_storages, + io_setup, + ) + .map_err(|err| AddBankSnapshotError::WriteStoragesList(Box::new(err)))? + ); + mark_bank_snapshot_as_loadable(&bank_snapshot_dir) .map_err(AddBankSnapshotError::MarkSnapshotLoadable)?; ( Some(flush_us), - Some(hard_link_us), Some(serialize_obsolete_accounts_us), + Some(write_storages_list_us), ) } else { (None, None, None) @@ -647,10 +612,11 @@ pub fn serialize_snapshot( "snapshot_bank", ("slot", slot, i64), ("bank_size", bank_snapshot_consumed_size, i64), + ("num_storages", snapshot_storages.len(), i64), ("status_cache_size", status_cache_consumed_size, i64), ("flush_storages_us", flush_storages_us, Option), - ("hard_link_storages_us", hard_link_storages_us, Option), ("serialize_obsolete_accounts_us", serialize_obsolete_accounts_us, Option), + ("write_storages_list_us", write_storages_list_us, Option), ("bank_serialize_us", bank_serialize.as_us(), i64), ("status_cache_serialize_us", status_cache_serialize_us, i64), ("write_version_file_us", write_version_file_us, i64), @@ -775,7 +741,7 @@ fn deserialize_obsolete_accounts( .join(snapshot_paths::SNAPSHOT_OBSOLETE_ACCOUNTS_FILENAME); let obsolete_accounts_reader = ReadAdapter::new(large_file_buf_reader( &obsolete_accounts_path, - OBSOLETE_ACCOUNTS_READ_BUF_SIZE, + AUX_SNAPSHOT_FILE_READ_BUF_SIZE, &IoSetupState::default(), )?); // If the file is too large return error @@ -795,6 +761,62 @@ fn deserialize_obsolete_accounts( )?) } +pub fn write_storages_list_to_snapshot( + bank_snapshot_dir: impl AsRef, + snapshot_storages: &[Arc], + io_setup: &IoSetupState, +) -> Result { + let storages_list = StoragesList::new_from_storages(snapshot_storages); + serialize_storages_list_to_snapshot(bank_snapshot_dir, storages_list, io_setup) +} + +fn serialize_storages_list_to_snapshot( + bank_snapshot_dir: impl AsRef, + storages_list: StoragesList, + io_setup: &IoSetupState, +) -> Result { + let storages_list_path = bank_snapshot_dir + .as_ref() + .join(snapshot_paths::SNAPSHOT_STORAGES_LIST_FILENAME); + let mut file_stream = SizeLimitedWriter::new( + large_file_buf_writer(&storages_list_path, io_setup)?, + MAX_STORAGES_LIST_FILE_SIZE, + ); + serde_snapshot::serialize_into(&mut file_stream, &storages_list).map_err(|err| { + IoError::other(format!( + "unable to serialize storages list to file '{}': {err}", + storages_list_path.display(), + )) + })?; + Ok(file_stream.bytes_written()) +} + +fn deserialize_storages_list( + storages_list_path: &Path, + maximum_storages_list_file_size: u64, +) -> Result { + let storages_list_reader = ReadAdapter::new(large_file_buf_reader( + storages_list_path, + AUX_SNAPSHOT_FILE_READ_BUF_SIZE, + &IoSetupState::default(), + )?); + // If the file is too large return error + let storages_list_file_metadata = fs::metadata(storages_list_path)?; + if storages_list_file_metadata.len() > maximum_storages_list_file_size { + let error_message = format!( + "too large storages list file to deserialize: '{}' has {} bytes (max size is \ + {maximum_storages_list_file_size} bytes)", + storages_list_path.display(), + storages_list_file_metadata.len(), + ); + return Err(IoError::other(error_message).into()); + } + + Ok(serde_snapshot::deserialize_wincode_from( + storages_list_reader, + )?) +} + pub fn serialize_snapshot_data_file( data_file_path: &Path, io_setup: &IoSetupState, @@ -961,111 +983,6 @@ fn check_deserialize_file_consumed( Ok(()) } -/// Return account path from the appendvec path after checking its format. -fn get_account_path_from_appendvec_path(appendvec_path: &Path) -> Option { - let run_path = appendvec_path.parent()?; - let run_file_name = run_path.file_name()?; - // All appendvec files should be under /run/. - // When generating the bank snapshot directory, they are hardlinked to /snapshot// - if run_file_name != ACCOUNTS_RUN_DIR { - error!( - "The account path {} does not have run/ as its immediate parent directory.", - run_path.display() - ); - return None; - } - let account_path = run_path.parent()?; - Some(account_path.to_path_buf()) -} - -/// From an appendvec path, derive the snapshot hardlink path. If the corresponding snapshot hardlink -/// directory does not exist, create it. -fn get_snapshot_accounts_hardlink_dir( - appendvec_path: &Path, - bank_slot: Slot, - account_paths: &mut HashSet, - hardlinks_dir: impl AsRef, -) -> std::result::Result { - let account_path = get_account_path_from_appendvec_path(appendvec_path).ok_or_else(|| { - GetSnapshotAccountsHardLinkDirError::GetAccountPath(appendvec_path.to_path_buf()) - })?; - - let snapshot_hardlink_dir = account_path - .join(ACCOUNTS_SNAPSHOT_DIR) - .join(bank_slot.to_string()); - - // Use the hashset to track, to avoid checking the file system. Only set up the hardlink directory - // and the symlink to it at the first time of seeing the account_path. - if !account_paths.contains(&account_path) { - let idx = account_paths.len(); - debug!( - "for appendvec_path {}, create hard-link path {}", - appendvec_path.display(), - snapshot_hardlink_dir.display() - ); - fs::create_dir_all(&snapshot_hardlink_dir).map_err(|err| { - GetSnapshotAccountsHardLinkDirError::CreateSnapshotHardLinkDir( - err, - snapshot_hardlink_dir.clone(), - ) - })?; - let symlink_path = hardlinks_dir.as_ref().join(format!("account_path_{idx}")); - symlink::symlink_dir(&snapshot_hardlink_dir, &symlink_path).map_err(|err| { - GetSnapshotAccountsHardLinkDirError::SymlinkSnapshotHardLinkDir { - source: err, - original: snapshot_hardlink_dir.clone(), - link: symlink_path, - } - })?; - account_paths.insert(account_path); - }; - - Ok(snapshot_hardlink_dir) -} - -/// Hard-link the files from accounts/ to snapshot//accounts/ -/// This keeps the appendvec files alive and with the bank snapshot. The slot and id -/// in the file names are also updated in case its file is a recycled one with inconsistent slot -/// and id. -pub fn hard_link_storages_to_snapshot( - bank_snapshot_dir: impl AsRef, - bank_slot: Slot, - snapshot_storages: &[Arc], -) -> std::result::Result<(), HardLinkStoragesToSnapshotError> { - let accounts_hardlinks_dir = bank_snapshot_dir - .as_ref() - .join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - fs::create_dir_all(&accounts_hardlinks_dir).map_err(|err| { - HardLinkStoragesToSnapshotError::CreateAccountsHardLinksDir( - err, - accounts_hardlinks_dir.clone(), - ) - })?; - - let mut account_paths: HashSet = HashSet::new(); - for storage in snapshot_storages { - let storage_path = storage.accounts.path(); - let snapshot_hardlink_dir = get_snapshot_accounts_hardlink_dir( - storage_path, - bank_slot, - &mut account_paths, - &accounts_hardlinks_dir, - )?; - // The appendvec could be recycled, so its filename may not be consistent to the slot and id. - // Use the storage slot and id to compose a consistent file name for the hard-link file. - let hardlink_filename = AccountsFile::file_name(storage.slot(), storage.id()); - let hard_link_path = snapshot_hardlink_dir.join(hardlink_filename); - fs::hard_link(storage_path, &hard_link_path).map_err(|err| { - HardLinkStoragesToSnapshotError::HardLinkStorage( - err, - storage_path.to_path_buf(), - hard_link_path, - ) - })?; - } - Ok(()) -} - /// Unarchives the given full and incremental snapshot archives, as long as they are compatible. pub fn verify_and_unarchive_snapshots( bank_snapshots_dir: impl AsRef, @@ -1095,7 +1012,6 @@ pub fn verify_and_unarchive_snapshots( account_paths, full_snapshot_archive_info.archive_format(), next_append_vec_id.clone(), - None, io_setup, )?; @@ -1122,7 +1038,6 @@ pub fn verify_and_unarchive_snapshots( account_paths, incremental_snapshot_archive_info.archive_format(), next_append_vec_id.clone(), - Some(incremental_snapshot_archive_info.base_slot()), io_setup, )?; ( @@ -1314,7 +1229,6 @@ fn unarchive_snapshot( account_paths: &[PathBuf], archive_format: ArchiveFormat, next_append_vec_id: Arc, - base_slot: Option, io_setup: &IoSetupState, ) -> Result { let unpack_dir = tempfile::Builder::new() @@ -1334,7 +1248,6 @@ fn unarchive_snapshot( io_setup, ); - let num_rebuilder_threads = num_cpus::get_physical().saturating_sub(1).max(1); let snapshot_result = snapshot_fields_from_files(&file_receiver).and_then( |SnapshotFieldsBundle { snapshot_version, @@ -1343,14 +1256,9 @@ fn unarchive_snapshot( append_vec_files, .. }| { - let snapshot_storage_lengths = - accounts_db_fields.get_storage_lengths_for_snapshot_slots(base_slot)?; let (storage, measure_untar) = measure_time!( - SnapshotStorageRebuilder::spawn_rebuilder_threads( - snapshot_storage_lengths, - append_vec_files, - file_receiver, - num_rebuilder_threads, + SnapshotStorageRebuilder::rebuild_storages( + append_vec_files.into_iter().chain(file_receiver), next_append_vec_id, SnapshotFrom::Archive, None, @@ -1373,8 +1281,15 @@ fn unarchive_snapshot( }) }, ); - unarchive_handle.join().unwrap()?; - snapshot_result + // Producer errors are usually the root cause (no files -> no reception). + let unarchive_result = unarchive_handle.join().expect("must join unarchive thread"); + match (unarchive_result, snapshot_result) { + // Rebuilder closed the receiver early; the producer's send failure is just the + // downstream symptom — surface the rebuilder's error instead. + (Err(SnapshotError::CrossbeamSend(_)), snap @ Err(_)) => snap, + (Err(err), _) => Err(err), + (Ok(()), snap) => snap, + } }) } @@ -1412,6 +1327,147 @@ fn spawn_streaming_snapshot_dir_files( (file_receiver, handle) } +/// Migrates a legacy (2.0.0) bank snapshot's hardlink-based storages into the new format. +/// +/// Walks `/accounts_hardlinks/`, follows each symlink to its +/// `/snapshot//` target, and renames each storage file there back into +/// `/run/`. Writes the derived storages list into the bank snapshot dir so the +/// load path can always read it from disk. Tears down the legacy directories (the +/// `accounts_hardlinks/` symlink dir and the whole `/snapshot/` tree) on success +/// so subsequent restarts go through the normal new-format path. +fn migrate_legacy_hardlinks(bank_snapshot_dir: &Path, account_run_paths: &[PathBuf]) -> Result<()> { + let accounts_hardlinks_dir = + bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); + let mut items: Vec = Vec::new(); + + for entry in fs::read_dir(&accounts_hardlinks_dir).map_err(|err| { + IoError::other(format!( + "failed to read legacy accounts hardlinks dir '{}': {err}", + accounts_hardlinks_dir.display(), + )) + })? { + let symlink_path = entry?.path(); + let snapshot_slot_dir = fs::read_link(&symlink_path).map_err(|err| { + IoError::other(format!( + "failed to read symlink '{}': {err}", + symlink_path.display(), + )) + })?; + // snapshot_slot_dir = `/snapshot//`. The account run dir is its + // grandparent + `run` (i.e. `/run`). + let run_dir = snapshot_slot_dir + .parent() + .and_then(Path::parent) + .ok_or_else(|| { + IoError::other(format!( + "invalid legacy hardlink target '{}'", + snapshot_slot_dir.display(), + )) + })? + .join(ACCOUNTS_RUN_DIR); + // The legacy snapshot was taken against the account paths in use at the time. If + // those have changed (e.g. the operator reconfigured `account_paths` while upgrading + // Agave), the run dir we just derived isn't one we're loading into — bail rather than + // silently writing files into a location nobody's reading from. + if !account_run_paths.contains(&run_dir) { + return Err(IoError::other(format!( + "legacy hardlink target '{}' points to run dir '{}' which is not in the current \ + account paths ({:?}); the account paths configuration has changed since this \ + snapshot was taken — load from a snapshot archive instead", + snapshot_slot_dir.display(), + run_dir.display(), + account_run_paths, + )) + .into()); + } + + for file_entry in fs::read_dir(&snapshot_slot_dir).map_err(|err| { + IoError::other(format!( + "failed to read legacy hardlink dir '{}': {err}", + snapshot_slot_dir.display(), + )) + })? { + let src = file_entry?.path(); + let Some(name) = src.file_name().and_then(|n| n.to_str()) else { + continue; + }; + let (slot, id) = get_slot_and_append_vec_id(name)?; + let dest = run_dir.join(name); + fs::rename(&src, &dest).map_err(|err| { + IoError::other(format!( + "failed to migrate legacy storage from '{}' to '{}': {err}", + src.display(), + dest.display(), + )) + })?; + items.push(StorageListItem { + slot, + id: id as AccountsFileId, + }); + } + } + + // Persist the derived list now: migration is destructive (it removes the legacy hardlinks + // below), and writing the storages list is what actually brings the bank snapshot to + // fastboot version >=3 compatibility. Doing it here means the snapshot stays loadable even + // if the validator never performs a proper teardown (e.g. crashes). + serialize_storages_list_to_snapshot( + bank_snapshot_dir, + StoragesList::from_items(items), + &IoSetupState::default(), + )?; + + // Tear down the legacy state so we don't repeat this migration: drop the bank snapshot's + // `accounts_hardlinks/` symlink dir and wipe each `/snapshot/` tree (catches + // both the per-slot dirs we just emptied and any orphans from older purged snapshots). + fs::remove_dir_all(&accounts_hardlinks_dir).map_err(|err| { + IoError::other(format!( + "failed to remove legacy accounts hardlinks dir '{}': {err}", + accounts_hardlinks_dir.display(), + )) + })?; + wipe_account_snapshot_dirs(account_run_paths); + + // Bump the fastboot version so subsequent loads take the normal 3.0+ path instead of + // re-running the migration (which would fail now that the hardlinks dir is gone). + mark_bank_snapshot_as_loadable(bank_snapshot_dir)?; + + Ok(()) +} + +/// Removes storage files from `account_paths` whose `(slot, id)` pair isn't listed in the +/// storages list (i.e. they don't belong to the snapshot being loaded). Files whose names +/// don't parse as `.` storage filenames are left alone. +fn prune_stale_storages(account_paths: &[PathBuf], storages_list: StoragesList) -> Result<()> { + let expected_storages = storages_list.into_slot_file_id_set(); + for account_path in account_paths { + let read_dir = fs::read_dir(account_path).map_err(|err| { + IoError::other(format!( + "failed to read account path '{}': {err}", + account_path.display(), + )) + })?; + for entry in read_dir { + let path = entry?.path(); + let Some(name) = path.file_name().and_then(|n| n.to_str()) else { + continue; + }; + let Ok((slot, id)) = get_slot_and_append_vec_id(name) else { + // Not a storage file name — leave it alone. + continue; + }; + if !expected_storages.contains(&(slot, id as AccountsFileId)) { + info!( + "Removing stale storage file '{}' not in storages list", + path.display(), + ); + fs::remove_file(&path)? + } + } + } + Ok(()) +} + /// Performs the common tasks when deserializing a snapshot /// /// Handles reading the snapshot file and version file, @@ -1426,8 +1482,6 @@ pub(crate) fn rebuild_storages_from_snapshot_dir( AccountsDbFields, )> { let bank_snapshot_dir = &snapshot_info.snapshot_dir; - let accounts_hardlinks = bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - let account_run_paths: HashSet<_> = HashSet::from_iter(account_paths); // With fastboot_version >= 2, obsolete accounts are tracked and stored in the snapshot // Even if obsolete accounts are not enabled, the snapshot may still contain obsolete accounts @@ -1445,56 +1499,22 @@ pub(crate) fn rebuild_storages_from_snapshot_dir( )) })?; - let read_dir = fs::read_dir(&accounts_hardlinks).map_err(|err| { - IoError::other(format!( - "failed to read accounts hardlinks dir '{}': {err}", - accounts_hardlinks.display(), - )) - })?; - for dir_entry in read_dir { - let symlink_path = dir_entry?.path(); - // The symlink point to /snapshot/ which contain the account files hardlinks - // The corresponding run path should be /run/ - let account_snapshot_path = fs::read_link(&symlink_path).map_err(|err| { - IoError::other(format!( - "failed to read symlink '{}': {err}", - symlink_path.display(), - )) - })?; - let account_run_path = account_snapshot_path - .parent() - .ok_or_else(|| SnapshotError::InvalidAccountPath(account_snapshot_path.clone()))? - .parent() - .ok_or_else(|| SnapshotError::InvalidAccountPath(account_snapshot_path.clone()))? - .join(ACCOUNTS_RUN_DIR); - if !account_run_paths.contains(&account_run_path) { - // The appendvec from the bank snapshot storage does not match any of the provided account_paths set. - // The account paths have changed so the snapshot is no longer usable. - return Err(SnapshotError::AccountPathsMismatch); - } - // Generate hard-links to make the account files available in the main accounts/, and let the new appendvec - // paths be in accounts/ - let read_dir = fs::read_dir(&account_snapshot_path).map_err(|err| { - IoError::other(format!( - "failed to read account snapshot dir '{}': {err}", - account_snapshot_path.display(), - )) - })?; - for file in read_dir { - let file_path = file?.path(); - let file_name = file_path - .file_name() - .ok_or_else(|| SnapshotError::InvalidAppendVecPath(file_path.to_path_buf()))?; - let dest_path = account_run_path.join(file_name); - fs::hard_link(&file_path, &dest_path).map_err(|err| { - IoError::other(format!( - "failed to hard link from '{}' to '{}': {err}", - file_path.display(), - dest_path.display(), - )) - })?; - } + // The bank snapshot lists the storage files belonging to it. Anything else in the account + // paths is from a later (post-snapshot) slot and must be removed before we load — the + // lt hash check at startup verifies the surviving storages. + let storages_list_path = + bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_STORAGES_LIST_FILENAME); + if !storages_list_path.exists() { + // Legacy (2.0.0) bank snapshot: storages live as hardlinks under + // `/snapshot//`, with symlinks in + // `/accounts_hardlinks/` tying them to the bank snapshot. Move the + // files back into `/run/` and write out the storages list so the load + // path below can read it like any other 3.0+ snapshot. + migrate_legacy_hardlinks(bank_snapshot_dir, account_paths)?; } + let storages_list = + deserialize_storages_list(&storages_list_path, MAX_STORAGES_LIST_FILE_SIZE)?; + prune_stale_storages(account_paths, storages_list)?; let snapshot_file_path = snapshot_info.snapshot_path(); let snapshot_version_path = bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_VERSION_FILENAME); @@ -1504,30 +1524,32 @@ pub(crate) fn rebuild_storages_from_snapshot_dir( account_paths, ); - let SnapshotFieldsBundle { - bank_fields, - accounts_db_fields, - append_vec_files, - .. - } = snapshot_fields_from_files(&file_receiver)?; - - let num_rebuilder_threads = num_cpus::get_physical().saturating_sub(1).max(1); + let snapshot_result = snapshot_fields_from_files(&file_receiver).and_then( + |SnapshotFieldsBundle { + bank_fields, + accounts_db_fields, + append_vec_files, + .. + }| { + let storage = SnapshotStorageRebuilder::rebuild_storages( + append_vec_files.into_iter().chain(file_receiver), + next_append_vec_id, + SnapshotFrom::Dir, + obsolete_accounts, + )?; + Ok((storage, bank_fields, accounts_db_fields)) + }, + ); - let snapshot_storage_lengths = - accounts_db_fields.get_storage_lengths_for_snapshot_slots(None)?; - let storage = SnapshotStorageRebuilder::spawn_rebuilder_threads( - snapshot_storage_lengths, - append_vec_files, - file_receiver, - num_rebuilder_threads, - next_append_vec_id, - SnapshotFrom::Dir, - obsolete_accounts, - )?; - stream_files_handle - .join() - .expect("should join file stream thread")?; - Ok((storage, bank_fields, accounts_db_fields)) + // Producer errors are usually the root cause (no files -> no reception). + let stream_files_result = stream_files_handle.join().expect("must join dir thread"); + match (stream_files_result, snapshot_result) { + // Rebuilder closed the receiver early; the producer's send failure is just the + // downstream symptom — surface the rebuilder's error instead. + (Err(SnapshotError::CrossbeamSend(_)), snap @ Err(_)) => snap, + (Err(err), _) => Err(err), + (Ok(()), snap) => snap, + } } /// Reads the `snapshot_version` from a file. Before opening the file, its size @@ -1683,7 +1705,7 @@ pub fn verify_unpacked_snapshots_dir_and_version( ) -> Result<(SnapshotVersion, BankSnapshotInfo)> { info!( "snapshot version: {}", - &unpacked_snapshots_dir_and_version.snapshot_version + unpacked_snapshots_dir_and_version.snapshot_version ); let snapshot_version = unpacked_snapshots_dir_and_version.snapshot_version; @@ -1717,6 +1739,23 @@ pub enum VerifyBank { NonDeterministic, } +/// For each account run dir, wipes the sibling `snapshot/` dir. +/// +/// Validator account paths are laid out as a parent containing two siblings: `run/` (the live +/// storage files) and `snapshot/` (legacy per-slot hardlink dirs, pre-3.0). `account_run_paths` +/// here holds the `run/` paths, so for each entry we walk up to the parent and wipe the +/// `snapshot/` sibling. Nothing new is written under `snapshot/` anymore, so any content is +/// either legacy hardlink dirs (written by pre-3.0 validators during fastboot) or orphans +/// from purged bank snapshots. Used by the legacy-hardlink migration path and the +/// archive-load path to drop that leftover state. +pub fn wipe_account_snapshot_dirs(account_run_paths: &[PathBuf]) { + for account_run_path in account_run_paths { + if let Some(parent) = account_run_path.parent() { + move_and_async_delete_path_contents(parent.join(ACCOUNTS_SNAPSHOT_DIR)); + } + } +} + /// Purges all bank snapshots pub fn purge_all_bank_snapshots(bank_snapshots_dir: impl AsRef) { let bank_snapshots = get_bank_snapshots(&bank_snapshots_dir); @@ -1773,12 +1812,14 @@ fn purge_bank_snapshots<'a>(bank_snapshots: impl IntoIterator) -> Result<()> { const FN_ERR: &str = "failed to purge bank snapshot"; + // Migration: snapshots written by pre-storages-list versions kept an `accounts_hardlinks/` + // subdir of symlinks pointing at hardlink dirs under `/snapshot//`. + // Follow them so the hardlink dirs don't outlive the owning bank snapshot when we purge at + // runtime (startup-time cleanup catches any leftovers). let accounts_hardlinks_dir = bank_snapshot_dir .as_ref() .join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); if accounts_hardlinks_dir.is_dir() { - // This directory contain symlinks to all accounts snapshot directories. - // They should all be removed. let read_dir = fs::read_dir(&accounts_hardlinks_dir).map_err(|err| { IoError::other(format!( "{FN_ERR}: failed to read accounts hardlinks dir '{}': {err}", @@ -1848,7 +1889,7 @@ mod tests { }, assert_matches::assert_matches, bincode::{deserialize_from, serialize_into}, - solana_accounts_db::accounts_file::AccountsFileProvider, + solana_accounts_db::accounts_file::{AccountsFile, AccountsFileProvider}, solana_hash::Hash, std::{convert::TryFrom, mem::size_of}, tempfile::NamedTempFile, @@ -2621,49 +2662,6 @@ mod tests { assert!(remaining_incremental_snapshot_archives.is_empty()); } - #[test] - fn test_get_snapshot_accounts_hardlink_dir() { - let slot: Slot = 1; - - let mut account_paths_set: HashSet = HashSet::new(); - - let bank_snapshots_dir_tmp = tempfile::TempDir::new().unwrap(); - let bank_snapshot_dir = bank_snapshots_dir_tmp.path().join(slot.to_string()); - let accounts_hardlinks_dir = - bank_snapshot_dir.join(snapshot_paths::SNAPSHOT_ACCOUNTS_HARDLINKS); - fs::create_dir_all(&accounts_hardlinks_dir).unwrap(); - - let (_tmp_dir, accounts_dir) = create_tmp_accounts_dir_for_tests(); - let appendvec_filename = format!("{slot}.0"); - let appendvec_path = accounts_dir.join(appendvec_filename); - - let ret = get_snapshot_accounts_hardlink_dir( - &appendvec_path, - slot, - &mut account_paths_set, - &accounts_hardlinks_dir, - ); - assert!(ret.is_ok()); - - let wrong_appendvec_path = appendvec_path - .parent() - .unwrap() - .parent() - .unwrap() - .join(appendvec_path.file_name().unwrap()); - let ret = get_snapshot_accounts_hardlink_dir( - &wrong_appendvec_path, - slot, - &mut account_paths_set, - accounts_hardlinks_dir, - ); - - assert_matches!( - ret, - Err(GetSnapshotAccountsHardLinkDirError::GetAccountPath(_)) - ); - } - #[test] fn test_get_snapshot_file_kind() { assert_eq!(None, get_snapshot_file_kind("file.txt")); @@ -2712,14 +2710,14 @@ mod tests { .unwrap(); // Deserialize - let deserialized_accounts = + let mut deserialized_accounts = deserialize_obsolete_accounts(bank_snapshot_dir, MAX_OBSOLETE_ACCOUNTS_FILE_SIZE) .unwrap() - .into_dashmap(); + .into_hashmap(); // Verify for storage in &snapshot_storages { - let obsolete_accounts = deserialized_accounts.remove(&storage.slot()).unwrap().1; + let obsolete_accounts = deserialized_accounts.remove(&storage.slot()).unwrap(); assert!(obsolete_accounts.into_tuple().2 == 0); } } @@ -2858,4 +2856,34 @@ mod tests { // scenario 9: all good assert!(is_bank_snapshot_complete(bank_snapshot_dir)); } + + #[test] + fn test_prune_stale_storages() { + let account_path = tempfile::TempDir::new().unwrap(); + // Files that belong to the snapshot. + let keep_a = account_path.path().join(AccountsFile::file_name(100, 1)); + let keep_b = account_path.path().join(AccountsFile::file_name(200, 2)); + // A stale storage file that should be removed. + let stale = account_path.path().join(AccountsFile::file_name(300, 3)); + // A non-storage filename — should be left alone. + let untouched = account_path.path().join("something_else.txt"); + for path in [&keep_a, &keep_b, &stale, &untouched] { + fs::write(path, b"x").unwrap(); + } + + let storages_list = StoragesList::from_items(vec![ + StorageListItem { slot: 100, id: 1 }, + StorageListItem { slot: 200, id: 2 }, + ]); + prune_stale_storages( + std::slice::from_ref(&account_path.path().to_path_buf()), + storages_list, + ) + .unwrap(); + + assert!(keep_a.exists(), "expected storage file was deleted"); + assert!(keep_b.exists(), "expected storage file was deleted"); + assert!(!stale.exists(), "stale storage file was not removed"); + assert!(untouched.exists(), "non-storage file was wrongly removed"); + } } diff --git a/runtime/src/snapshot_utils/snapshot_storage_rebuilder.rs b/runtime/src/snapshot_utils/snapshot_storage_rebuilder.rs index 4e21cf4f88b..7b4ead28357 100644 --- a/runtime/src/snapshot_utils/snapshot_storage_rebuilder.rs +++ b/runtime/src/snapshot_utils/snapshot_storage_rebuilder.rs @@ -7,13 +7,7 @@ use { remap_and_reconstruct_single_storage, }, agave_fs::FileInfo, - crossbeam_channel::{Receiver, Sender, select, unbounded}, - dashmap::DashMap, log::*, - rayon::{ - ThreadPool, ThreadPoolBuilder, - iter::{IntoParallelIterator, ParallelIterator}, - }, solana_accounts_db::{ account_storage::AccountStorageMap, accounts_db::{AccountsFileId, AtomicAccountsFileId}, @@ -23,139 +17,71 @@ use { collections::HashMap, path::PathBuf, str::FromStr as _, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, - time::Instant, + sync::{Arc, atomic::Ordering}, + time::{Duration, Instant}, }, }; +const PROGRESS_LOG_INTERVAL: Duration = Duration::from_secs(2); + /// Stores state for rebuilding snapshot storages #[derive(Debug)] pub(crate) struct SnapshotStorageRebuilder { - /// Receiver for unpacked snapshot storage files - file_receiver: Receiver, - /// Number of threads to rebuild with - num_threads: usize, - /// Snapshot storage lengths - from the snapshot file - snapshot_storage_lengths: HashMap, /// Container for storing rebuilt snapshot storages storage: AccountStorageMap, /// Tracks next append_vec_id next_append_vec_id: Arc, - /// Tracker for number of processed slots - processed_slot_count: AtomicUsize, /// Tracks the number of collisions in AccountsFileId - num_collisions: AtomicUsize, + num_collisions: usize, + /// Number of storage slots that have been rebuilt so far + processed_slot_count: usize, /// Rebuild from the snapshot files or archives snapshot_from: SnapshotFrom, /// obsolete accounts for all storages - obsolete_accounts: DashMap, + obsolete_accounts: HashMap, } impl SnapshotStorageRebuilder { - /// Create the SnapshotStorageRebuilder for storing state during rebuilding - /// - pre-allocates data for storage file infos - fn new( - file_receiver: Receiver, - num_threads: usize, + /// Rebuild snapshot storages on the current thread by consuming `files`. + pub(crate) fn rebuild_storages( + files: impl IntoIterator, next_append_vec_id: Arc, - snapshot_storage_lengths: HashMap, snapshot_from: SnapshotFrom, obsolete_accounts: Option, - ) -> Self { - let storage = AccountStorageMap::with_capacity(snapshot_storage_lengths.len()); - Self { - file_receiver, - num_threads, - snapshot_storage_lengths, - storage, + ) -> Result { + let mut rebuilder = Self { + storage: AccountStorageMap::default(), next_append_vec_id, - processed_slot_count: AtomicUsize::new(0), - num_collisions: AtomicUsize::new(0), + num_collisions: 0, + processed_slot_count: 0, snapshot_from, obsolete_accounts: obsolete_accounts - .map(|map| map.into_dashmap()) + .map(|map| map.into_hashmap()) .unwrap_or_default(), - } - } - - /// Synchronously spawns threads to rebuild snapshot storages returning when they complete - /// - /// Spawn threads for processing buffered append_vec_files, and then received files - pub(crate) fn spawn_rebuilder_threads( - snapshot_storage_lengths: HashMap, - append_vec_files: Vec, - file_receiver: Receiver, - num_threads: usize, - next_append_vec_id: Arc, - snapshot_from: SnapshotFrom, - obsolete_accounts: Option, - ) -> Result { - let rebuilder = Arc::new(SnapshotStorageRebuilder::new( - file_receiver, - num_threads, - next_append_vec_id, - snapshot_storage_lengths, - snapshot_from, - obsolete_accounts, - )); - - let thread_pool = rebuilder.build_thread_pool(); + }; - if snapshot_from == SnapshotFrom::Archive { - // Synchronously process buffered append_vec_files - thread_pool.install(|| rebuilder.process_buffered_files(append_vec_files))?; - } + let mut last_log_time = Instant::now(); - // Asynchronously spawn threads to process received append_vec_files - let (exit_sender, exit_receiver) = unbounded(); - for _ in 0..rebuilder.num_threads { - Self::spawn_receiver_thread(&thread_pool, exit_sender.clone(), rebuilder.clone()); + for file_info in files { + rebuilder.process_append_vec_file(file_info)?; + let now = Instant::now(); + if now.duration_since(last_log_time) >= PROGRESS_LOG_INTERVAL { + rebuilder.log_progress(); + last_log_time = now; + } } - drop(exit_sender); // drop otherwise loop below will never end - // wait for asynchronous threads to complete - rebuilder.wait_for_completion(exit_receiver)?; - Ok(Arc::try_unwrap(rebuilder).unwrap().storage) + Ok(rebuilder.storage) } - /// Processes buffered append_vec_files - fn process_buffered_files(&self, append_vec_files: Vec) -> Result<(), SnapshotError> { - append_vec_files - .into_par_iter() - .map(|file_info| self.process_append_vec_file(file_info)) - .collect::>() + fn log_progress(&self) { + info!( + "rebuilt storages for {} slots with {} collisions", + self.processed_slot_count, self.num_collisions, + ); } - /// Spawn a single thread to process received append_vec_files - fn spawn_receiver_thread( - thread_pool: &ThreadPool, - exit_sender: Sender>, - rebuilder: Arc, - ) { - thread_pool.spawn(move || { - for file_info in rebuilder.file_receiver.iter() { - match rebuilder.process_append_vec_file(file_info) { - Ok(_) => {} - Err(err) => { - exit_sender - .send(Err(err)) - .expect("sender should be connected"); - return; - } - } - } - - exit_sender - .send(Ok(())) - .expect("sender should be connected"); - }) - } - - /// Process an append_vec_file - fn process_append_vec_file(&self, file_info: FileInfo) -> Result<(), SnapshotError> { + fn process_append_vec_file(&mut self, file_info: FileInfo) -> Result<(), SnapshotError> { let filename = file_info.path.file_name().unwrap().to_str().unwrap(); if let Ok((slot, append_vec_id)) = get_slot_and_append_vec_id(filename) { if self.snapshot_from == SnapshotFrom::Dir { @@ -167,38 +93,35 @@ impl SnapshotStorageRebuilder { .fetch_max((append_vec_id + 1) as AccountsFileId, Ordering::Relaxed); } self.process_complete_slot(slot, file_info)?; - self.processed_slot_count.fetch_add(1, Ordering::AcqRel); + self.processed_slot_count += 1; } Ok(()) } /// Process a slot that has received all storage entries - fn process_complete_slot(&self, slot: Slot, file_info: FileInfo) -> Result<(), SnapshotError> { + fn process_complete_slot( + &mut self, + slot: Slot, + file_info: FileInfo, + ) -> Result<(), SnapshotError> { let filename = file_info.path.file_name().unwrap().to_str().unwrap(); let (_, old_append_vec_id) = get_slot_and_append_vec_id(filename)?; - let Some(¤t_len) = self.snapshot_storage_lengths.get(&slot) else { - return Err(SnapshotError::RebuildStorages(format!( - "account storage file '{filename}' for slot outside of expected range in snapshot" - ))); - }; let storage_entry = match &self.snapshot_from { SnapshotFrom::Archive => remap_and_reconstruct_single_storage( slot, old_append_vec_id, - current_len, file_info, &self.next_append_vec_id, - &self.num_collisions, + &mut self.num_collisions, )?, SnapshotFrom::Dir => reconstruct_single_storage( &slot, file_info, - current_len, old_append_vec_id as AccountsFileId, self.obsolete_accounts .remove(&slot) - .map(|(_, accounts)| accounts.into_tuple()), + .map(|accounts| accounts.into_tuple()), )?, }; @@ -213,48 +136,6 @@ impl SnapshotStorageRebuilder { Ok(()) } } - - /// Wait for the completion of the rebuilding threads - fn wait_for_completion( - &self, - exit_receiver: Receiver>, - ) -> Result<(), SnapshotError> { - let num_slots = self.snapshot_storage_lengths.len(); - let mut last_log_time = Instant::now(); - loop { - select! { - recv(exit_receiver) -> maybe_exit_signal => { - match maybe_exit_signal { - Ok(Ok(_)) => continue, // thread exited successfully - Ok(Err(err)) => { // thread exited with error - return Err(err); - } - Err(_) => break, // all threads have exited - channel disconnected - } - } - default(std::time::Duration::from_millis(100)) => { - let now = Instant::now(); - if now.duration_since(last_log_time).as_millis() >= 2000 { - let num_processed_slots = self.processed_slot_count.load(Ordering::Relaxed); - let num_collisions = self.num_collisions.load(Ordering::Relaxed); - info!("rebuilt storages for {num_processed_slots}/{num_slots} slots with {num_collisions} collisions"); - last_log_time = now; - } - } - } - } - - Ok(()) - } - - /// Builds thread pool to rebuild with - fn build_thread_pool(&self) -> ThreadPool { - ThreadPoolBuilder::default() - .thread_name(|i| format!("solRbuildSnap{i:02}")) - .num_threads(self.num_threads) - .build() - .expect("new rayon threadpool") - } } /// Get the slot and append vec id from the filename diff --git a/runtime/src/stake_account.rs b/runtime/src/stake_account.rs index 41692e29589..c625b05cd0e 100644 --- a/runtime/src/stake_account.rs +++ b/runtime/src/stake_account.rs @@ -1,7 +1,5 @@ #[cfg(feature = "dev-context-only-utils")] use qualifier_attr::qualifiers; -#[cfg(feature = "frozen-abi")] -use solana_frozen_abi::abi_example::AbiExample; use { solana_account::{AccountSharedData, ReadableAccount, state_traits::StateMut}, solana_instruction::error::InstructionError, @@ -13,18 +11,43 @@ use { std::marker::PhantomData, thiserror::Error, }; +#[cfg(feature = "frozen-abi")] +use { + solana_frozen_abi::{abi_example::AbiExample, stable_abi::StableAbi}, + solana_stake_interface::{stake_flags::StakeFlags, state::Meta}, +}; /// An account and a stake state deserialized from the account. /// Generic type T enforces type-safety so that StakeAccount can /// only wrap a stake-state which is a Delegation; whereas StakeAccount<()> /// wraps any account with stake state. +#[cfg_attr(feature = "frozen-abi", derive(StableAbi, StableAbiSample))] #[derive(Clone, Debug, Default)] pub struct StakeAccount { + // Skipped by the custom (delegation/stake-format) serializer; sample the default. + #[cfg_attr(feature = "frozen-abi", stable_abi_sample(with = "Default::default()"))] account: AccountSharedData, + #[cfg_attr( + feature = "frozen-abi", + stable_abi_sample(with = "sample_delegated_stake_state(rng)") + )] stake_state: StakeStateV2, _phantom: PhantomData, } +/// Samples a random `StakeStateV2::Stake`; the delegation-format serializer unwraps +/// `delegation_ref()`, which would panic on any other variant. +#[cfg(feature = "frozen-abi")] +fn sample_delegated_stake_state( + rng: &mut (impl solana_frozen_abi::rand::RngCore + ?Sized), +) -> StakeStateV2 { + StakeStateV2::Stake( + Meta::random(rng), + Stake::random(rng), + StakeFlags::random(rng), + ) +} + #[derive(Debug, Error)] pub enum Error { #[error(transparent)] @@ -108,13 +131,7 @@ impl PartialEq> for StakeAccount { #[cfg(feature = "frozen-abi")] impl AbiExample for StakeAccount { fn example() -> Self { - use { - solana_account::Account, - solana_stake_interface::{ - stake_flags::StakeFlags, - state::{Meta, Stake}, - }, - }; + use solana_account::Account; let stake_state = StakeStateV2::Stake(Meta::example(), Stake::example(), StakeFlags::example()); let mut account = Account::example(); diff --git a/runtime/src/stake_delegation.rs b/runtime/src/stake_delegation.rs new file mode 100644 index 00000000000..a9ed035e677 --- /dev/null +++ b/runtime/src/stake_delegation.rs @@ -0,0 +1,57 @@ +//! Dispatch helpers for stake delegation warmup/cooldown math. + +use { + solana_clock::Epoch, + solana_stake_interface::{ + stake_history::StakeHistoryGetEntry, + state::{Delegation, Stake, StakeActivationStatus}, + }, +}; + +#[inline] +pub(crate) fn delegation_effective_stake( + delegation: &Delegation, + epoch: Epoch, + history: &T, + new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, +) -> u64 { + if use_fixed_point_stake_math { + delegation.stake_v2(epoch, history, new_rate_activation_epoch) + } else { + #[allow(deprecated)] + delegation.stake(epoch, history, new_rate_activation_epoch) + } +} + +#[inline] +pub(crate) fn delegation_activation_status( + delegation: &Delegation, + epoch: Epoch, + history: &T, + new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, +) -> StakeActivationStatus { + if use_fixed_point_stake_math { + delegation.stake_activating_and_deactivating_v2(epoch, history, new_rate_activation_epoch) + } else { + #[allow(deprecated)] + delegation.stake_activating_and_deactivating(epoch, history, new_rate_activation_epoch) + } +} + +#[inline] +pub(crate) fn effective_stake( + stake: &Stake, + epoch: Epoch, + history: &T, + new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, +) -> u64 { + if use_fixed_point_stake_math { + stake.stake_v2(epoch, history, new_rate_activation_epoch) + } else { + #[allow(deprecated)] + stake.stake(epoch, history, new_rate_activation_epoch) + } +} diff --git a/runtime/src/stake_history.rs b/runtime/src/stake_history.rs index e56b91a2638..b26ef4f558f 100644 --- a/runtime/src/stake_history.rs +++ b/runtime/src/stake_history.rs @@ -12,7 +12,7 @@ use { }; /// The SDK's stake history with clone-on-write semantics -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Default, Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct StakeHistory(Arc); diff --git a/runtime/src/stake_weighted_timestamp.rs b/runtime/src/stake_weighted_timestamp.rs index 24fd608c468..62fef9ebf86 100644 --- a/runtime/src/stake_weighted_timestamp.rs +++ b/runtime/src/stake_weighted_timestamp.rs @@ -27,7 +27,7 @@ pub(crate) fn calculate_stake_weighted_timestamp( unique_timestamps: I, stakes: &HashMap, slot: Slot, - slot_duration: Duration, + elapsed_slot_duration: impl Fn(Slot, Slot) -> Duration, epoch_start_timestamp: Option<(Slot, UnixTimestamp)>, max_allowable_drift: MaxAllowableDrift, ) -> Option @@ -40,7 +40,7 @@ where let mut total_stake: u128 = 0; for (vote_pubkey, slot_timestamp) in unique_timestamps { let (timestamp_slot, timestamp) = slot_timestamp.borrow(); - let offset = slot_duration.saturating_mul(slot.saturating_sub(*timestamp_slot) as u32); + let offset = elapsed_slot_duration(*timestamp_slot, slot); let estimate = timestamp.saturating_add(offset.as_secs() as i64); let stake = stakes .get(vote_pubkey.borrow()) @@ -67,8 +67,7 @@ where } // Bound estimate by `max_allowable_drift` since the start of the epoch if let Some((epoch_start_slot, epoch_start_timestamp)) = epoch_start_timestamp { - let poh_estimate_offset = - slot_duration.saturating_mul(slot.saturating_sub(epoch_start_slot) as u32); + let poh_estimate_offset = elapsed_slot_duration(epoch_start_slot, slot); let estimate_offset = Duration::from_secs((estimate as u64).saturating_sub(epoch_start_timestamp as u64)); let max_allowable_drift_fast = @@ -103,6 +102,12 @@ pub mod tests { solana_native_token::LAMPORTS_PER_SOL, }; + fn constant_elapsed_slot_duration(slot_duration: Duration) -> impl Fn(Slot, Slot) -> Duration { + move |from_slot, to_slot| { + slot_duration.saturating_mul(to_slot.saturating_sub(from_slot) as u32) + } + } + #[test] fn test_calculate_stake_weighted_timestamp_uses_median() { let recent_timestamp: UnixTimestamp = 1_578_909_061; @@ -166,7 +171,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), None, max_allowable_drift, ) @@ -189,7 +194,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), None, max_allowable_drift, ) @@ -212,7 +217,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), None, max_allowable_drift, ) @@ -260,7 +265,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), None, max_allowable_drift, ) @@ -297,7 +302,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), None, max_allowable_drift, ) @@ -363,7 +368,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -384,7 +389,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -405,7 +410,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -425,7 +430,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -509,7 +514,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), allowable_drift_25, ) @@ -520,7 +525,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), allowable_drift_50, ) @@ -550,7 +555,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), allowable_drift_25, ) @@ -561,7 +566,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), allowable_drift_50, ) @@ -641,7 +646,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -671,7 +676,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -701,7 +706,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) @@ -767,7 +772,7 @@ pub mod tests { &unique_timestamps, &stakes, slot as Slot, - slot_duration, + constant_elapsed_slot_duration(slot_duration), Some((0, epoch_start_timestamp)), max_allowable_drift, ) diff --git a/runtime/src/stakes.rs b/runtime/src/stakes.rs index cdd05a9bfbf..a347e2a6ea5 100644 --- a/runtime/src/stakes.rs +++ b/runtime/src/stakes.rs @@ -1,7 +1,14 @@ //! Stakes serve as a cache of stake and vote accounts to derive //! node stakes +#[cfg(feature = "frozen-abi")] +use solana_frozen_abi::stable_abi::{context::SequenceLenMax, sample_collection_sized}; use { - crate::{stake_account, stake_history::StakeHistory}, + crate::{ + alpenglow_epoch_type::RewardEpochDelegatedStakes, + stake_account, + stake_delegation::{delegation_activation_status, delegation_effective_stake}, + stake_history::StakeHistory, + }, imbl::HashMap as ImblHashMap, log::error, num_derive::ToPrimitive, @@ -20,7 +27,6 @@ use { solana_vote_interface::state::VoteStateVersions, std::{ collections::HashMap, - ops::Add, sync::{Arc, RwLock, RwLockReadGuard}, }, thiserror::Error, @@ -62,6 +68,7 @@ pub enum InvalidCacheEntryReason { } type StakeAccount = stake_account::StakeAccount; +pub(crate) type DelegatedStakes = ImblHashMap; #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] #[derive(Default, Debug)] @@ -81,6 +88,7 @@ impl StakesCache { pubkey: &Pubkey, account: &impl ReadableAccount, new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, ) { // TODO: If the account is already cached as a vote or stake account // but the owner changes, then this needs to evict the account from @@ -97,7 +105,11 @@ impl StakesCache { }; } else if stake_program::check_id(owner) { let mut stakes = self.0.write().unwrap(); - stakes.remove_stake_delegation(pubkey, new_rate_activation_epoch); + stakes.remove_stake_delegation( + pubkey, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); } return; } @@ -109,11 +121,7 @@ impl StakesCache { // drop the old account after releasing the lock let _old_vote_account = { let mut stakes = self.0.write().unwrap(); - stakes.upsert_vote_account( - pubkey, - vote_account, - new_rate_activation_epoch, - ) + stakes.upsert_vote_account(pubkey, vote_account) }; } Err(_) => { @@ -139,11 +147,16 @@ impl StakesCache { *pubkey, stake_account, new_rate_activation_epoch, + use_fixed_point_stake_math, ); } Err(_) => { let mut stakes = self.0.write().unwrap(); - stakes.remove_stake_delegation(pubkey, new_rate_activation_epoch); + stakes.remove_stake_delegation( + pubkey, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); } } } @@ -154,9 +167,19 @@ impl StakesCache { next_epoch: Epoch, stake_history: StakeHistory, vote_accounts: VoteAccounts, + delegated_stakes: DelegatedStakes, + ) { + let mut stakes = self.0.write().unwrap(); + stakes.activate_epoch(next_epoch, stake_history, vote_accounts, delegated_stakes) + } + + pub(crate) fn refresh_delegated_stakes( + &self, + new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, ) { let mut stakes = self.0.write().unwrap(); - stakes.activate_epoch(next_epoch, stake_history, vote_accounts) + stakes.refresh_delegated_stakes(new_rate_activation_epoch, use_fixed_point_stake_math); } } @@ -167,13 +190,14 @@ impl StakesCache { /// account and StakeStateV2 deserialized from the account. Doing so, will remove /// the need to load the stake account from accounts-db when working with /// stake-delegations. -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Default, Clone, PartialEq, Debug, Serialize)] #[cfg_attr( feature = "dev-context-only-utils", field_qualifiers( vote_accounts(pub), stake_delegations(pub), + delegated_stakes(pub), unused(pub), epoch(pub), stake_history(pub), @@ -184,8 +208,17 @@ pub struct Stakes { vote_accounts: VoteAccounts, /// stake_delegations + #[cfg_attr( + feature = "frozen-abi", + stable_abi_sample(with = "sample_collection_sized(rng, SequenceLenMax(1))") + )] stake_delegations: ImblHashMap, + /// current effective stake delegated to each vote account pubkey + #[cfg_attr(feature = "frozen-abi", stable_abi_sample(with = "Default::default()"))] + #[serde(skip)] + delegated_stakes: DelegatedStakes, + /// unused unused: u64, @@ -202,6 +235,7 @@ impl Stakes { vote_accounts, epoch, stake_delegations: ImblHashMap::new(), + delegated_stakes: DelegatedStakes::default(), unused: 0, stake_history: StakeHistory::default(), } @@ -232,6 +266,7 @@ impl Stakes { let Self { vote_accounts, stake_delegations: _, + delegated_stakes: _, unused: _, epoch, stake_history, @@ -244,10 +279,11 @@ impl Stakes { pub(crate) fn new_from_accounts_for_genesis<'a, T: ReadableAccount + 'a>( new_rate_activation_epoch: Option, accounts: impl IntoIterator, + use_fixed_point_stake_math: bool, ) -> Self { let stake_history = StakeHistory::default(); let mut vote_accounts = VoteAccountsHashMap::default(); - let mut delegated_stakes: HashMap = HashMap::default(); + let mut delegated_stakes = DelegatedStakes::default(); let mut stake_delegations = ImblHashMap::new(); let epoch = 0; @@ -257,33 +293,40 @@ impl Stakes { } if solana_vote_program::check_id(account.owner()) { - if VoteStateVersions::is_correct_size_and_initialized(account.data()) { - if let Ok(vote_account) = + if VoteStateVersions::is_correct_size_and_initialized(account.data()) + && let Ok(vote_account) = VoteAccount::try_from(create_account_shared_data(account)) - { - vote_accounts.insert(*pubkey, (0, vote_account)); - } + { + vote_accounts.insert(*pubkey, (0, vote_account)); } - } else if stake_program::check_id(account.owner()) { - if let Ok(stake_account) = + } else if stake_program::check_id(account.owner()) + && let Ok(stake_account) = StakeAccount::try_from(create_account_shared_data(account)) - { - let delegation = stake_account.delegation(); - let stake = delegation.stake(epoch, &stake_history, new_rate_activation_epoch); + { + let delegation = stake_account.delegation(); + let stake = delegation_effective_stake( + delegation, + epoch, + &stake_history, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); + if stake != 0 { *delegated_stakes.entry(delegation.voter_pubkey).or_default() += stake; - stake_delegations.insert(*pubkey, stake_account); } + stake_delegations.insert(*pubkey, stake_account); } } let mut vote_accounts = VoteAccounts::from(Arc::new(vote_accounts)); - for (vote_pubkey, stake) in delegated_stakes { - vote_accounts.add_stake(&vote_pubkey, stake); + for (vote_pubkey, stake) in &delegated_stakes { + vote_accounts.add_stake(vote_pubkey, *stake); } Self { vote_accounts, stake_delegations, + delegated_stakes, unused: 0, epoch, stake_history, @@ -316,15 +359,13 @@ impl Stakes { // Assert that all valid vote-accounts referenced in stake delegations are already // contained in `stakes.vote_account`. let voter_pubkey = &delegation.voter_pubkey; - if stakes.vote_accounts.get(voter_pubkey).is_none() { - if let Some(account) = get_account(voter_pubkey) { - if VoteStateVersions::is_correct_size_and_initialized(account.data()) - && VoteAccount::try_from(account.clone()).is_ok() - { - error!("vote account not cached: {voter_pubkey}, {account:?}"); - return Err(Error::VoteAccountNotCached(*voter_pubkey)); - } - } + if stakes.vote_accounts.get(voter_pubkey).is_none() + && let Some(account) = get_account(voter_pubkey) + && VoteStateVersions::is_correct_size_and_initialized(account.data()) + && VoteAccount::try_from(account.clone()).is_ok() + { + error!("vote account not cached: {voter_pubkey}, {account:?}"); + return Err(Error::VoteAccountNotCached(*voter_pubkey)); } let stake_account = StakeAccount::try_from(stake_account)?; @@ -357,6 +398,7 @@ impl Stakes { Ok(Self { vote_accounts: stakes.vote_accounts.clone(), stake_delegations, + delegated_stakes: DelegatedStakes::default(), unused: stakes.unused, epoch: stakes.epoch, stake_history: stakes.stake_history, @@ -369,12 +411,16 @@ impl Stakes { vote_accounts: VoteAccounts, stake_delegations: ImblHashMap, ) -> Self { + let stake_history = StakeHistory::default(); + let delegated_stakes = + Self::calculate_delegated_stakes(&stake_delegations, epoch, &stake_history, None, true); Self { vote_accounts, stake_delegations, + delegated_stakes, unused: 0, epoch, - stake_history: StakeHistory::default(), + stake_history, } } @@ -388,38 +434,68 @@ impl Stakes { thread_pool: &ThreadPool, new_rate_activation_epoch: Option, stake_delegations: &[(&Pubkey, &StakeAccount)], - ) -> (StakeHistory, VoteAccounts) { + use_fixed_point_stake_math: bool, + ) -> ( + StakeHistory, + VoteAccounts, + DelegatedStakes, + RewardEpochDelegatedStakes, + ) { // Wrap up the prev epoch by adding new stake history entry for the // prev epoch. - let stake_history_entry = thread_pool.install(|| { + let (stake_history_entry, effective_delegated_stakes) = thread_pool.install(|| { stake_delegations .par_iter() .fold( - StakeActivationStatus::default, - |acc, (_stake_pubkey, stake_account)| { + || (StakeActivationStatus::default(), HashMap::default()), + |(acc, mut delegated_stakes), (_stake_pubkey, stake_account)| { let delegation = stake_account.delegation(); - acc + delegation.stake_activating_and_deactivating( + let activation_status = delegation_activation_status( + delegation, self.epoch, &self.stake_history, new_rate_activation_epoch, + use_fixed_point_stake_math, + ); + *delegated_stakes.entry(delegation.voter_pubkey).or_default() += + activation_status.effective; + (acc + activation_status, delegated_stakes) + }, + ) + .reduce( + || (StakeActivationStatus::default(), HashMap::default()), + |(activation_status_a, delegated_stakes_a), + (activation_status_b, delegated_stakes_b)| { + ( + activation_status_a + activation_status_b, + merge_delegated_stakes(delegated_stakes_a, delegated_stakes_b), ) }, ) - .reduce(StakeActivationStatus::default, Add::add) }); let mut stake_history = self.stake_history.clone(); stake_history.add(self.epoch, stake_history_entry); // Refresh the stake distribution of vote accounts for the next epoch, // using new stake history. - let vote_accounts = refresh_vote_accounts( + let (vote_accounts, delegated_stakes) = refresh_vote_accounts( thread_pool, next_epoch, &self.vote_accounts, stake_delegations, &stake_history, new_rate_activation_epoch, + use_fixed_point_stake_math, ); - (stake_history, vote_accounts) + let reward_epoch_delegated_stakes = RewardEpochDelegatedStakes { + epoch: self.epoch, + delegated_stakes: effective_delegated_stakes, + }; + ( + stake_history, + vote_accounts, + delegated_stakes, + reward_epoch_delegated_stakes, + ) } pub(crate) fn activate_epoch( @@ -427,26 +503,73 @@ impl Stakes { next_epoch: Epoch, stake_history: StakeHistory, vote_accounts: VoteAccounts, + delegated_stakes: DelegatedStakes, ) { self.epoch = next_epoch; self.stake_history = stake_history; self.vote_accounts = vote_accounts; + self.delegated_stakes = delegated_stakes; } - /// Sum the stakes that point to the given voter_pubkey - fn calculate_stake( + fn calculate_delegated_stakes( stake_delegations: &ImblHashMap, - voter_pubkey: &Pubkey, epoch: Epoch, stake_history: &StakeHistory, new_rate_activation_epoch: Option, - ) -> u64 { - stake_delegations - .values() - .map(StakeAccount::delegation) - .filter(|delegation| &delegation.voter_pubkey == voter_pubkey) - .map(|delegation| delegation.stake(epoch, stake_history, new_rate_activation_epoch)) - .sum() + use_fixed_point_stake_math: bool, + ) -> DelegatedStakes { + let mut delegated_stakes = DelegatedStakes::new(); + for stake_account in stake_delegations.values() { + let delegation = stake_account.delegation(); + let stake = delegation_effective_stake( + delegation, + epoch, + stake_history, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); + if stake != 0 { + *delegated_stakes.entry(delegation.voter_pubkey).or_default() += stake; + } + } + delegated_stakes + } + + fn refresh_delegated_stakes( + &mut self, + new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, + ) { + self.delegated_stakes = Self::calculate_delegated_stakes( + &self.stake_delegations, + self.epoch, + &self.stake_history, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); + } + + fn add_delegated_stake(&mut self, voter_pubkey: Pubkey, stake: u64) { + if stake == 0 { + return; + } + *self.delegated_stakes.entry(voter_pubkey).or_default() += stake; + } + + fn sub_delegated_stake(&mut self, voter_pubkey: &Pubkey, stake: u64) { + if stake == 0 { + return; + } + let current_stake = self + .delegated_stakes + .get_mut(voter_pubkey) + .expect("subtraction from missing delegated stake"); + *current_stake = current_stake + .checked_sub(stake) + .expect("subtraction value exceeds delegated stake"); + if *current_stake == 0 { + self.delegated_stakes.remove(voter_pubkey); + } } fn remove_vote_account(&mut self, vote_pubkey: &Pubkey) -> Option { @@ -457,14 +580,18 @@ impl Stakes { &mut self, stake_pubkey: &Pubkey, new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, ) { if let Some(stake_account) = self.stake_delegations.remove(stake_pubkey) { let removed_delegation = stake_account.delegation(); - let removed_stake = removed_delegation.stake( + let removed_stake = delegation_effective_stake( + removed_delegation, self.epoch, &self.stake_history, new_rate_activation_epoch, + use_fixed_point_stake_math, ); + self.sub_delegated_stake(&removed_delegation.voter_pubkey, removed_stake); self.vote_accounts .sub_stake(&removed_delegation.voter_pubkey, removed_stake); } @@ -474,20 +601,17 @@ impl Stakes { &mut self, vote_pubkey: &Pubkey, vote_account: VoteAccount, - new_rate_activation_epoch: Option, ) -> Option { debug_assert_ne!(vote_account.lamports(), 0u64); - let stake_delegations = &self.stake_delegations; - self.vote_accounts.insert(*vote_pubkey, vote_account, || { - Self::calculate_stake( - stake_delegations, - vote_pubkey, - self.epoch, - &self.stake_history, - new_rate_activation_epoch, - ) - }) + let calculate_delegated_stake = || { + self.delegated_stakes + .get(vote_pubkey) + .copied() + .unwrap_or_default() + }; + self.vote_accounts + .insert(*vote_pubkey, vote_account, calculate_delegated_stake) } fn upsert_stake_delegation( @@ -495,22 +619,36 @@ impl Stakes { stake_pubkey: Pubkey, stake_account: StakeAccount, new_rate_activation_epoch: Option, + use_fixed_point_stake_math: bool, ) { debug_assert_ne!(stake_account.lamports(), 0u64); let delegation = stake_account.delegation(); let voter_pubkey = delegation.voter_pubkey; - let stake = delegation.stake(self.epoch, &self.stake_history, new_rate_activation_epoch); + let stake = delegation_effective_stake( + delegation, + self.epoch, + &self.stake_history, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); match self.stake_delegations.insert(stake_pubkey, stake_account) { - None => self.vote_accounts.add_stake(&voter_pubkey, stake), + None => { + self.add_delegated_stake(voter_pubkey, stake); + self.vote_accounts.add_stake(&voter_pubkey, stake); + } Some(old_stake_account) => { let old_delegation = old_stake_account.delegation(); let old_voter_pubkey = old_delegation.voter_pubkey; - let old_stake = old_delegation.stake( + let old_stake = delegation_effective_stake( + old_delegation, self.epoch, &self.stake_history, new_rate_activation_epoch, + use_fixed_point_stake_math, ); if voter_pubkey != old_voter_pubkey || stake != old_stake { + self.sub_delegated_stake(&old_voter_pubkey, old_stake); + self.add_delegated_stake(voter_pubkey, stake); self.vote_accounts.sub_stake(&old_voter_pubkey, old_stake); self.vote_accounts.add_stake(&voter_pubkey, stake); } @@ -571,6 +709,7 @@ impl From> for Stakes { Self { vote_accounts: stakes.vote_accounts, stake_delegations, + delegated_stakes: DelegatedStakes::default(), unused: stakes.unused, epoch: stakes.epoch, stake_history: stakes.stake_history, @@ -591,6 +730,7 @@ impl From> for Stakes { Self { vote_accounts: stakes.vote_accounts, stake_delegations, + delegated_stakes: DelegatedStakes::default(), unused: stakes.unused, epoch: stakes.epoch, stake_history: stakes.stake_history, @@ -611,6 +751,7 @@ impl From> for Stakes { Self { vote_accounts: stakes.vote_accounts, stake_delegations, + delegated_stakes: DelegatedStakes::default(), unused: stakes.unused, epoch: stakes.epoch, stake_history: stakes.stake_history, @@ -618,6 +759,19 @@ impl From> for Stakes { } } +fn merge_delegated_stakes( + mut stakes: HashMap, + other: HashMap, +) -> HashMap { + if stakes.len() < other.len() { + return merge_delegated_stakes(other, stakes); + } + for (pubkey, stake) in other { + *stakes.entry(pubkey).or_default() += stake; + } + stakes +} + fn refresh_vote_accounts( thread_pool: &ThreadPool, epoch: Epoch, @@ -625,9 +779,9 @@ fn refresh_vote_accounts( stake_delegations: &[(&Pubkey, &StakeAccount)], stake_history: &StakeHistory, new_rate_activation_epoch: Option, -) -> VoteAccounts { - type StakesHashMap = HashMap; - fn merge(mut stakes: StakesHashMap, other: StakesHashMap) -> StakesHashMap { + use_fixed_point_stake_math: bool, +) -> (VoteAccounts, DelegatedStakes) { + fn merge(mut stakes: DelegatedStakes, other: DelegatedStakes) -> DelegatedStakes { if stakes.len() < other.len() { return merge(other, stakes); } @@ -640,17 +794,25 @@ fn refresh_vote_accounts( stake_delegations .par_iter() .fold( - HashMap::default, + DelegatedStakes::default, |mut delegated_stakes, (_stake_pubkey, stake_account)| { let delegation = stake_account.delegation(); - let entry = delegated_stakes.entry(delegation.voter_pubkey).or_default(); - *entry += delegation.stake(epoch, stake_history, new_rate_activation_epoch); + let stake = delegation_effective_stake( + delegation, + epoch, + stake_history, + new_rate_activation_epoch, + use_fixed_point_stake_math, + ); + if stake != 0 { + *delegated_stakes.entry(delegation.voter_pubkey).or_default() += stake; + } delegated_stakes }, ) - .reduce(HashMap::default, merge) + .reduce(DelegatedStakes::default, merge) }); - vote_accounts + let vote_accounts = vote_accounts .iter() .map(|(&vote_pubkey, vote_account)| { let delegated_stake = delegated_stakes @@ -659,14 +821,15 @@ fn refresh_vote_accounts( .unwrap_or_default(); (vote_pubkey, (delegated_stake, vote_account.clone())) }) - .collect() + .collect(); + (vote_accounts, delegated_stakes) } #[cfg(test)] pub(crate) mod tests { use { super::*, - crate::stake_utils, + crate::{stake_delegation::effective_stake, stake_utils}, rayon::ThreadPoolBuilder, solana_account::WritableAccount, solana_pubkey::Pubkey, @@ -682,6 +845,7 @@ pub(crate) mod tests { Self { vote_accounts: stakes.vote_accounts, stake_delegations: ImblHashMap::from_iter(stakes.stake_delegations), + delegated_stakes: DelegatedStakes::default(), unused: stakes.unused, epoch: stakes.epoch, stake_history: stakes.stake_history, @@ -757,8 +921,8 @@ pub(crate) mod tests { let ((vote_pubkey, vote_account), (stake_pubkey, mut stake_account)) = create_staked_node_accounts(10, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); let stake = stake_account .deserialize_data::() .unwrap() @@ -768,28 +932,32 @@ pub(crate) mod tests { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); + let expected_stake = + effective_stake(&stake, i, &StakeHistory::default(), None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), - stake.stake(i, &StakeHistory::default(), None) + expected_stake ); } stake_account.set_lamports(42); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); + let expected_stake = + effective_stake(&stake, i, &StakeHistory::default(), None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), - stake.stake(i, &StakeHistory::default(), None) + expected_stake ); // stays old stake, because only 10 is activated } // activate more let mut stake_account = create_stake_account(42, &vote_pubkey, &solana_pubkey::new_rand(), &rent); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); let stake = stake_account .deserialize_data::() .unwrap() @@ -799,14 +967,16 @@ pub(crate) mod tests { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); + let expected_stake = + effective_stake(&stake, i, &StakeHistory::default(), None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), - stake.stake(i, &StakeHistory::default(), None) + expected_stake ); // now stake of 42 is activated } stake_account.set_lamports(0); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); @@ -826,14 +996,14 @@ pub(crate) mod tests { let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); let ((vote11_pubkey, vote11_account), (stake11_pubkey, stake11_account)) = create_staked_node_accounts(20, &rent); - stakes_cache.check_and_store(&vote11_pubkey, &vote11_account, None); - stakes_cache.check_and_store(&stake11_pubkey, &stake11_account, None); + stakes_cache.check_and_store(&vote11_pubkey, &vote11_account, None, true); + stakes_cache.check_and_store(&stake11_pubkey, &stake11_account, None, true); let vote11_node_pubkey = VoteStateV4::deserialize(vote11_account.data(), &vote11_pubkey) .unwrap() @@ -860,8 +1030,8 @@ pub(crate) mod tests { let ((vote_pubkey, mut vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); { let stakes = stakes_cache.stakes(); @@ -871,7 +1041,7 @@ pub(crate) mod tests { } vote_account.set_lamports(0); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); { let stakes = stakes_cache.stakes(); @@ -881,7 +1051,7 @@ pub(crate) mod tests { } vote_account.set_lamports(1); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); { let stakes = stakes_cache.stakes(); @@ -895,7 +1065,7 @@ pub(crate) mod tests { let mut pushed = vote_account.data().to_vec(); pushed.push(0); vote_account.set_data(pushed); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); { let stakes = stakes_cache.stakes(); @@ -906,7 +1076,7 @@ pub(crate) mod tests { // Vote account uninitialized vote_account.set_data(vec![0; VoteStateV4::size_of()]); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); { let stakes = stakes_cache.stakes(); @@ -916,7 +1086,7 @@ pub(crate) mod tests { } vote_account.set_data(cache_data); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); { let stakes = stakes_cache.stakes(); @@ -940,11 +1110,11 @@ pub(crate) mod tests { let ((vote_pubkey2, vote_account2), (_stake_pubkey2, stake_account2)) = create_staked_node_accounts(10, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); - stakes_cache.check_and_store(&vote_pubkey2, &vote_account2, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); + stakes_cache.check_and_store(&vote_pubkey2, &vote_account2, None, true); // delegates to vote_pubkey - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); let stake = stake_account .deserialize_data::() @@ -956,16 +1126,18 @@ pub(crate) mod tests { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); assert!(vote_accounts.get(&vote_pubkey).is_some()); + let expected_stake = + effective_stake(&stake, stakes.epoch, &stakes.stake_history, None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), - stake.stake(stakes.epoch, &stakes.stake_history, None) + expected_stake ); assert!(vote_accounts.get(&vote_pubkey2).is_some()); assert_eq!(vote_accounts.get_delegated_stake(&vote_pubkey2), 0); } // delegates to vote_pubkey2 - stakes_cache.check_and_store(&stake_pubkey, &stake_account2, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account2, None, true); { let stakes = stakes_cache.stakes(); @@ -973,9 +1145,11 @@ pub(crate) mod tests { assert!(vote_accounts.get(&vote_pubkey).is_some()); assert_eq!(vote_accounts.get_delegated_stake(&vote_pubkey), 0); assert!(vote_accounts.get(&vote_pubkey2).is_some()); + let expected_stake = + effective_stake(&stake, stakes.epoch, &stakes.stake_history, None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey2), - stake.stake(stakes.epoch, &stakes.stake_history, None) + expected_stake ); } } @@ -993,11 +1167,11 @@ pub(crate) mod tests { let stake_pubkey2 = solana_pubkey::new_rand(); let stake_account2 = create_stake_account(10, &vote_pubkey, &stake_pubkey2, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); // delegates to vote_pubkey - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); - stakes_cache.check_and_store(&stake_pubkey2, &stake_account2, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); + stakes_cache.check_and_store(&stake_pubkey2, &stake_account2, None, true); { let stakes = stakes_cache.stakes(); @@ -1015,36 +1189,55 @@ pub(crate) mod tests { let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); let stake = stake_account .deserialize_data::() .unwrap() .stake() .unwrap(); + let initial_expected_stake = { + let stakes = stakes_cache.stakes(); + effective_stake(&stake, stakes.epoch, &stakes.stake_history, None, true) + }; { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), - stake.stake(stakes.epoch, &stakes.stake_history, None) + initial_expected_stake ); } let thread_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap(); let next_epoch = 3; - let (stake_history, vote_accounts) = { + let (stake_history, vote_accounts, delegated_stakes, effective_delegated_stakes) = { let stakes = stakes_cache.stakes(); let stake_delegations = stakes.stake_delegations_vec(); - stakes.calculate_activated_stake(next_epoch, &thread_pool, None, &stake_delegations) + stakes.calculate_activated_stake( + next_epoch, + &thread_pool, + None, + &stake_delegations, + true, + ) }; - stakes_cache.activate_epoch(next_epoch, stake_history, vote_accounts); + assert_eq!( + effective_delegated_stakes + .delegated_stakes + .get(&vote_pubkey) + .copied(), + Some(initial_expected_stake) + ); + stakes_cache.activate_epoch(next_epoch, stake_history, vote_accounts, delegated_stakes); { let stakes = stakes_cache.stakes(); let vote_accounts = stakes.vote_accounts(); + let expected_stake = + effective_stake(&stake, stakes.epoch, &stakes.stake_history, None, true); assert_eq!( vote_accounts.get_delegated_stake(&vote_pubkey), - stake.stake(stakes.epoch, &stakes.stake_history, None) + expected_stake ); } } @@ -1060,8 +1253,8 @@ pub(crate) mod tests { let ((vote_pubkey, vote_account), (stake_pubkey, stake_account)) = create_staked_node_accounts(10, &rent); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); { let stakes = stakes_cache.stakes(); @@ -1075,6 +1268,7 @@ pub(crate) mod tests { &stake_pubkey, &AccountSharedData::new(1, 0, &stake::program::id()), None, + true, ); { let stakes = stakes_cache.stakes(); diff --git a/runtime/src/stakes/serde_stakes.rs b/runtime/src/stakes/serde_stakes.rs index 65312975ed2..ef7fd052272 100644 --- a/runtime/src/stakes/serde_stakes.rs +++ b/runtime/src/stakes/serde_stakes.rs @@ -92,6 +92,7 @@ impl From> for SerdeStakeAccountsToDelegationFormat { let Stakes { vote_accounts, stake_delegations, + delegated_stakes: _, unused, epoch, stake_history, @@ -112,6 +113,7 @@ impl From> for SerdeStakeAccountsToStakeFormat { let Stakes { vote_accounts, stake_delegations, + delegated_stakes: _, unused, epoch, stake_history, @@ -235,6 +237,7 @@ mod tests { let stake_account_stakes = Stakes { vote_accounts: VoteAccounts::default(), stake_delegations, + delegated_stakes: ImblHashMap::default(), unused: 0, epoch: 0, stake_history: StakeHistory::default(), @@ -288,7 +291,7 @@ mod tests { &node_pubkey, rng.random_range(0..1_000_000), // lamports ); - stakes_cache.check_and_store(&vote_pubkey, &vote_account, None); + stakes_cache.check_and_store(&vote_pubkey, &vote_account, None, true); for _ in 0..rng.random_range(10usize..20) { let stake_pubkey = solana_pubkey::new_rand(); let rent = Rent::free(); @@ -299,7 +302,7 @@ mod tests { &rent, rng.random_range(0..1_000_000), // lamports ); - stakes_cache.check_and_store(&stake_pubkey, &stake_account, None); + stakes_cache.check_and_store(&stake_pubkey, &stake_account, None, true); } } let stakes: Stakes = stakes_cache.stakes().clone(); diff --git a/runtime/src/status_cache.rs b/runtime/src/status_cache.rs index a783fe5cd2b..6f2db82f5f5 100644 --- a/runtime/src/status_cache.rs +++ b/runtime/src/status_cache.rs @@ -1,9 +1,12 @@ #[cfg(feature = "shuttle-test")] use shuttle::sync::{Arc, Mutex}; +#[cfg(not(feature = "shuttle-test"))] +use std::sync::{Arc, Mutex}; use { ahash::{HashMap, HashMapExt as _}, log::*, serde::Serialize, + smallvec::SmallVec, solana_accounts_db::ancestors::Ancestors, solana_clock::{MAX_RECENT_BLOCKHASHES, Slot}, solana_hash::Hash, @@ -12,11 +15,6 @@ use { num::{NonZero, NonZeroUsize}, }, }; -#[cfg(not(feature = "shuttle-test"))] -use { - rand::{Rng, rng}, - std::sync::{Arc, Mutex}, -}; // The maximum number of entries to store in the cache. This is the same as the number of recent // blockhashes because we automatically reject txs that use older blockhashes so we don't need to @@ -175,16 +173,10 @@ impl StatusCache { key: K, ancestors: &Ancestors, ) -> Option<(Slot, T)> { - let keys: Vec<_> = self.cache.keys().copied().collect(); - - for blockhash in keys.iter() { + self.cache.keys().find_map(|blockhash| { trace!("get_status_any_blockhash: trying {blockhash}"); - let status = self.get_status(&key, blockhash, ancestors); - if status.is_some() { - return status; - } - } - None + self.get_status(&key, blockhash, ancestors) + }) } /// Add a known root fork. @@ -196,6 +188,11 @@ impl StatusCache { self.purge_roots(); } + pub fn add_roots>(&mut self, forks: I) { + self.roots.extend(forks); + self.purge_roots(); + } + pub fn roots(&self) -> &HashSet { &self.roots } @@ -220,15 +217,10 @@ impl StatusCache { let max_key_index = key.as_ref().len().saturating_sub(CACHED_KEY_SIZE + 1); // Get the cache entry for this blockhash. - let (max_slot, key_index, hash_map) = - self.cache.entry(*transaction_blockhash).or_insert_with(|| { - // DFS tests need deterministic behavior - #[cfg(feature = "shuttle-test")] - let key_index = 0; - #[cfg(not(feature = "shuttle-test"))] - let key_index = rng().random_range(0..max_key_index + 1); - (slot, key_index, HashMap::new()) - }); + let (max_slot, key_index, hash_map) = self + .cache + .entry(*transaction_blockhash) + .or_insert_with(|| (slot, 0, HashMap::new())); // Update the max slot observed to contain txs using this blockhash. *max_slot = std::cmp::max(slot, *max_slot); @@ -247,12 +239,20 @@ impl StatusCache { } pub fn purge_roots(&mut self) { - while self.roots.len() > self.max_root_entries() { - if let Some(min) = self.roots.iter().min().cloned() { - self.roots.remove(&min); - self.cache.retain(|_, (fork, _, _)| *fork > min); - self.slot_deltas.retain(|slot, _| *slot > min); - } + let max_root_entries = self.max_root_entries(); + if self.roots.len() > max_root_entries { + let num_roots_to_purge = self.roots.len() - max_root_entries; + let mut roots = self + .roots + .iter() + .copied() + .collect::>(); + let (_, cutoff, _) = roots.select_nth_unstable(num_roots_to_purge - 1); + let cutoff = *cutoff; + + self.roots.retain(|root| *root > cutoff); + self.cache.retain(|_, (fork, _, _)| *fork > cutoff); + self.slot_deltas.retain(|slot, _| *slot > cutoff); } } @@ -368,17 +368,17 @@ mod tests { .all(|(hash, (slot, key_index, hash_map))| { if let Some((other_slot, other_key_index, other_hash_map)) = other.cache.get(hash) + && slot == other_slot + && key_index == other_key_index { - if slot == other_slot && key_index == other_key_index { - return hash_map.iter().all(|(slice, fork_map)| { - if let Some(other_fork_map) = other_hash_map.get(slice) { - // all this work just to compare the highest forks in the fork map - // per entry - return fork_map.last() == other_fork_map.last(); - } - false - }); - } + return hash_map.iter().all(|(slice, fork_map)| { + if let Some(other_fork_map) = other_hash_map.get(slice) { + // all this work just to compare the highest forks in the fork map + // per entry + return fork_map.last() == other_fork_map.last(); + } + false + }); } false }) diff --git a/runtime/src/validated_block_finalization.rs b/runtime/src/validated_block_finalization.rs index ca11b582374..30d2080667d 100644 --- a/runtime/src/validated_block_finalization.rs +++ b/runtime/src/validated_block_finalization.rs @@ -7,17 +7,19 @@ use { crate::bank::Bank, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType}, - fraction::Fraction, + certificate::{ + CertSignature, Certificate, CertificateType, FastFinalizeCert, FinalizeCert, NotarCert, + }, + consensus_message::Block, + finalized_slot::FinalizedSlot, + unverified_vote_message::UnverifiedCertificate, }, - log::warn, solana_bls_signatures::BlsError, solana_clock::Slot, - solana_entry::block_component::{FinalCertificate, VotesAggregate}, - solana_hash::Hash, + solana_entry::block_component::{BlockFinalizationCert, VotesAggregate}, solana_pubkey::Pubkey, solana_signer_store::{DecodeError, Decoded, decode}, - std::{collections::HashSet, num::NonZeroU64}, + std::collections::HashSet, thiserror::Error, }; @@ -29,16 +31,8 @@ pub enum BlockFinalizationCertError { BlsError(CertificateType, BlsError), /// Certificate signature verification failed - #[error("Certificate signature for {0:?} verification failed")] - SignatureVerificationFailed(CertificateType), - - /// Insufficient stake for finalization - #[error("Insufficient stake for {cert:?}: got {got}, required {required}")] - InsufficientStake { - cert: CertificateType, - got: Fraction, - required: Fraction, - }, + #[error("Certificate verification for {0:?} failed")] + CertificateVerificationFailed(CertificateType), /// Missing epoch stakes for slot #[error("Missing epoch stakes for {0:?}")] @@ -62,12 +56,12 @@ enum ValidatedBlockFinalizationCertKind { /// Slow finalization: requires both a Finalize cert and a Notarize cert for the same slot Finalize { /// The finalize certificate - finalize_cert: Certificate, + finalize_cert: FinalizeCert, /// The notarize certificate - notarize_cert: Certificate, + notarize_cert: NotarCert, }, /// Fast finalization: a single FastFinalize certificate - FastFinalize(Certificate), + FastFinalize(FastFinalizeCert), } /// A validated proof that a block has been finalized. @@ -92,72 +86,57 @@ impl ValidatedBlockFinalizationCert { /// - Certificate signature verification fails /// - The certificates don't meet the required stake thresholds pub fn try_from_footer( - final_cert: FinalCertificate, + block_final_cert: BlockFinalizationCert, bank: &Bank, + shred_version: u16, ) -> Result { - let (slot, block_id) = (final_cert.slot, final_cert.block_id); + let block = Block { + slot: block_final_cert.slot, + block_id: block_final_cert.block_id, + }; - if let Some(notar_aggregate) = final_cert.notar_aggregate { + if let Some(notar_aggregate) = block_final_cert.notar_aggregate { // Slow finalization - let notarize_cert_type = CertificateType::Notarize(slot, block_id); - let finalize_cert_type = CertificateType::Finalize(slot); + let notarize_cert_type = CertificateType::Notarize(block); + let finalize_cert_type = CertificateType::Finalize(block.slot); - let notarize_cert = Certificate { + let unverified_notar_cert = UnverifiedCertificate { cert_type: notarize_cert_type, signature: notar_aggregate .uncompress_signature() .map_err(|e| BlockFinalizationCertError::BlsError(notarize_cert_type, e))?, bitmap: notar_aggregate.into_bitmap(), + shred_version, }; - let finalize_cert = Certificate { + let unverified_finalize_cert = UnverifiedCertificate { cert_type: finalize_cert_type, - signature: final_cert + signature: block_final_cert .final_aggregate .uncompress_signature() .map_err(|e| BlockFinalizationCertError::BlsError(finalize_cert_type, e))?, - bitmap: final_cert.final_aggregate.into_bitmap(), + bitmap: block_final_cert.final_aggregate.into_bitmap(), + shred_version, }; // Verify both certificates - let (notarize_stake, total_stake) = Self::verify_certificate(bank, ¬arize_cert)?; - let (finalize_stake, _) = Self::verify_certificate(bank, &finalize_cert)?; - - let notarize_percent = - Fraction::new(notarize_stake, NonZeroU64::new(total_stake).unwrap()); - let finalize_percent = - Fraction::new(finalize_stake, NonZeroU64::new(total_stake).unwrap()); - let notarize_threshold = notarize_cert.cert_type.limits_and_vote_types().0; - let finalize_threshold = finalize_cert.cert_type.limits_and_vote_types().0; - - if notarize_percent < notarize_threshold { - warn!( - "Received a slow finalization in the footer for slot {slot} block_id \ - {block_id:?} in bank slot {} with notarize {notarize_percent} stake \ - expecting at least {notarize_threshold}", - bank.slot() - ); - return Err(BlockFinalizationCertError::InsufficientStake { - cert: notarize_cert_type, - got: notarize_percent, - required: notarize_threshold, - }); - } - - if finalize_percent < finalize_threshold { - warn!( - "Received a slow finalization in the footer for slot {slot} block_id \ - {block_id:?} in bank slot {} with finalize {finalize_percent} stake \ - expecting at least {finalize_threshold}", - bank.slot() - ); - return Err(BlockFinalizationCertError::InsufficientStake { - cert: finalize_cert_type, - got: finalize_percent, - required: finalize_threshold, - }); - } + let notar_signature = Self::verify_certificate(bank, unverified_notar_cert)?; + let finalize_signature = Self::verify_certificate(bank, unverified_finalize_cert)?; + + let finalize_signers = + Self::extract_signers(bank, finalize_cert_type, &finalize_signature.bitmap)?; + let mut signers = + Self::extract_signers(bank, notarize_cert_type, ¬ar_signature.bitmap)?; + signers.extend(finalize_signers); + + let notarize_cert = NotarCert { + block, + signature: notar_signature, + }; + let finalize_cert = FinalizeCert { + slot: block.slot, + signature: finalize_signature, + }; - let signers = Self::extract_signers(bank, &finalize_cert)?; Ok(Self { kind: ValidatedBlockFinalizationCertKind::Finalize { finalize_cert, @@ -167,41 +146,23 @@ impl ValidatedBlockFinalizationCert { }) } else { // Fast finalization - let fast_finalize_cert_type = CertificateType::FinalizeFast(slot, block_id); + let fast_finalize_cert_type = CertificateType::FinalizeFast(block); - let fast_finalize_cert = Certificate { + let fast_finalize_cert = UnverifiedCertificate { cert_type: fast_finalize_cert_type, - signature: final_cert + signature: block_final_cert .final_aggregate .uncompress_signature() .map_err(|e| { BlockFinalizationCertError::BlsError(fast_finalize_cert_type, e) })?, - bitmap: final_cert.final_aggregate.into_bitmap(), + bitmap: block_final_cert.final_aggregate.into_bitmap(), + shred_version, }; - let (finalize_stake, total_stake) = - Self::verify_certificate(bank, &fast_finalize_cert)?; - - let finalize_percent = - Fraction::new(finalize_stake, NonZeroU64::new(total_stake).unwrap()); - let finalize_threshold = fast_finalize_cert.cert_type.limits_and_vote_types().0; - - if finalize_percent < finalize_threshold { - warn!( - "Received a fast finalization in the footer for slot {slot} block_id \ - {block_id:?} in bank slot {} with {finalize_percent} stake expecting at \ - least {finalize_threshold}", - bank.slot() - ); - return Err(BlockFinalizationCertError::InsufficientStake { - cert: fast_finalize_cert_type, - got: finalize_percent, - required: finalize_threshold, - }); - } - - let signers = Self::extract_signers(bank, &fast_finalize_cert)?; + let signature = Self::verify_certificate(bank, fast_finalize_cert)?; + let signers = Self::extract_signers(bank, fast_finalize_cert_type, &signature.bitmap)?; + let fast_finalize_cert = FastFinalizeCert { block, signature }; Ok(Self { kind: ValidatedBlockFinalizationCertKind::FastFinalize(fast_finalize_cert), signers, @@ -218,19 +179,25 @@ impl ValidatedBlockFinalizationCert { /// The caller must ensure that the provided certificates have been properly validated. /// Using unvalidated certificates will compromise the type's safety guarantees. pub fn from_validated_slow( - finalize_cert: Certificate, - notarize_cert: Certificate, + finalize_cert: FinalizeCert, + notarize_cert: NotarCert, bank: &Bank, ) -> Self { - debug_assert!(finalize_cert.cert_type.is_slow_finalization()); - debug_assert!(notarize_cert.cert_type.is_notarize()); - debug_assert_eq!( - notarize_cert.cert_type.slot(), - finalize_cert.cert_type.slot() - ); + debug_assert_eq!(notarize_cert.block.slot, finalize_cert.slot); - let signers = Self::extract_signers(bank, &finalize_cert) - .expect("Certificate should have been validated"); + let finalize_signers = Self::extract_signers( + bank, + CertificateType::Finalize(finalize_cert.slot), + &finalize_cert.signature.bitmap, + ) + .expect("Certificate should have been validated"); + let mut signers = Self::extract_signers( + bank, + CertificateType::Notarize(notarize_cert.block), + ¬arize_cert.signature.bitmap, + ) + .expect("Certificate should have been validated"); + signers.extend(finalize_signers); Self { kind: ValidatedBlockFinalizationCertKind::Finalize { finalize_cert, @@ -248,11 +215,10 @@ impl ValidatedBlockFinalizationCert { /// # Safety /// The caller must ensure that the provided certificate has been properly validated. /// Using an unvalidated certificate will compromise the type's safety guarantees. - pub fn from_validated_fast(cert: Certificate, bank: &Bank) -> Self { - debug_assert!(cert.cert_type.is_fast_finalization()); - - let signers = - Self::extract_signers(bank, &cert).expect("Certificate should have been validated"); + pub fn from_validated_fast(cert: FastFinalizeCert, bank: &Bank) -> Self { + let cert_type = CertificateType::FinalizeFast(cert.block); + let signers = Self::extract_signers(bank, cert_type, &cert.signature.bitmap) + .expect("Certificate should have been validated"); Self { kind: ValidatedBlockFinalizationCertKind::FastFinalize(cert), signers, @@ -260,35 +226,28 @@ impl ValidatedBlockFinalizationCert { } /// Returns the slot that is finalized. - pub fn slot(&self) -> Slot { + pub fn slot(&self) -> FinalizedSlot { match &self.kind { ValidatedBlockFinalizationCertKind::Finalize { finalize_cert, notarize_cert, } => { - debug_assert_eq!( - finalize_cert.cert_type.slot(), - notarize_cert.cert_type.slot() - ); - finalize_cert.cert_type.slot() + debug_assert_eq!(finalize_cert.slot, notarize_cert.block.slot); + FinalizedSlot::Slow(finalize_cert.slot) } ValidatedBlockFinalizationCertKind::FastFinalize(certificate) => { - certificate.cert_type.slot() + FinalizedSlot::Fast(certificate.block.slot) } } } /// Returns the block that is finalized (slot, block_id). - pub fn block(&self) -> (Slot, Hash) { + pub fn block(&self) -> Block { match &self.kind { - ValidatedBlockFinalizationCertKind::Finalize { notarize_cert, .. } => notarize_cert - .cert_type - .to_block() - .expect("notarize certificate has block"), - ValidatedBlockFinalizationCertKind::FastFinalize(cert) => cert - .cert_type - .to_block() - .expect("fast finalize certificate has block"), + ValidatedBlockFinalizationCertKind::Finalize { notarize_cert, .. } => { + notarize_cert.block + } + ValidatedBlockFinalizationCertKind::FastFinalize(cert) => cert.block, } } @@ -300,9 +259,42 @@ impl ValidatedBlockFinalizationCert { ) } + /// Returns cloned copies of the certificates that prove this finalization. + /// + /// For slow finalization this returns the finalize certificate followed by the notarize + /// certificate. For fast finalization this returns the single fast-finalize certificate. + pub fn clone_certificates(&self) -> Vec { + match &self.kind { + ValidatedBlockFinalizationCertKind::Finalize { + finalize_cert, + notarize_cert, + } => { + let notar = Certificate { + cert_type: CertificateType::Notarize(notarize_cert.block), + signature: notarize_cert.signature.signature, + bitmap: notarize_cert.signature.bitmap.clone(), + }; + let finalize = Certificate { + cert_type: CertificateType::Finalize(finalize_cert.slot), + signature: finalize_cert.signature.signature, + bitmap: finalize_cert.signature.bitmap.clone(), + }; + vec![finalize, notar] + } + ValidatedBlockFinalizationCertKind::FastFinalize(cert) => { + let cert = Certificate { + cert_type: CertificateType::FinalizeFast(cert.block), + signature: cert.signature.signature, + bitmap: cert.signature.bitmap.clone(), + }; + vec![cert] + } + } + } + /// Returns the data needed to calculating and paying vote rewards. pub fn vote_rewards_input(&self) -> (&HashSet, Slot) { - (&self.signers, self.slot()) + (&self.signers, self.slot().slot()) } /// Consumes self and returns the contained certificates and the signers. @@ -315,75 +307,95 @@ impl ValidatedBlockFinalizationCert { ValidatedBlockFinalizationCertKind::Finalize { finalize_cert, notarize_cert, - } => (finalize_cert, Some(notarize_cert)), - ValidatedBlockFinalizationCertKind::FastFinalize(cert) => (cert, None), + } => { + let notar = Certificate { + cert_type: CertificateType::Notarize(notarize_cert.block), + signature: notarize_cert.signature.signature, + bitmap: notarize_cert.signature.bitmap, + }; + let finalize = Certificate { + cert_type: CertificateType::Finalize(finalize_cert.slot), + signature: finalize_cert.signature.signature, + bitmap: finalize_cert.signature.bitmap, + }; + (finalize, Some(notar)) + } + ValidatedBlockFinalizationCertKind::FastFinalize(cert) => { + let cert = Certificate { + cert_type: CertificateType::FinalizeFast(cert.block), + signature: cert.signature.signature, + bitmap: cert.signature.bitmap, + }; + (cert, None) + } }; (signers, final_cert, notar_cert) } - /// Converts this validated certificate into a [`FinalCertificate`] for inclusion in a block footer. - pub fn to_final_certificate(&self) -> FinalCertificate { + /// Converts this validated certificate into a [`BlockFinalizationCert`] for inclusion in a block footer. + pub fn to_block_final_cert(&self) -> BlockFinalizationCert { match &self.kind { ValidatedBlockFinalizationCertKind::Finalize { finalize_cert, notarize_cert, } => { - let slot = finalize_cert.cert_type.slot(); - let block_id = notarize_cert - .cert_type - .to_block() - .expect("notarize certificates correspond to blocks") - .1; - FinalCertificate { + let slot = finalize_cert.slot; + let block_id = notarize_cert.block.block_id; + BlockFinalizationCert { slot, block_id, - final_aggregate: VotesAggregate::from_certificate(finalize_cert), - notar_aggregate: Some(VotesAggregate::from_certificate(notarize_cert)), + final_aggregate: VotesAggregate::from_cert_signature( + finalize_cert.signature.clone(), + ), + notar_aggregate: Some(VotesAggregate::from_cert_signature( + notarize_cert.signature.clone(), + )), } } ValidatedBlockFinalizationCertKind::FastFinalize(cert) => { - let (slot, block_id) = cert - .cert_type - .to_block() - .expect("fast finalizations correspond to blocks"); - FinalCertificate { - slot, - block_id, - final_aggregate: VotesAggregate::from_certificate(cert), + let block = cert.block; + BlockFinalizationCert { + slot: block.slot, + block_id: block.block_id, + final_aggregate: VotesAggregate::from_cert_signature(cert.signature.clone()), notar_aggregate: None, } } } } - /// Verifies the certificate, returning (stake present in certificate, total stake in validator set) on success. + /// Verifies the certificate and returns `CertSignature` on success. fn verify_certificate( bank: &Bank, - cert: &Certificate, - ) -> Result<(u64, u64), BlockFinalizationCertError> { - bank.verify_certificate(cert) - .map_err(|_| BlockFinalizationCertError::SignatureVerificationFailed(cert.cert_type)) + cert: UnverifiedCertificate, + ) -> Result { + let cert_type = cert.cert_type; + let cert = bank + .verify_certificate(cert) + .map_err(|_| BlockFinalizationCertError::CertificateVerificationFailed(cert_type))?; + debug_assert_eq!(cert.cert_type, cert_type); + Ok(CertSignature { + signature: cert.signature, + bitmap: cert.bitmap, + }) } fn extract_signers( bank: &Bank, - cert: &Certificate, + cert_type: CertificateType, + bitmap: &[u8], ) -> Result, BlockFinalizationCertError> { - let Some(epoch_stakes) = bank.epoch_stakes_from_slot(cert.cert_type.slot()) else { - return Err(BlockFinalizationCertError::MissingEpochStakes( - cert.cert_type, - )); + let Some(epoch_stakes) = bank.epoch_stakes_from_slot(cert_type.slot()) else { + return Err(BlockFinalizationCertError::MissingEpochStakes(cert_type)); }; let rank_map = epoch_stakes.bls_pubkey_to_rank_map(); - let decoded = decode(&cert.bitmap, rank_map.len()); + let decoded = decode(bitmap, rank_map.len()); let ranks = match decoded { Ok(Decoded::Base2(ranks)) => ranks, Ok(Decoded::Base3(_, _)) => { - return Err(BlockFinalizationCertError::Base3Finalization( - cert.cert_type, - )); + return Err(BlockFinalizationCertError::Base3Finalization(cert_type)); } - Err(err) => return Err(BlockFinalizationCertError::DecodeError(cert.cert_type, err)), + Err(err) => return Err(BlockFinalizationCertError::DecodeError(cert_type, err)), }; Ok(ranks @@ -404,10 +416,12 @@ mod tests { crate::genesis_utils::{ ValidatorVoteKeypairs, create_genesis_config_with_alpenglow_vote_accounts, }, - agave_votor_messages::vote::Vote, + agave_votor_messages::{vote::Vote, wire::get_vote_payload_to_sign}, bitvec::prelude::*, + rand::Rng, solana_bls_signatures::SignatureProjective, solana_entry::block_component::VotesAggregate, + solana_hash::Hash, solana_signer_store::encode_base2, std::sync::Arc, }; @@ -433,15 +447,15 @@ mod tests { (bank, validator_keypairs) } - /// Build a certificate by manually aggregating BLS signatures and encoding bitmap. + /// Build a cert signature by manually aggregating BLS signatures and encoding bitmap. /// `signing_ranks` specifies which validator ranks are signing. - fn build_certificate_manual( - cert_type: CertificateType, + fn build_cert_signature( vote: Vote, + shred_version: u16, signing_ranks: &[usize], validator_keypairs: &[ValidatorVoteKeypairs], - ) -> Certificate { - let serialized_vote = bincode::serialize(&vote).unwrap(); + ) -> CertSignature { + let serialized_vote = get_vote_payload_to_sign(vote, shred_version); // Aggregate signatures let mut signature = SignatureProjective::identity(); @@ -458,13 +472,28 @@ mod tests { } let bitmap = encode_base2(&bitvec).expect("Failed to encode bitmap"); - Certificate { - cert_type, + CertSignature { signature: signature.into(), bitmap, } } + fn vote_pubkeys_for_ranks(bank: &Bank, ranks: &[usize]) -> HashSet { + let rank_map = bank + .epoch_stakes_from_slot(bank.slot()) + .unwrap() + .bls_pubkey_to_rank_map(); + ranks + .iter() + .map(|rank| { + rank_map + .get_pubkey_stake_entry(*rank) + .unwrap() + .vote_account_pubkey + }) + .collect() + } + #[test] fn test_verify_final_cert_valid() { // Create 10 validators with descending stakes (1000, 900, 800, ...) @@ -474,71 +503,157 @@ mod tests { .map(|i| (1000u64).saturating_sub((i as u64).saturating_mul(100))) .collect(); let (bank, validator_keypairs) = create_bank_with_bls_validators(num_validators, stakes); + let shred_version = rand::rng().random(); - let slot = bank.slot(); - let block_id = Hash::new_unique(); + let block = Block { + slot: bank.slot(), + block_id: Hash::new_unique(), + }; // Test 1: Fast finalize (requires 80% stake = 4400) // Top 6 validators = 1000+900+800+700+600+500 = 4500 (>= 80%) { - let cert_type = CertificateType::FinalizeFast(slot, block_id); - let vote = Vote::new_notarization_vote(slot, block_id); + let cert_type = CertificateType::FinalizeFast(block); + let vote = Vote::new_notarization_vote(block); let signing_ranks: Vec = (0..6).collect(); - let fast_finalize_cert = - build_certificate_manual(cert_type, vote, &signing_ranks, &validator_keypairs); + let fast_finalize_cert_sig = + build_cert_signature(vote, shred_version, &signing_ranks, &validator_keypairs); + let fast_finalize_cert = Certificate { + cert_type, + signature: fast_finalize_cert_sig.signature, + bitmap: fast_finalize_cert_sig.bitmap.clone(), + }; - let final_cert = FinalCertificate { - slot, - block_id, - final_aggregate: VotesAggregate::from_certificate(&fast_finalize_cert), + let block_final_cert = BlockFinalizationCert { + slot: block.slot, + block_id: block.block_id, + final_aggregate: VotesAggregate::from_cert_signature(fast_finalize_cert_sig), notar_aggregate: None, }; - let result = ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank); - assert!( - result.is_ok(), - "Valid fast finalize certificate should pass verification: {result:?}" - ); + let validated = ValidatedBlockFinalizationCert::try_from_footer( + block_final_cert, + &bank, + shred_version, + ) + .expect("Valid fast finalize certificate should pass verification"); + assert_eq!(validated.clone_certificates(), vec![fast_finalize_cert]); } // Test 2: Slow finalize (requires 60% stake = 3300 for both certs) // Top 4 validators = 1000+900+800+700 = 3400 (>= 60%) { - let notarize_cert_type = CertificateType::Notarize(slot, block_id); - let notarize_vote = Vote::new_notarization_vote(slot, block_id); + let notarize_cert_type = CertificateType::Notarize(block); + let notarize_vote = Vote::new_notarization_vote(block); let notarize_signing_ranks: Vec = (0..4).collect(); - let notarize_cert = build_certificate_manual( - notarize_cert_type, + let notarize_cert_sig = build_cert_signature( notarize_vote, + shred_version, ¬arize_signing_ranks, &validator_keypairs, ); + let notarize_cert = Certificate { + cert_type: notarize_cert_type, + signature: notarize_cert_sig.signature, + bitmap: notarize_cert_sig.bitmap.clone(), + }; - let finalize_cert_type = CertificateType::Finalize(slot); - let finalize_vote = Vote::new_finalization_vote(slot); + let finalize_cert_type = CertificateType::Finalize(block.slot); + let finalize_vote = Vote::new_finalization_vote(block.slot); let finalize_signing_ranks: Vec = (0..4).collect(); - let finalize_cert = build_certificate_manual( - finalize_cert_type, + let finalize_cert_sig = build_cert_signature( finalize_vote, + shred_version, &finalize_signing_ranks, &validator_keypairs, ); + let finalize_cert = Certificate { + cert_type: finalize_cert_type, + signature: finalize_cert_sig.signature, + bitmap: finalize_cert_sig.bitmap.clone(), + }; - let final_cert = FinalCertificate { - slot, - block_id, - final_aggregate: VotesAggregate::from_certificate(&finalize_cert), - notar_aggregate: Some(VotesAggregate::from_certificate(¬arize_cert)), + let block_final_cert = BlockFinalizationCert { + slot: block.slot, + block_id: block.block_id, + final_aggregate: VotesAggregate::from_cert_signature(finalize_cert_sig), + notar_aggregate: Some(VotesAggregate::from_cert_signature(notarize_cert_sig)), }; - let result = ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank); - assert!( - result.is_ok(), - "Valid slow finalize certificate should pass verification: {result:?}" + let validated = ValidatedBlockFinalizationCert::try_from_footer( + block_final_cert, + &bank, + shred_version, + ) + .expect("Valid slow finalize certificate should pass verification"); + assert_eq!( + validated.clone_certificates(), + vec![finalize_cert, notarize_cert] ); } } + #[test] + fn test_slow_finalization_signers_include_notarize_and_finalize_signers() { + let stakes = vec![1000, 900, 800, 700, 100]; + let (bank, validator_keypairs) = create_bank_with_bls_validators(stakes.len(), stakes); + let shred_version = rand::rng().random(); + + let block = Block { + slot: bank.slot(), + block_id: Hash::new_unique(), + }; + + let notarize_vote = Vote::new_notarization_vote(block); + let notarize_signing_ranks = vec![0, 1, 3]; + let notarize_cert = NotarCert { + block, + signature: build_cert_signature( + notarize_vote, + shred_version, + ¬arize_signing_ranks, + &validator_keypairs, + ), + }; + + let finalize_vote = Vote::new_finalization_vote(block.slot); + let finalize_signing_ranks = vec![0, 1, 2]; + let finalize_cert = FinalizeCert { + slot: block.slot, + signature: build_cert_signature( + finalize_vote, + shred_version, + &finalize_signing_ranks, + &validator_keypairs, + ), + }; + + let mut expected_signers = vote_pubkeys_for_ranks(&bank, ¬arize_signing_ranks); + expected_signers.extend(vote_pubkeys_for_ranks(&bank, &finalize_signing_ranks)); + + let block_final_cert = BlockFinalizationCert { + slot: block.slot, + block_id: block.block_id, + final_aggregate: VotesAggregate::from_cert_signature(finalize_cert.signature.clone()), + notar_aggregate: Some(VotesAggregate::from_cert_signature( + notarize_cert.signature.clone(), + )), + }; + let validated = + ValidatedBlockFinalizationCert::try_from_footer(block_final_cert, &bank, shred_version) + .expect("Valid slow finalize certificate should pass verification"); + let (signers, _, _) = validated.into_parts(); + assert_eq!(signers, expected_signers); + + let validated = ValidatedBlockFinalizationCert::from_validated_slow( + finalize_cert, + notarize_cert, + &bank, + ); + let (signers, _, _) = validated.into_parts(); + assert_eq!(signers, expected_signers); + } + #[test] fn test_verify_final_cert_insufficient_stake() { // Create 10 validators with descending stakes (1000, 900, 800, ...) @@ -548,72 +663,82 @@ mod tests { .map(|i| (1000u64).saturating_sub((i as u64).saturating_mul(100))) .collect(); let (bank, validator_keypairs) = create_bank_with_bls_validators(num_validators, stakes); + let shred_version = rand::rng().random(); - let slot = bank.slot(); - let block_id = Hash::new_unique(); + let block = Block { + slot: bank.slot(), + block_id: Hash::new_unique(), + }; // Fast finalize with insufficient stake (requires 80% = 4400) // Top 5 validators = 1000+900+800+700+600 = 4000 (< 80%) { - let cert_type = CertificateType::FinalizeFast(slot, block_id); - let vote = Vote::new_notarization_vote(slot, block_id); + let vote = Vote::new_notarization_vote(block); let signing_ranks: Vec = (0..5).collect(); - let fast_finalize_cert = - build_certificate_manual(cert_type, vote, &signing_ranks, &validator_keypairs); + let fast_finalize_cert_sig = + build_cert_signature(vote, shred_version, &signing_ranks, &validator_keypairs); - let final_cert = FinalCertificate { - slot, - block_id, - final_aggregate: VotesAggregate::from_certificate(&fast_finalize_cert), + let block_final_cert = BlockFinalizationCert { + slot: block.slot, + block_id: block.block_id, + final_aggregate: VotesAggregate::from_cert_signature(fast_finalize_cert_sig), notar_aggregate: None, }; - let result = ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank); + let err = ValidatedBlockFinalizationCert::try_from_footer( + block_final_cert, + &bank, + shred_version, + ) + .unwrap_err(); assert!( matches!( - result, - Err(BlockFinalizationCertError::InsufficientStake { .. }) + err, + BlockFinalizationCertError::CertificateVerificationFailed { .. } ), - "Fast finalize with insufficient stake should fail verification" + "Fast finalize with insufficient stake should fail verification. {err:?}" ); } // Slow finalize with insufficient notarize stake (requires 60% = 3300) // Top 3 validators = 1000+900+800 = 2700 (< 60%) { - let notarize_cert_type = CertificateType::Notarize(slot, block_id); - let notarize_vote = Vote::new_notarization_vote(slot, block_id); + let notarize_vote = Vote::new_notarization_vote(block); let notarize_signing_ranks: Vec = (0..3).collect(); - let notarize_cert = build_certificate_manual( - notarize_cert_type, + let notarize_cert_sig = build_cert_signature( notarize_vote, + shred_version, ¬arize_signing_ranks, &validator_keypairs, ); // Finalize cert has enough stake - let finalize_cert_type = CertificateType::Finalize(slot); - let finalize_vote = Vote::new_finalization_vote(slot); + let finalize_vote = Vote::new_finalization_vote(block.slot); let finalize_signing_ranks: Vec = (0..4).collect(); - let finalize_cert = build_certificate_manual( - finalize_cert_type, + let finalize_cert_sig = build_cert_signature( finalize_vote, + shred_version, &finalize_signing_ranks, &validator_keypairs, ); - let final_cert = FinalCertificate { - slot, - block_id, - final_aggregate: VotesAggregate::from_certificate(&finalize_cert), - notar_aggregate: Some(VotesAggregate::from_certificate(¬arize_cert)), + let block_final_cert = BlockFinalizationCert { + slot: block.slot, + block_id: block.block_id, + final_aggregate: VotesAggregate::from_cert_signature(finalize_cert_sig), + notar_aggregate: Some(VotesAggregate::from_cert_signature(notarize_cert_sig)), }; - let result = ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank); + let err = ValidatedBlockFinalizationCert::try_from_footer( + block_final_cert, + &bank, + shred_version, + ) + .unwrap_err(); assert!( matches!( - result, - Err(BlockFinalizationCertError::InsufficientStake { .. }) + err, + BlockFinalizationCertError::CertificateVerificationFailed { .. } ), "Slow finalize with insufficient notarize stake should fail verification" ); @@ -623,40 +748,43 @@ mod tests { // Notarize has enough stake, but finalize doesn't { // Notarize cert has enough stake - let notarize_cert_type = CertificateType::Notarize(slot, block_id); - let notarize_vote = Vote::new_notarization_vote(slot, block_id); + let notarize_vote = Vote::new_notarization_vote(block); let notarize_signing_ranks: Vec = (0..4).collect(); - let notarize_cert = build_certificate_manual( - notarize_cert_type, + let notarize_cert_sig = build_cert_signature( notarize_vote, + shred_version, ¬arize_signing_ranks, &validator_keypairs, ); // Finalize cert has insufficient stake // Top 3 validators = 1000+900+800 = 2700 (< 60%) - let finalize_cert_type = CertificateType::Finalize(slot); - let finalize_vote = Vote::new_finalization_vote(slot); + let finalize_vote = Vote::new_finalization_vote(block.slot); let finalize_signing_ranks: Vec = (0..3).collect(); - let finalize_cert = build_certificate_manual( - finalize_cert_type, + let finalize_cert_sig = build_cert_signature( finalize_vote, + shred_version, &finalize_signing_ranks, &validator_keypairs, ); - let final_cert = FinalCertificate { - slot, - block_id, - final_aggregate: VotesAggregate::from_certificate(&finalize_cert), - notar_aggregate: Some(VotesAggregate::from_certificate(¬arize_cert)), + let block_final_cert = BlockFinalizationCert { + slot: block.slot, + block_id: block.block_id, + final_aggregate: VotesAggregate::from_cert_signature(finalize_cert_sig), + notar_aggregate: Some(VotesAggregate::from_cert_signature(notarize_cert_sig)), }; - let result = ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank); + let err = ValidatedBlockFinalizationCert::try_from_footer( + block_final_cert, + &bank, + shred_version, + ) + .unwrap_err(); assert!( matches!( - result, - Err(BlockFinalizationCertError::InsufficientStake { .. }) + err, + BlockFinalizationCertError::CertificateVerificationFailed { .. } ), "Slow finalize with insufficient finalize stake should fail verification" ); diff --git a/runtime/src/validated_reward_certificate.rs b/runtime/src/validated_reward_certificate.rs index c6b454a2f98..947c5fda955 100644 --- a/runtime/src/validated_reward_certificate.rs +++ b/runtime/src/validated_reward_certificate.rs @@ -2,8 +2,10 @@ use { crate::bank::Bank, agave_bls_cert_verify::cert_verify::{Error as BlsCertVerifyError, verify_base2}, agave_votor_messages::{ + consensus_message::Block, reward_certificate::{NUM_SLOTS_FOR_REWARD, NotarRewardCertificate, SkipRewardCertificate}, vote::Vote, + wire::get_vote_payload_to_sign, }, solana_bls_signatures::BlsError, solana_clock::Slot, @@ -79,6 +81,7 @@ impl ValidatedRewardCert { /// If validation of the provided reward certs succeeds, returns an instance of [`ValidatedRewardCert`]. pub fn try_new( bank: &Bank, + shred_version: u16, skip: &Option, notar: &Option, ) -> Result, Error> { @@ -101,8 +104,7 @@ impl ValidatedRewardCert { if let Some(skip) = skip { let vote = Vote::new_skip_vote(skip.slot); - // unwrap should be safe as we contructed the vote ourselves. - let payload = bincode::serialize(&vote).unwrap(); + let payload = get_vote_payload_to_sign(vote, shred_version); verify_base2( &payload, &skip.signature, @@ -112,9 +114,11 @@ impl ValidatedRewardCert { )? } if let Some(notar) = notar { - let vote = Vote::new_notarization_vote(notar.slot, notar.block_id); - // unwrap should be safe as we contructed the vote ourselves. - let payload = bincode::serialize(&vote).unwrap(); + let vote = Vote::new_notarization_vote(Block { + slot: notar.slot, + block_id: notar.block_id, + }); + let payload = get_vote_payload_to_sign(vote, shred_version); verify_base2( &payload, ¬ar.signature, @@ -132,6 +136,31 @@ impl ValidatedRewardCert { })) } + /// Constructs a [`ValidatedRewardCert`] for a block produced locally. + /// + /// The leader-side reward certificate builder receives verified votes and + /// tracks the validator set while aggregating them, so block production + /// only needs the reward slot and validator set for bank reward + /// calculation. + pub fn try_new_for_leader( + current_slot: Slot, + skip: &Option, + notar: &Option, + validators: impl IntoIterator, + ) -> Result, Error> { + let Some(reward_slot) = extract_slot(current_slot, skip, notar)? else { + return Ok(None); + }; + let validators: HashSet<_> = validators.into_iter().collect(); + if validators.is_empty() { + return Ok(None); + } + Ok(Some(Self { + validators, + reward_slot, + })) + } + pub(crate) fn slot(&self) -> Slot { self.reward_slot } @@ -159,6 +188,7 @@ mod tests { }, agave_votor_messages::consensus_message::VoteMessage, bitvec::vec::BitVec, + rand::Rng, solana_bls_signatures::{ Keypair as BlsKeypair, Signature as BLSSignature, SignatureCompressed as BlsSignatureCompressed, SignatureProjective, @@ -170,9 +200,9 @@ mod tests { std::collections::HashMap, }; - fn new_vote(vote: Vote, rank: usize, keypair: &BlsKeypair) -> VoteMessage { - let serialized = bincode::serialize(&vote).unwrap(); - let signature = keypair.sign(&serialized).into(); + fn new_vote(vote: Vote, rank: usize, keypair: &BlsKeypair, shred_version: u16) -> VoteMessage { + let payload = get_vote_payload_to_sign(vote, shred_version); + let signature = keypair.sign(&payload).into(); VoteMessage { vote, signature, @@ -207,6 +237,7 @@ mod tests { let validator_keypairs = (0..num_validators) .map(|_| ValidatorVoteKeypairs::new_rand()) .collect::>(); + let shred_version = rand::rng().random(); let keypair_map = validator_keypairs .iter() .map(|k| { @@ -238,27 +269,34 @@ mod tests { }) .collect::>(); - let blockid = Hash::new_unique(); - let notar_vote = Vote::new_notarization_vote(reward_slot, blockid); + let block_id = Hash::new_unique(); + let notar_vote = Vote::new_notarization_vote(Block { + slot: reward_slot, + block_id, + }); let notar_votes = (0..num_notar_validators) - .map(|rank| new_vote(notar_vote, rank, signing_keys[rank])) + .map(|rank| new_vote(notar_vote, rank, signing_keys[rank], shred_version)) .collect::>(); let (signature, bitmap) = build_sig_bitmap(¬ar_votes); let notar_reward_cert = - NotarRewardCertificate::try_new(reward_slot, blockid, signature, bitmap).unwrap(); + NotarRewardCertificate::try_new(reward_slot, block_id, signature, bitmap).unwrap(); let skip_vote = Vote::new_skip_vote(reward_slot); let skip_votes = (num_notar_validators..num_validators) - .map(|rank| new_vote(skip_vote, rank, signing_keys[rank])) + .map(|rank| new_vote(skip_vote, rank, signing_keys[rank], shred_version)) .collect::>(); let (signature, bitmap) = build_sig_bitmap(&skip_votes); let skip_reward_cert = SkipRewardCertificate::try_new(reward_slot, signature, bitmap).unwrap(); - let validated_reward_cert = - ValidatedRewardCert::try_new(&bank, &Some(skip_reward_cert), &Some(notar_reward_cert)) - .unwrap() - .unwrap(); + let validated_reward_cert = ValidatedRewardCert::try_new( + &bank, + shred_version, + &Some(skip_reward_cert), + &Some(notar_reward_cert), + ) + .unwrap() + .unwrap(); assert_eq!(validated_reward_cert.validators.len(), num_validators); } } diff --git a/runtime/tests/vote_account.rs b/runtime/tests/vote_account.rs index d1ffcdad0ed..d606f2a675a 100644 --- a/runtime/tests/vote_account.rs +++ b/runtime/tests/vote_account.rs @@ -8,7 +8,7 @@ use { solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, solana_pubkey::Pubkey, solana_runtime::{ - bank::{MAX_ALPENGLOW_VOTE_ACCOUNTS, VAT_TO_BURN_PER_EPOCH}, + bank::{DEFAULT_VAT_TO_BURN_PER_EPOCH, MAX_ALPENGLOW_VOTE_ACCOUNTS}, test_utils::{ new_rand_vote_account, new_rand_vote_accounts, new_staked_vote_accounts, staked_nodes, }, @@ -381,7 +381,7 @@ fn test_clone_and_filter_for_vat_filters_non_alpenglow() { let filtered = vote_accounts.clone_and_filter_for_vat(new_limit, MIN_STAKE_FOR_STAKED_ACCOUNT); assert_eq!(filtered.len(), MAX_ALPENGLOW_VOTE_ACCOUNTS); // Check that all filtered accounts have bls pubkey. - for (_pubkey, (_stake, vote_account)) in filtered.as_ref().iter() { + for (_stake, vote_account) in filtered.as_ref().values() { assert!( vote_account .vote_state_view() @@ -393,7 +393,7 @@ fn test_clone_and_filter_for_vat_filters_non_alpenglow() { let new_limit = MAX_ALPENGLOW_VOTE_ACCOUNTS - 500; let filtered = vote_accounts.clone_and_filter_for_vat(new_limit, MIN_STAKE_FOR_STAKED_ACCOUNT); assert!(filtered.len() <= new_limit); - for (_pubkey, (_stake, vote_account)) in filtered.as_ref().iter() { + for (_stake, vote_account) in filtered.as_ref().values() { assert!( vote_account .vote_state_view() @@ -445,14 +445,14 @@ fn test_clone_and_filter_for_vat_not_enough_lamports() { MAX_STAKE_FOR_STAKED_ACCOUNT, |index| { if index < entries_to_modify { - VAT_TO_BURN_PER_EPOCH - 1 + DEFAULT_VAT_TO_BURN_PER_EPOCH - 1 } else { 10_000_000_000 } }, ); - let filtered = - vote_accounts.clone_and_filter_for_vat(MAX_ALPENGLOW_VOTE_ACCOUNTS, VAT_TO_BURN_PER_EPOCH); + let filtered = vote_accounts + .clone_and_filter_for_vat(MAX_ALPENGLOW_VOTE_ACCOUNTS, DEFAULT_VAT_TO_BURN_PER_EPOCH); assert!(filtered.len() <= MAX_ALPENGLOW_VOTE_ACCOUNTS - entries_to_modify); } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f25b5b1407b..2d45363a5be 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.95.0" +channel = "1.96.1" diff --git a/scheduler-bindings/Cargo.toml b/scheduler-bindings/Cargo.toml index 05b0235b018..ae966018f5a 100644 --- a/scheduler-bindings/Cargo.toml +++ b/scheduler-bindings/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] diff --git a/scheduling-utils/Cargo.toml b/scheduling-utils/Cargo.toml index 365a921b4e3..7717053914e 100644 --- a/scheduling-utils/Cargo.toml +++ b/scheduling-utils/Cargo.toml @@ -9,12 +9,17 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] dev-context-only-utils = ["dep:wincode", "dep:solana-transaction"] [dependencies] -agave-feature-set = { workspace = true, features = ["agave-unstable-api"] } +agave-feature-set = { workspace = true } agave-scheduler-bindings = { workspace = true } agave-transaction-view = { workspace = true } ahash = { workspace = true } @@ -23,6 +28,7 @@ shaq = { workspace = true } slotmap = { workspace = true } solana-fee = { workspace = true, features = ["agave-unstable-api"] } solana-pubkey = { workspace = true } +solana-runtime-transaction = { workspace = true } solana-transaction = { workspace = true, optional = true, features = ["serde"] } solana-transaction-error = { workspace = true } thiserror = { workspace = true } @@ -33,6 +39,7 @@ libc = { workspace = true } nix = { workspace = true, features = ["socket", "uio"] } [dev-dependencies] +solana-pubkey = { workspace = true, features = ["rand"] } tempfile = { workspace = true } [target."cfg(unix)".dev-dependencies] diff --git a/scheduling-utils/src/bridge/bindings.rs b/scheduling-utils/src/bridge/bindings.rs index ccdf8a2dc13..a2378a19743 100644 --- a/scheduling-utils/src/bridge/bindings.rs +++ b/scheduling-utils/src/bridge/bindings.rs @@ -18,6 +18,7 @@ use { slotmap::SlotMap, solana_fee::FeeFeatures, solana_pubkey::Pubkey, + solana_runtime_transaction::sanitize_config::sanitize_config, std::ptr::NonNull, thiserror::Error, }; @@ -164,7 +165,7 @@ where let tx = unsafe { TransactionPtr::from_raw_parts(ptr, tx.len()) }; // Sanitize the transaction, drop it immediately if it fails sanitization. - match SanitizedTransactionView::try_new_sanitized(tx, true) { + match SanitizedTransactionView::try_new_sanitized(tx, &sanitize_config(true)) { Ok(tx) => { let key = self.state.insert(TransactionState { dead: false, @@ -254,7 +255,8 @@ where }; // Sanitize the transaction, drop it immediately if it fails sanitization. - let Ok(tx) = SanitizedTransactionView::try_new_sanitized(tx, true) else { + let Ok(tx) = SanitizedTransactionView::try_new_sanitized(tx, &sanitize_config(true)) + else { // SAFETY: // - We own `tx` exclusively. // - The previous `TransactionPtr` has been dropped by `try_new_sanitized`. diff --git a/scripts/agave-build-lists.sh b/scripts/agave-build-lists.sh index fd72133bc06..17c7d76f0ee 100755 --- a/scripts/agave-build-lists.sh +++ b/scripts/agave-build-lists.sh @@ -31,7 +31,6 @@ AGAVE_BINS_VAL_OP=( AGAVE_BINS_DCOU=( agave-ledger-tool - solana-bench-tps ) # These bins are deprecated and will be removed in a future release @@ -47,7 +46,6 @@ DCOU_TAINTED_PACKAGES=( agave-store-tool solana-accounts-cluster-bench solana-banking-bench - solana-bench-tps solana-local-cluster solana-svm-test-harness solana-svm-test-harness-fixture diff --git a/scripts/build-downstream-anchor-projects.sh b/scripts/build-downstream-anchor-projects.sh deleted file mode 100755 index 4fd2fc34aa7..00000000000 --- a/scripts/build-downstream-anchor-projects.sh +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env bash -# -# Builds known downstream projects against local solana source -# - -set -e -cd "$(dirname "$0")"/.. -source ci/_ -source scripts/patch-crates.sh -source scripts/read-cargo-variable.sh -source scripts/patch-spl-crates-for-anchor.sh - -anchor_version=$1 -solana_ver=$(readCargoVariable version Cargo.toml) -solana_dir=$PWD -cargo="$solana_dir"/cargo -cargo_build_sbf="$solana_dir"/cargo-build-sbf -cargo_test_sbf="$solana_dir"/cargo-test-sbf - -mkdir -p target/downstream-projects-anchor -cd target/downstream-projects-anchor - -update_anchor_dependencies() { - declare project_root="$1" - declare anchor_ver="$2" - declare tomls=() - while IFS='' read -r line; do tomls+=("$line"); done < <(find "$project_root" -name Cargo.toml) - - sed -i -e "s#\(anchor-lang = \"\)[^\"]*\(\"\)#\1=$anchor_ver\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(anchor-spl = \"\)[^\"]*\(\"\)#\1=$anchor_ver\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(anchor-lang = { version = \"\)[^\"]*\(\"\)#\1=$anchor_ver\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(anchor-spl = { version = \"\)[^\"]*\(\"\)#\1=$anchor_ver\2#g" "${tomls[@]}" || return $? -} - -patch_crates_io_anchor() { - declare Cargo_toml="$1" - declare anchor_dir="$2" - cat >> "$Cargo_toml" <] [options] + Build using instead of the version defined in rust-toolchain.toml. Options: + --dcou-check-only Only check that dcou feature activation is correct and exit (no build). --debug Build with debug profile instead of release profile. --release-with-debug Build with release-with-debug profile instead of release profile. --release-with-lto Build with release-with-lto profile instead of release profile. @@ -42,7 +43,7 @@ usage: $0 [+] [options] --no-build-end-user-bins Do not build end user binaries. --no-build-platform-tools Do not build solana-platform-tools. --no-build-validator-bins Do not build validator binaries. - --no-perf-libs Do not fetch and install perf-libs. (Note: Not using this flag may require internet at build time) + --no-perf-libs Deprecated no-op. perf-libs are no longer applicable to agave. --no-spl-token Do not fetch and install SPL-Token. (Note: Not using this flag requires internet at build time) --help Show this help information and exit. EOF @@ -58,18 +59,21 @@ buildProfileArg='--profile release' buildProfile='release' # Build selection +dcouCheckOnly= noBuildDCOUBins= noBuildDeprecatedBins= noBuildDevBins= noBuildEndUserBins= noBuildPlatformTools= noBuildValidatorBins= -noPerfLibs= noSPLToken= while [[ -n $1 ]]; do if [[ ${1:0:1} = - ]]; then - if [[ $1 = --debug ]]; then + if [[ $1 = --dcou-check-only ]]; then + dcouCheckOnly=true + shift + elif [[ $1 = --debug ]]; then buildProfileArg= # the default cargo profile is 'debug' buildProfile='debug' shift @@ -100,7 +104,7 @@ while [[ -n $1 ]]; do noBuildValidatorBins=true shift elif [[ $1 = --no-perf-libs ]]; then - noPerfLibs=true + echo "WARNING: --no-perf-libs has been deprecated and is now a no-op. perf-libs are no longer applicable to agave." >&2 shift elif [[ $1 = --no-spl-token ]]; then noSPLToken=true @@ -125,15 +129,19 @@ while [[ -n $1 ]]; do fi done -if [[ -z "$installDir" ]]; then - usage "Install directory not specified" - exit 1 -fi -installDir="$(mkdir -p "$installDir"; cd "$installDir"; pwd)" -mkdir -p "$installDir/bin/deps" +if [[ -n "$dcouCheckOnly" ]]; then + echo "(dcou check mode: ignore installDir)" +else + if [[ -z "$installDir" ]]; then + usage "Install directory not specified" + fi + + installDir="$(mkdir -p "$installDir"; cd "$installDir"; pwd)" + mkdir -p "$installDir/bin/deps" -echo "Install location: $installDir ($buildProfile)" + echo "Install location: $installDir ($buildProfile)" +fi cd "$(dirname "$0")"/.. @@ -205,6 +213,21 @@ check_dcou() { exit 1 fi + # Likewise, make sure the dev tools really do activate dcou. Done before + # building so that `--dcou-check` can verify both expectations up front. + if [[ ${#dcouBinArgs[@]} -gt 0 ]]; then + if ! check_dcou --manifest-path "dev-bins/Cargo.toml" "${dcouBinArgs[@]}"; then + echo 'dcou feature activation is incorrectly deactivated!' + exit 1 + fi + fi + + # Stop here if we only want to check the dcou feature activation. + if [[ -n "$dcouCheckOnly" ]]; then + echo 'dcou feature activation check passed.' + exit 0 + fi + # Build our production binaries without dcou. if [[ ${#binArgs[@]} -gt 0 ]]; then cargo_build "${binArgs[@]}" --workspace @@ -212,10 +235,6 @@ check_dcou() { # Finally, build the remaining dev tools with dcou. if [[ ${#dcouBinArgs[@]} -gt 0 ]]; then - if ! check_dcou --manifest-path "dev-bins/Cargo.toml" "${dcouBinArgs[@]}"; then - echo 'dcou feature activation is incorrectly remain to be deactivated!' - exit 1 - fi cargo_build --manifest-path "dev-bins/Cargo.toml" "${dcouBinArgs[@]}" fi @@ -229,6 +248,11 @@ check_dcou() { fi ) +# The subshell above exits early in dcou-check mode; stop here before installing. +if [[ -n "$dcouCheckOnly" ]]; then + exit 0 +fi + for bin in "${BINS[@]}"; do cp -fv "target/$buildProfile/$bin" "$installDir"/bin done @@ -237,14 +261,6 @@ for bin in "${DCOU_BINS[@]}"; do cp -fv "dev-bins/target/$buildProfile/$bin" "$installDir"/bin done -if [[ -z "$noPerfLibs" && $OSTYPE != msys ]]; then - ./fetch-perf-libs.sh - - if [[ -d target/perf-libs ]]; then - cp -a target/perf-libs "$installDir"/bin/perf-libs - fi -fi - if [[ -z "$noBuildPlatformTools" ]]; then # shellcheck disable=SC2086 # Don't want to double quote $rust_version "$cargo" $maybeRustVersion build --manifest-path syscalls/gen-syscall-list/Cargo.toml @@ -253,9 +269,8 @@ if [[ -z "$noBuildPlatformTools" ]]; then source "$SOLANA_ROOT"/scripts/cargo-build-sbf-version.sh # shellcheck disable=SC2086 + # starting from cargo-build-sbf v4.1.0, `cargo install cargo-build-sbf` installs both `cargo-build-sbf` and `cargo-test-sbf` "$cargo" $maybeRustVersion install --locked cargo-build-sbf --root "$installDir" $maybeCargoBuildSbfVersionArg - # shellcheck disable=SC2086 - "$cargo" $maybeRustVersion install --locked cargo-test-sbf --root "$installDir" $maybeCargoTestSbfVersionArg fi ( diff --git a/scripts/patch-crates.sh b/scripts/patch-crates.sh index 4047d37ff84..3868fadb34d 100644 --- a/scripts/patch-crates.sh +++ b/scripts/patch-crates.sh @@ -61,7 +61,6 @@ update_solana_dependencies() { solana-storage-bigtable solana-storage-proto solana-streamer - solana-svm-transaction solana-test-validator solana-tpu-client solana-transaction-status @@ -146,7 +145,6 @@ patch_crates_io_solana_no_header() { crates_map+=("solana-storage-bigtable storage-bigtable") crates_map+=("solana-storage-proto storage-proto") crates_map+=("solana-streamer streamer") - crates_map+=("solana-svm-transaction svm-transaction") crates_map+=("solana-test-validator test-validator") crates_map+=("solana-tpu-client tpu-client") crates_map+=("solana-transaction-status transaction-status") diff --git a/scripts/patch-spl-crates-for-anchor.sh b/scripts/patch-spl-crates-for-anchor.sh deleted file mode 100644 index 3ef8672b19d..00000000000 --- a/scripts/patch-spl-crates-for-anchor.sh +++ /dev/null @@ -1,78 +0,0 @@ -spl_associated_token_account_version= -spl_pod_version= -spl_token_version= -spl_token_2022_version= -spl_token_group_interface_version= -spl_token_metadata_interface_version= -spl_tlv_account_resolution_version= -spl_transfer_hook_interface_version= -spl_type_length_value_version= - -get_spl_versions() { - declare spl_dir="$1" - spl_associated_token_account_version=$(readCargoVariable version "$spl_dir/associated-token-account/program/Cargo.toml") - spl_pod_version=$(readCargoVariable version "$spl_dir/libraries/pod/Cargo.toml") - spl_token_version=$(readCargoVariable version "$spl_dir/token/program/Cargo.toml") - spl_token_2022_version=$(readCargoVariable version "$spl_dir/token/program-2022/Cargo.toml"| head -c1) # only use the major version for convenience - spl_token_group_interface_version=$(readCargoVariable version "$spl_dir/token-group/interface/Cargo.toml") - spl_token_metadata_interface_version=$(readCargoVariable version "$spl_dir/token-metadata/interface/Cargo.toml") - spl_tlv_account_resolution_version=$(readCargoVariable version "$spl_dir/libraries/tlv-account-resolution/Cargo.toml") - spl_transfer_hook_interface_version=$(readCargoVariable version "$spl_dir/token/transfer-hook/interface/Cargo.toml") - spl_type_length_value_version=$(readCargoVariable version "$spl_dir/libraries/type-length-value/Cargo.toml") -} - -patch_spl_crates() { - declare project_root="$1" - declare Cargo_toml="$2" - declare spl_dir="$3" - update_spl_dependencies "$project_root" - patch_crates_io "$Cargo_toml" "$spl_dir" -} - -update_spl_dependencies() { - declare project_root="$1" - declare tomls=() - while IFS='' read -r line; do tomls+=("$line"); done < <(find "$project_root" -name Cargo.toml) - - sed -i -e "s#\(spl-associated-token-account = \"\)[^\"]*\(\"\)#\1$spl_associated_token_account_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-associated-token-account = { version = \"\)[^\"]*\(\"\)#\1$spl_associated_token_account_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-pod = \"\)[^\"]*\(\"\)#\1$spl_pod_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-pod = { version = \"\)[^\"]*\(\"\)#\1$spl_pod_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-token = \"\)[^\"]*\(\"\)#\1$spl_token_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-token = { version = \"\)[^\"]*\(\"\)#\1$spl_token_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-token-2022 = \"\).*\(\"\)#\1$spl_token_2022_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-token-2022 = { version = \"\)[^\"]*\(\"\)#\1$spl_token_2022_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-token-group-interface = \"\)[^\"]*\(\"\)#\1=$spl_token_group_interface_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-token-group-interface = { version = \"\)[^\"]*\(\"\)#\1=$spl_token_group_interface_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-token-metadata-interface = \"\)[^\"]*\(\"\)#\1=$spl_token_metadata_interface_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-token-metadata-interface = { version = \"\)[^\"]*\(\"\)#\1=$spl_token_metadata_interface_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-tlv-account-resolution = \"\)[^\"]*\(\"\)#\1=$spl_tlv_account_resolution_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-tlv-account-resolution = { version = \"\)[^\"]*\(\"\)#\1=$spl_tlv_account_resolution_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-transfer-hook-interface = \"\)[^\"]*\(\"\)#\1=$spl_transfer_hook_interface_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-transfer-hook-interface = { version = \"\)[^\"]*\(\"\)#\1=$spl_transfer_hook_interface_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-type-length-value = \"\)[^\"]*\(\"\)#\1=$spl_type_length_value_version\2#g" "${tomls[@]}" || return $? - sed -i -e "s#\(spl-type-length-value = { version = \"\)[^\"]*\(\"\)#\1=$spl_type_length_value_version\2#g" "${tomls[@]}" || return $? - - # patch ahash. This is super brittle; putting here for convenience, since we are already iterating through the tomls - ahash_minor_version="0.8" - sed -i -e "s#\(ahash = \"\)[^\"]*\(\"\)#\1$ahash_minor_version\2#g" "${tomls[@]}" || return $? -} - -patch_crates_io() { - declare Cargo_toml="$1" - declare spl_dir="$2" - cat >> "$Cargo_toml" <( - bank_forks: &Arc>, + bank_forks: Arc>, receiver: Receiver, client: Client, config: Config, @@ -178,7 +178,7 @@ impl SendTransactionService { ); let retry_thread = Self::retry_thread( - bank_forks.clone(), + bank_forks, client, retry_transactions, config, @@ -233,15 +233,14 @@ impl SendTransactionService { stats.received_transactions.fetch_add(1, Ordering::Relaxed); let entry = transactions.entry(transaction_info.signature); let mut new_transaction = false; - if let Entry::Vacant(_) = entry { - if !retry_transactions + if let Entry::Vacant(_) = entry + && !retry_transactions .lock() .unwrap() .contains_key(&transaction_info.signature) - { - entry.or_insert(transaction_info); - new_transaction = true; - } + { + entry.or_insert(transaction_info); + new_transaction = true; } if !new_transaction { stats @@ -435,15 +434,15 @@ impl SendTransactionService { let max_retries = transaction_info.get_max_retries(default_max_retries, service_max_retries); - if let Some(max_retries) = max_retries { - if transaction_info.retries >= max_retries { - info!("Dropping transaction due to max retries: {signature}"); - result.max_retries_elapsed += 1; - stats - .transactions_exceeding_max_retries - .fetch_add(1, Ordering::Relaxed); - return false; - } + if let Some(max_retries) = max_retries + && transaction_info.retries >= max_retries + { + info!("Dropping transaction due to max retries: {signature}"); + result.max_retries_elapsed += 1; + stats + .transactions_exceeding_max_retries + .fetch_add(1, Ordering::Relaxed); + return false; } match signature_status { @@ -469,10 +468,10 @@ impl SendTransactionService { let max_retries = transaction_info .get_max_retries(default_max_retries, service_max_retries); - if let Some(max_retries) = max_retries { - if transaction_info.retries >= max_retries { - exceeded_retries_transactions.push(*signature); - } + if let Some(max_retries) = max_retries + && transaction_info.retries >= max_retries + { + exceeded_retries_transactions.push(*signature); } } else if let Some(last) = transaction_info.last_sent_time { result.last_sent_time = Some( @@ -537,7 +536,7 @@ mod test { use { super::*, crate::test_utils::create_client_for_tests, - crossbeam_channel::{bounded, unbounded}, + crossbeam_channel::bounded, solana_account::AccountSharedData, solana_genesis_config::create_genesis_config, solana_nonce::{self as nonce, state::DurableNonce}, @@ -555,13 +554,13 @@ mod test { async fn service_exit() { let bank = Bank::default_for_tests(); let bank_forks = BankForks::new_rw_arc(bank); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let client = create_client_for_tests(Handle::current(), "127.0.0.1:0".parse().unwrap(), None, 1); let send_transaction_service = SendTransactionService::new( - &bank_forks, + bank_forks, receiver, client.clone(), Config { @@ -598,7 +597,7 @@ mod test { let client = create_client_for_tests(Handle::current(), "127.0.0.1:0".parse().unwrap(), None, 1); let _send_transaction_service = SendTransactionService::new( - &bank_forks, + bank_forks, receiver, client.clone(), Config { diff --git a/snapshots/Cargo.toml b/snapshots/Cargo.toml index fa106448c55..048b89797e9 100644 --- a/snapshots/Cargo.toml +++ b/snapshots/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -48,3 +50,6 @@ zstd = { workspace = true } agave-logger = { path = "../logger", features = ["agave-unstable-api"] } assert_matches = { workspace = true } solana-hash = { workspace = true, features = ["atomic"] } + +[lints] +workspace = true diff --git a/snapshots/src/archive.rs b/snapshots/src/archive.rs index 235e2f0621f..2759bec5d13 100644 --- a/snapshots/src/archive.rs +++ b/snapshots/src/archive.rs @@ -4,7 +4,7 @@ use { snapshot_archive_info::SnapshotArchiveInfo, snapshot_hash::SnapshotHash, }, agave_fs::{ - buffered_reader::FileBufRead as _, buffered_writer::large_file_buf_writer, + FileSize, buffered_reader::FileBufRead as _, buffered_writer::large_file_buf_writer, io_setup::IoSetupState, }, log::info, @@ -28,6 +28,13 @@ use { // and towards the end of archive (sizes equalize) writes are >256KiB / file. const INTERLEAVE_TAR_ENTRIES_SMALL_TO_LARGE_RATIO: (usize, usize) = (4, 1); +// Max number of storage files to open at once for archiving. Bounds extra fd +// usage and io_uring prefetch queue depth. A multiple of the interleave ratio +// sum keeps each chunk balanced between small and large files. +const STORAGE_FILE_OPEN_CHUNK_SIZE: usize = 25 + * (INTERLEAVE_TAR_ENTRIES_SMALL_TO_LARGE_RATIO.0 + + INTERLEAVE_TAR_ENTRIES_SMALL_TO_LARGE_RATIO.1); + /// Archives a snapshot into `archive_path` pub fn archive_snapshot( snapshot_archive_kind: SnapshotArchiveKind, @@ -130,37 +137,64 @@ pub fn archive_snapshot( let use_page_cache = matches!(snapshot_archive_kind, SnapshotArchiveKind::Incremental(_)); let use_direct_io = io_setup.use_direct_io && !use_page_cache; - // Pre-open every storage file (with the buffered reader's - // direct-IO setting) so they outlive `buf_reader` — the reader's - // `'a` lifetime needs to borrow into them across loop iterations. - let storage_files = open_storage_files(storages_orderer.iter(), use_direct_io) - .map_err(E::StorageFileBufReaderError)?; + + // Walk storages and their (lazily-opened) file handles in chunks, + // bounding how many archive-mode fds are simultaneously open. + let mut storage_file_pairs = storages_orderer + .iter() + .zip(open_storage_files(storages_orderer.iter(), use_direct_io)); let mut buf_reader = storage_file_buf_reader(ACCOUNT_STORAGE_MAX_BUFFER_SIZE, use_page_cache, io_setup) .map_err(E::StorageFileBufReaderError)?; - for (storage, file) in storages_orderer.iter().zip(storage_files.iter()) { - let path_in_archive = Path::new(ACCOUNTS_DIR) - .join(AccountsFile::file_name(storage.slot(), storage.id())); - - buf_reader - .set_file(file.as_ref(), storage.accounts.len() as u64) - .map_err(|err| { - E::AccountStorageReaderError(err, storage.path().to_path_buf()) - })?; - let reader = - AccountStorageReader::new(storage, Some(snapshot_slot), &mut buf_reader) + let mut chunk = Vec::with_capacity(STORAGE_FILE_OPEN_CHUNK_SIZE); + loop { + chunk.clear(); + for (storage, file) in (&mut storage_file_pairs).take(STORAGE_FILE_OPEN_CHUNK_SIZE) + { + chunk.push((storage, file.map_err(E::StorageFileBufReaderError)?)); + } + if chunk.is_empty() { + break; + } + // Cheaply re-bind the reader to scope file borrows to this chunk. + let mut chunk_reader = buf_reader.rebind().map_err(E::StorageFileBufReaderError)?; + + // Queue the whole chunk for read-ahead before consuming any of + // it, so the io_uring pipeline can saturate across files. + for (storage, file) in &chunk { + chunk_reader + .add_file_to_prefetch(file.as_ref(), storage.accounts.len() as FileSize) + .map_err(E::StorageFileBufReaderError)?; + } + + for (storage, file) in &chunk { + let path_in_archive = Path::new(ACCOUNTS_DIR) + .join(AccountsFile::file_name(storage.slot(), storage.id())); + + chunk_reader + .set_file(file.as_ref(), storage.accounts.len() as FileSize) .map_err(|err| { E::AccountStorageReaderError(err, storage.path().to_path_buf()) })?; - let mut header = tar::Header::new_gnu(); - header.set_path(path_in_archive).map_err(|err| { - E::ArchiveAccountStorageFile(err, storage.path().to_path_buf()) - })?; - header.set_size(reader.len() as u64); - header.set_cksum(); - archive.append(&header, reader).map_err(|err| { - E::ArchiveAccountStorageFile(err, storage.path().to_path_buf()) - })?; + let reader = + AccountStorageReader::new(storage, Some(snapshot_slot), &mut chunk_reader) + .map_err(|err| { + E::AccountStorageReaderError(err, storage.path().to_path_buf()) + })?; + let mut header = tar::Header::new_gnu(); + header.set_path(path_in_archive).map_err(|err| { + E::ArchiveAccountStorageFile(err, storage.path().to_path_buf()) + })?; + header.set_size(reader.len() as u64); + header.set_cksum(); + archive.append(&header, reader).map_err(|err| { + E::ArchiveAccountStorageFile(err, storage.path().to_path_buf()) + })?; + } + + buf_reader = chunk_reader + .rebind() + .map_err(E::StorageFileBufReaderError)?; } archive.into_inner().map_err(E::FinishArchive)?; diff --git a/snapshots/src/error.rs b/snapshots/src/error.rs index 3ea464d31ff..ce92a94a05d 100644 --- a/snapshots/src/error.rs +++ b/snapshots/src/error.rs @@ -24,6 +24,9 @@ pub enum SnapshotError { #[error("deserialization error: {0}")] DeserializeWincode(#[from] wincode::ReadError), + #[error("serialization error: {0}")] + SerializeWincode(#[from] wincode::WriteError), + #[error("crossbeam send error: {0}")] CrossbeamSend(#[from] crossbeam_channel::SendError), @@ -72,9 +75,6 @@ pub enum SnapshotError { #[error("snapshot epoch stakes are invalid: {0}")] VerifyEpochStakes(#[from] VerifyEpochStakesError), - #[error("slot in storages map {0} exceeds snapshot slot: {1}")] - MismatchedSnapshotStorageSlot(Slot, Slot), - #[error("bank_snapshot_info new_from_dir failed: {0}")] NewFromDir(#[from] SnapshotNewFromDirError), @@ -199,9 +199,6 @@ pub enum AddBankSnapshotError { #[error("failed to flush storage '{1}': {0}")] FlushStorage(#[source] AccountsFileError, PathBuf), - #[error("failed to hard link storages: {0}")] - HardLinkStorages(#[source] HardLinkStoragesToSnapshotError), - #[error("failed to serialize bank: {0}")] SerializeBank(#[source] Box), @@ -211,6 +208,9 @@ pub enum AddBankSnapshotError { #[error("failed to serialize obsolete accounts: {0}")] SerializeObsoleteAccounts(#[source] Box), + #[error("failed to write storages list: {0}")] + WriteStoragesList(#[source] Box), + #[error("failed to write snapshot version file '{1}': {0}")] WriteSnapshotVersionFile(#[source] io::Error, PathBuf), @@ -275,33 +275,3 @@ pub enum ArchiveSnapshotPackageError { #[error("failed to initialize file buffered reader: {0}")] StorageFileBufReaderError(#[source] io::Error), } - -/// Errors that can happen in `hard_link_storages_to_snapshot()` -#[derive(Error, Debug)] -pub enum HardLinkStoragesToSnapshotError { - #[error("failed to create accounts hard links dir '{1}': {0}")] - CreateAccountsHardLinksDir(#[source] io::Error, PathBuf), - - #[error("failed to get the snapshot's accounts hard link dir: {0}")] - GetSnapshotHardLinksDir(#[from] GetSnapshotAccountsHardLinkDirError), - - #[error("failed to hard link storage from '{1}' to '{2}': {0}")] - HardLinkStorage(#[source] io::Error, PathBuf, PathBuf), -} - -/// Errors that can happen in `get_snapshot_accounts_hardlink_dir()` -#[derive(Error, Debug)] -pub enum GetSnapshotAccountsHardLinkDirError { - #[error("invalid account storage path '{0}'")] - GetAccountPath(PathBuf), - - #[error("failed to create the snapshot hard link dir '{1}': {0}")] - CreateSnapshotHardLinkDir(#[source] io::Error, PathBuf), - - #[error("failed to symlink snapshot hard link dir '{link}' to '{original}': {source}")] - SymlinkSnapshotHardLinkDir { - source: io::Error, - original: PathBuf, - link: PathBuf, - }, -} diff --git a/snapshots/src/hardened_unpack.rs b/snapshots/src/hardened_unpack.rs index 475213e5d8f..b8b717e49a3 100644 --- a/snapshots/src/hardened_unpack.rs +++ b/snapshots/src/hardened_unpack.rs @@ -87,7 +87,7 @@ enum UnpackPath<'a> { #[allow(clippy::arithmetic_side_effects)] fn unpack_archive<'a, C>( input: impl Read, - mut file_creator: Box, + mut file_creator: Box, apparent_limit_size: u64, actual_limit_size: u64, limit_count: u64, @@ -315,7 +315,7 @@ fn validate_inside_dst(dst: &Path, file_dst: &Path) -> Result { /// sends entry file paths through the `sender` channel pub(super) fn streaming_unpack_snapshot( input: impl Read, - file_creator: Box, + file_creator: Box, ledger_dir: &Path, account_paths: &[PathBuf], ) -> Result<()> { @@ -324,7 +324,7 @@ pub(super) fn streaming_unpack_snapshot( fn unpack_snapshot_with_processors( input: impl Read, - file_creator: Box, + file_creator: Box, ledger_dir: &Path, account_paths: &[PathBuf], mut accounts_path_processor: F, @@ -417,7 +417,7 @@ fn is_valid_snapshot_archive_entry(parts: &[&str], kind: tar::EntryType) -> bool pub(super) fn unpack_genesis( input: impl Read, - file_creator: Box, + file_creator: Box, unpack_dir: &Path, max_genesis_archive_unpacked_size: u64, ) -> Result<()> { diff --git a/snapshots/src/paths.rs b/snapshots/src/paths.rs index ad1c634e59f..3598f28d2e5 100644 --- a/snapshots/src/paths.rs +++ b/snapshots/src/paths.rs @@ -22,6 +22,7 @@ pub const SNAPSHOT_STATUS_CACHE_FILENAME: &str = "status_cache"; pub const SNAPSHOT_VERSION_FILENAME: &str = "version"; pub const SNAPSHOT_FASTBOOT_VERSION_FILENAME: &str = "fastboot_version"; pub const SNAPSHOT_ACCOUNTS_HARDLINKS: &str = "accounts_hardlinks"; +pub const SNAPSHOT_STORAGES_LIST_FILENAME: &str = "storages_list"; pub const SNAPSHOT_ARCHIVE_DOWNLOAD_DIR: &str = "remote"; pub const SNAPSHOT_OBSOLETE_ACCOUNTS_FILENAME: &str = "obsolete_accounts"; /// When a snapshot is taken of a bank, the state is serialized under this directory. diff --git a/snapshots/src/unarchive.rs b/snapshots/src/unarchive.rs index ed76cdf9aa6..e85312d815f 100644 --- a/snapshots/src/unarchive.rs +++ b/snapshots/src/unarchive.rs @@ -1,15 +1,17 @@ use { crate::{ ArchiveFormat, ArchiveFormatDecompressor, + error::SnapshotError, hardened_unpack::{self, UnpackError}, }, agave_fs::{FileInfo, buffered_reader, file_io::file_creator, io_setup::IoSetupState}, bzip2::bufread::BzDecoder, - crossbeam_channel::Sender, + crossbeam_channel::{SendError, Sender}, std::{ fs, io::{self, BufRead, BufReader}, path::{Path, PathBuf}, + sync::OnceLock, thread::{self, Scope, ScopedJoinHandle}, time::Instant, }, @@ -33,8 +35,10 @@ pub fn streaming_unarchive_snapshot<'scope, 'env: 'scope>( snapshot_archive_path: PathBuf, archive_format: ArchiveFormat, io_setup: &'env IoSetupState, -) -> ScopedJoinHandle<'scope, Result<(), UnpackError>> { +) -> ScopedJoinHandle<'scope, Result<(), SnapshotError>> { let do_unpack = move |archive_path: &Path| { + let first_failed_send = OnceLock::::new(); + let first_failed_send_ref = &first_failed_send; let (decompressor, file_creator) = { // Bound the buffers based on input archive size (decompression multiplies content size, // but buffering more than origin isn't necessary). @@ -47,15 +51,15 @@ pub fn streaming_unarchive_snapshot<'scope, 'env: 'scope>( ( decompressor, file_creator(write_buf_size, io_setup, move |file_info| { - let result = file_sender.send(file_info); - if let Err(err) = result { - panic!( - "failed to send path '{}' from unpacker to rebuilder: {err}", - err.0.path.display(), - ); + match file_sender.send(file_info) { + // Channel owns the file now — don't pass it back for closing. + Ok(()) => None, + Err(SendError(FileInfo { file, path, .. })) => { + let _ = first_failed_send_ref.set(path); + // Hand the file back so the creator closes it. + Some(file) + } } - // Don't pass `File` back to file creator, so it's not closed (owned by channel now) - None })?, ) }; @@ -66,13 +70,16 @@ pub fn streaming_unarchive_snapshot<'scope, 'env: 'scope>( ledger_dir.as_path(), &account_paths, ) + .map(|()| first_failed_send.into_inner()) }; - thread::Builder::new() .name("solTarUnpack".to_string()) - .spawn_scoped(scope, move || { - do_unpack(&snapshot_archive_path) - .map_err(|err| UnpackError::Unpack(Box::new(err), snapshot_archive_path)) + .spawn_scoped(scope, move || -> Result<(), SnapshotError> { + match do_unpack(&snapshot_archive_path) { + Err(err) => Err(UnpackError::Unpack(Box::new(err), snapshot_archive_path).into()), + Ok(Some(path)) => Err(SnapshotError::CrossbeamSend(SendError(path))), + Ok(None) => Ok(()), + } }) .unwrap() } diff --git a/stake-accounts/Cargo.toml b/stake-accounts/Cargo.toml index a83553e3bd8..2bea8eebd91 100644 --- a/stake-accounts/Cargo.toml +++ b/stake-accounts/Cargo.toml @@ -11,6 +11,11 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +# `all-features` can't be used: remote-wallet-hidraw and remote-wallet-libusb +# forward to hidapi's mutually-exclusive linux backends. Document the default +# (hidraw) backend plus the unstable API. +features = ["agave-unstable-api"] +rustdoc-args = ["--cfg=docsrs"] [features] default = ["remote-wallet-hidraw"] @@ -47,3 +52,6 @@ solana-version = { workspace = true } solana-client-traits = { workspace = true } solana-program-binaries = { path = "../program-binaries", features = ["agave-unstable-api"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/storage-bigtable/Cargo.toml b/storage-bigtable/Cargo.toml index b51f10aac1a..ebbc0eba87b 100644 --- a/storage-bigtable/Cargo.toml +++ b/storage-bigtable/Cargo.toml @@ -11,12 +11,21 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] name = "solana_storage_bigtable" [features] +frozen-abi = [ + "dep:solana-frozen-abi", + "dep:solana-frozen-abi-macro", + "solana-signature/frozen-abi", + "solana-transaction/frozen-abi", + "solana-transaction-error/frozen-abi", +] agave-unstable-api = [] [dependencies] @@ -35,6 +44,12 @@ prost-types = { workspace = true } serde = { workspace = true } smpl_jwt = { workspace = true } solana-clock = { workspace = true } +solana-frozen-abi = { workspace = true, optional = true, features = [ + "frozen-abi", +] } +solana-frozen-abi-macro = { workspace = true, optional = true, features = [ + "frozen-abi", +] } solana-message = { workspace = true } solana-metrics = { workspace = true } solana-pubkey = { workspace = true } @@ -57,3 +72,6 @@ solana-keypair = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } solana-system-transaction = { workspace = true } solana-transaction-context = { path = "../transaction-context", features = ["agave-unstable-api", "bincode"] } + +[lints] +workspace = true diff --git a/storage-bigtable/build-proto/Cargo.toml b/storage-bigtable/build-proto/Cargo.toml index f2b65e7a58c..0d18679375b 100644 --- a/storage-bigtable/build-proto/Cargo.toml +++ b/storage-bigtable/build-proto/Cargo.toml @@ -19,3 +19,6 @@ tonic-prost-build = { workspace = true } # envar to point to the installed binary [target."cfg(not(windows))".dependencies] protobuf-src = { workspace = true } + +[lints] +workspace = true diff --git a/storage-bigtable/src/bigtable.rs b/storage-bigtable/src/bigtable.rs index a1971c72596..c64fa09dc58 100644 --- a/storage-bigtable/src/bigtable.rs +++ b/storage-bigtable/src/bigtable.rs @@ -385,10 +385,10 @@ impl) -> InterceptedRequestResult> BigTable { let started = Instant::now(); while let Some(res) = rrr.message().await? { - if let Some(timeout) = self.timeout { - if Instant::now().duration_since(started) > timeout { - return Err(Error::Timeout); - } + if let Some(timeout) = self.timeout + && Instant::now().duration_since(started) > timeout + { + return Err(Error::Timeout); } for (i, mut chunk) in res.chunks.into_iter().enumerate() { // The comments for `read_rows_response::CellChunk` provide essential details for @@ -723,12 +723,12 @@ impl) -> InterceptedRequestResult> BigTable { while let Some(res) = response.message().await? { for entry in res.entries { - if let Some(status) = entry.status { - if status.code != 0 { - eprintln!("delete_rows error {}: {}", status.code, status.message); - warn!("delete_rows error {}: {}", status.code, status.message); - return Err(Error::RowDeleteFailed); - } + if let Some(status) = entry.status + && status.code != 0 + { + eprintln!("delete_rows error {}: {}", status.code, status.message); + warn!("delete_rows error {}: {}", status.code, status.message); + return Err(Error::RowDeleteFailed); } } } @@ -779,12 +779,12 @@ impl) -> InterceptedRequestResult> BigTable { while let Some(res) = response.message().await? { for entry in res.entries { - if let Some(status) = entry.status { - if status.code != 0 { - eprintln!("put_row_data error {}: {}", status.code, status.message); - warn!("put_row_data error {}: {}", status.code, status.message); - return Err(Error::RowWriteFailed); - } + if let Some(status) = entry.status + && status.code != 0 + { + eprintln!("put_row_data error {}: {}", status.code, status.message); + warn!("put_row_data error {}: {}", status.code, status.message); + return Err(Error::RowWriteFailed); } } } diff --git a/storage-bigtable/src/compression.rs b/storage-bigtable/src/compression.rs index ed42a49967c..548e2f8f930 100644 --- a/storage-bigtable/src/compression.rs +++ b/storage-bigtable/src/compression.rs @@ -1,5 +1,13 @@ use std::io::{self, BufReader, Read, Write}; +#[cfg_attr( + feature = "frozen-abi", + derive(StableAbi, StableAbiSample, PartialEq), + frozen_abi( + abi_digest = "6Mqz7t1A92unKc6Ngs1p2GhiRUdWwVSNLSifDDh4KcXv", + test_roundtrip = "eq_and_wire" + ) +)] #[derive(Debug, serde::Serialize, serde::Deserialize)] pub enum CompressionMethod { NoCompression, diff --git a/storage-bigtable/src/lib.rs b/storage-bigtable/src/lib.rs index 322a55ab399..2f65b7d600c 100644 --- a/storage-bigtable/src/lib.rs +++ b/storage-bigtable/src/lib.rs @@ -40,6 +40,10 @@ use { #[macro_use] extern crate solana_metrics; +#[cfg_attr(feature = "frozen-abi", macro_use)] +#[cfg(feature = "frozen-abi")] +extern crate solana_frozen_abi_macro; + mod access_token; mod bigtable; mod compression; @@ -118,6 +122,14 @@ fn key_to_slot(key: &str) -> Option { // Note: in order to continue to support old bincode-serialized bigtable entries, if new fields are // added to ConfirmedBlock, they must either be excluded or set to `default_on_eof` here // +#[cfg_attr( + feature = "frozen-abi", + derive(StableAbi, StableAbiSample, PartialEq), + frozen_abi( + abi_digest = "AqgEWHGTni7ZV6JGTPkvewggW5YQutUEWv3bMUbN7o3f", + test_roundtrip = "eq_and_wire" + ) +)] #[derive(Serialize, Deserialize)] struct StoredConfirmedBlock { previous_blockhash: String, @@ -181,12 +193,34 @@ impl From for ConfirmedBlock { } } +#[cfg_attr(feature = "frozen-abi", derive(StableAbi, StableAbiSample, PartialEq))] #[derive(Serialize, Deserialize)] struct StoredConfirmedBlockTransaction { + #[cfg_attr( + feature = "frozen-abi", + stable_abi_sample(with = "sample_bincode_compatible_transaction(rng)") + )] transaction: VersionedTransaction, meta: Option, } +// `VersionedTransaction`'s V1 layout is wincode-only and has no bincode equivalent, so sampling it +// would make the ABI digest unstable against the future wincode migration. Restrict the sample to +// the legacy/v0 versions — the only formats present in historical bincode-serialized bigtable +// blocks — which encode identically under bincode and wincode. +#[cfg(feature = "frozen-abi")] +fn sample_bincode_compatible_transaction( + rng: &mut (impl solana_frozen_abi::rand::RngCore + ?Sized), +) -> VersionedTransaction { + use solana_frozen_abi::stable_abi::StableAbi; + loop { + let transaction = VersionedTransaction::random(rng); + if !matches!(transaction.message, solana_message::VersionedMessage::V1(_)) { + return transaction; + } + } +} + #[cfg(test)] impl From for StoredConfirmedBlockTransaction { fn from(value: TransactionWithStatusMeta) -> Self { @@ -223,6 +257,7 @@ impl From for TransactionWithStatusMeta { } } +#[cfg_attr(feature = "frozen-abi", derive(StableAbi, StableAbiSample, PartialEq))] #[derive(Serialize, Deserialize)] struct StoredConfirmedBlockTransactionStatusMeta { err: Option, @@ -281,6 +316,7 @@ impl From for StoredConfirmedBlockTransactionStatusMeta { type StoredConfirmedBlockRewards = Vec; +#[cfg_attr(feature = "frozen-abi", derive(StableAbi, StableAbiSample, PartialEq))] #[derive(Serialize, Deserialize)] struct StoredConfirmedBlockReward { pubkey: String, @@ -311,6 +347,14 @@ impl From for StoredConfirmedBlockReward { } // A serialized `TransactionInfo` is stored in the `tx` table +#[cfg_attr( + feature = "frozen-abi", + derive(StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "3RJqJCwpbxdqKp5PLDeoE3xkawxtJYBuZmVPEHYFB8bc", + test_roundtrip = "eq_and_wire" + ) +)] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] struct TransactionInfo { slot: Slot, // The slot that contains the block with this transaction in it @@ -354,6 +398,14 @@ impl From for TransactionStatus { } } +#[cfg_attr( + feature = "frozen-abi", + derive(StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "3j7JBoVWnTHm2vMpZtJUCV2vjbaNdbAHtCrb42UUV3VX", + test_roundtrip = "eq_and_wire" + ) +)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] struct LegacyTransactionByAddrInfo { pub signature: Signature, // The transaction signature @@ -1128,9 +1180,8 @@ impl LedgerStorage { } Some(Ok(fetched_tx_info)) => { warn!( - "skipped tx row {} because the bigtable entry ({:?}) did not match to \ - {:?}", - signature, fetched_tx_info, &expected_tx_info, + "skipped tx row {signature} because the bigtable entry \ + ({fetched_tx_info:?}) did not match to {expected_tx_info:?}", ); } Some(Err(err)) => { diff --git a/storage-proto/Cargo.toml b/storage-proto/Cargo.toml index a715cf2c3ea..147f4ae2323 100644 --- a/storage-proto/Cargo.toml +++ b/storage-proto/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -48,3 +50,6 @@ protobuf-src = { workspace = true } [dev-dependencies] enum-iterator = { workspace = true } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/storage-proto/src/convert.rs b/storage-proto/src/convert.rs index 590e248a762..e61df820e78 100644 --- a/storage-proto/src/convert.rs +++ b/storage-proto/src/convert.rs @@ -78,14 +78,14 @@ impl From for Vec { } } -impl From for (Vec, Option) { +impl From for RewardsAndNumPartitions { fn from(rewards: generated::Rewards) -> Self { - ( - rewards.rewards.into_iter().map(|r| r.into()).collect(), - rewards + Self { + rewards: rewards.rewards.into_iter().map(|r| r.into()).collect(), + num_partitions: rewards .num_partitions .map(|generated::NumPartitions { num_partitions }| num_partitions), - ) + } } } @@ -800,78 +800,78 @@ impl TryFrom for TransactionError { type Error = &'static str; fn try_from(transaction_error: tx_by_addr::TransactionError) -> Result { - if transaction_error.transaction_error == 8 { - if let Some(instruction_error) = transaction_error.instruction_error { - if let Some(custom) = instruction_error.custom { - return Ok(TransactionError::InstructionError( - instruction_error.index as u8, - InstructionError::Custom(custom.custom), - )); - } - - let ie = match instruction_error.error { - 0 => InstructionError::GenericError, - 1 => InstructionError::InvalidArgument, - 2 => InstructionError::InvalidInstructionData, - 3 => InstructionError::InvalidAccountData, - 4 => InstructionError::AccountDataTooSmall, - 5 => InstructionError::InsufficientFunds, - 6 => InstructionError::IncorrectProgramId, - 7 => InstructionError::MissingRequiredSignature, - 8 => InstructionError::AccountAlreadyInitialized, - 9 => InstructionError::UninitializedAccount, - 10 => InstructionError::UnbalancedInstruction, - 11 => InstructionError::ModifiedProgramId, - 12 => InstructionError::ExternalAccountLamportSpend, - 13 => InstructionError::ExternalAccountDataModified, - 14 => InstructionError::ReadonlyLamportChange, - 15 => InstructionError::ReadonlyDataModified, - 16 => InstructionError::DuplicateAccountIndex, - 17 => InstructionError::ExecutableModified, - 18 => InstructionError::RentEpochModified, - #[allow(deprecated)] - 19 => InstructionError::NotEnoughAccountKeys, - 20 => InstructionError::AccountDataSizeChanged, - 21 => InstructionError::AccountNotExecutable, - 22 => InstructionError::AccountBorrowFailed, - 23 => InstructionError::AccountBorrowOutstanding, - 24 => InstructionError::DuplicateAccountOutOfSync, - 26 => InstructionError::InvalidError, - 27 => InstructionError::ExecutableDataModified, - 28 => InstructionError::ExecutableLamportChange, - 29 => InstructionError::ExecutableAccountNotRentExempt, - 30 => InstructionError::UnsupportedProgramId, - 31 => InstructionError::CallDepth, - 32 => InstructionError::MissingAccount, - 33 => InstructionError::ReentrancyNotAllowed, - 34 => InstructionError::MaxSeedLengthExceeded, - 35 => InstructionError::InvalidSeeds, - 36 => InstructionError::InvalidRealloc, - 37 => InstructionError::ComputationalBudgetExceeded, - 38 => InstructionError::PrivilegeEscalation, - 39 => InstructionError::ProgramEnvironmentSetupFailure, - 40 => InstructionError::ProgramFailedToComplete, - 41 => InstructionError::ProgramFailedToCompile, - 42 => InstructionError::Immutable, - 43 => InstructionError::IncorrectAuthority, - 44 => InstructionError::BorshIoError, - 45 => InstructionError::AccountNotRentExempt, - 46 => InstructionError::InvalidAccountOwner, - 47 => InstructionError::ArithmeticOverflow, - 48 => InstructionError::UnsupportedSysvar, - 49 => InstructionError::IllegalOwner, - 50 => InstructionError::MaxAccountsDataAllocationsExceeded, - 51 => InstructionError::MaxAccountsExceeded, - 52 => InstructionError::MaxInstructionTraceLengthExceeded, - 53 => InstructionError::BuiltinProgramsMustConsumeComputeUnits, - _ => return Err("Invalid InstructionError"), - }; - + if transaction_error.transaction_error == 8 + && let Some(instruction_error) = transaction_error.instruction_error + { + if let Some(custom) = instruction_error.custom { return Ok(TransactionError::InstructionError( instruction_error.index as u8, - ie, + InstructionError::Custom(custom.custom), )); } + + let ie = match instruction_error.error { + 0 => InstructionError::GenericError, + 1 => InstructionError::InvalidArgument, + 2 => InstructionError::InvalidInstructionData, + 3 => InstructionError::InvalidAccountData, + 4 => InstructionError::AccountDataTooSmall, + 5 => InstructionError::InsufficientFunds, + 6 => InstructionError::IncorrectProgramId, + 7 => InstructionError::MissingRequiredSignature, + 8 => InstructionError::AccountAlreadyInitialized, + 9 => InstructionError::UninitializedAccount, + 10 => InstructionError::UnbalancedInstruction, + 11 => InstructionError::ModifiedProgramId, + 12 => InstructionError::ExternalAccountLamportSpend, + 13 => InstructionError::ExternalAccountDataModified, + 14 => InstructionError::ReadonlyLamportChange, + 15 => InstructionError::ReadonlyDataModified, + 16 => InstructionError::DuplicateAccountIndex, + 17 => InstructionError::ExecutableModified, + 18 => InstructionError::RentEpochModified, + #[allow(deprecated)] + 19 => InstructionError::NotEnoughAccountKeys, + 20 => InstructionError::AccountDataSizeChanged, + 21 => InstructionError::AccountNotExecutable, + 22 => InstructionError::AccountBorrowFailed, + 23 => InstructionError::AccountBorrowOutstanding, + 24 => InstructionError::DuplicateAccountOutOfSync, + 26 => InstructionError::InvalidError, + 27 => InstructionError::ExecutableDataModified, + 28 => InstructionError::ExecutableLamportChange, + 29 => InstructionError::ExecutableAccountNotRentExempt, + 30 => InstructionError::UnsupportedProgramId, + 31 => InstructionError::CallDepth, + 32 => InstructionError::MissingAccount, + 33 => InstructionError::ReentrancyNotAllowed, + 34 => InstructionError::MaxSeedLengthExceeded, + 35 => InstructionError::InvalidSeeds, + 36 => InstructionError::InvalidRealloc, + 37 => InstructionError::ComputationalBudgetExceeded, + 38 => InstructionError::PrivilegeEscalation, + 39 => InstructionError::ProgramEnvironmentSetupFailure, + 40 => InstructionError::ProgramFailedToComplete, + 41 => InstructionError::ProgramFailedToCompile, + 42 => InstructionError::Immutable, + 43 => InstructionError::IncorrectAuthority, + 44 => InstructionError::BorshIoError, + 45 => InstructionError::AccountNotRentExempt, + 46 => InstructionError::InvalidAccountOwner, + 47 => InstructionError::ArithmeticOverflow, + 48 => InstructionError::UnsupportedSysvar, + 49 => InstructionError::IllegalOwner, + 50 => InstructionError::MaxAccountsDataAllocationsExceeded, + 51 => InstructionError::MaxAccountsExceeded, + 52 => InstructionError::MaxInstructionTraceLengthExceeded, + 53 => InstructionError::BuiltinProgramsMustConsumeComputeUnits, + _ => return Err("Invalid InstructionError"), + }; + + return Ok(TransactionError::InstructionError( + instruction_error.index as u8, + ie, + )); } if let Some(transaction_details) = transaction_error.transaction_details { diff --git a/streamer/Cargo.toml b/streamer/Cargo.toml index bbb1a01b7f1..543b7e93d72 100644 --- a/streamer/Cargo.toml +++ b/streamer/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -33,12 +35,12 @@ log = { workspace = true } nix = { workspace = true, features = ["net", "poll", "signal"] } num_cpus = { workspace = true } pem = { workspace = true } -percentage = { workspace = true } quinn = { workspace = true } rand = { workspace = true } rustls = { workspace = true } smallvec = { workspace = true } solana-keypair = { workspace = true } +solana-measure = { workspace = true } solana-metrics = { workspace = true } solana-net-utils = { workspace = true } solana-packet = { workspace = true } @@ -60,4 +62,8 @@ chrono = { workspace = true, features = ["now"] } clap = { version = "4.5.31", features = ["cargo", "derive", "error-context"] } solana-message = { workspace = true } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api", "dev-context-only-utils"] } +solana-pubkey = { workspace = true, features = ["rand"] } solana-streamer = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/streamer/examples/swqos.rs b/streamer/examples/swqos.rs index 67bc2049355..ae3312a7d35 100644 --- a/streamer/examples/swqos.rs +++ b/streamer/examples/swqos.rs @@ -142,7 +142,7 @@ async fn main() -> anyhow::Result<()> { NaiveTime::MIN, ); let logfile = std::fs::File::create(&path)?; - info!("Logfile in {}", &path); + info!("Logfile in {path}"); let mut logfile = std::io::BufWriter::new(logfile); let mut sum = 0; for batch in receiver { diff --git a/streamer/src/evicting_sender.rs b/streamer/src/evicting_sender.rs index 847952a684c..9b7d0a5fa40 100644 --- a/streamer/src/evicting_sender.rs +++ b/streamer/src/evicting_sender.rs @@ -1,15 +1,24 @@ use { crate::streamer::ChannelSend, - crossbeam_channel::{Receiver, SendError, Sender, TryRecvError, TrySendError, bounded}, + crossbeam_channel::{Receiver, Sender, TryRecvError, TrySendError, bounded}, }; /// A sender implementation that evicts the oldest message when the channel is full. -#[derive(Clone)] pub struct EvictingSender { sender: Sender, receiver: Receiver, } +// Manual implementation of Clone since `T` is not required to implement Clone. +impl Clone for EvictingSender { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + receiver: self.receiver.clone(), + } + } +} + impl EvictingSender { /// Create a new evicting sender with provided sender, receiver. #[inline] @@ -29,11 +38,6 @@ impl ChannelSend for EvictingSender where T: Send + 'static, { - #[inline] - fn send(&self, msg: T) -> std::result::Result<(), SendError> { - self.sender.send(msg) - } - fn try_send(&self, msg: T) -> std::result::Result<(), TrySendError> { let Err(e) = self.sender.try_send(msg) else { return Ok(()); diff --git a/streamer/src/nonblocking/quic.rs b/streamer/src/nonblocking/quic.rs index 396d6397349..7592b601e87 100644 --- a/streamer/src/nonblocking/quic.rs +++ b/streamer/src/nonblocking/quic.rs @@ -22,7 +22,7 @@ use { solana_packet::Meta, solana_perf::packet::{BytesPacket, PacketBatch}, solana_pubkey::Pubkey, - solana_tls_utils::get_pubkey_from_tls_certificate, + solana_tls_utils::get_remote_pubkey, std::{ array, fmt, iter::repeat_with, @@ -329,6 +329,16 @@ where } if let Ok(Some(incoming)) = timeout_connection { + // our connection/handshake abuse mitigation policy is one of shed + // fast and bound resource consumption. attempting to be "smarter" + // before a peer has asserted control over their ip address by + // completing the retry challenge creates a scenario whereby peers + // can attack one another via ip spoofing. employ the following + // * limit duration of in-flight connection attempts with a timeout + // * protect against connection attempt bursts with a global rate-limiter + // * rate-limit abusive peers by (control-asserted) ip + // * cap total connections per-peer/ip + stats .total_incoming_connection_attempts .fetch_add(1, Ordering::Relaxed); @@ -403,17 +413,6 @@ where tasks.wait().await; } -pub fn get_remote_pubkey(connection: &Connection) -> Option { - // Use the client cert only if it is self signed and the chain length is 1. - connection - .peer_identity()? - .downcast::>() - .ok() - .filter(|certs| certs.len() == 1)? - .first() - .and_then(get_pubkey_from_tls_certificate) -} - pub fn get_connection_stake( connection: &Connection, staked_nodes: &RwLock, @@ -1141,7 +1140,7 @@ pub mod test { }, }, assert_matches::assert_matches, - crossbeam_channel::{Receiver, unbounded}, + crossbeam_channel::{Receiver, bounded}, quinn::{ApplicationClose, ConnectionError}, solana_keypair::Keypair, solana_net_utils::sockets::bind_to_localhost_unique, @@ -1588,7 +1587,7 @@ pub mod test { async fn test_quic_server_unstaked_node_connect_failure() { agave_logger::setup(); let s = bind_to_localhost_unique().expect("should bind"); - let (sender, _) = unbounded(); + let (sender, _) = bounded(1024); let keypair = Keypair::new(); let server_address = s.local_addr().unwrap(); let staked_nodes = Arc::new(RwLock::new(StakedNodes::default())); @@ -1624,7 +1623,7 @@ pub mod test { async fn test_quic_server_multiple_streams() { agave_logger::setup(); let s = bind_to_localhost_unique().expect("should bind"); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let keypair = Keypair::new(); let server_address = s.local_addr().unwrap(); let staked_nodes = Arc::new(RwLock::new(StakedNodes::default())); diff --git a/streamer/src/nonblocking/simple_qos.rs b/streamer/src/nonblocking/simple_qos.rs index b26dbc03b8a..177533e4d58 100644 --- a/streamer/src/nonblocking/simple_qos.rs +++ b/streamer/src/nonblocking/simple_qos.rs @@ -326,22 +326,21 @@ impl QosController for SimpleQos { update_open_connections_stat(&self.stats, &connection_table_l); } - if connection_table_l.total_size < self.config.max_staked_connections { - if let Ok((last_update, cancel_connection, stream_counter)) = self + if connection_table_l.total_size < self.config.max_staked_connections + && let Ok((last_update, cancel_connection, stream_counter)) = self .cache_new_connection( client_connection_tracker, connection, connection_table_l, conn_context, ) - { - self.stats - .connection_added_from_staked_peer - .fetch_add(1, Ordering::Relaxed); - conn_context.last_update = last_update; - conn_context.stream_counter = Some(stream_counter); - return Some(cancel_connection); - } + { + self.stats + .connection_added_from_staked_peer + .fetch_add(1, Ordering::Relaxed); + conn_context.last_update = last_update; + conn_context.stream_counter = Some(stream_counter); + return Some(cancel_connection); } None } diff --git a/streamer/src/nonblocking/stream_throttle.rs b/streamer/src/nonblocking/stream_throttle.rs index f4056b673ed..413139234b1 100644 --- a/streamer/src/nonblocking/stream_throttle.rs +++ b/streamer/src/nonblocking/stream_throttle.rs @@ -3,7 +3,6 @@ use { nonblocking::{qos::OpaqueStreamerCounter, quic::ConnectionPeerType}, quic::StreamerStats, }, - percentage::Percentage, std::{ sync::{ Arc, RwLock, @@ -16,8 +15,8 @@ use { /// Max TPS allowed for unstaked connection const MAX_UNSTAKED_TPS: u64 = 200; -/// Expected % of max TPS to be consumed by unstaked connections -const EXPECTED_UNSTAKED_STREAMS_PERCENT: u64 = 20; +/// Expected fraction of max TPS to be consumed by unstaked connections +const EXPECTED_UNSTAKED_STREAMS_RATIO: f64 = 0.20; pub const STREAM_THROTTLING_INTERVAL_MS: u64 = 100; pub const STREAM_THROTTLING_INTERVAL: Duration = @@ -30,7 +29,7 @@ const STREAM_LOAD_EMA_INTERVAL_MS: u64 = 5; // before throttling activates. const STREAM_LOAD_EMA_INTERVAL_COUNT: u64 = 40; -const STAKED_THROTTLING_ON_LOAD_THRESHOLD_PERCENT: u64 = 95; +const STAKED_THROTTLING_ON_LOAD_THRESHOLD_RATIO: f64 = 0.95; pub(crate) struct StakedStreamLoadEMA { current_load_ema: AtomicU64, @@ -53,7 +52,7 @@ impl StakedStreamLoadEMA { let allow_unstaked_streams = max_unstaked_connections > 0; let max_staked_load_in_ms = if allow_unstaked_streams { max_streams_per_ms - - Percentage::from(EXPECTED_UNSTAKED_STREAMS_PERCENT).apply_to(max_streams_per_ms) + - ((EXPECTED_UNSTAKED_STREAMS_RATIO * (max_streams_per_ms as f64)) as u64) } else { max_streams_per_ms }; @@ -68,9 +67,9 @@ impl StakedStreamLoadEMA { 0 }; - let staked_throttling_on_load_threshold = - Percentage::from(STAKED_THROTTLING_ON_LOAD_THRESHOLD_PERCENT) - .apply_to(max_staked_load_in_ema_interval); + let staked_throttling_on_load_threshold = (STAKED_THROTTLING_ON_LOAD_THRESHOLD_RATIO + * (max_staked_load_in_ema_interval as f64)) + as u64; Self { current_load_ema: AtomicU64::default(), diff --git a/streamer/src/nonblocking/swqos.rs b/streamer/src/nonblocking/swqos.rs index c84e6b2a025..8ec246f5c60 100644 --- a/streamer/src/nonblocking/swqos.rs +++ b/streamer/src/nonblocking/swqos.rs @@ -20,7 +20,6 @@ use { }, streamer::StakedNodes, }, - percentage::Percentage, quinn::{Connection, VarInt}, solana_time_utils as timing, std::{ @@ -246,10 +245,9 @@ impl SwQos { stats: Arc, ) { if unstaked_connection_table.total_size >= max_unstaked_connections { - const PRUNE_TABLE_TO_PERCENTAGE: u8 = 90; - let max_percentage_full = Percentage::from(PRUNE_TABLE_TO_PERCENTAGE); - - let max_connections = max_percentage_full.apply_to(max_unstaked_connections); + // Prune the connection table down to 90% capacity + const PRUNE_TABLE_RATIO: f64 = 0.90; + let max_connections = (PRUNE_TABLE_RATIO * (max_unstaked_connections as f64)) as usize; let num_pruned = unstaked_connection_table.prune_oldest(max_connections); stats .num_evictions_unstaked diff --git a/streamer/src/nonblocking/testing_utilities.rs b/streamer/src/nonblocking/testing_utilities.rs index 06c8e8f50ee..fdb2faae378 100644 --- a/streamer/src/nonblocking/testing_utilities.rs +++ b/streamer/src/nonblocking/testing_utilities.rs @@ -10,7 +10,7 @@ use { quic_socket::QuicSocket, streamer::StakedNodes, }, - crossbeam_channel::{Receiver, Sender, unbounded}, + crossbeam_channel::{Receiver, Sender, bounded}, quinn::{ ClientConfig, Connection, EndpointConfig, IdleTimeout, TokioRuntime, TransportConfig, crypto::rustls::QuicClientConfig, @@ -121,7 +121,7 @@ pub fn setup_quic_server( qos_config: SwQosConfig, ) -> SpawnTestServerResult { let sockets = create_quic_server_sockets(); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let keypair = Keypair::new(); let server_address = sockets[0].local_addr().unwrap(); let staked_nodes = Arc::new(RwLock::new(option_staked_nodes.unwrap_or_default())); diff --git a/streamer/src/packet.rs b/streamer/src/packet.rs index 4136373b076..62b45173520 100644 --- a/streamer/src/packet.rs +++ b/streamer/src/packet.rs @@ -285,10 +285,10 @@ pub fn send_to( ) -> Result<()> { for p in batch.iter() { let addr = p.meta().socket_addr(); - if socket_addr_space.check(&addr) { - if let Some(data) = p.data(..) { - socket.send_to(data, addr)?; - } + if socket_addr_space.check(&addr) + && let Some(data) = p.data(..) + { + socket.send_to(data, addr)?; } } Ok(()) diff --git a/streamer/src/quic.rs b/streamer/src/quic.rs index f8b3fab6a86..d8b6b64b9a3 100644 --- a/streamer/src/quic.rs +++ b/streamer/src/quic.rs @@ -724,7 +724,7 @@ mod test { check_multiple_streams, make_client_endpoint, make_client_endpoint_with_bind_ip, }, }, - crossbeam_channel::{Receiver, unbounded}, + crossbeam_channel::{Receiver, bounded}, solana_net_utils::sockets::bind_to_localhost_unique, solana_pubkey::Pubkey, solana_signer::Signer, @@ -755,7 +755,7 @@ mod test { Arc, ) { let s = bind_to_localhost_unique().expect("should bind"); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let keypair = Keypair::new(); let server_address = s.local_addr().unwrap(); let cancel = CancellationToken::new(); @@ -791,7 +791,7 @@ mod test { let server_params = QuicStreamerConfig::default_for_tests(); let s = bind_to_localhost_unique().expect("should bind"); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let keypair = Keypair::new(); let server_address = s.local_addr().unwrap(); let cancel = CancellationToken::new(); @@ -846,7 +846,7 @@ mod test { fn test_quic_server_multiple_streams() { agave_logger::setup(); let s = bind_to_localhost_unique().expect("should bind"); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let keypair = Keypair::new(); let server_address = s.local_addr().unwrap(); let staked_nodes = Arc::new(RwLock::new(StakedNodes::default())); @@ -1016,11 +1016,11 @@ mod test { .await; // Rejection can happen at handshake or when opening streams. - if let Ok(connection) = post_ban { - if let Ok(mut stream) = connection.open_uni().await { - let _ = stream.write_all(&[7u8]).await; - let _ = stream.finish(); - } + if let Ok(connection) = post_ban + && let Ok(mut stream) = connection.open_uni().await + { + let _ = stream.write_all(&[7u8]).await; + let _ = stream.finish(); } // Ensure nothing from the post-ban attempt made it through. @@ -1038,7 +1038,7 @@ mod test { fn test_quic_server_unstaked_node_connect_failure() { agave_logger::setup(); let s = bind_to_localhost_unique().expect("should bind"); - let (sender, _) = unbounded(); + let (sender, _) = bounded(1024); let keypair = Keypair::new(); let server_address = s.local_addr().unwrap(); let staked_nodes = Arc::new(RwLock::new(StakedNodes::default())); diff --git a/streamer/src/quic_socket.rs b/streamer/src/quic_socket.rs index 30b883d851f..27e673e3c6e 100644 --- a/streamer/src/quic_socket.rs +++ b/streamer/src/quic_socket.rs @@ -7,7 +7,6 @@ use { }, bytes::Bytes, crossbeam_channel::TrySendError, - nix::ifaddrs::getifaddrs, quinn::{ AsyncUdpSocket, Runtime, TokioRuntime, UdpPoller, udp::{EcnCodepoint as QuinnEcnCodepoint, RecvMeta, Transmit}, @@ -17,10 +16,7 @@ use { io::{self, IoSliceMut}, net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, pin::Pin, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, + sync::Arc, task::{Context, Poll}, }, }; @@ -151,8 +147,7 @@ impl AsyncUdpSocket for QuicXdpTxSocket { // `QuicXdpSender` could enqueue, throughput may be temporarily suboptimal until the UDP // socket becomes writable. The reverse mismatch is also possible: the UDP poller is ready // but the selected `QuicXdpSender` channel is full. In this case `try_send` fails with - // `WouldBlock`, and the caller invokes `poll_writable` again, which can select another - // channel in the next round. + // `WouldBlock`, and the caller invokes `poll_writable` again before retrying. self.udp_socket.clone().create_io_poller() } @@ -230,24 +225,20 @@ impl AsyncUdpSocket for QuicXdpTxSocket { } } -/// [`QuicXdpSender`] wraps [`XdpSender`] and provides round-robin sender selection. +/// [`QuicXdpSender`] wraps [`XdpSender`] and provides destination-based sender selection. /// -/// This wrapper provides a simple round-robin sender index for each packet sent. It is required -/// because `AsyncUdpSocket::try_send` does not provide a way to specify the sender index. If the -/// `XdpSender` has only one sender, the index is always 0. +/// This wrapper maps each remote IP to a stable XDP sender index. Keeping packets for the same +/// remote host on one TX queue avoids queue-induced reordering within packet bursts. struct QuicXdpSender { xdp_sender: XdpSender, src_addr: SocketAddrV4, - next_sender_index: AtomicUsize, } impl QuicXdpSender { fn new(xdp_sender: XdpSender, src_addr: SocketAddrV4) -> Self { - let next_sender_index = AtomicUsize::new(0); Self { xdp_sender, src_addr, - next_sender_index, } } @@ -258,23 +249,31 @@ impl QuicXdpSender { ecn: Option, payload: Bytes, ) -> Result<(), TrySendError> { - let sender_idx = self.next_sender_index.fetch_add(1, Ordering::Relaxed); + // Keep packets for the same remote IP on the same XDP TX queue when there is more than + // one queue. Avoid hashing entirely for the common single-sender case. + let sender_key = if self.xdp_sender.len() == 1 { + 0 + } else { + fold_xor(destination_ip_key(&destination)) as usize + }; let src_ip = src_ip.unwrap_or(*self.src_addr.ip()); - // Respect Quinn's per-packet source IP, used for wildcard-bound sockets, while - // keeping the port from `self.src_addr`. + // Respect Quinn's per-packet source IP, used for wildcard-bound sockets, while keeping the + // port from `self.src_addr`. let src_addr = SocketAddrV4::new(src_ip, self.src_addr.port()); let ecn = ecn.map(quinn_ecn_to_xdp); - self.xdp_sender.try_send( - sender_idx, - BytesTxPacket::new(src_addr, destination, ecn, payload), - ) + let mut packet = BytesTxPacket::new(src_addr, destination, ecn, payload); + packet.set_allow_mtu_overflow(true); + self.xdp_sender.try_send(sender_key, packet) } } /// Collects IPv4 addresses assigned to local network interfaces. +#[cfg(target_os = "linux")] fn collect_local_ipv4_ips() -> io::Result> { + use nix::ifaddrs::getifaddrs; + let mut ips = Vec::new(); for ifa in getifaddrs().map_err(io::Error::other)? { let Some(addr) = ifa.address else { continue }; @@ -288,6 +287,11 @@ fn collect_local_ipv4_ips() -> io::Result> { Ok(ips) } +#[cfg(not(target_os = "linux"))] +fn collect_local_ipv4_ips() -> io::Result> { + Ok(Vec::new()) +} + #[inline] const fn quinn_ecn_to_xdp(ecn: QuinnEcnCodepoint) -> XdpEcnCodepoint { match ecn { @@ -296,3 +300,18 @@ const fn quinn_ecn_to_xdp(ecn: QuinnEcnCodepoint) -> XdpEcnCodepoint { QuinnEcnCodepoint::Ce => XdpEcnCodepoint::Ce, } } + +#[inline] +fn fold_xor(mut x: u32) -> u32 { + x ^= x >> 16; + x ^= x >> 8; + x +} + +#[inline] +fn destination_ip_key(destination: &SocketAddr) -> u32 { + match destination { + SocketAddr::V4(destination) => u32::from(*destination.ip()), + SocketAddr::V6(_) => unreachable!("IPv6 destinations are rejected before AF_XDP send"), + } +} diff --git a/streamer/src/streamer.rs b/streamer/src/streamer.rs index 7bed9a26f5f..cf20bd9ffd9 100644 --- a/streamer/src/streamer.rs +++ b/streamer/src/streamer.rs @@ -11,6 +11,7 @@ use { }, crossbeam_channel::{Receiver, RecvTimeoutError, SendError, Sender, TrySendError}, histogram::Histogram, + solana_measure::measure::Measure, solana_net_utils::{ SocketAddrSpace, multihomed_sockets::{ @@ -19,11 +20,10 @@ use { }, }, solana_pubkey::Pubkey, - solana_time_utils::timestamp, std::{ cmp::Reverse, collections::HashMap, - net::{IpAddr, UdpSocket}, + net::{IpAddr, SocketAddr, UdpSocket}, sync::{ Arc, atomic::{AtomicBool, AtomicUsize, Ordering}, @@ -40,8 +40,6 @@ use { }; pub trait ChannelSend: Send + 'static { - fn send(&self, msg: T) -> std::result::Result<(), SendError>; - fn try_send(&self, msg: T) -> std::result::Result<(), TrySendError>; fn is_empty(&self) -> bool; @@ -53,11 +51,6 @@ impl ChannelSend for Sender where T: Send + 'static, { - #[inline] - fn send(&self, msg: T) -> std::result::Result<(), SendError> { - self.send(msg) - } - #[inline] fn try_send(&self, msg: T) -> std::result::Result<(), TrySendError> { self.try_send(msg) @@ -459,26 +452,6 @@ impl StakedNodes { } } -fn recv_send( - sock: &UdpSocket, - r: &PacketBatchReceiver, - socket_addr_space: &SocketAddrSpace, - stats: &mut Option, -) -> Result<()> { - let timer = Duration::new(1, 0); - let packet_batch = r.recv_timeout(timer)?; - if let Some(stats) = stats { - packet_batch.iter().for_each(|p| stats.record(p)); - } - let packets = packet_batch.iter().filter_map(|pkt| { - let addr = pkt.meta().socket_addr(); - let data = pkt.data(..)?; - socket_addr_space.check(&addr).then_some((data, addr)) - }); - batch_send(sock, packets.collect::>())?; - Ok(()) -} - pub fn recv_packet_batches( recvr: &PacketBatchReceiver, soft_receive_limit: usize, @@ -507,26 +480,27 @@ pub fn recv_packet_batches( Ok((packet_batches, num_packets, recv_duration)) } -pub fn responder_atomic( - name: &'static str, - sockets: Arc<[UdpSocket]>, - bind_ip_addrs: Arc, - r: PacketBatchReceiver, +struct ServeRepairSocketProvider { + socket: Arc, socket_addr_space: SocketAddrSpace, - stats_reporter_sender: Option>>, -) -> JoinHandle<()> { - Builder::new() - .name(format!("solRspndr{name}")) - .spawn(move || { - responder_loop( - MultihomedSocketProvider::new(sockets, bind_ip_addrs), - name, - r, - socket_addr_space, - stats_reporter_sender, - ); - }) - .unwrap() +} + +impl ResponseSender for ServeRepairSocketProvider { + fn send_batch(&self, batch: PacketBatch) -> std::result::Result<(), SendPktsError> { + let packets = filter_packets_by_socket_addr_space(batch.iter(), &self.socket_addr_space); + batch_send(self.socket.as_ref(), packets.collect::>()) + } +} + +pub fn filter_packets_by_socket_addr_space<'a>( + packets: impl Iterator> + 'a, + socket_addr_space: &'a SocketAddrSpace, +) -> impl Iterator + 'a { + packets.filter_map(move |pkt| { + let addr = pkt.meta().socket_addr(); + let data = pkt.data(..)?; + socket_addr_space.check(&addr).then_some((data, addr)) + }) } pub fn responder( @@ -540,26 +514,40 @@ pub fn responder( .name(format!("solRspndr{name}")) .spawn(move || { responder_loop( - FixedSocketProvider::new(sock), name, r, - socket_addr_space, + ServeRepairSocketProvider { + socket: sock, + socket_addr_space, + }, stats_reporter_sender, ); }) .unwrap() } -fn responder_loop( - provider: P, +pub trait ResponseSender { + /// Send a batch of packets. + /// + /// Returns Ok if all the packets with valid destination within batch were sent successfully, + /// and returns an error if any packet within the batch failed to send with number of failed + /// packets. + fn send_batch(&self, batch: PacketBatch) -> std::result::Result<(), SendPktsError>; +} + +pub fn responder_loop( name: &'static str, r: PacketBatchReceiver, - socket_addr_space: SocketAddrSpace, + sender: G, stats_reporter_sender: Option>>, ) { + const SEND_REPORTING_INTERVAL: Duration = Duration::from_secs(1); let mut errors = 0; let mut last_error = None; - let mut last_print = 0; + let mut send_elapsed_us: u64 = 0; + let mut send_batch_count: u64 = 0; + + let mut now = Instant::now(); let mut stats = None; if stats_reporter_sender.is_some() { @@ -567,28 +555,58 @@ fn responder_loop( } loop { - let sock = provider.current_socket_ref(); - if let Err(e) = recv_send(sock, &r, &socket_addr_space, &mut stats) { - match e { - StreamerError::RecvTimeout(RecvTimeoutError::Disconnected) => break, - StreamerError::RecvTimeout(RecvTimeoutError::Timeout) => (), - _ => { - errors += 1; - last_error = Some(e); - } + let timer = Duration::new(1, 0); + let packet_batch = match r.recv_timeout(timer) { + Ok(batch) => Some(batch), + Err(RecvTimeoutError::Disconnected) => break, + Err(RecvTimeoutError::Timeout) => None, + }; + if let Some(packet_batch) = packet_batch { + if let Some(stats) = stats.as_mut() { + packet_batch.iter().for_each(|p| stats.record(p)); } + let mut measure_send = Measure::start("send batch"); + if let Err(e) = sender.send_batch(packet_batch) { + errors += 1; + last_error = Some(StreamerError::SendPktsError(e)); + } + measure_send.stop(); + send_elapsed_us = send_elapsed_us.saturating_add(measure_send.as_us()); + send_batch_count = send_batch_count.saturating_add(1); } - let now = timestamp(); - if now - last_print > 1000 && errors != 0 { - datapoint_info!(name, ("errors", errors, i64),); - info!("{name} last-error: {last_error:?} count: {errors}"); - last_print = now; - errors = 0; - } - if let Some(ref stats_reporter_sender) = stats_reporter_sender { - if let Some(ref mut stats) = stats { - stats.maybe_submit(name, stats_reporter_sender); + + // Metrics reporting + let sample_duration = now.elapsed(); + if sample_duration > SEND_REPORTING_INTERVAL { + datapoint_info!( + name, + // how long it took to send batches of packets during this interval + ("streamer-send-egress_time_us", send_elapsed_us as i64, i64), + ( + "streamer-send-egress_batch_count", + send_batch_count as i64, + i64 + ), + ( + "streamer-send-egress_sample_duration_ms", + sample_duration.as_millis() as i64, + i64 + ), + ); + send_elapsed_us = 0; + send_batch_count = 0; + if errors != 0 { + datapoint_info!(name, ("errors", errors, i64),); + info!("{name} last-error: {last_error:?} count: {errors}"); + errors = 0; + last_error = None; } + now = Instant::now(); + } + if let Some(ref stats_reporter_sender) = stats_reporter_sender + && let Some(ref mut stats) = stats + { + stats.maybe_submit(name, stats_reporter_sender); } } } @@ -601,7 +619,7 @@ mod test { packet::{PACKET_DATA_SIZE, Packet, RecycledPacketBatch}, streamer::{receiver, responder}, }, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_net_utils::sockets::bind_to_localhost_unique, solana_perf::recycler::Recycler, std::{ @@ -641,7 +659,7 @@ mod test { let addr = read.local_addr().unwrap(); let send = bind_to_localhost_unique().expect("should bind sender"); let exit = Arc::new(AtomicBool::new(false)); - let (s_reader, r_reader) = unbounded(); + let (s_reader, r_reader) = bounded(1024); let stats = Arc::new(StreamerReceiveStats::new("test")); let t_receiver = receiver( "solRcvrTest".to_string(), @@ -656,7 +674,7 @@ mod test { ); const NUM_PACKETS: usize = 5; let t_responder = { - let (s_responder, r_responder) = unbounded(); + let (s_responder, r_responder) = bounded(1024); let t_responder = responder( "SendTest", Arc::new(send), diff --git a/svm-callback/Cargo.toml b/svm-callback/Cargo.toml index 37772d763c8..46382a93d72 100644 --- a/svm-callback/Cargo.toml +++ b/svm-callback/Cargo.toml @@ -9,6 +9,11 @@ license = { workspace = true } edition = { workspace = true } readme = false +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] diff --git a/svm-callback/src/lib.rs b/svm-callback/src/lib.rs index 4073fb37cf1..744ad2c75e3 100644 --- a/svm-callback/src/lib.rs +++ b/svm-callback/src/lib.rs @@ -33,7 +33,7 @@ pub trait InvokeContextCallback { } /// Runtime callbacks for transaction processing. -pub trait TransactionProcessingCallback: InvokeContextCallback { +pub trait TransactionProcessingCallback { fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)>; fn inspect_account(&self, _address: &Pubkey, _account_state: AccountState, _is_writable: bool) { diff --git a/svm-feature-set/Cargo.toml b/svm-feature-set/Cargo.toml index 6c4b24d9022..b6758caa35a 100644 --- a/svm-feature-set/Cargo.toml +++ b/svm-feature-set/Cargo.toml @@ -9,6 +9,11 @@ license = { workspace = true } edition = { workspace = true } readme = false +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] diff --git a/svm-log-collector/Cargo.toml b/svm-log-collector/Cargo.toml index 70ee805d50f..3691684e904 100644 --- a/svm-log-collector/Cargo.toml +++ b/svm-log-collector/Cargo.toml @@ -11,9 +11,14 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] [dependencies] log = { workspace = true } + +[lints] +workspace = true diff --git a/svm-measure/Cargo.toml b/svm-measure/Cargo.toml index e415a31acd3..e5f9bc77b02 100644 --- a/svm-measure/Cargo.toml +++ b/svm-measure/Cargo.toml @@ -11,6 +11,11 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] + +[lints] +workspace = true diff --git a/svm-test-harness/.gitignore b/svm-test-harness/.gitignore deleted file mode 100644 index 995bba6cb2c..00000000000 --- a/svm-test-harness/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dump diff --git a/svm-test-harness/Cargo.toml b/svm-test-harness/Cargo.toml deleted file mode 100644 index 02ec56401cd..00000000000 --- a/svm-test-harness/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "solana-svm-test-harness" -description = "Solana SVM test harness." -documentation = "https://docs.rs/solana-svm-test-harness" -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = { workspace = true } -workspace = "../dev-bins" -publish = false - -[[bin]] -name = "test_exec_instr" -path = "bin/test_exec_instr.rs" -required-features = ["fuzz"] - -[features] -agave-unstable-api = [] -dev-context-only-utils = [] -dummy-for-ci-check = ["fuzz"] -fuzz = [ - "dep:clap", - "dep:prost", - "solana-svm-test-harness-fixture/fuzz", - "solana-svm-test-harness-instr/fuzz", -] - -[dependencies] -clap = { version = "4.5.2", features = ["derive"], optional = true } -prost = { workspace = true, optional = true } -solana-svm-test-harness-fixture = { workspace = true } -solana-svm-test-harness-instr = { workspace = true } - -[lints] -workspace = true diff --git a/svm-test-harness/Makefile b/svm-test-harness/Makefile deleted file mode 100644 index d00378429a4..00000000000 --- a/svm-test-harness/Makefile +++ /dev/null @@ -1,8 +0,0 @@ - -CARGO?=cargo - -test: | binaries - -binaries: - $(CARGO) rustc --manifest-path ./Cargo.toml --lib --release --features fuzz --crate-type cdylib - $(CARGO) build --manifest-path ./Cargo.toml --bins --release --features fuzz diff --git a/svm-test-harness/bin/test_exec_instr.rs b/svm-test-harness/bin/test_exec_instr.rs deleted file mode 100644 index 653f50b208a..00000000000 --- a/svm-test-harness/bin/test_exec_instr.rs +++ /dev/null @@ -1,52 +0,0 @@ -use { - clap::Parser, - prost::Message, - solana_svm_test_harness::instr::{ - fixture::proto::InstrFixture as ProtoInstrFixture, fuzz::execute_instr_proto, - }, - std::path::PathBuf, -}; - -#[derive(Parser)] -#[command(version, about, long_about = None)] -struct Cli { - inputs: Vec, -} - -fn exec(input: &PathBuf) -> bool { - let blob = std::fs::read(input).unwrap(); - let fixture = ProtoInstrFixture::decode(&blob[..]).unwrap(); - let Some(context) = fixture.input else { - println!("No context found."); - return false; - }; - - let Some(expected) = fixture.output else { - println!("No fixture found."); - return false; - }; - let Some(effects) = execute_instr_proto(context) else { - println!("FAIL: No instruction effects returned for input: {input:?}",); - return false; - }; - - let ok = effects == expected; - - if ok { - println!("OK: {input:?}"); - } else { - println!("FAIL: {input:?}"); - } - ok -} - -fn main() { - let cli = Cli::parse(); - let mut fail_cnt: i32 = 0; - for input in cli.inputs { - if !exec(&input) { - fail_cnt = fail_cnt.saturating_add(1); - } - } - std::process::exit(fail_cnt); -} diff --git a/svm-test-harness/fixture/Cargo.toml b/svm-test-harness/fixture/Cargo.toml deleted file mode 100644 index a8cf8def630..00000000000 --- a/svm-test-harness/fixture/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "solana-svm-test-harness-fixture" -description = "Solana SVM test harness fixture types." -documentation = "https://docs.rs/solana-svm-test-harness-fixture" -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = { workspace = true } -workspace = "../../dev-bins" -publish = false - -[features] -agave-unstable-api = [] -dev-context-only-utils = [] -dummy-for-ci-check = ["fuzz"] -fuzz = [ - "dep:agave-feature-set", - "dep:bincode", - "dep:prost", - "dep:prost-build", - "dep:protosol", - "solana-pubkey/default", -] - -[dependencies] -agave-feature-set = { workspace = true, optional = true } -bincode = { workspace = true, optional = true } -prost = { workspace = true, optional = true } -protosol = { workspace = true, optional = true } -solana-account = { workspace = true } -solana-instruction = { workspace = true } -solana-instruction-error = { workspace = true, features = ["serde"] } -solana-pubkey = { workspace = true } -solana-svm-feature-set = { workspace = true } -thiserror = { workspace = true } - -[build-dependencies] -prost-build = { workspace = true, optional = true } - -[lints] -workspace = true diff --git a/svm-test-harness/fixture/build.rs b/svm-test-harness/fixture/build.rs deleted file mode 100644 index f3010ef2a4c..00000000000 --- a/svm-test-harness/fixture/build.rs +++ /dev/null @@ -1,44 +0,0 @@ -fn main() -> Result<(), Box> { - // Only compile protobuf files when the fuzz feature is enabled. - #[cfg(feature = "fuzz")] - { - use std::{env, fs, path::PathBuf}; - - // Get absolute proto dir from producer - let proto_dir = PathBuf::from( - env::var("DEP_PROTOSOL_PROTO_DIR") - .expect("protosol did not expose PROTO_DIR, did protosol build.rs run first?"), - ); - - println!("cargo:rerun-if-env-changed=DEP_PROTOSOL_PROTO_DIR"); - println!("cargo:rerun-if-changed={}", proto_dir.display()); - - // Collect absolute .proto paths - let mut proto_files = vec![]; - for entry in fs::read_dir(&proto_dir)? { - let path = entry?.path(); - if path.extension().and_then(|e| e.to_str()) == Some("proto") { - println!("cargo:rerun-if-changed={}", path.display()); - proto_files.push(path); - } - } - - // Ensure deterministic order for rebuilds - proto_files.sort(); - - // Compile protos into Rust - let out_dir = PathBuf::from(env::var("OUT_DIR")?); - let mut config = prost_build::Config::new(); - config.out_dir(&out_dir); - - config.compile_protos( - &proto_files - .iter() - .map(|p| p.display().to_string()) - .collect::>(), - &[proto_dir.to_str().unwrap()], - )?; - } - - Ok(()) -} diff --git a/svm-test-harness/fixture/src/account_state.rs b/svm-test-harness/fixture/src/account_state.rs deleted file mode 100644 index 0f6132cb301..00000000000 --- a/svm-test-harness/fixture/src/account_state.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Account state conversions for protobuf support. - -#![cfg(feature = "fuzz")] - -use { - crate::{error::FixtureError, proto::AcctState as ProtoAccount}, - solana_account::Account, - solana_pubkey::Pubkey, -}; - -// Default `rent_epoch` field value for all accounts. -const RENT_EXEMPT_RENT_EPOCH: u64 = u64::MAX; - -impl TryFrom for (Pubkey, Account) { - type Error = FixtureError; - - fn try_from(value: ProtoAccount) -> Result { - let ProtoAccount { - address, - owner, - lamports, - data, - executable, - .. - } = value; - - let pubkey = Pubkey::try_from(address).map_err(FixtureError::InvalidPubkeyBytes)?; - let owner = Pubkey::try_from(owner).map_err(FixtureError::InvalidPubkeyBytes)?; - - Ok(( - pubkey, - Account { - data, - executable, - lamports, - owner, - rent_epoch: RENT_EXEMPT_RENT_EPOCH, - }, - )) - } -} - -impl From<(Pubkey, Account)> for ProtoAccount { - fn from(value: (Pubkey, Account)) -> Self { - let Account { - lamports, - data, - owner, - executable, - .. - } = value.1; - - ProtoAccount { - address: value.0.to_bytes().to_vec(), - owner: owner.to_bytes().to_vec(), - lamports, - data, - executable, - } - } -} diff --git a/svm-test-harness/fixture/src/error.rs b/svm-test-harness/fixture/src/error.rs deleted file mode 100644 index d26f56b5a0d..00000000000 --- a/svm-test-harness/fixture/src/error.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Error types for fixture conversions. - -#![cfg(feature = "fuzz")] - -use thiserror::Error; - -#[derive(Debug, Error, PartialEq)] -pub enum FixtureError { - #[error("Invalid fixture input")] - InvalidFixtureInput, - - #[error("Invalid public key bytes")] - InvalidPubkeyBytes(Vec), - - #[error("An account is missing for instruction account index {0}")] - AccountMissingForInstrAccount(usize), -} diff --git a/svm-test-harness/fixture/src/instr_context.rs b/svm-test-harness/fixture/src/instr_context.rs deleted file mode 100644 index eee3f2ea9a0..00000000000 --- a/svm-test-harness/fixture/src/instr_context.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Instruction context (input). - -use { - solana_account::Account, solana_instruction::Instruction, solana_pubkey::Pubkey, - solana_svm_feature_set::SVMFeatureSet, -}; - -/// Instruction context fixture. -pub struct InstrContext { - pub feature_set: SVMFeatureSet, - pub accounts: Vec<(Pubkey, Account)>, - pub instruction: Instruction, -} - -#[cfg(feature = "fuzz")] -use { - crate::{error::FixtureError, proto::InstrContext as ProtoInstrContext}, - agave_feature_set::FeatureSet, - solana_instruction::AccountMeta, -}; - -#[cfg(feature = "fuzz")] -impl TryFrom for InstrContext { - type Error = FixtureError; - - fn try_from(value: ProtoInstrContext) -> Result { - let program_id = Pubkey::new_from_array( - value - .program_id - .try_into() - .map_err(FixtureError::InvalidPubkeyBytes)?, - ); - - let agave_feature_set: FeatureSet = value - .epoch_context - .as_ref() - .and_then(|epoch_ctx| epoch_ctx.features.as_ref()) - .map(|fs| fs.into()) - .unwrap_or_default(); - let feature_set = agave_feature_set.runtime_features(); - - let accounts: Vec<(Pubkey, Account)> = value - .accounts - .into_iter() - .map(|acct_state| acct_state.try_into()) - .collect::, _>>()?; - - let instruction_accounts = value - .instr_accounts - .into_iter() - .map(|acct| { - if acct.index as usize >= accounts.len() { - return Err(FixtureError::AccountMissingForInstrAccount( - acct.index as usize, - )); - } - Ok(AccountMeta { - pubkey: accounts[acct.index as usize].0, - is_signer: acct.is_signer, - is_writable: acct.is_writable, - }) - }) - .collect::, _>>()?; - - if instruction_accounts.len() > 128 { - return Err(FixtureError::InvalidFixtureInput); - } - - let instruction = Instruction { - accounts: instruction_accounts, - data: value.data, - program_id, - }; - - Ok(Self { - feature_set, - accounts, - instruction, - }) - } -} diff --git a/svm-test-harness/fixture/src/lib.rs b/svm-test-harness/fixture/src/lib.rs deleted file mode 100644 index f3a2c8d1fb3..00000000000 --- a/svm-test-harness/fixture/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Fixture types for SVM testing. -//! -//! This module provides native Rust types for testing program execution. -//! When the `fuzz` feature is enabled, it also includes conversions -//! between Firedancer's protobuf payloads and Solana SDK types. - -pub mod account_state; -pub mod error; -pub mod feature_set; -pub mod instr_context; -pub mod instr_effects; - -#[cfg(feature = "fuzz")] -pub mod proto { - include!(concat!(env!("OUT_DIR"), "/org.solana.sealevel.v1.rs")); -} diff --git a/svm-test-harness/instr/Cargo.toml b/svm-test-harness/instr/Cargo.toml deleted file mode 100644 index b4f2faf886b..00000000000 --- a/svm-test-harness/instr/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "solana-svm-test-harness-instr" -description = "Solana SVM test harness for instruction execution." -documentation = "https://docs.rs/solana-svm-test-harness-instr" -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = { workspace = true } -workspace = "../../dev-bins" -publish = false - -[features] -agave-unstable-api = [] -dev-context-only-utils = [] -dummy-for-ci-check = ["fuzz"] -metrics = ["solana-program-runtime/metrics", "solana-svm/metrics"] -fuzz = [ - "dep:agave-feature-set", - "dep:agave-precompiles", - "dep:prost", - "solana-pubkey/default", - "solana-svm-test-harness-fixture/fuzz", -] - -[dependencies] -agave-feature-set = { workspace = true, optional = true } -agave-precompiles = { workspace = true, optional = true } -bincode = { workspace = true } -env_logger = { workspace = true } -prost = { workspace = true, optional = true } -solana-account = { workspace = true } -solana-bpf-loader-program = { workspace = true } -solana-clock = { workspace = true, features = ["sysvar"] } -solana-compute-budget = { workspace = true } -solana-compute-budget-program = { workspace = true } -solana-instruction = { workspace = true } -solana-instruction-error = { workspace = true, features = ["serde"] } -solana-precompile-error = { workspace = true } -solana-program-runtime = { workspace = true, features = ["dev-context-only-utils"] } -solana-pubkey = { workspace = true } -solana-rent = { workspace = true, features = ["sysvar"] } -solana-sdk-ids = { workspace = true } -solana-svm = { workspace = true } -solana-svm-callback = { workspace = true } -solana-svm-feature-set = { workspace = true } -solana-svm-log-collector = { workspace = true } -solana-svm-test-harness-fixture = { workspace = true } -solana-svm-timings = { workspace = true } -solana-svm-transaction = { workspace = true } -solana-syscalls = { workspace = true } -solana-system-program = { workspace = true } -solana-sysvar-id = { workspace = true } -solana-transaction-context = { workspace = true } - -[lints] -workspace = true diff --git a/svm-test-harness/instr/src/builtins.rs b/svm-test-harness/instr/src/builtins.rs deleted file mode 100644 index f18c146d7e4..00000000000 --- a/svm-test-harness/instr/src/builtins.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! SVM-resident builtin programs for the test harness. - -use { - solana_program_runtime::{ - invoke_context::BuiltinFunctionRegisterer, solana_sbpf::program::BuiltinFunctionDefinition, - }, - solana_pubkey::Pubkey, - solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable}, -}; - -pub struct SvmBuiltinPrototype { - pub program_id: Pubkey, - pub name: &'static str, - pub register_fn: BuiltinFunctionRegisterer, -} - -pub static SVM_BUILTINS: &[SvmBuiltinPrototype] = &[ - SvmBuiltinPrototype { - program_id: solana_system_program::id(), - name: "system_program", - register_fn: solana_system_program::system_processor::Entrypoint::register, - }, - SvmBuiltinPrototype { - program_id: bpf_loader_deprecated::id(), - name: "solana_bpf_loader_deprecated_program", - register_fn: solana_bpf_loader_program::Entrypoint::register, - }, - SvmBuiltinPrototype { - program_id: bpf_loader::id(), - name: "solana_bpf_loader_program", - register_fn: solana_bpf_loader_program::Entrypoint::register, - }, - SvmBuiltinPrototype { - program_id: bpf_loader_upgradeable::id(), - name: "solana_bpf_loader_upgradeable_program", - register_fn: solana_bpf_loader_program::Entrypoint::register, - }, - SvmBuiltinPrototype { - program_id: solana_sdk_ids::compute_budget::id(), - name: "compute_budget_program", - register_fn: solana_compute_budget_program::Entrypoint::register, - }, -]; diff --git a/svm-test-harness/instr/src/file.rs b/svm-test-harness/instr/src/file.rs deleted file mode 100644 index f74d11c78ce..00000000000 --- a/svm-test-harness/instr/src/file.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Module for loading files from local filesystem. -//! -//! When compiling a Solana program with `cargo build-sbf`, the environment -//! variable `SBF_OUT_DIR` defaults to `target/deploy` and the compiled program -//! ELF file is written to `target/deploy/program_name.so`. -//! -//! As a result, the default search paths for ELF files are: -//! -//! * `tests/fixtures` -//! * `BPF_OUT_DIR` -//! * `SBF_OUT_DIR` -//! * The current working directory -//! -//! Since these functions are intended for the local filesystem and for testing -//! purposes, they will panic if the file is not found or if there is an -//! error reading the file. - -use std::{ - fs::File, - io::Read, - path::{Path, PathBuf}, -}; - -fn default_shared_object_dirs() -> Vec { - let mut search_path = vec![PathBuf::from("tests/fixtures")]; - - if let Ok(bpf_out_dir) = std::env::var("BPF_OUT_DIR") { - search_path.push(PathBuf::from(bpf_out_dir)); - } - - if let Ok(sbf_out_dir) = std::env::var("SBF_OUT_DIR") { - search_path.push(PathBuf::from(sbf_out_dir)); - } - - if let Ok(dir) = std::env::current_dir() { - search_path.push(dir); - } - - search_path -} - -fn find_file(filename: &str) -> Option { - for dir in default_shared_object_dirs() { - let candidate = dir.join(filename); - if candidate.exists() { - return Some(candidate); - } - } - None -} - -/// Read the contents of a file into a `Vec`. -pub fn read_file>(path: P) -> Vec { - let path = path.as_ref(); - let mut file = File::open(path) - .unwrap_or_else(|e| panic!("Failed to open file {}: {}", path.display(), e)); - - let mut file_data = Vec::new(); - file.read_to_end(&mut file_data) - .unwrap_or_else(|e| panic!("Failed to read file {}: {}", path.display(), e)); - file_data -} - -/// Load a program ELF file from the local filesystem by program name. -/// -/// The program ELF file is expected to be located in one of the default search -/// paths: -/// -/// * `tests/fixtures` -/// * `BPF_OUT_DIR` -/// * `SBF_OUT_DIR` -/// * The current working directory -/// -/// The name of the program ELF file is expected to be `{program_name}.so`. -pub fn load_program_elf(program_name: &str) -> Vec { - let file_name = format!("{program_name}.so"); - let program_file = find_file(&file_name) - .unwrap_or_else(|| panic!("Failed to find program ELF file: {file_name}")); - read_file(program_file) -} diff --git a/svm-test-harness/instr/src/fuzz.rs b/svm-test-harness/instr/src/fuzz.rs deleted file mode 100644 index 7bad716d442..00000000000 --- a/svm-test-harness/instr/src/fuzz.rs +++ /dev/null @@ -1,145 +0,0 @@ -#![allow(clippy::missing_safety_doc)] - -use { - crate::{ - execute_instr_with_callback, - fixture::{ - instr_context::InstrContext, - proto::{InstrContext as ProtoInstrContext, InstrEffects as ProtoInstrEffects}, - }, - logger, - }, - agave_precompiles::{get_precompile, is_precompile}, - prost::Message, - solana_compute_budget::compute_budget::ComputeBudget, - solana_precompile_error::PrecompileError, - solana_pubkey::Pubkey, - solana_svm_callback::InvokeContextCallback, - solana_syscalls::create_program_runtime_environment, - std::{env, ffi::c_int}, -}; - -/// Callback with full precompile support for fuzz testing. -/// -/// All precompiles are enabled for fuzz testing. -struct FuzzInstrContextCallback; - -impl InvokeContextCallback for FuzzInstrContextCallback { - fn is_precompile(&self, program_id: &Pubkey) -> bool { - is_precompile(program_id, |_| true) - } - - fn process_precompile( - &self, - program_id: &Pubkey, - data: &[u8], - instruction_datas: Vec<&[u8]>, - ) -> std::result::Result<(), PrecompileError> { - if let Some(precompile) = get_precompile(program_id, |_| true) { - precompile.verify( - data, - &instruction_datas, - &agave_feature_set::FeatureSet::all_enabled(), - ) - } else { - Err(PrecompileError::InvalidPublicKey) - } - } -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn sol_compat_init(_log_level: i32) { - unsafe { - env::set_var("SOLANA_RAYON_THREADS", "1"); - env::set_var("RAYON_NUM_THREADS", "1"); - } - if env::var("ENABLE_SOLANA_LOGGER").is_ok() { - /* Pairs with RUST_LOG={trace,debug,info,etc} */ - logger::setup(); - } -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn sol_compat_fini() {} - -pub fn execute_instr_proto(input: ProtoInstrContext) -> Option { - let cu_avail = input.cu_avail; - - let Ok(instr_context) = InstrContext::try_from(input) else { - return None; - }; - - let feature_set = &instr_context.feature_set; - let simd_0268_active = feature_set.raise_cpi_nesting_limit_to_8; - - let compute_budget = { - let mut budget = ComputeBudget::new_with_defaults(simd_0268_active); - budget.compute_unit_limit = cu_avail; - budget - }; - - // When testing with protobuf, we fill the sysvar cache from input accounts. - let sysvar_cache = { - let mut cache = solana_program_runtime::sysvar_cache::SysvarCache::default(); - crate::sysvar_cache::fill_from_accounts(&mut cache, &instr_context.accounts); - cache - }; - - // When testing with protobuf, we fill the program cache from input accounts. - let mut program_cache = { - let slot = sysvar_cache.get_clock().unwrap().slot; - let environment = create_program_runtime_environment( - &instr_context.feature_set, - &compute_budget.to_budget(), - false, /* deployment */ - false, /* debugging_features */ - ) - .unwrap(); - - let mut cache = crate::program_cache::new_with_builtins(slot); - crate::program_cache::fill_from_accounts( - &mut cache, - &environment, - &instr_context.accounts, - slot, - ) - .unwrap(); - - cache - }; - - let instr_effects = execute_instr_with_callback( - &instr_context, - &FuzzInstrContextCallback, - &compute_budget, - &mut program_cache, - &sysvar_cache, - ); - instr_effects.map(Into::into) -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn sol_compat_instr_execute_v1( - out_ptr: *mut u8, - out_psz: *mut u64, - in_ptr: *mut u8, - in_sz: u64, -) -> c_int { - let in_slice = unsafe { std::slice::from_raw_parts(in_ptr, in_sz as usize) }; - let Ok(instr_context) = ProtoInstrContext::decode(in_slice) else { - return 0; - }; - let Some(instr_effects) = execute_instr_proto(instr_context) else { - return 0; - }; - let out_slice = unsafe { std::slice::from_raw_parts_mut(out_ptr, (*out_psz) as usize) }; - let out_vec = instr_effects.encode_to_vec(); - if out_vec.len() > out_slice.len() { - return 0; - } - out_slice[..out_vec.len()].copy_from_slice(&out_vec); - unsafe { - *out_psz = out_vec.len() as u64; - } - 1 -} diff --git a/svm-test-harness/instr/src/harness.rs b/svm-test-harness/instr/src/harness.rs deleted file mode 100644 index 6f0d38656d3..00000000000 --- a/svm-test-harness/instr/src/harness.rs +++ /dev/null @@ -1,336 +0,0 @@ -//! Instruction harness. - -use { - crate::fixture::{instr_context::InstrContext, instr_effects::InstrEffects}, - solana_compute_budget::compute_budget::ComputeBudget, - solana_instruction_error::InstructionError, - solana_program_runtime::{ - invoke_context::{EnvironmentConfig, InvokeContext, mock_compile_message}, - loaded_programs::{ - ProgramCacheForTxBatch, ProgramRuntimeEnvironment, ProgramRuntimeEnvironments, - }, - sysvar_cache::SysvarCache, - }, - solana_pubkey::Pubkey, - solana_svm_callback::InvokeContextCallback, - solana_svm_log_collector::LogCollector, - solana_svm_timings::ExecuteTimings, - solana_svm_transaction::svm_message::SVMStaticMessage, - solana_syscalls::create_program_runtime_environment, - solana_transaction_context::transaction::TransactionContext, - std::rc::Rc, -}; - -/// Default callback with no precompile support. -struct DefaultCallback; - -impl InvokeContextCallback for DefaultCallback {} - -/// Execute a single instruction against the Solana VM. -/// -/// This version does not support precompiles. Use [`execute_instr_with_callback`] -/// if you need precompile support. -pub fn execute_instr( - input: InstrContext, - compute_budget: &ComputeBudget, - program_cache: &mut ProgramCacheForTxBatch, - sysvar_cache: &SysvarCache, -) -> Option { - execute_instr_with_callback( - &input, - &DefaultCallback, - compute_budget, - program_cache, - sysvar_cache, - ) -} - -/// Execute a single instruction against the Solana VM with a custom callback. -pub fn execute_instr_with_callback( - input: &InstrContext, - callback: &C, - compute_budget: &ComputeBudget, - program_cache: &mut ProgramCacheForTxBatch, - sysvar_cache: &SysvarCache, -) -> Option { - let mut compute_units_consumed = 0; - let mut timings = ExecuteTimings::default(); - - let log_collector = LogCollector::new_ref(); - let feature_set = input.feature_set; - - let rent = sysvar_cache.get_rent().unwrap(); - let program_id = &input.instruction.program_id; - let loader_key = program_cache.find(program_id)?.account_owner(); - - let (sanitized_message, transaction_accounts) = - mock_compile_message(&input.instruction, &input.accounts, program_id, &loader_key)?; - - let mut transaction_context = TransactionContext::new( - transaction_accounts, - (*rent).clone(), - compute_budget.max_instruction_stack_depth, - compute_budget.max_instruction_trace_length, - sanitized_message.num_instructions(), - ); - - let program_runtime_environment = create_program_runtime_environment( - &input.feature_set, - &compute_budget.to_budget(), - false, /* deployment */ - false, /* debugging_features */ - ) - .unwrap(); - - let result = { - #[expect(deprecated)] - let (blockhash, blockhash_lamports_per_signature) = sysvar_cache - .get_recent_blockhashes() - .ok() - .and_then(|x| (*x).last().cloned()) - .map(|x| (x.blockhash, x.fee_calculator.lamports_per_signature)) - .unwrap_or_default(); - - let program_runtime_environments = ProgramRuntimeEnvironments::new( - ProgramRuntimeEnvironment::clone(&program_runtime_environment), - program_runtime_environment, - ); - let mut invoke_context = InvokeContext::new( - &mut transaction_context, - program_cache, - EnvironmentConfig::new( - blockhash, - blockhash_lamports_per_signature, - false, - callback, - &feature_set, - &program_runtime_environments, - sysvar_cache, - ), - Some(log_collector.clone()), - compute_budget.to_budget(), - compute_budget.to_cost(), - ); - - invoke_context - .prepare_top_level_instructions(&sanitized_message) - .ok()?; - - if invoke_context.is_precompile(&input.instruction.program_id) { - invoke_context.process_precompile( - &input.instruction.program_id, - &input.instruction.data, - [input.instruction.data.as_slice()].into_iter(), - ) - } else { - invoke_context.process_instruction(&mut compute_units_consumed, &mut timings) - } - }; - - let cu_avail = compute_budget - .compute_unit_limit - .saturating_sub(compute_units_consumed); - let return_data = transaction_context.get_return_data().1.to_vec(); - - let logs = Rc::try_unwrap(log_collector) - .ok() - .map(|cell| cell.into_inner().into_messages()) - .unwrap_or_default(); - - let account_keys: Vec = (0..transaction_context.get_number_of_accounts()) - .map(|index| { - *transaction_context - .get_key_of_account_at_index(index) - .clone() - .unwrap() - }) - .collect::>(); - - Some(InstrEffects { - custom_err: if let Err(InstructionError::Custom(code)) = result { - Some(code) - } else { - None - }, - result: result.err(), - resulting_accounts: transaction_context - .deconstruct_without_keys() - .unwrap() - .into_iter() - .zip(account_keys) - .map(|(account, key)| (key, account.into())) - .collect(), - cu_avail, - return_data, - logs, - }) -} - -#[cfg(test)] -mod tests { - use { - super::*, - solana_account::Account, - solana_instruction::{AccountMeta, Instruction}, - solana_pubkey::Pubkey, - solana_svm_feature_set::SVMFeatureSet, - solana_sysvar_id::SysvarId, - }; - - #[test] - fn test_system_program_exec() { - let system_program_id = solana_sdk_ids::system_program::id(); - let native_loader_id = solana_sdk_ids::native_loader::id(); - let sysvar_id = solana_sysvar_id::id(); - - let from_pubkey = Pubkey::new_from_array([1u8; 32]); - let to_pubkey = Pubkey::new_from_array([2u8; 32]); - - let cu_avail = 10000u64; - let slot = 10; - let feature_set = SVMFeatureSet::default(); - - // Create Clock sysvar - let clock = solana_clock::Clock { - slot, - ..Default::default() - }; - let clock_data = bincode::serialize(&clock).unwrap(); - - // Create Rent sysvar - let rent = solana_rent::Rent::default(); - let rent_data = bincode::serialize(&rent).unwrap(); - - // Build the instruction context. - let context = InstrContext { - feature_set, - accounts: vec![ - ( - from_pubkey, - Account { - lamports: 1000, - data: vec![], - owner: system_program_id, - executable: false, - rent_epoch: u64::MAX, - }, - ), - ( - to_pubkey, - Account { - lamports: 0, - data: vec![], - owner: system_program_id, - executable: false, - rent_epoch: u64::MAX, - }, - ), - ( - system_program_id, - Account { - lamports: 10000000, - data: b"Solana Program".to_vec(), - owner: native_loader_id, - executable: true, - rent_epoch: u64::MAX, - }, - ), - ( - solana_clock::Clock::id(), - Account { - lamports: 1, - data: clock_data, - owner: sysvar_id, - executable: false, - rent_epoch: u64::MAX, - }, - ), - ( - solana_rent::Rent::id(), - Account { - lamports: 1, - data: rent_data, - owner: sysvar_id, - executable: false, - rent_epoch: u64::MAX, - }, - ), - ], - instruction: Instruction { - program_id: system_program_id, - accounts: vec![ - AccountMeta { - pubkey: from_pubkey, - is_signer: true, - is_writable: true, - }, - AccountMeta { - pubkey: to_pubkey, - is_signer: false, - is_writable: true, - }, - ], - data: vec![ - // Transfer - 0x02, 0x00, 0x00, 0x00, // Lamports - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - }, - }; - - // Set up the Compute Budget. - let compute_budget = { - let mut budget = ComputeBudget::new_with_defaults(false); - budget.compute_unit_limit = cu_avail; - budget - }; - - // Create Sysvar Cache - let mut sysvar_cache = SysvarCache::default(); - crate::sysvar_cache::fill_from_accounts(&mut sysvar_cache, &context.accounts); - - // Create Program Cache - let mut program_cache = crate::program_cache::new_with_builtins(slot); - - let environments = create_program_runtime_environment( - &context.feature_set, - &compute_budget.to_budget(), - false, /* deployment */ - false, /* debugging_features */ - ) - .unwrap(); - - crate::program_cache::fill_from_accounts( - &mut program_cache, - &environments, - &context.accounts, - slot, - ) - .unwrap(); - - // Execute the instruction. - let effects = execute_instr(context, &compute_budget, &mut program_cache, &sysvar_cache) - .expect("Instruction execution should succeed"); - - // Verify the results. - assert_eq!(effects.result, None); - assert_eq!(effects.custom_err, None); - assert_eq!(effects.cu_avail, 9850u64); - assert_eq!(effects.return_data, Vec::::new(),); - - // Verify account changes. - let from_account = effects - .resulting_accounts - .iter() - .find(|(k, _)| k == &from_pubkey) - .unwrap(); - assert_eq!(from_account.1.lamports, 999); - - let to_account = effects - .resulting_accounts - .iter() - .find(|(k, _)| k == &to_pubkey) - .unwrap(); - assert_eq!(to_account.1.lamports, 1); - } -} diff --git a/svm-test-harness/instr/src/keyed_account.rs b/svm-test-harness/instr/src/keyed_account.rs deleted file mode 100644 index b0323aac45d..00000000000 --- a/svm-test-harness/instr/src/keyed_account.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Keyed account helpers. - -use { - crate::builtins::SVM_BUILTINS, solana_account::Account, solana_pubkey::Pubkey, - solana_rent::Rent, -}; - -fn create_keyed_account_for_builtin_program(program_id: &Pubkey, name: &str) -> (Pubkey, Account) { - let data = name.as_bytes().to_vec(); - let lamports = Rent::default().minimum_balance(data.len()); - let account = Account { - lamports, - data, - owner: solana_sdk_ids::native_loader::id(), - executable: true, - ..Default::default() - }; - (*program_id, account) -} - -pub fn keyed_account_for_system_program() -> (Pubkey, Account) { - create_keyed_account_for_builtin_program(&SVM_BUILTINS[0].program_id, SVM_BUILTINS[0].name) -} - -pub fn keyed_account_for_compute_budget_program() -> (Pubkey, Account) { - create_keyed_account_for_builtin_program(&SVM_BUILTINS[4].program_id, SVM_BUILTINS[4].name) -} diff --git a/svm-test-harness/instr/src/lib.rs b/svm-test-harness/instr/src/lib.rs deleted file mode 100644 index c3c22bccbb4..00000000000 --- a/svm-test-harness/instr/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Solana SVM test harness for instruction execution. -//! -//! This crate provides an API for Agave's program runtime in order to -//! execute program instructions directly against the VM. - -mod builtins; -pub mod file; -mod harness; -pub mod keyed_account; -pub mod logger; -pub mod program_cache; -pub mod sysvar_cache; - -pub use { - harness::{execute_instr, execute_instr_with_callback}, - solana_svm_test_harness_fixture as fixture, -}; - -#[cfg(feature = "fuzz")] -pub mod fuzz; diff --git a/svm-test-harness/instr/src/logger.rs b/svm-test-harness/instr/src/logger.rs deleted file mode 100644 index 68bcbbe774f..00000000000 --- a/svm-test-harness/instr/src/logger.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Logging utils. - -/// Initialize a test logger with default error filter. -pub fn setup() { - let _ = env_logger::Builder::from_env(env_logger::Env::new().default_filter_or("error")) - .format_timestamp_nanos() - .is_test(true) - .try_init(); -} diff --git a/svm-test-harness/instr/src/program_cache.rs b/svm-test-harness/instr/src/program_cache.rs deleted file mode 100644 index 12b68a0dc5e..00000000000 --- a/svm-test-harness/instr/src/program_cache.rs +++ /dev/null @@ -1,130 +0,0 @@ -use { - crate::builtins::SVM_BUILTINS, - solana_account::{Account, AccountSharedData}, - solana_compute_budget::compute_budget::ComputeBudget, - solana_instruction_error::InstructionError, - solana_program_runtime::{ - loaded_programs::{ProgramCacheForTxBatch, ProgramRuntimeEnvironment}, - program_cache_entry::ProgramCacheEntry, - program_metrics::LoadProgramMetrics, - }, - solana_pubkey::Pubkey, - solana_svm_callback::{InvokeContextCallback, TransactionProcessingCallback}, - solana_svm_feature_set::SVMFeatureSet, - solana_svm_timings::ExecuteTimings, - solana_syscalls::create_program_runtime_environment, - std::{collections::HashSet, sync::Arc}, -}; - -/// Create a new `ProgramCacheForTxBatch` instance populated with builtins. -pub fn new_with_builtins(slot: u64) -> ProgramCacheForTxBatch { - let mut cache = ProgramCacheForTxBatch::default(); - cache.set_slot_for_tests(slot); - - for builtin in SVM_BUILTINS { - cache.replenish( - builtin.program_id, - Arc::new(ProgramCacheEntry::new_builtin( - 0u64, - builtin.name.len(), - builtin.register_fn, - )), - ); - } - - cache -} - -/// Add a program loaded from ELF bytes to the cache. -pub fn add_program( - cache: &mut ProgramCacheForTxBatch, - program_id: &Pubkey, - loader_key: &Pubkey, - elf: &[u8], - feature_set: &SVMFeatureSet, - compute_budget: &ComputeBudget, -) { - let program_runtime_environment = create_program_runtime_environment( - feature_set, - &compute_budget.to_budget(), - false, /* reject_deployment_of_broken_elfs */ - false, /* debugging_features */ - ) - .unwrap(); - - let entry = ProgramCacheEntry::new( - loader_key, - program_runtime_environment, - 0, // deployment_slot - 0, // effective_slot - elf, - elf.len(), - &mut LoadProgramMetrics::default(), - ) - .unwrap(); - - cache.replenish(*program_id, Arc::new(entry)); -} - -/// Populate a `ProgramCacheForTxBatch` via `load_program_with_pubkey` from any program accounts. -pub fn fill_from_accounts( - program_cache: &mut ProgramCacheForTxBatch, - program_runtime_environment: &ProgramRuntimeEnvironment, - accounts: &[(Pubkey, Account)], - slot: u64, -) -> Result<(), InstructionError> { - let mut newly_loaded_programs = HashSet::::new(); - - for acc in accounts { - // FD rejects duplicate account loads - if !newly_loaded_programs.insert(acc.0) { - return Err(InstructionError::UnsupportedProgramId); - } - - if program_cache.find(&acc.0).is_none() { - // load_program_with_pubkey expects the owner to be one of the bpf loader - if !solana_sdk_ids::bpf_loader_deprecated::check_id(&acc.1.owner) - && !solana_sdk_ids::bpf_loader::check_id(&acc.1.owner) - && !solana_sdk_ids::bpf_loader_upgradeable::check_id(&acc.1.owner) - { - continue; - } - // https://github.com/anza-xyz/agave/blob/af6930da3a99fd0409d3accd9bbe449d82725bd6/svm/src/program_loader.rs#L124 - /* pub fn load_program_with_pubkey( - callbacks: &CB, - program_cache: &ProgramCache, - pubkey: &Pubkey, - slot: Slot, - effective_epoch: Epoch, - epoch_schedule: &EpochSchedule, - reload: bool, - ) -> Option> { */ - if let Some((loaded_program, _last_modification_slot)) = - solana_svm::program_loader::load_program_with_pubkey( - &FillFromAccountsCallback(accounts), - program_runtime_environment, - &acc.0, - slot, - &mut ExecuteTimings::default(), - ) - { - program_cache.replenish(acc.0, loaded_program); - } - } - } - - Ok(()) -} - -struct FillFromAccountsCallback<'a>(&'a [(Pubkey, Account)]); - -impl InvokeContextCallback for FillFromAccountsCallback<'_> {} - -impl TransactionProcessingCallback for FillFromAccountsCallback<'_> { - fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, u64)> { - self.0 - .iter() - .find(|(found_pubkey, _)| *found_pubkey == *pubkey) - .map(|(_, account)| (AccountSharedData::from(account.clone()), 0u64)) - } -} diff --git a/svm-test-harness/instr/src/sysvar_cache.rs b/svm-test-harness/instr/src/sysvar_cache.rs deleted file mode 100644 index bdc952d168c..00000000000 --- a/svm-test-harness/instr/src/sysvar_cache.rs +++ /dev/null @@ -1,16 +0,0 @@ -use { - solana_account::{Account, ReadableAccount}, - solana_program_runtime::sysvar_cache::SysvarCache, - solana_pubkey::Pubkey, -}; - -/// Populate a `SysvarCache` via `fill_missing_entries` from any sysvar accounts. -pub fn fill_from_accounts(sysvar_cache: &mut SysvarCache, accounts: &[(Pubkey, Account)]) { - sysvar_cache.fill_missing_entries(|pubkey, callbackback| { - if let Some(account) = accounts.iter().find(|(key, _)| key == pubkey) { - if account.1.lamports() > 0 { - callbackback(account.1.data()); - } - } - }); -} diff --git a/svm-test-harness/src/lib.rs b/svm-test-harness/src/lib.rs deleted file mode 100644 index 2260ce1c867..00000000000 --- a/svm-test-harness/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Solana SVM test harness. - -pub use {solana_svm_test_harness_fixture as fixture, solana_svm_test_harness_instr as instr}; diff --git a/svm-timings/Cargo.toml b/svm-timings/Cargo.toml index 2c5ca50dc8b..71753c6db3e 100644 --- a/svm-timings/Cargo.toml +++ b/svm-timings/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -22,3 +24,6 @@ solana-pubkey = { workspace = true } [dev-dependencies] solana-pubkey = { workspace = true, features = ["rand"] } + +[lints] +workspace = true diff --git a/svm-transaction/Cargo.toml b/svm-transaction/Cargo.toml deleted file mode 100644 index 6316c00020c..00000000000 --- a/svm-transaction/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "solana-svm-transaction" -description = "Solana SVM Transaction" -documentation = "https://docs.rs/solana-svm-transaction" -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = "2024" - -[features] -agave-unstable-api = [] - -[dependencies] -solana-hash = { workspace = true } -solana-message = { workspace = true } -solana-pubkey = { workspace = true } -solana-sdk-ids = { workspace = true } -solana-signature = { workspace = true } -solana-transaction = { workspace = true } - -[dev-dependencies] -solana-message = { workspace = true, features = ["wincode"] } -solana-nonce = { workspace = true } -solana-pubkey = { workspace = true, features = ["std"] } -solana-system-interface = { workspace = true, features = ["bincode"] } -static_assertions = { workspace = true } -test-case = { workspace = true } diff --git a/svm-transaction/src/instruction.rs b/svm-transaction/src/instruction.rs deleted file mode 100644 index acf1cb06502..00000000000 --- a/svm-transaction/src/instruction.rs +++ /dev/null @@ -1,24 +0,0 @@ -use solana_message::compiled_instruction::CompiledInstruction; - -/// A non-owning version of [`CompiledInstruction`] that references -/// slices of account indexes and data. -// `program_id_index` is still owned, as it is a simple u8. -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct SVMInstruction<'a> { - /// Index into the transaction keys array indicating the program account that executes this instruction. - pub program_id_index: u8, - /// Ordered indices into the transaction keys array indicating which accounts to pass to the program. - pub accounts: &'a [u8], - /// The program input data. - pub data: &'a [u8], -} - -impl<'a> From<&'a CompiledInstruction> for SVMInstruction<'a> { - fn from(ix: &'a CompiledInstruction) -> Self { - Self { - program_id_index: ix.program_id_index, - accounts: ix.accounts.as_slice(), - data: ix.data.as_slice(), - } - } -} diff --git a/svm-transaction/src/lib.rs b/svm-transaction/src/lib.rs deleted file mode 100644 index d49d5c57a98..00000000000 --- a/svm-transaction/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#![cfg(feature = "agave-unstable-api")] -pub mod instruction; -pub mod message_address_table_lookup; -pub mod svm_message; -pub mod svm_transaction; - -mod tests; diff --git a/svm-transaction/src/message_address_table_lookup.rs b/svm-transaction/src/message_address_table_lookup.rs deleted file mode 100644 index 4bb013df36f..00000000000 --- a/svm-transaction/src/message_address_table_lookup.rs +++ /dev/null @@ -1,23 +0,0 @@ -use {solana_message::v0, solana_pubkey::Pubkey}; - -/// A non-owning version of [`v0::MessageAddressTableLookup`]. -/// This simply references the data in the original message. -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct SVMMessageAddressTableLookup<'a> { - /// Address lookup table account key - pub account_key: &'a Pubkey, - /// List of indexes used to load writable account addresses - pub writable_indexes: &'a [u8], - /// List of indexes used to load readonly account addresses - pub readonly_indexes: &'a [u8], -} - -impl<'a> From<&'a v0::MessageAddressTableLookup> for SVMMessageAddressTableLookup<'a> { - fn from(lookup: &'a v0::MessageAddressTableLookup) -> Self { - Self { - account_key: &lookup.account_key, - writable_indexes: &lookup.writable_indexes, - readonly_indexes: &lookup.readonly_indexes, - } - } -} diff --git a/svm-transaction/src/svm_message.rs b/svm-transaction/src/svm_message.rs deleted file mode 100644 index 4fb7aa34d60..00000000000 --- a/svm-transaction/src/svm_message.rs +++ /dev/null @@ -1,160 +0,0 @@ -use { - crate::{ - instruction::SVMInstruction, message_address_table_lookup::SVMMessageAddressTableLookup, - }, - core::fmt::Debug, - solana_hash::Hash, - solana_message::AccountKeys, - solana_pubkey::Pubkey, - solana_sdk_ids::{ed25519_program, secp256k1_program, secp256r1_program, system_program}, - solana_transaction::versioned::TransactionVersion, -}; - -mod sanitized_message; -mod sanitized_transaction; -// inlined to avoid solana-nonce dep -#[cfg(test)] -static_assertions::const_assert_eq!( - NONCED_TX_MARKER_IX_INDEX, - solana_nonce::NONCED_TX_MARKER_IX_INDEX -); -const NONCED_TX_MARKER_IX_INDEX: u8 = 0; - -pub trait SVMStaticMessage { - /// Get the transaction version. - fn version(&self) -> TransactionVersion; - - /// Return the number of transaction-level signatures in the message. - fn num_transaction_signatures(&self) -> u64; - /// Return the number of ed25519 precompile signatures in the message. - fn num_ed25519_signatures(&self) -> u64 { - default_precompile_signature_count(&ed25519_program::ID, self.program_instructions_iter()) - } - /// Return the number of secp256k1 precompile signatures in the message. - fn num_secp256k1_signatures(&self) -> u64 { - default_precompile_signature_count(&secp256k1_program::ID, self.program_instructions_iter()) - } - /// Return the number of secp256r1 precompile signatures in the message. - fn num_secp256r1_signatures(&self) -> u64 { - default_precompile_signature_count(&secp256r1_program::ID, self.program_instructions_iter()) - } - - /// Returns the number of requested write-locks in this message. - /// This does not consider if write-locks are demoted. - fn num_write_locks(&self) -> u64; - - /// Return the recent blockhash. - fn recent_blockhash(&self) -> &Hash; - - /// Return the number of instructions in the message. - fn num_instructions(&self) -> usize; - - /// Return an iterator over the instructions in the message. - fn instructions_iter(&self) -> impl Iterator>; - - /// Return an iterator over the instructions in the message, paired with - /// the pubkey of the program. - fn program_instructions_iter( - &self, - ) -> impl Iterator)> + Clone; - - /// Return the list of static account keys. - fn static_account_keys(&self) -> &[Pubkey]; - - /// Return the fee-payer - fn fee_payer(&self) -> &Pubkey; - - /// Get the number of lookup tables. - fn num_lookup_tables(&self) -> usize; - - /// Get message address table lookups used in the message - fn message_address_table_lookups( - &self, - ) -> impl Iterator>; -} - -// - Debug to support legacy logging -pub trait SVMMessage: Debug + SVMStaticMessage { - /// Return the account keys. - fn account_keys(&self) -> AccountKeys<'_>; - - /// Returns `true` if the account at `index` is writable. - fn is_writable(&self, index: usize) -> bool; - - /// Returns `true` if the account at `index` is signer. - fn is_signer(&self, index: usize) -> bool; - - /// Returns true if the account at the specified index is invoked as a - /// program in top-level instructions of this message. - fn is_invoked(&self, key_index: usize) -> bool; - - /// Returns true if the account at the specified index is an input to some - /// program instruction in this message. - fn is_instruction_account(&self, key_index: usize) -> bool { - if let Ok(key_index) = u8::try_from(key_index) { - self.instructions_iter() - .any(|ix| ix.accounts.contains(&key_index)) - } else { - false - } - } - - /// If the message uses a durable nonce, return the pubkey of the nonce account - fn get_durable_nonce(&self) -> Option<&Pubkey> { - let account_keys = self.account_keys(); - self.instructions_iter() - .nth(usize::from(NONCED_TX_MARKER_IX_INDEX)) - .filter( - |ix| match account_keys.get(usize::from(ix.program_id_index)) { - Some(program_id) => system_program::check_id(program_id), - _ => false, - }, - ) - .filter(|ix| { - /// Serialized value of [`SystemInstruction::AdvanceNonceAccount`]. - const SERIALIZED_ADVANCE_NONCE_ACCOUNT: [u8; 4] = 4u32.to_le_bytes(); - const SERIALIZED_SIZE: usize = SERIALIZED_ADVANCE_NONCE_ACCOUNT.len(); - - ix.data - .get(..SERIALIZED_SIZE) - .map(|data| data == SERIALIZED_ADVANCE_NONCE_ACCOUNT) - .unwrap_or(false) - }) - .and_then(|ix| { - ix.accounts.first().and_then(|idx| { - let index = usize::from(*idx); - if index >= self.static_account_keys().len() || !self.is_writable(index) { - None - } else { - account_keys.get(index) - } - }) - }) - } - - /// For the instruction at `index`, return an iterator over input accounts - /// that are signers. - fn get_ix_signers(&self, index: usize) -> impl Iterator { - self.instructions_iter() - .nth(index) - .into_iter() - .flat_map(|ix| { - ix.accounts - .iter() - .copied() - .map(usize::from) - .filter(|index| self.is_signer(*index)) - .filter_map(|signer_index| self.account_keys().get(signer_index)) - }) - } -} - -fn default_precompile_signature_count<'a>( - precompile: &Pubkey, - instructions: impl Iterator)>, -) -> u64 { - instructions - .filter(|(program_id, _)| *program_id == precompile) - .map(|(_, ix)| u64::from(ix.data.first().copied().unwrap_or(0))) - .sum() -} diff --git a/svm-transaction/src/svm_message/sanitized_message.rs b/svm-transaction/src/svm_message/sanitized_message.rs deleted file mode 100644 index e1e890f3fe7..00000000000 --- a/svm-transaction/src/svm_message/sanitized_message.rs +++ /dev/null @@ -1,89 +0,0 @@ -use { - crate::{ - instruction::SVMInstruction, - message_address_table_lookup::SVMMessageAddressTableLookup, - svm_message::{SVMMessage, SVMStaticMessage}, - }, - solana_hash::Hash, - solana_message::{AccountKeys, SanitizedMessage}, - solana_pubkey::Pubkey, - solana_transaction::versioned::TransactionVersion, -}; - -impl SVMStaticMessage for SanitizedMessage { - fn version(&self) -> TransactionVersion { - match self { - SanitizedMessage::Legacy(_) => TransactionVersion::LEGACY, - SanitizedMessage::V0(_) => TransactionVersion::Number(0), - SanitizedMessage::V1(_) => TransactionVersion::Number(1), - } - } - - fn num_transaction_signatures(&self) -> u64 { - u64::from(self.header().num_required_signatures) - } - - fn num_write_locks(&self) -> u64 { - SanitizedMessage::num_write_locks(self) - } - - fn recent_blockhash(&self) -> &Hash { - SanitizedMessage::recent_blockhash(self) - } - - fn num_instructions(&self) -> usize { - SanitizedMessage::instructions(self).len() - } - - fn instructions_iter(&self) -> impl Iterator> { - SanitizedMessage::instructions(self) - .iter() - .map(SVMInstruction::from) - } - - fn program_instructions_iter( - &self, - ) -> impl Iterator)> + Clone { - SanitizedMessage::program_instructions_iter(self) - .map(|(pubkey, ix)| (pubkey, SVMInstruction::from(ix))) - } - - fn static_account_keys(&self) -> &[Pubkey] { - SanitizedMessage::static_account_keys(self) - } - - fn fee_payer(&self) -> &Pubkey { - SanitizedMessage::fee_payer(self) - } - - fn num_lookup_tables(&self) -> usize { - SanitizedMessage::message_address_table_lookups(self).len() - } - - fn message_address_table_lookups( - &self, - ) -> impl Iterator> { - SanitizedMessage::message_address_table_lookups(self) - .iter() - .map(SVMMessageAddressTableLookup::from) - } -} - -// Implement for the "reference" `SanitizedMessage` type. -impl SVMMessage for SanitizedMessage { - fn account_keys(&self) -> AccountKeys<'_> { - SanitizedMessage::account_keys(self) - } - - fn is_writable(&self, index: usize) -> bool { - SanitizedMessage::is_writable(self, index) - } - - fn is_signer(&self, index: usize) -> bool { - SanitizedMessage::is_signer(self, index) - } - - fn is_invoked(&self, key_index: usize) -> bool { - SanitizedMessage::is_invoked(self, key_index) - } -} diff --git a/svm-transaction/src/svm_message/sanitized_transaction.rs b/svm-transaction/src/svm_message/sanitized_transaction.rs deleted file mode 100644 index e396a3976b5..00000000000 --- a/svm-transaction/src/svm_message/sanitized_transaction.rs +++ /dev/null @@ -1,79 +0,0 @@ -use { - crate::{ - instruction::SVMInstruction, - message_address_table_lookup::SVMMessageAddressTableLookup, - svm_message::{SVMMessage, SVMStaticMessage}, - }, - solana_hash::Hash, - solana_message::AccountKeys, - solana_pubkey::Pubkey, - solana_transaction::{sanitized::SanitizedTransaction, versioned::TransactionVersion}, -}; - -impl SVMStaticMessage for SanitizedTransaction { - fn version(&self) -> TransactionVersion { - SVMStaticMessage::version(SanitizedTransaction::message(self)) - } - - fn num_transaction_signatures(&self) -> u64 { - SVMStaticMessage::num_transaction_signatures(SanitizedTransaction::message(self)) - } - - fn num_write_locks(&self) -> u64 { - SVMStaticMessage::num_write_locks(SanitizedTransaction::message(self)) - } - - fn recent_blockhash(&self) -> &Hash { - SVMStaticMessage::recent_blockhash(SanitizedTransaction::message(self)) - } - - fn num_instructions(&self) -> usize { - SVMStaticMessage::num_instructions(SanitizedTransaction::message(self)) - } - - fn instructions_iter(&self) -> impl Iterator> { - SVMStaticMessage::instructions_iter(SanitizedTransaction::message(self)) - } - - fn program_instructions_iter( - &self, - ) -> impl Iterator)> + Clone { - SVMStaticMessage::program_instructions_iter(SanitizedTransaction::message(self)) - } - - fn static_account_keys(&self) -> &[Pubkey] { - SVMStaticMessage::static_account_keys(SanitizedTransaction::message(self)) - } - - fn fee_payer(&self) -> &Pubkey { - SVMStaticMessage::fee_payer(SanitizedTransaction::message(self)) - } - - fn num_lookup_tables(&self) -> usize { - SVMStaticMessage::num_lookup_tables(SanitizedTransaction::message(self)) - } - - fn message_address_table_lookups( - &self, - ) -> impl Iterator> { - SVMStaticMessage::message_address_table_lookups(SanitizedTransaction::message(self)) - } -} - -impl SVMMessage for SanitizedTransaction { - fn account_keys(&self) -> AccountKeys<'_> { - SVMMessage::account_keys(SanitizedTransaction::message(self)) - } - - fn is_writable(&self, index: usize) -> bool { - SVMMessage::is_writable(SanitizedTransaction::message(self), index) - } - - fn is_signer(&self, index: usize) -> bool { - SVMMessage::is_signer(SanitizedTransaction::message(self), index) - } - - fn is_invoked(&self, key_index: usize) -> bool { - SVMMessage::is_invoked(SanitizedTransaction::message(self), key_index) - } -} diff --git a/svm-transaction/src/svm_transaction.rs b/svm-transaction/src/svm_transaction.rs deleted file mode 100644 index a45063384f4..00000000000 --- a/svm-transaction/src/svm_transaction.rs +++ /dev/null @@ -1,11 +0,0 @@ -use {crate::svm_message::SVMMessage, solana_signature::Signature}; - -mod sanitized_transaction; - -pub trait SVMTransaction: SVMMessage { - /// Get the first signature of the message. - fn signature(&self) -> &Signature; - - /// Get all the signatures of the message. - fn signatures(&self) -> &[Signature]; -} diff --git a/svm-transaction/src/svm_transaction/sanitized_transaction.rs b/svm-transaction/src/svm_transaction/sanitized_transaction.rs deleted file mode 100644 index 19d102d65a6..00000000000 --- a/svm-transaction/src/svm_transaction/sanitized_transaction.rs +++ /dev/null @@ -1,14 +0,0 @@ -use { - crate::svm_transaction::SVMTransaction, solana_signature::Signature, - solana_transaction::sanitized::SanitizedTransaction, -}; - -impl SVMTransaction for SanitizedTransaction { - fn signature(&self) -> &Signature { - SanitizedTransaction::signature(self) - } - - fn signatures(&self) -> &[Signature] { - SanitizedTransaction::signatures(self) - } -} diff --git a/svm-transaction/src/tests.rs b/svm-transaction/src/tests.rs deleted file mode 100644 index dbca7e721ba..00000000000 --- a/svm-transaction/src/tests.rs +++ /dev/null @@ -1,292 +0,0 @@ -#![cfg(test)] -use { - crate::svm_message::SVMMessage, - solana_hash::Hash, - solana_message::{ - MessageHeader, SanitizedMessage, SanitizedVersionedMessage, SimpleAddressLoader, - VersionedMessage, - compiled_instruction::CompiledInstruction, - legacy, - v0::{self, LoadedAddresses, MessageAddressTableLookup}, - }, - solana_pubkey::Pubkey, - solana_sdk_ids::system_program, - solana_system_interface::instruction::SystemInstruction, - std::collections::HashSet, -}; - -#[test] -fn test_get_durable_nonce() { - fn create_message_for_test( - num_signers: u8, - num_writable: u8, - account_keys: Vec, - instructions: Vec, - loaded_addresses: Option, - ) -> SanitizedMessage { - let header = MessageHeader { - num_required_signatures: num_signers, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: u8::try_from(account_keys.len()) - .unwrap() - .checked_sub(num_writable) - .unwrap(), - }; - let (versioned_message, loader) = match loaded_addresses { - None => ( - VersionedMessage::Legacy(legacy::Message { - header, - account_keys, - recent_blockhash: Hash::default(), - instructions, - }), - SimpleAddressLoader::Disabled, - ), - Some(loaded_addresses) => ( - VersionedMessage::V0(v0::Message { - header, - account_keys, - recent_blockhash: Hash::default(), - instructions, - address_table_lookups: vec![MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: (0..loaded_addresses.writable.len()) - .map(|x| x as u8) - .collect(), - readonly_indexes: (0..loaded_addresses.readonly.len()) - .map(|x| loaded_addresses.writable.len().saturating_add(x) as u8) - .collect(), - }], - }), - SimpleAddressLoader::Enabled(loaded_addresses), - ), - }; - SanitizedMessage::try_new( - SanitizedVersionedMessage::try_new(versioned_message).unwrap(), - loader, - &HashSet::new(), - ) - .unwrap() - } - - // No instructions - no nonce - { - let message = create_message_for_test(1, 1, vec![Pubkey::new_unique()], vec![], None); - assert!(SanitizedMessage::get_durable_nonce(&message).is_none()); - assert!(SVMMessage::get_durable_nonce(&message).is_none()); - } - - // system program id instruction - invalid - { - let message = create_message_for_test( - 1, - 1, - vec![Pubkey::new_unique(), system_program::id()], - vec![CompiledInstruction::new_from_raw_parts(1, vec![], vec![])], - None, - ); - assert!(SanitizedMessage::get_durable_nonce(&message).is_none()); - assert!(SVMMessage::get_durable_nonce(&message).is_none()); - } - - // system program id instruction - not nonce - { - let message = create_message_for_test( - 1, - 1, - vec![Pubkey::new_unique(), system_program::id()], - vec![CompiledInstruction::new( - 1, - &SystemInstruction::Transfer { lamports: 1 }, - vec![0, 0], - )], - None, - ); - assert!(SanitizedMessage::get_durable_nonce(&message).is_none()); - assert!(SVMMessage::get_durable_nonce(&message).is_none()); - } - - // system program id - nonce instruction (no accounts) - { - let message = create_message_for_test( - 1, - 1, - vec![Pubkey::new_unique(), system_program::id()], - vec![CompiledInstruction::new( - 1, - &SystemInstruction::AdvanceNonceAccount, - vec![], - )], - None, - ); - assert!(SanitizedMessage::get_durable_nonce(&message).is_none()); - assert!(SVMMessage::get_durable_nonce(&message).is_none()); - } - - // system program id - nonce instruction (non-fee-payer, non-writable) - { - let payer = Pubkey::new_unique(); - let nonce = Pubkey::new_unique(); - let message = create_message_for_test( - 1, - 1, - vec![payer, nonce, system_program::id()], - vec![CompiledInstruction::new( - 1, - &SystemInstruction::AdvanceNonceAccount, - vec![1], - )], - None, - ); - assert!(SanitizedMessage::get_durable_nonce(&message).is_none()); - assert!(SVMMessage::get_durable_nonce(&message).is_none()); - } - - // system program id - nonce instruction fee-payer - { - let payer_nonce = Pubkey::new_unique(); - let message = create_message_for_test( - 1, - 1, - vec![payer_nonce, system_program::id()], - vec![CompiledInstruction::new( - 1, - &SystemInstruction::AdvanceNonceAccount, - vec![0], - )], - None, - ); - assert_eq!( - SanitizedMessage::get_durable_nonce(&message), - Some(&payer_nonce) - ); - assert_eq!(SVMMessage::get_durable_nonce(&message), Some(&payer_nonce)); - } - - // system program id - nonce instruction w/ trailing bytes fee-payer - { - let payer_nonce = Pubkey::new_unique(); - let mut instruction_bytes = vec![4, 0, 0, 0]; - instruction_bytes.push(0); // add a trailing byte - let message = create_message_for_test( - 1, - 1, - vec![payer_nonce, system_program::id()], - vec![CompiledInstruction::new_from_raw_parts( - 1, - instruction_bytes, - vec![0], - )], - None, - ); - assert_eq!( - SanitizedMessage::get_durable_nonce(&message), - Some(&payer_nonce) - ); - assert_eq!(SVMMessage::get_durable_nonce(&message), Some(&payer_nonce)); - } - - // system program id - nonce instruction (non-fee-payer) - { - let payer = Pubkey::new_unique(); - let nonce = Pubkey::new_unique(); - let message = create_message_for_test( - 1, - 2, - vec![payer, nonce, system_program::id()], - vec![CompiledInstruction::new( - 2, - &SystemInstruction::AdvanceNonceAccount, - vec![1], - )], - None, - ); - assert_eq!(SanitizedMessage::get_durable_nonce(&message), Some(&nonce)); - assert_eq!(SVMMessage::get_durable_nonce(&message), Some(&nonce)); - } - - // system program id - nonce instruction (non-fee-payer, multiple accounts) - { - let payer = Pubkey::new_unique(); - let other = Pubkey::new_unique(); - let nonce = Pubkey::new_unique(); - let message = create_message_for_test( - 1, - 3, - vec![payer, other, nonce, system_program::id()], - vec![CompiledInstruction::new( - 3, - &SystemInstruction::AdvanceNonceAccount, - vec![2, 1, 0], - )], - None, - ); - assert_eq!(SanitizedMessage::get_durable_nonce(&message), Some(&nonce)); - assert_eq!(SVMMessage::get_durable_nonce(&message), Some(&nonce)); - } - - // system program id - nonce instruction (non-fee-payer, loaded account) - { - let payer = Pubkey::new_unique(); - let nonce = Pubkey::new_unique(); - let message = create_message_for_test( - 1, - 1, - vec![payer, system_program::id()], - vec![CompiledInstruction::new( - 1, - &SystemInstruction::AdvanceNonceAccount, - vec![2, 0, 1], - )], - Some(LoadedAddresses { - writable: vec![nonce], - readonly: vec![], - }), - ); - assert_eq!(SanitizedMessage::get_durable_nonce(&message), Some(&nonce)); - assert_eq!(SVMMessage::get_durable_nonce(&message), None); - } -} - -#[test] -fn test_get_ix_signers() { - let signer0 = Pubkey::new_unique(); - let signer1 = Pubkey::new_unique(); - let non_signer = Pubkey::new_unique(); - let loader_key = Pubkey::new_unique(); - let instructions = vec![ - CompiledInstruction::new(3, &(), vec![2, 0]), - CompiledInstruction::new(3, &(), vec![0, 1]), - CompiledInstruction::new(3, &(), vec![0, 0]), - ]; - - let message = SanitizedMessage::try_from_legacy_message( - legacy::Message::new_with_compiled_instructions( - 2, - 1, - 2, - vec![signer0, signer1, non_signer, loader_key], - Hash::default(), - instructions, - ), - &HashSet::default(), - ) - .unwrap(); - - assert_eq!( - SVMMessage::get_ix_signers(&message, 0).collect::>(), - HashSet::from_iter([&signer0]) - ); - assert_eq!( - SVMMessage::get_ix_signers(&message, 1).collect::>(), - HashSet::from_iter([&signer0, &signer1]) - ); - assert_eq!( - SVMMessage::get_ix_signers(&message, 2).collect::>(), - HashSet::from_iter([&signer0]) - ); - assert_eq!( - SVMMessage::get_ix_signers(&message, 3).collect::>(), - HashSet::default() - ); -} diff --git a/svm-type-overrides/Cargo.toml b/svm-type-overrides/Cargo.toml index 93a70c79b0f..2a89fa4fd73 100644 --- a/svm-type-overrides/Cargo.toml +++ b/svm-type-overrides/Cargo.toml @@ -8,6 +8,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] shuttle-test = ["dep:shuttle"] @@ -17,3 +22,6 @@ executor = ["dep:futures"] futures = { workspace = true, optional = true } rand = { workspace = true } shuttle = { workspace = true, optional = true } + +[lints] +workspace = true diff --git a/svm/Cargo.toml b/svm/Cargo.toml index a767df95618..926586e0d49 100644 --- a/svm/Cargo.toml +++ b/svm/Cargo.toml @@ -7,10 +7,12 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -19,8 +21,32 @@ name = "solana_svm" [features] default = ["metrics"] agave-unstable-api = [] -dummy-for-ci-check = ["metrics"] -dev-context-only-utils = ["dep:qualifier_attr", "solana-program-runtime/dev-context-only-utils"] +conformance = [ + "dep:agave-feature-set", + "dep:agave-precompiles", + "dep:bincode", + "dep:prost", + "dep:protosol", + "dep:solana-poseidon", + "dep:solana-vote-program", + "dep:solana-zk-elgamal-proof-program", + "dep:xxhash-rust", + "dev-context-only-utils", + "solana-instruction-error/serde", + "solana-pubkey/default", +] +dev-context-only-utils = [ + "dep:qualifier_attr", + "dep:solana-bpf-loader-program", + "dep:solana-compute-budget", + "dep:solana-compute-budget-program", + "dep:solana-instruction-error", + "dep:solana-precompile-error", + "dep:solana-syscalls", + "dep:solana-system-program", + "solana-program-runtime/dev-context-only-utils", +] +dummy-for-ci-check = ["conformance", "metrics"] frozen-abi = [ "dep:solana-frozen-abi", "dep:solana-frozen-abi-macro", @@ -38,13 +64,21 @@ shuttle-test = [ svm-internal = ["dep:qualifier_attr"] [dependencies] +agave-feature-set = { workspace = true, optional = true } +agave-precompiles = { workspace = true, optional = true } ahash = { workspace = true } +bincode = { workspace = true, optional = true } log = { workspace = true } percentage = { workspace = true } +prost = { version = "0.11", optional = true } +protosol = { workspace = true, optional = true } qualifier_attr = { workspace = true, optional = true } serde = { workspace = true, features = ["rc"] } solana-account = { workspace = true } +solana-bpf-loader-program = { workspace = true, optional = true } solana-clock = { workspace = true } +solana-compute-budget = { workspace = true, optional = true } +solana-compute-budget-program = { workspace = true, optional = true } solana-fee-structure = { workspace = true } solana-frozen-abi = { workspace = true, optional = true, features = [ "frozen-abi", @@ -54,12 +88,15 @@ solana-frozen-abi-macro = { workspace = true, optional = true, features = [ ] } solana-hash = { workspace = true } solana-instruction = { workspace = true, features = ["std"] } +solana-instruction-error = { workspace = true, optional = true } solana-instructions-sysvar = { workspace = true } solana-loader-v3-interface = { workspace = true, features = ["bincode"] } solana-loader-v4-interface = { workspace = true } solana-message = { workspace = true } solana-nonce = { workspace = true } solana-nonce-account = { workspace = true, features = ["wincode"] } +solana-poseidon = { workspace = true, optional = true } +solana-precompile-error = { workspace = true, optional = true } solana-program-entrypoint = { workspace = true } solana-program-pack = { workspace = true } solana-program-runtime = { workspace = true } @@ -73,12 +110,17 @@ solana-svm-measure = { workspace = true } solana-svm-timings = { workspace = true } solana-svm-transaction = { workspace = true } solana-svm-type-overrides = { workspace = true } +solana-syscalls = { workspace = true, optional = true } solana-system-interface = { workspace = true } +solana-system-program = { workspace = true, optional = true } solana-sysvar-id = { workspace = true } solana-transaction-context = { workspace = true } solana-transaction-error = { workspace = true } +solana-vote-program = { workspace = true, optional = true } +solana-zk-elgamal-proof-program = { workspace = true, optional = true } spl-generic-token = { workspace = true } thiserror = { workspace = true } +xxhash-rust = { workspace = true, optional = true, features = ["xxh64"] } [dev-dependencies] assert_matches = { workspace = true } diff --git a/svm/doc/spec.md b/svm/doc/spec.md index 16c311bb9b4..74333b443e0 100644 --- a/svm/doc/spec.md +++ b/svm/doc/spec.md @@ -28,7 +28,7 @@ We envision the following applications for SVM The SVM is currently viewed as realizing two stages of the Transaction Engine Execution pipeline as described in Solana Architecture documentation - [https://docs.solana.com/validator/runtime#execution](https://docs.solana.com/validator/runtime#execution), + [https://docs.anza.xyz/validator/runtime#execution](https://docs.anza.xyz/validator/runtime#execution), namely ‘load accounts’ and ‘execute’ stages. - **SVM Rollups** diff --git a/svm/src/account_loader.rs b/svm/src/account_loader.rs index 65b576534fa..b395d0f4278 100644 --- a/svm/src/account_loader.rs +++ b/svm/src/account_loader.rs @@ -142,6 +142,9 @@ pub(crate) struct LoadedTransactionAccount { )] pub struct LoadedTransaction { pub accounts: Vec, + /// Parallel to `accounts`: whether each account must be written back. Empty + /// until execution. + pub touched_flags: Box<[bool]>, pub fee_details: FeeDetails, pub rollback_accounts: RollbackAccounts, pub(crate) compute_budget: SVMTransactionExecutionBudget, @@ -290,6 +293,7 @@ impl<'a, CB: TransactionProcessingCallback> AccountLoader<'a, CB> { &mut self, message: &impl SVMMessage, transaction_accounts: &[KeyedAccountSharedData], + touched_flags: &[bool], current_slot: Slot, ) { for (i, (address, account)) in (0..message.account_keys().len()).zip(transaction_accounts) { @@ -297,6 +301,11 @@ impl<'a, CB: TransactionProcessingCallback> AccountLoader<'a, CB> { continue; } + // Skip write-locked accounts the transaction left unmodified. + if !touched_flags[i] { + continue; + } + // Accounts that are invoked and also not passed as an instruction // account to a program don't need to be stored because it's assumed // to be impossible for a committable transaction to modify an @@ -321,14 +330,6 @@ impl TransactionProcessingCallback for Accoun } } -// NOTE this is a required subtrait of TransactionProcessingCallback. -// It may make sense to break out a second subtrait just for the above two functions, -// but this would be a nontrivial breaking change and require careful consideration. -impl solana_svm_callback::InvokeContextCallback - for AccountLoader<'_, CB> -{ -} - /// Set the rent epoch to u64::MAX if the account is rent exempt. /// /// TODO: This function is used to update the rent epoch of an account. Once we @@ -427,6 +428,8 @@ pub(crate) fn load_transaction( match load_result { Ok(accounts) => TransactionLoadResult::Loaded(LoadedTransaction { accounts, + // Populated after execution by execute_loaded_transaction. + touched_flags: Box::default(), fee_details: tx_details.fee_details, rollback_accounts: tx_details.rollback_accounts, compute_budget: tx_details.compute_budget, @@ -578,7 +581,7 @@ fn load_transaction_accounts( // Attempt to load and collect remaining non-fee payer accounts. for (account_index, account_key) in account_keys.iter().enumerate().skip(1) { let loaded_account = - load_transaction_account(account_loader, message, account_key, account_index, rent); + load_transaction_account(account_loader, message, account_key, account_index, rent)?; collect_loaded_account(account_loader, account_key, loaded_account)?; } @@ -604,33 +607,33 @@ fn load_transaction_account( account_key: &Pubkey, account_index: usize, rent: &Rent, -) -> LoadedTransactionAccount { +) -> Result { let is_writable = message.is_writable(account_index); if solana_sdk_ids::sysvar::instructions::check_id(account_key) { // Since the instructions sysvar is constructed by the SVM and modified // for each transaction instruction, it cannot be loaded. - LoadedTransactionAccount { + Ok(LoadedTransactionAccount { loaded_size: 0, - account: construct_instructions_account(message), - } + account: construct_instructions_account(message)?, + }) } else if let Some(mut loaded_account) = account_loader.load_transaction_account(account_key, is_writable) { if is_writable { update_rent_exempt_status_for_account(rent, &mut loaded_account.account); } - loaded_account + Ok(loaded_account) } else { let mut default_account = AccountSharedData::default(); default_account.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); - LoadedTransactionAccount { + Ok(LoadedTransactionAccount { loaded_size: default_account.data().len(), account: default_account, - } + }) } } -fn construct_instructions_account(message: &impl SVMMessage) -> AccountSharedData { +fn construct_instructions_account(message: &impl SVMMessage) -> Result { let account_keys = message.account_keys(); let mut decompiled_instructions = Vec::with_capacity(message.num_instructions()); for (program_id, instruction) in message.program_instructions_iter() { @@ -654,11 +657,12 @@ fn construct_instructions_account(message: &impl SVMMessage) -> AccountSharedDat }); } - AccountSharedData::from(Account { - data: construct_instructions_data(&decompiled_instructions), + Ok(AccountSharedData::from(Account { + data: construct_instructions_data(&decompiled_instructions) + .map_err(|_err| TransactionError::MaxLoadedAccountsDataSizeExceeded)?, owner: sysvar::id(), ..Account::default() - }) + })) } #[cfg(test)] @@ -676,6 +680,7 @@ mod tests { LegacyMessage, Message, MessageHeader, SanitizedMessage, compiled_instruction::CompiledInstruction, v0::{LoadedAddresses, LoadedMessage}, + v1, }, solana_native_token::LAMPORTS_PER_SOL, solana_nonce::{self as nonce, versions::Versions as NonceVersions}, @@ -689,7 +694,7 @@ mod tests { }, solana_signature::Signature, solana_signer::Signer, - solana_svm_callback::{InvokeContextCallback, TransactionProcessingCallback}, + solana_svm_callback::TransactionProcessingCallback, solana_system_transaction::transfer, solana_transaction::{Transaction, sanitized::SanitizedTransaction}, solana_transaction_context::{ @@ -730,8 +735,6 @@ mod tests { } } - impl InvokeContextCallback for TestCallbacks {} - impl TransactionProcessingCallback for TestCallbacks { fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { self.accounts_map @@ -806,6 +809,62 @@ mod tests { SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())) } + #[test] + fn test_update_accounts_for_successful_tx_skips_untouched() { + let fee_payer = Keypair::new(); + let (touched_key, untouched_key) = (Pubkey::new_unique(), Pubkey::new_unique()); + let program = Pubkey::new_unique(); + + // Writable accounts at indices 0, 1, 2; readonly program at index 3. + let instructions = vec![CompiledInstruction::new(3, &(), vec![1, 2])]; + let message = new_unchecked_sanitized_message(Message::new_with_compiled_instructions( + 1, // num_required_signatures + 0, // num_readonly_signed_accounts + 1, // num_readonly_unsigned_accounts -> only the program is readonly + vec![fee_payer.pubkey(), touched_key, untouched_key, program], + Hash::default(), + instructions, + )); + let transaction_accounts = [ + ( + fee_payer.pubkey(), + AccountSharedData::new(100, 0, &Pubkey::default()), + ), + ( + touched_key, + AccountSharedData::new(1, 0, &Pubkey::default()), + ), + ( + untouched_key, + AccountSharedData::new(2, 0, &Pubkey::default()), + ), + (program, AccountSharedData::new(3, 0, &Pubkey::default())), + ]; + + // Fee payer (index 0) and the VM-modified account (index 1) are marked; + // the writable account at index 2 is left untouched. + let touched_flags: Box<[bool]> = [true, true, false, false].into(); + + let callbacks = TestCallbacks::default(); + let mut account_loader: AccountLoader = (&callbacks).into(); + account_loader.update_accounts_for_successful_tx( + &message, + &transaction_accounts, + &touched_flags, + 5, + ); + + // Only touched writable accounts propagate to the in-batch loader cache. + assert!( + account_loader + .loaded_accounts + .contains_key(&fee_payer.pubkey()) + ); + assert!(account_loader.loaded_accounts.contains_key(&touched_key)); + assert!(!account_loader.loaded_accounts.contains_key(&untouched_key)); + assert!(!account_loader.loaded_accounts.contains_key(&program)); + } + #[test] fn test_load_accounts_unknown_program_id() { let mut accounts: Vec = Vec::new(); @@ -1155,14 +1214,14 @@ mod tests { acc.increase_calculated_data_size(data_size, &mut error_metrics) .is_ok() ); - assert_eq!(data_size as u32, acc.clone().into()); + assert_eq!(data_size as u32, u32::from(acc.clone())); // OK - loaded data size meets limit assert!( acc.increase_calculated_data_size(1, &mut error_metrics) .is_ok() ); - assert_eq!(requested_data_size_limit, acc.clone().into()); + assert_eq!(requested_data_size_limit, u32::from(acc.clone())); // fail - loading more data would exceed limit // data size helper reports the limit only @@ -1170,7 +1229,7 @@ mod tests { acc.increase_calculated_data_size(1, &mut error_metrics), Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) ); - assert_eq!(requested_data_size_limit, acc.into()); + assert_eq!(requested_data_size_limit, u32::from(acc)); let mut acc = LoadedTransactionDataSize::with_max_size(requested_data_size_limit); @@ -1180,7 +1239,7 @@ mod tests { acc.increase_calculated_data_size(u32::MAX as usize + 1, &mut error_metrics), Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) ); - assert_eq!(requested_data_size_limit, acc.into()); + assert_eq!(requested_data_size_limit, u32::from(acc)); } struct ValidateFeePayerTestParameter { @@ -1407,9 +1466,9 @@ mod tests { is_writable_account_cache: vec![false], }; let message = SanitizedMessage::V0(loaded_message); - let shared_data = construct_instructions_account(&message); + let shared_data = construct_instructions_account(&message).unwrap(); let expected = AccountSharedData::from(Account { - data: construct_instructions_data(&message.decompile_instructions()), + data: construct_instructions_data(&message.decompile_instructions()).unwrap(), owner: sysvar::id(), ..Account::default() }); @@ -2149,6 +2208,7 @@ mod tests { ), (key3.pubkey(), account_data), ], + touched_flags: Box::default(), fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::default(), compute_budget: SVMTransactionExecutionBudget::default(), @@ -2214,6 +2274,66 @@ mod tests { )); } + #[test] + fn test_load_accounts_v1_instructions_sysvar_overflow() { + const NUM_INSTRUCTIONS: usize = 64; + const ACCOUNTS_PER_INSTRUCTION: usize = 31; + + let fee_payer = Pubkey::new_unique(); + let instructions_sysvar = sysvar::instructions::id(); + let program = native_loader::id(); + let message = v1::Message::new( + MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 2, + }, + v1::TransactionConfig::empty(), + Hash::default(), + vec![fee_payer, instructions_sysvar, program], + vec![ + CompiledInstruction { + program_id_index: 2, + accounts: vec![1; ACCOUNTS_PER_INSTRUCTION], + data: vec![], + }; + NUM_INSTRUCTIONS + ], + ); + message.validate().unwrap(); + assert!( + 1 + message.size() + Signature::default().as_ref().len() <= v1::MAX_TRANSACTION_SIZE + ); + + let sanitized_message = + SanitizedMessage::V1(v1::CachedMessage::new(message, &HashSet::new())); + let mock_bank = TestCallbacks::default(); + let mut account_loader = (&mock_bank).into(); + + let fee_payer_account = AccountSharedData::new(200, 0, &Pubkey::default()); + let load_result = load_transaction( + &mut account_loader, + &sanitized_message, + Ok(ValidatedTransactionDetails { + loaded_fee_payer_account: LoadedTransactionAccount { + account: fee_payer_account, + loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE, + }, + ..ValidatedTransactionDetails::default() + }), + &mut TransactionErrorMetrics::default(), + &Rent::default(), + ); + + assert!(matches!( + load_result, + TransactionLoadResult::FeesOnly(FeesOnlyTransaction { + load_error: TransactionError::MaxLoadedAccountsDataSizeExceeded, + .. + }), + )); + } + #[test] fn test_update_rent_exempt_status_for_account() { let rent = Rent::default(); diff --git a/svm/src/conformance/account_state.rs b/svm/src/conformance/account_state.rs new file mode 100644 index 00000000000..144e9818ee0 --- /dev/null +++ b/svm/src/conformance/account_state.rs @@ -0,0 +1,51 @@ +//! Account state conversions for protobuf support. + +use {protosol::protos::AcctState as ProtoAccount, solana_account::Account, solana_pubkey::Pubkey}; + +// Default `rent_epoch` field value for all accounts. +const RENT_EXEMPT_RENT_EPOCH: u64 = u64::MAX; + +/// Convert a protobuf account state into a `(Pubkey, Account)` pair. +pub fn account_from_proto(value: ProtoAccount) -> (Pubkey, Account) { + let ProtoAccount { + address, + owner, + lamports, + data, + executable, + .. + } = value; + + let pubkey = Pubkey::try_from(address).expect("invalid account address bytes"); + let owner = Pubkey::try_from(owner).expect("invalid account owner bytes"); + + ( + pubkey, + Account { + data, + executable, + lamports, + owner, + rent_epoch: RENT_EXEMPT_RENT_EPOCH, + }, + ) +} + +/// Convert a `(Pubkey, Account)` pair into a protobuf account state. +pub fn account_to_proto(value: (Pubkey, Account)) -> ProtoAccount { + let Account { + lamports, + data, + owner, + executable, + .. + } = value.1; + + ProtoAccount { + address: value.0.to_bytes().to_vec(), + owner: owner.to_bytes().to_vec(), + lamports, + data, + executable, + } +} diff --git a/svm/src/conformance/callback.rs b/svm/src/conformance/callback.rs new file mode 100644 index 00000000000..af32c24f355 --- /dev/null +++ b/svm/src/conformance/callback.rs @@ -0,0 +1,39 @@ +//! Invoke-context callbacks shared by the conformance harnesses. + +use solana_svm_callback::InvokeContextCallback; +#[cfg(feature = "conformance")] +use { + agave_feature_set::FeatureSet, + agave_precompiles::{get_precompile, is_precompile}, + solana_precompile_error::PrecompileError, + solana_pubkey::Pubkey, +}; + +/// Default callback. No precompile support. +pub struct DefaultCallback; + +impl InvokeContextCallback for DefaultCallback {} + +/// Conformance callback. Full precompile support across all features. +#[cfg(feature = "conformance")] +pub struct ConformanceCallback; + +#[cfg(feature = "conformance")] +impl InvokeContextCallback for ConformanceCallback { + fn is_precompile(&self, program_id: &Pubkey) -> bool { + is_precompile(program_id, |_| true) + } + + fn process_precompile( + &self, + program_id: &Pubkey, + data: &[u8], + instruction_datas: Vec<&[u8]>, + ) -> Result<(), PrecompileError> { + if let Some(precompile) = get_precompile(program_id, |_| true) { + precompile.verify(data, &instruction_datas, &FeatureSet::all_enabled()) + } else { + Err(PrecompileError::InvalidPublicKey) + } + } +} diff --git a/svm/src/conformance/elf_loader.rs b/svm/src/conformance/elf_loader.rs new file mode 100644 index 00000000000..adedefe42a3 --- /dev/null +++ b/svm/src/conformance/elf_loader.rs @@ -0,0 +1,146 @@ +//! ELF loader conformance harness. + +use { + crate::conformance::{ + err::elf_error_code, + fd_hash::{fd_hash_u64_without_seed, fd_hash_without_seed}, + feature_set::feature_set_from_proto, + }, + prost::Message, + protosol::protos::{ + ElfLoaderCtx as ProtoElfLoaderCtx, ElfLoaderEffects as ProtoElfLoaderEffects, + }, + solana_compute_budget::compute_budget::ComputeBudget, + solana_program_runtime::solana_sbpf::{ebpf, elf::Executable}, + solana_syscalls::create_program_runtime_environment, + std::{collections::BTreeSet, ffi::c_int}, +}; + +pub fn execute_elf_loader(input: &ProtoElfLoaderCtx) -> ProtoElfLoaderEffects { + let feature_set = input + .features + .as_ref() + .map(feature_set_from_proto) + .unwrap_or_default() + .runtime_features(); + let simd_0268_active = feature_set.raise_cpi_nesting_limit_to_8; + let compute_budget = ComputeBudget::new_with_defaults(simd_0268_active); + + let program_runtime_environment = create_program_runtime_environment( + &feature_set, + &compute_budget.to_budget(), + input.deploy_checks, + std::env::var("ENABLE_VM_TRACING").is_ok(), + ) + .unwrap(); + + let executable = match Executable::load(&input.elf_data, (*program_runtime_environment).clone()) + { + Ok(executable) => executable, + Err(err) => { + return ProtoElfLoaderEffects { + err_code: elf_error_code(&err), + ..Default::default() + }; + } + }; + + let (text_vaddr, text_bytes) = executable.get_text_bytes(); + let calldests: Vec = executable + .get_function_registry() + .iter() + .map(|(_, (_, address))| address as u64) + .collect::>() + .into_iter() + .collect(); + + ProtoElfLoaderEffects { + err_code: 0, + rodata_hash: fd_hash_without_seed(executable.get_ro_section()), + entry_pc: executable.get_entrypoint_instruction_offset() as u64, + text_off: text_vaddr.saturating_sub(ebpf::MM_BYTECODE_START), + text_cnt: (text_bytes.len() / 8) as u64, + calldests_hash: fd_hash_u64_without_seed(&calldests), + } +} + +/// # Safety +/// +/// `in_ptr` must point to `in_sz` initialized bytes. `out_ptr` must point +/// to a writable buffer of at least `*out_psz` bytes. On return, `*out_psz` +/// is updated to the number of bytes written. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sol_compat_elf_loader_v1( + out_ptr: *mut u8, + out_psz: *mut u64, + in_ptr: *mut u8, + in_sz: u64, +) -> c_int { + if in_ptr.is_null() || in_sz == 0 { + return 0; + } + if out_psz.is_null() || out_ptr.is_null() { + return 0; + } + let in_slice = unsafe { std::slice::from_raw_parts(in_ptr, in_sz as usize) }; + let Ok(ctx) = ProtoElfLoaderCtx::decode(in_slice) else { + return 0; + }; + + let effects = execute_elf_loader(&ctx); + + let out_slice = unsafe { std::slice::from_raw_parts_mut(out_ptr, (*out_psz) as usize) }; + let out_vec = effects.encode_to_vec(); + if out_vec.len() > out_slice.len() { + return 0; + } + out_slice[..out_vec.len()].copy_from_slice(&out_vec); + unsafe { *out_psz = out_vec.len() as u64 }; + + 1 +} + +#[cfg(test)] +mod tests { + use { + super::*, agave_feature_set::enable_sbpf_v3_deployment_and_execution, + protosol::protos::FeatureSet as ProtoFeatureSet, + }; + + const NOOP_ALIGNED: &[u8] = + include_bytes!("../../../programs/bpf_loader/test_elfs/out/noop_aligned.so"); + const NOOP_UNALIGNED: &[u8] = + include_bytes!("../../../programs/bpf_loader/test_elfs/out/noop_unaligned.so"); + const SBPFV3_RETURN_OK: &[u8] = + include_bytes!("../../../programs/bpf_loader/test_elfs/out/sbpfv3_return_ok.so"); + + fn assert_loads_ok(elf: &[u8], deploy_checks: bool, features: Option) { + let effects = execute_elf_loader(&ProtoElfLoaderCtx { + features, + elf_data: elf.to_vec(), + deploy_checks, + }); + assert_eq!(effects.err_code, 0); + assert!(effects.text_cnt > 0); + } + + #[test] + fn test_load_noop_aligned() { + assert_loads_ok(NOOP_ALIGNED, false, None); + } + + #[test] + fn test_load_noop_unaligned_with_deploy_checks() { + assert_loads_ok(NOOP_UNALIGNED, true, None); + } + + #[test] + fn test_load_sbpf_v3_with_feature_enabled() { + // The v3 ELF only loads when the SBPF v3 feature is active. + let v3 = enable_sbpf_v3_deployment_and_execution::id(); + let features = ProtoFeatureSet { + features: vec![u64::from_le_bytes(v3.to_bytes()[..8].try_into().unwrap())], + }; + assert_loads_ok(SBPFV3_RETURN_OK, false, Some(features)); + } +} diff --git a/svm/src/conformance/err.rs b/svm/src/conformance/err.rs new file mode 100644 index 00000000000..9f331aab337 --- /dev/null +++ b/svm/src/conformance/err.rs @@ -0,0 +1,105 @@ +//! Error-code mapping for VM execution results. + +use { + solana_instruction::error::InstructionError, + solana_poseidon::PoseidonSyscallError, + solana_program_runtime::{ + cpi::CpiError, + memory::MemoryTranslationError, + solana_sbpf::{ + elf::ElfError, + error::{EbpfError, StableResult}, + }, + }, + solana_syscalls::SyscallError, +}; + +pub(crate) fn elf_error_code(error: &ElfError) -> u32 { + (error.discriminant() as u32).saturating_add(1) +} + +fn ebpf_error_code(error: &EbpfError) -> i64 { + (error.discriminant() as i64).saturating_add(1) +} + +fn syscall_error_code(error: &SyscallError) -> i64 { + (error.discriminant() as i64).saturating_add(1) +} + +pub(crate) fn instruction_error_code(error: &InstructionError) -> i32 { + let serialized = bincode::serialize(error).unwrap(); + i32::from_le_bytes(serialized[0..4].try_into().unwrap()).saturating_add(1) +} + +/// A VM `program_result` mapped into the fields a conformance fixture compares. +pub(crate) struct UnpackedResult { + /// Error number, or `0` on success. + pub error: i64, + /// Which error taxonomy `error` belongs to (`ERR_KIND_*`). + pub error_kind: i32, + /// The program return value (`r0`), only meaningful on success. + pub r0: u64, +} + +impl UnpackedResult { + const ERR_KIND_UNSPECIFIED: i32 = 0; + const ERR_KIND_EBPF: i32 = 1; + const ERR_KIND_SYSCALL: i32 = 2; + const ERR_KIND_INSTRUCTION: i32 = 3; + + fn ok(r0: u64) -> Self { + Self { + error: 0, + error_kind: Self::ERR_KIND_UNSPECIFIED, + r0, + } + } + + fn err(error: i64, error_kind: i32) -> Self { + Self { + error, + error_kind, + r0: 0, + } + } + + fn from_ebpf_err(ebpf_err: EbpfError) -> Self { + // Agave wraps syscall-side failures in `EbpfError::SyscallError`; recover + // the concrete error by downcasting so we report the matching code and + // kind. Anything else is a plain VM error. + match &ebpf_err { + EbpfError::SyscallError(boxed) => { + if let Some(e) = boxed.downcast_ref::() { + Self::err(instruction_error_code(e) as i64, Self::ERR_KIND_INSTRUCTION) + } else if let Some(e) = boxed.downcast_ref::() { + Self::err(syscall_error_code(e), Self::ERR_KIND_SYSCALL) + } else if let Some(e) = boxed.downcast_ref::() { + Self::err( + syscall_error_code(&e.clone().into()), + Self::ERR_KIND_SYSCALL, + ) + } else if let Some(e) = boxed.downcast_ref::() { + Self::err( + syscall_error_code(&e.clone().into()), + Self::ERR_KIND_SYSCALL, + ) + } else if let Some(e) = boxed.downcast_ref::() { + Self::err(ebpf_error_code(e), Self::ERR_KIND_EBPF) + } else if boxed.downcast_ref::().is_some() { + Self::err(-1, Self::ERR_KIND_SYSCALL) + } else { + Self::err(-1, Self::ERR_KIND_UNSPECIFIED) + } + } + _ => Self::err(ebpf_error_code(&ebpf_err), Self::ERR_KIND_EBPF), + } + } +} + +/// Map a VM `program_result` to its [`UnpackedResult`]. +pub(crate) fn unpack_stable_result(program_result: StableResult) -> UnpackedResult { + match program_result { + StableResult::Ok(r0) => UnpackedResult::ok(r0), + StableResult::Err(ebpf_err) => UnpackedResult::from_ebpf_err(ebpf_err), + } +} diff --git a/svm/src/conformance/fd_hash.rs b/svm/src/conformance/fd_hash.rs new file mode 100644 index 00000000000..a624df1a7c9 --- /dev/null +++ b/svm/src/conformance/fd_hash.rs @@ -0,0 +1,35 @@ +//! Hashing used to summarize binary blobs (e.g. ELF sections, serialized VM +//! memory) for conformance comparisons. + +use xxhash_rust::xxh64::xxh64; + +pub fn fd_hash_without_seed(buf: &[u8]) -> u64 { + fd_hash(0, buf) +} + +pub fn fd_hash_u64_without_seed(buf: &[u64]) -> u64 { + let bytes = unsafe { + std::slice::from_raw_parts(buf.as_ptr().cast::(), std::mem::size_of_val(buf)) + }; + fd_hash(0, bytes) +} + +pub fn fd_hash(seed: u64, buf: &[u8]) -> u64 { + xxh64(buf, seed) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fd_hash_matches_xxh64() { + // Reference values from Firedancer's `fd_hash` (XXH64). The empty-input, + // seed-0 case is the canonical XXH64 test vector. + let long: Vec = (0u8..100).collect(); + assert_eq!(fd_hash(0, &[]), 0xef46db3751d8e999); + assert_eq!(fd_hash(0, b"hello world"), 0x45ab6734b21e6968); + assert_eq!(fd_hash(42, b"hello world"), 0x69c2b68f9d9352a1); + assert_eq!(fd_hash(0, &long), 0x6ac1e58032166597); + } +} diff --git a/svm-test-harness/fixture/src/feature_set.rs b/svm/src/conformance/feature_set.rs similarity index 66% rename from svm-test-harness/fixture/src/feature_set.rs rename to svm/src/conformance/feature_set.rs index fca4d6c3cc8..47c86b49bbb 100644 --- a/svm-test-harness/fixture/src/feature_set.rs +++ b/svm/src/conformance/feature_set.rs @@ -1,10 +1,8 @@ //! Feature set conversions for protobuf support. -#![cfg(feature = "fuzz")] - use { - crate::proto::FeatureSet as ProtoFeatureSet, agave_feature_set::{FEATURE_NAMES, FeatureSet}, + protosol::protos::FeatureSet as ProtoFeatureSet, solana_pubkey::Pubkey, std::{collections::HashMap, sync::LazyLock}, }; @@ -28,14 +26,13 @@ static INDEXED_FEATURES: LazyLock> = LazyLock::new(|| { .collect() }); -impl From<&ProtoFeatureSet> for FeatureSet { - fn from(value: &ProtoFeatureSet) -> Self { - let mut feature_set = FeatureSet::default(); - for id in &value.features { - if let Some(pubkey) = INDEXED_FEATURES.get(id) { - feature_set.activate(pubkey, 0); - } +/// Build a `FeatureSet` from a protobuf feature set. +pub fn feature_set_from_proto(value: &ProtoFeatureSet) -> FeatureSet { + let mut feature_set = FeatureSet::default(); + for id in &value.features { + if let Some(pubkey) = INDEXED_FEATURES.get(id) { + feature_set.activate(pubkey, 0); } - feature_set } + feature_set } diff --git a/svm/src/conformance/instr/context.rs b/svm/src/conformance/instr/context.rs new file mode 100644 index 00000000000..8f014f6e49a --- /dev/null +++ b/svm/src/conformance/instr/context.rs @@ -0,0 +1,100 @@ +//! Instruction context (input). + +#[cfg(feature = "conformance")] +use { + crate::conformance::{account_state::account_from_proto, feature_set::feature_set_from_proto}, + protosol::protos::InstrContext as ProtoInstrContext, + solana_instruction::AccountMeta, +}; +use { + solana_account::Account, solana_instruction::Instruction, + solana_program_runtime::execution_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, + solana_pubkey::Pubkey, solana_svm_feature_set::SVMFeatureSet, +}; + +/// Inputs to a single instruction. +pub struct InstrContext { + pub feature_set: SVMFeatureSet, + pub accounts: Vec<(Pubkey, Account)>, + pub instruction: Instruction, + pub cu_avail: u64, +} + +impl InstrContext { + /// Create a new [`InstrContext`] with the default compute unit budget + /// (200,000 CUs). + pub fn new_with_default_budget( + feature_set: SVMFeatureSet, + accounts: Vec<(Pubkey, Account)>, + instruction: Instruction, + ) -> Self { + Self { + feature_set, + accounts, + instruction, + cu_avail: DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64, + } + } +} + +#[cfg(feature = "conformance")] +impl From for InstrContext { + fn from(value: ProtoInstrContext) -> Self { + let program_id = Pubkey::try_from(value.program_id).expect("invalid program_id bytes"); + + let feature_set = value + .features + .as_ref() + .map(feature_set_from_proto) + .unwrap_or_default() + .runtime_features(); + + let accounts: Vec<(Pubkey, Account)> = + value.accounts.into_iter().map(account_from_proto).collect(); + + let instruction_accounts = value + .instr_accounts + .into_iter() + .map(|acct| { + let pubkey = accounts + .get(acct.index as usize) + .map(|(pk, _)| *pk) + .expect("instruction account index out of bounds"); + AccountMeta { + pubkey, + is_signer: acct.is_signer, + is_writable: acct.is_writable, + } + }) + .collect::>(); + + // Match Firedancer harness limit (FD_INSTR_ACCT_MAX = 1094) + // which is derived from the MTU + // (see FD_BPF_INSTR_ACCT_MAX comment in Firedancer) + // + // TODO: This limit exceeds 255 because native programs can currently + // be invoked with more than 255 instruction accounts. Once the + // feature gate restricting instruction accounts to 255 is activated + // (https://github.com/anza-xyz/feature-gate-tracker/issues/115), + // this limit should be tightened to 255, eliminating any ambiguity. + assert!( + instruction_accounts.len() <= 1094, + "too many instruction accounts", + ); + + let instruction = Instruction { + accounts: instruction_accounts, + data: value.data, + program_id, + }; + + let cu_avail = value.cu_avail; + + Self { + feature_set, + accounts, + instruction, + cu_avail, + } + } +} diff --git a/svm-test-harness/fixture/src/instr_effects.rs b/svm/src/conformance/instr/effects.rs similarity index 66% rename from svm-test-harness/fixture/src/instr_effects.rs rename to svm/src/conformance/instr/effects.rs index bdc2b754548..5ca95b429e1 100644 --- a/svm-test-harness/fixture/src/instr_effects.rs +++ b/svm/src/conformance/instr/effects.rs @@ -1,6 +1,11 @@ //! Instruction effects (output). -use {solana_account::Account, solana_instruction_error::InstructionError, solana_pubkey::Pubkey}; +#[cfg(feature = "conformance")] +use { + crate::conformance::{account_state::account_to_proto, err::instruction_error_code}, + protosol::protos::InstrEffects as ProtoInstrEffects, +}; +use {solana_account::Account, solana_instruction::error::InstructionError, solana_pubkey::Pubkey}; /// Represents the effects of a single instruction. pub struct InstrEffects { @@ -22,10 +27,7 @@ impl InstrEffects { } } -#[cfg(feature = "fuzz")] -use {crate::proto::InstrEffects as ProtoInstrEffects, bincode}; - -#[cfg(feature = "fuzz")] +#[cfg(feature = "conformance")] impl From for ProtoInstrEffects { fn from(value: InstrEffects) -> Self { let InstrEffects { @@ -40,14 +42,13 @@ impl From for ProtoInstrEffects { Self { result: result .as_ref() - .map(|error| { - let serialized_err = bincode::serialize(error).unwrap(); - i32::from_le_bytes((&serialized_err[0..4]).try_into().unwrap()) - .saturating_add(1) - }) + .map(instruction_error_code) .unwrap_or_default(), custom_err: custom_err.unwrap_or_default(), - modified_accounts: resulting_accounts.into_iter().map(Into::into).collect(), + modified_accounts: resulting_accounts + .into_iter() + .map(account_to_proto) + .collect(), cu_avail, return_data, } diff --git a/svm/src/conformance/instr/harness.rs b/svm/src/conformance/instr/harness.rs new file mode 100644 index 00000000000..55c7f3eed8e --- /dev/null +++ b/svm/src/conformance/instr/harness.rs @@ -0,0 +1,477 @@ +//! Conformance harness. + +use { + super::{context::InstrContext, effects::InstrEffects}, + crate::{ + conformance::{ + callback::DefaultCallback, + setup::{ + InvokeContextFields, compute_budget, prepare_invoke_context_fields, + program_runtime_environments, + }, + }, + message_processor::process_message, + }, + solana_instruction::error::InstructionError, + solana_program_runtime::{ + invoke_context::InvokeContext, loaded_programs::ProgramCacheForTxBatch, + sysvar_cache::SysvarCache, + }, + solana_pubkey::Pubkey, + solana_svm_callback::InvokeContextCallback, + solana_svm_timings::ExecuteTimings, + solana_transaction_error::TransactionError, + std::rc::Rc, +}; +#[cfg(feature = "conformance")] +use { + crate::conformance::{ + callback::ConformanceCallback, + programs::{fill_program_cache_from_accounts, new_program_cache_with_builtins}, + setup::sysvar_cache_from_accounts, + }, + agave_precompiles::is_precompile, + prost::Message, + protosol::protos::{InstrContext as ProtoInstrContext, InstrEffects as ProtoInstrEffects}, + std::ffi::c_int, +}; + +/// Execute a single instruction against the Solana VM with the default +/// (no-precompile) callback. +pub fn execute_instr( + input: &InstrContext, + program_cache: &mut ProgramCacheForTxBatch, + sysvar_cache: &SysvarCache, +) -> InstrEffects { + execute_instr_with_callback(input, &DefaultCallback, program_cache, sysvar_cache) +} + +/// Execute a single instruction against the Solana VM with a custom callback. +pub fn execute_instr_with_callback( + input: &InstrContext, + callback: &C, + program_cache: &mut ProgramCacheForTxBatch, + sysvar_cache: &SysvarCache, +) -> InstrEffects { + let mut compute_units_consumed = 0; + let mut timings = ExecuteTimings::default(); + + let mut compute_budget = compute_budget(&input.feature_set); + compute_budget.compute_unit_limit = input.cu_avail; // Clamp budget for execution by cu_avail + + let loader_key = program_cache + .find(&input.instruction.program_id) + .expect("program not loaded in cache") + .account_owner(); + + let program_runtime_environments = + program_runtime_environments(&input.feature_set, &compute_budget); + + let InvokeContextFields { + sanitized_message, + mut transaction_context, + environment_config, + log_collector, + execution_budget, + execution_cost, + } = prepare_invoke_context_fields( + input, + callback, + &loader_key, + sysvar_cache, + &compute_budget, + &program_runtime_environments, + ); + + let result = { + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + program_cache, + environment_config, + Some(log_collector.clone()), + execution_budget, + execution_cost, + ); + + match process_message( + &sanitized_message, + &mut invoke_context, + &mut timings, + &mut compute_units_consumed, + ) { + Ok(()) => Ok(()), + Err(TransactionError::InstructionError(_, err)) => Err(err), + // `process_message` only ever returns `InstructionError`-shaped + // failures. + Err(_) => unreachable!(), + } + }; + + let cu_avail = compute_budget + .compute_unit_limit + .saturating_sub(compute_units_consumed); + let return_data = transaction_context.get_return_data().1.to_vec(); + + let logs = Rc::try_unwrap(log_collector) + .ok() + .map(|cell| cell.into_inner().into_messages()) + .unwrap_or_default(); + + let account_keys: Vec = (0..transaction_context.get_number_of_accounts()) + .map(|index| { + *transaction_context + .get_key_of_account_at_index(index) + .clone() + .unwrap() + }) + .collect::>(); + + InstrEffects { + custom_err: if let Err(InstructionError::Custom(code)) = result { + Some(code) + } else { + None + }, + result: result.err(), + resulting_accounts: transaction_context + .deconstruct_without_keys() + .unwrap() + .into_iter() + .zip(account_keys) + .map(|(account, key)| (key, account.into())) + .collect(), + cu_avail, + return_data, + logs, + } +} + +#[cfg(feature = "conformance")] +pub fn execute_instr_proto(input: ProtoInstrContext) -> ProtoInstrEffects { + let instr_context = InstrContext::from(input); + + // When testing with protobuf, we fill the sysvar cache from input accounts. + let sysvar_cache = sysvar_cache_from_accounts(&instr_context.accounts); + + // When testing with protobuf, we fill the program cache from input accounts. + let mut program_cache = { + let slot = sysvar_cache.get_clock().unwrap().slot; + let feature_set = &instr_context.feature_set; + let compute_budget = compute_budget(feature_set); + let environments = program_runtime_environments(feature_set, &compute_budget); + + let mut cache = new_program_cache_with_builtins(slot); + fill_program_cache_from_accounts( + &mut cache, + environments.get_env_for_deployment(), + &instr_context.accounts, + slot, + ); + + cache + }; + + let mut effects = execute_instr_with_callback( + &instr_context, + &ConformanceCallback, + &mut program_cache, + &sysvar_cache, + ); + + // Precompile verification failures surface as `Custom`, but Firedancer + // reports a custom error code of 0 for precompiles. + if effects.custom_err.is_some() + && is_precompile(&instr_context.instruction.program_id, |_| true) + { + effects.custom_err = Some(0); + } + + // TODO: Firedancer's tooling compares resulting account contents even + // when execution fails, so the harness must report them. Account + // contents are not meaningful on error (partial writes can diverge based + // on timing, e.g. with direct mapping or builtins), so once the tooling + // supports it, the harness should skip the account comparison on error + // entirely, which would also make the CU-exhaustion workaround below + // unnecessary. + direct_mapping_handle_cu_exhaustion( + instr_context.feature_set.virtual_address_space_adjustments, + effects.cu_avail, + effects.result.is_some(), + effects + .resulting_accounts + .iter_mut() + .map(|(_, account)| &mut account.data), + ); + + effects.into() +} + +/// Due to how Firedancer's VM CU accounting works, when +/// virtual_address_space_adjustments is enabled and execution fails with the +/// CU meter exhausted, we cannot compare the data region of the accounts with +/// Agave. Clears each supplied data buffer in that case. +#[cfg(feature = "conformance")] +fn direct_mapping_handle_cu_exhaustion<'a>( + virtual_address_space_adjustments_active: bool, + cu_avail: u64, + has_err: bool, + account_data: impl IntoIterator>, +) { + if virtual_address_space_adjustments_active && cu_avail == 0 && has_err { + for data in account_data { + data.clear(); + } + } +} + +/// # Safety +/// +/// `in_ptr` must point to `in_sz` initialized bytes. `out_ptr` must point +/// to a writable buffer of at least `*out_psz` bytes. On return, `*out_psz` +/// is updated to the number of bytes written. +#[cfg(feature = "conformance")] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sol_compat_instr_execute_v1( + out_ptr: *mut u8, + out_psz: *mut u64, + in_ptr: *mut u8, + in_sz: u64, +) -> c_int { + let in_slice = unsafe { std::slice::from_raw_parts(in_ptr, in_sz as usize) }; + let Ok(instr_context) = ProtoInstrContext::decode(in_slice) else { + return 0; + }; + let instr_effects = execute_instr_proto(instr_context); + let out_slice = unsafe { std::slice::from_raw_parts_mut(out_ptr, (*out_psz) as usize) }; + let out_vec = instr_effects.encode_to_vec(); + if out_vec.len() > out_slice.len() { + return 0; + } + out_slice[..out_vec.len()].copy_from_slice(&out_vec); + unsafe { + *out_psz = out_vec.len() as u64; + } + 1 +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::conformance::programs::{ + add_program_to_program_cache, keyed_account_for_system_program, + new_program_cache_with_builtins, + }, + solana_account::Account, + solana_instruction::Instruction, + solana_rent::Rent, + solana_svm_feature_set::SVMFeatureSet, + solana_system_program::system_processor::DEFAULT_COMPUTE_UNITS as SYSTEM_TRANSFER_CUS, + std::cell::RefCell, + test_case::test_case, + }; + + const NOOP_ELF: &[u8] = + include_bytes!("../../../../programs/bpf_loader/test_elfs/out/noop_aligned.so"); + + const FROM_BASE_LAMPORTS: u64 = 5_000; + const TO_BASE_LAMPORTS: u64 = 1_000; + + #[derive(Default)] + struct CountingCallback { + // Just a simple little mock so we can test our callback is being used. + precompile_checks: RefCell, + } + + impl InvokeContextCallback for CountingCallback { + fn is_precompile(&self, _program_id: &Pubkey) -> bool { + *self.precompile_checks.borrow_mut() += 1; + false + } + } + + fn system_account_with_lamports(lamports: u64) -> Account { + Account { + lamports, + data: vec![], + owner: solana_sdk_ids::system_program::id(), + executable: false, + rent_epoch: u64::MAX, + } + } + + fn sysvar_cache_with_rent() -> SysvarCache { + let mut sysvar_cache = SysvarCache::default(); + sysvar_cache.fill_missing_entries(|pubkey, callback| { + if pubkey == &solana_sdk_ids::sysvar::rent::id() { + let rent_data = bincode::serialize(&Rent::default()).unwrap(); + callback(&rent_data); + } + }); + sysvar_cache + } + + #[cfg(feature = "conformance")] + fn proto_account(pubkey: Pubkey, account: Account) -> protosol::protos::AcctState { + protosol::protos::AcctState { + address: pubkey.to_bytes().to_vec(), + owner: account.owner.to_bytes().to_vec(), + lamports: account.lamports, + data: account.data, + executable: account.executable, + } + } + + #[cfg(feature = "conformance")] + fn proto_sysvar_account( + pubkey: Pubkey, + sysvar: &T, + ) -> protosol::protos::AcctState { + protosol::protos::AcctState { + address: pubkey.to_bytes().to_vec(), + owner: solana_sdk_ids::sysvar::id().to_bytes().to_vec(), + lamports: 1, + data: bincode::serialize(sysvar).unwrap(), + executable: false, + } + } + + fn build_system_transfer_context(from: &Pubkey, to: &Pubkey, amount: u64) -> InstrContext { + let feature_set = SVMFeatureSet::default(); + let accounts = vec![ + ( + *from, + system_account_with_lamports(FROM_BASE_LAMPORTS + amount), + ), + (*to, system_account_with_lamports(TO_BASE_LAMPORTS)), + keyed_account_for_system_program(), + ]; + let instruction = solana_system_interface::instruction::transfer(from, to, amount); + InstrContext { + feature_set, + accounts, + instruction, + cu_avail: SYSTEM_TRANSFER_CUS, + } + } + + fn assert_system_transfer_effects( + effects: &InstrEffects, + from: &Pubkey, + to: &Pubkey, + amount: u64, + ) { + // Success + assert_eq!(effects.result, None); + assert_eq!(effects.custom_err, None); + // CUs exhausted + assert_eq!(effects.cu_avail, 0); + // Lamports transferred + assert_eq!( + effects.get_account(from).unwrap().lamports, + FROM_BASE_LAMPORTS + ); + assert_eq!( + effects.get_account(to).unwrap().lamports, + TO_BASE_LAMPORTS + amount + ); + } + + #[test] + fn test_system_program_exec() { + let from = Pubkey::new_unique(); + let to = Pubkey::new_unique(); + let amount = 1_000; + let context = build_system_transfer_context(&from, &to, amount); + let sysvar_cache = sysvar_cache_with_rent(); + let mut program_cache = new_program_cache_with_builtins(0); + + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); + assert_system_transfer_effects(&effects, &from, &to, amount); + } + + #[test] + fn test_system_program_exec_with_callback() { + let from = Pubkey::new_unique(); + let to = Pubkey::new_unique(); + let amount = 1_000; + let context = build_system_transfer_context(&from, &to, amount); + let sysvar_cache = sysvar_cache_with_rent(); + let mut program_cache = new_program_cache_with_builtins(0); + + let callback = CountingCallback::default(); + + let effects = + execute_instr_with_callback(&context, &callback, &mut program_cache, &sysvar_cache); + assert_system_transfer_effects(&effects, &from, &to, amount); + } + + #[test_case(solana_sdk_ids::bpf_loader_deprecated::id(); "loader_v1")] + #[test_case(solana_sdk_ids::bpf_loader::id(); "loader_v2")] + #[test_case(solana_sdk_ids::bpf_loader_upgradeable::id(); "loader_v3")] + fn test_bpf_noop_program_exec(loader_key: Pubkey) { + let program_id = Pubkey::new_unique(); + let context = InstrContext::new_with_default_budget( + SVMFeatureSet::default(), + vec![], + Instruction::new_with_bytes(program_id, &[], vec![]), + ); + let sysvar_cache = sysvar_cache_with_rent(); + + let mut program_cache = new_program_cache_with_builtins(0); + add_program_to_program_cache( + &mut program_cache, + &program_id, + &loader_key, + NOOP_ELF, + &context.feature_set, + ); + + let effects = execute_instr(&context, &mut program_cache, &sysvar_cache); + assert_eq!(effects.result, None); + assert_eq!(effects.custom_err, None); + } + + #[cfg(feature = "conformance")] + #[test] + #[should_panic(expected = "invariant violation: duplicate account load")] + fn test_duplicate_accounts_panic_with_invariant_violation() { + let from = Pubkey::new_unique(); + let to = Pubkey::new_unique(); + let duplicate = Pubkey::new_unique(); + let instruction = solana_system_interface::instruction::transfer(&from, &to, 1); + + execute_instr_proto(ProtoInstrContext { + program_id: solana_sdk_ids::system_program::id().to_bytes().to_vec(), + accounts: vec![ + proto_account(from, system_account_with_lamports(FROM_BASE_LAMPORTS)), + proto_account(to, system_account_with_lamports(TO_BASE_LAMPORTS)), + proto_account(duplicate, system_account_with_lamports(1)), + proto_account(duplicate, system_account_with_lamports(1)), + proto_account( + keyed_account_for_system_program().0, + keyed_account_for_system_program().1, + ), + proto_sysvar_account( + solana_sdk_ids::sysvar::clock::id(), + &solana_clock::Clock::default(), + ), + ], + instr_accounts: vec![ + protosol::protos::InstrAcct { + index: 0, + is_signer: true, + is_writable: true, + }, + protosol::protos::InstrAcct { + index: 1, + is_signer: false, + is_writable: true, + }, + ], + data: instruction.data, + cu_avail: SYSTEM_TRANSFER_CUS, + features: None, + }); + } +} diff --git a/svm/src/conformance/instr/mod.rs b/svm/src/conformance/instr/mod.rs new file mode 100644 index 00000000000..585f5d4b20e --- /dev/null +++ b/svm/src/conformance/instr/mod.rs @@ -0,0 +1,5 @@ +//! Instruction conformance harness. + +pub mod context; +pub mod effects; +pub mod harness; diff --git a/svm/src/conformance/mod.rs b/svm/src/conformance/mod.rs new file mode 100644 index 00000000000..70cd70a22c6 --- /dev/null +++ b/svm/src/conformance/mod.rs @@ -0,0 +1,20 @@ +//! Solana SVM conformance. + +#[cfg(feature = "conformance")] +pub mod account_state; +pub mod callback; +#[cfg(feature = "conformance")] +pub mod elf_loader; +#[cfg(feature = "conformance")] +mod err; +#[cfg(feature = "conformance")] +pub mod fd_hash; +#[cfg(feature = "conformance")] +pub mod feature_set; +pub mod instr; +pub mod programs; +#[cfg(feature = "conformance")] +pub mod serialization; +mod setup; +#[cfg(feature = "conformance")] +pub mod syscall; diff --git a/svm/src/conformance/programs.rs b/svm/src/conformance/programs.rs new file mode 100644 index 00000000000..5a58d847b5b --- /dev/null +++ b/svm/src/conformance/programs.rs @@ -0,0 +1,186 @@ +//! Program helpers: builtins, keyed accounts, and program cache. + +#[cfg(feature = "metrics")] +use solana_program_runtime::program_metrics::LoadProgramMetrics; +use { + crate::program_loader::load_program_with_pubkey, + solana_account::{Account, AccountSharedData}, + solana_compute_budget::compute_budget::ComputeBudget, + solana_program_runtime::{ + invoke_context::BuiltinFunctionRegisterer, + loaded_programs::{ProgramCacheForTxBatch, ProgramRuntimeEnvironment}, + program_cache_entry::ProgramCacheEntry, + solana_sbpf::program::BuiltinFunctionDefinition, + }, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable}, + solana_svm_callback::TransactionProcessingCallback, + solana_svm_feature_set::SVMFeatureSet, + solana_svm_timings::ExecuteTimings, + solana_syscalls::create_program_runtime_environment, + std::{collections::HashSet, sync::Arc}, +}; + +struct SvmBuiltinPrototype { + pub program_id: Pubkey, + pub name: &'static str, + pub register_fn: BuiltinFunctionRegisterer, +} + +static SVM_BUILTINS: &[SvmBuiltinPrototype] = &[ + SvmBuiltinPrototype { + program_id: solana_system_program::id(), + name: "system_program", + register_fn: solana_system_program::system_processor::Entrypoint::register, + }, + SvmBuiltinPrototype { + program_id: bpf_loader_deprecated::id(), + name: "solana_bpf_loader_deprecated_program", + register_fn: solana_bpf_loader_program::Entrypoint::register, + }, + SvmBuiltinPrototype { + program_id: bpf_loader::id(), + name: "solana_bpf_loader_program", + register_fn: solana_bpf_loader_program::Entrypoint::register, + }, + SvmBuiltinPrototype { + program_id: bpf_loader_upgradeable::id(), + name: "solana_bpf_loader_upgradeable_program", + register_fn: solana_bpf_loader_program::Entrypoint::register, + }, + SvmBuiltinPrototype { + program_id: solana_sdk_ids::compute_budget::id(), + name: "compute_budget_program", + register_fn: solana_compute_budget_program::Entrypoint::register, + }, + #[cfg(feature = "conformance")] + SvmBuiltinPrototype { + program_id: solana_vote_program::id(), + name: "vote_program", + register_fn: solana_vote_program::vote_processor::Entrypoint::register, + }, + #[cfg(feature = "conformance")] + SvmBuiltinPrototype { + program_id: solana_sdk_ids::zk_elgamal_proof_program::id(), + name: "zk_elgamal_proof_program", + register_fn: solana_zk_elgamal_proof_program::Entrypoint::register, + }, +]; + +fn create_keyed_account_for_builtin_program(program_id: &Pubkey, name: &str) -> (Pubkey, Account) { + let data = name.as_bytes().to_vec(); + let lamports = Rent::default().minimum_balance(data.len()); + let account = Account { + lamports, + data, + owner: solana_sdk_ids::native_loader::id(), + executable: true, + ..Default::default() + }; + (*program_id, account) +} + +pub fn keyed_account_for_system_program() -> (Pubkey, Account) { + create_keyed_account_for_builtin_program(&SVM_BUILTINS[0].program_id, SVM_BUILTINS[0].name) +} + +pub fn keyed_account_for_compute_budget_program() -> (Pubkey, Account) { + create_keyed_account_for_builtin_program(&SVM_BUILTINS[4].program_id, SVM_BUILTINS[4].name) +} + +pub fn new_program_cache_with_builtins(slot: u64) -> ProgramCacheForTxBatch { + let mut cache = ProgramCacheForTxBatch::default(); + cache.set_slot_for_tests(slot); + + for builtin in SVM_BUILTINS { + cache.replenish( + builtin.program_id, + Arc::new(ProgramCacheEntry::new_builtin( + 0u64, + builtin.name.len(), + builtin.register_fn, + )), + ); + } + + cache +} + +/// Add a program loaded from ELF bytes to the cache. +pub fn add_program_to_program_cache( + cache: &mut ProgramCacheForTxBatch, + program_id: &Pubkey, + loader_key: &Pubkey, + elf: &[u8], + feature_set: &SVMFeatureSet, +) { + let compute_budget = ComputeBudget::new_with_defaults(feature_set.raise_cpi_nesting_limit_to_8); + let program_runtime_environment = create_program_runtime_environment( + feature_set, + &compute_budget.to_budget(), + false, /* reject_deployment_of_broken_elfs */ + false, /* debugging_features */ + ) + .unwrap(); + + let entry = ProgramCacheEntry::new( + loader_key, + program_runtime_environment, + 0, // deployment_slot + 0, // effective_slot + elf, + elf.len(), + #[cfg(feature = "metrics")] + &mut LoadProgramMetrics::default(), + ) + .unwrap(); + + cache.replenish(*program_id, Arc::new(entry)); +} + +/// Populate a `ProgramCacheForTxBatch` via `load_program_with_pubkey` from any program accounts. +pub fn fill_program_cache_from_accounts( + program_cache: &mut ProgramCacheForTxBatch, + program_runtime_environment: &ProgramRuntimeEnvironment, + accounts: &[(Pubkey, Account)], + slot: u64, +) { + let mut newly_loaded_programs = HashSet::::new(); + + for acc in accounts { + assert!( + newly_loaded_programs.insert(acc.0), + "invariant violation: duplicate account load", + ); + + if program_cache.find(&acc.0).is_none() { + if !solana_sdk_ids::bpf_loader_deprecated::check_id(&acc.1.owner) + && !solana_sdk_ids::bpf_loader::check_id(&acc.1.owner) + && !solana_sdk_ids::bpf_loader_upgradeable::check_id(&acc.1.owner) + { + continue; + } + if let Some((loaded_program, _last_modification_slot)) = load_program_with_pubkey( + &FillFromAccountsCallback(accounts), + program_runtime_environment, + &acc.0, + slot, + &mut ExecuteTimings::default(), + ) { + program_cache.replenish(acc.0, loaded_program); + } + } + } +} + +struct FillFromAccountsCallback<'a>(&'a [(Pubkey, Account)]); + +impl TransactionProcessingCallback for FillFromAccountsCallback<'_> { + fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, u64)> { + self.0 + .iter() + .find(|(found_pubkey, _)| *found_pubkey == *pubkey) + .map(|(_, account)| (AccountSharedData::from(account.clone()), 0u64)) + } +} diff --git a/svm/src/conformance/serialization.rs b/svm/src/conformance/serialization.rs new file mode 100644 index 00000000000..bfeb15900e8 --- /dev/null +++ b/svm/src/conformance/serialization.rs @@ -0,0 +1,294 @@ +//! VM serialization conformance harness. + +use { + crate::conformance::{ + callback::DefaultCallback, + fd_hash::fd_hash, + instr::context::InstrContext, + setup::{ + InvokeContextFields, compute_budget, prepare_invoke_context_fields, program_loader_key, + program_runtime_environments, sysvar_cache_from_accounts, + }, + }, + prost::Message, + protosol::protos::{ + InstrContext as ProtoInstrContext, VmInputMemoryRegion as ProtoVmInputMemoryRegion, + VmSerializationEffects as ProtoVmSerializationEffects, + VmSerializedAccountMetadata as ProtoVmSerializedAccountMetadata, + }, + solana_instruction::error::InstructionError, + solana_message::SanitizedMessage, + solana_program_runtime::{ + invoke_context::InvokeContext, + loaded_programs::ProgramCacheForTxBatch, + memory_context::SerializedAccountMetadata, + serialization::serialize_parameters, + solana_sbpf::{ + aligned_memory::AlignedMemory, ebpf::HOST_ALIGN, memory_region::MemoryRegion, + }, + }, + solana_svm_feature_set::SVMFeatureSet, + std::ffi::c_int, +}; + +pub fn execute_vm_serialize(input: ProtoInstrContext) -> ProtoVmSerializationEffects { + let instr_context = InstrContext::from(input); + + let feature_set = instr_context.feature_set; + + let compute_budget = compute_budget(&feature_set); + // No CU limit for this harness. + + let sysvar_cache = sysvar_cache_from_accounts(&instr_context.accounts); + let program_id = instr_context.instruction.program_id; + let loader_key = program_loader_key(&instr_context.accounts, &program_id); + + let program_runtime_environments = program_runtime_environments(&feature_set, &compute_budget); + + // We're only testing the parameter serialization, so use an empty cache. + let mut program_cache = ProgramCacheForTxBatch::default(); + + let InvokeContextFields { + sanitized_message, + mut transaction_context, + environment_config, + log_collector, + execution_budget, + execution_cost, + } = prepare_invoke_context_fields( + &instr_context, + &DefaultCallback, + &loader_key, + &sysvar_cache, + &compute_budget, + &program_runtime_environments, + ); + + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache, + environment_config, + Some(log_collector.clone()), + execution_budget, + execution_cost, + ); + + match push_and_serialize_parameters(&mut invoke_context, &sanitized_message, &feature_set) { + Ok(SerializedParameters { + aligned_memory, + input_memory_regions, + account_metadata, + }) => { + let serialized_memory_hash = fd_hash(0, aligned_memory.as_slice()); + let vm_input_memory_regions = input_memory_regions + .iter() + .map(memory_region_to_proto) + .collect(); + let serialized_account_metadata = account_metadata + .iter() + .map(serialized_acct_meta_to_proto) + .collect(); + ProtoVmSerializationEffects { + has_error: false, + serialized_memory_hash, + vm_input_memory_regions, + serialized_account_metadata, + } + } + Err(_) => ProtoVmSerializationEffects { + has_error: true, + ..Default::default() + }, + } +} + +/// The product of serializing a program's input parameters into VM memory: the +/// serialized region itself plus the metadata needed to map accounts back out. +pub(crate) struct SerializedParameters { + pub(crate) aligned_memory: AlignedMemory, + pub(crate) input_memory_regions: Vec, + pub(crate) account_metadata: Vec, +} + +/// Push the message's single top-level instruction onto `invoke_context`, then +/// serialize that instruction's program input parameters into VM memory. +pub(crate) fn push_and_serialize_parameters<'ix_data>( + invoke_context: &mut InvokeContext<'_, 'ix_data>, + sanitized_message: &'ix_data SanitizedMessage, + feature_set: &SVMFeatureSet, +) -> Result { + invoke_context + .prepare_top_level_instructions(sanitized_message) + .expect("failed to prepare top-level instructions"); + invoke_context + .push() + .expect("failed to push instruction context"); + + let instruction_context = invoke_context + .transaction_context + .get_current_instruction_context() + .unwrap(); + serialize_parameters( + &instruction_context, + feature_set.virtual_address_space_adjustments, + feature_set.account_data_direct_mapping, + feature_set.direct_account_pointers_in_program_input, + ) + .map( + |(aligned_memory, input_memory_regions, account_metadata, _instruction_data_offset)| { + SerializedParameters { + aligned_memory, + input_memory_regions, + account_metadata, + } + }, + ) +} + +fn memory_region_to_proto(region: &MemoryRegion) -> ProtoVmInputMemoryRegion { + ProtoVmInputMemoryRegion { + vm_address: region.vm_addr, + region_size: region.len, + is_writable: region.writable, + } +} + +fn serialized_acct_meta_to_proto( + meta: &SerializedAccountMetadata, +) -> ProtoVmSerializedAccountMetadata { + ProtoVmSerializedAccountMetadata { + original_data_len: meta.original_data_len as u64, + vm_data_addr: meta.vm_data_addr, + vm_key_addr: meta.vm_key_addr, + vm_lamports_addr: meta.vm_lamports_addr, + vm_owner_addr: meta.vm_owner_addr, + } +} + +/// # Safety +/// +/// `in_ptr` must point to `in_sz` initialized bytes. `out_ptr` must point +/// to a writable buffer of at least `*out_psz` bytes. On return, `*out_psz` +/// is updated to the number of bytes written. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sol_compat_vm_serialize_execute_v1( + out_ptr: *mut u8, + out_psz: *mut u64, + in_ptr: *mut u8, + in_sz: u64, +) -> c_int { + let in_slice = unsafe { std::slice::from_raw_parts(in_ptr, in_sz as usize) }; + let Ok(instr_context) = ProtoInstrContext::decode(in_slice) else { + return 0; + }; + + let effects = execute_vm_serialize(instr_context); + let out_slice = unsafe { std::slice::from_raw_parts_mut(out_ptr, (*out_psz) as usize) }; + let out_vec = effects.encode_to_vec(); + if out_vec.len() > out_slice.len() { + return 0; + } + out_slice[..out_vec.len()].copy_from_slice(&out_vec); + unsafe { *out_psz = out_vec.len() as u64 }; + + 1 +} + +#[cfg(test)] +mod tests { + use { + super::*, + protosol::protos::{AcctState as ProtoAcctState, InstrAcct as ProtoInstrAcct}, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_sdk_ids::{bpf_loader_deprecated, bpf_loader_upgradeable, sysvar}, + }; + + const PROGRAM_ID: [u8; 32] = [7; 32]; + + fn account(address: u8, owner: Pubkey, data_len: usize) -> ProtoAcctState { + ProtoAcctState { + address: vec![address; 32], + lamports: 1, + data: vec![0; data_len], + executable: false, + owner: owner.to_bytes().to_vec(), + } + } + + fn instr_acct(index: u32, is_writable: bool, is_signer: bool) -> ProtoInstrAcct { + ProtoInstrAcct { + index, + is_writable, + is_signer, + } + } + + fn rent_sysvar() -> ProtoAcctState { + ProtoAcctState { + address: sysvar::rent::id().to_bytes().to_vec(), + lamports: 1, + data: bincode::serialize(&Rent::default()).unwrap(), + executable: false, + owner: sysvar::id().to_bytes().to_vec(), + } + } + + fn serialize( + mut accounts: Vec, + instr_accounts: Vec, + ) -> ProtoVmSerializationEffects { + accounts.push(rent_sysvar()); // <-- Loads Rent into SysvarCache + execute_vm_serialize(ProtoInstrContext { + program_id: PROGRAM_ID.to_vec(), + accounts, + instr_accounts, + ..Default::default() + }) + } + + #[test] + fn test_serialize_single_account() { + let program_id = Pubkey::new_from_array(PROGRAM_ID); + let effects = serialize( + vec![ + account(1, program_id, 8), + account(PROGRAM_ID[0], bpf_loader_upgradeable::id(), 0), + ], + vec![instr_acct(0, true, false)], + ); + assert!(!effects.has_error); + assert_eq!(effects.serialized_account_metadata.len(), 1); + } + + #[test] + fn test_serialize_multiple_accounts() { + let program_id = Pubkey::new_from_array(PROGRAM_ID); + let effects = serialize( + vec![ + account(1, program_id, 4), + account(2, program_id, 16), + account(PROGRAM_ID[0], bpf_loader_upgradeable::id(), 0), + ], + vec![instr_acct(0, true, true), instr_acct(1, false, false)], + ); + assert!(!effects.has_error); + assert_eq!(effects.serialized_account_metadata.len(), 2); + } + + #[test] + fn test_serialize_deprecated_loader_program() { + // A program owned by the deprecated loader exercises the unaligned + // serialization path. + let program_id = Pubkey::new_from_array(PROGRAM_ID); + let effects = serialize( + vec![ + account(1, program_id, 8), + account(PROGRAM_ID[0], bpf_loader_deprecated::id(), 0), + ], + vec![instr_acct(0, true, false)], + ); + assert!(!effects.has_error); + assert_eq!(effects.serialized_account_metadata.len(), 1); + } +} diff --git a/svm/src/conformance/setup.rs b/svm/src/conformance/setup.rs new file mode 100644 index 00000000000..a65689194fe --- /dev/null +++ b/svm/src/conformance/setup.rs @@ -0,0 +1,166 @@ +//! Shared setup helpers for the execution harnesses. + +#[cfg(feature = "conformance")] +use solana_account::ReadableAccount; +use { + crate::conformance::instr::context::InstrContext, + solana_account::Account, + solana_compute_budget::compute_budget::ComputeBudget, + solana_hash::Hash, + solana_instruction::Instruction, + solana_message::SanitizedMessage, + solana_program_runtime::{ + execution_budget::{SVMTransactionExecutionBudget, SVMTransactionExecutionCost}, + invoke_context::{EnvironmentConfig, mock_compile_message}, + loaded_programs::{ProgramRuntimeEnvironment, ProgramRuntimeEnvironments}, + sysvar_cache::SysvarCache, + }, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_svm_callback::InvokeContextCallback, + solana_svm_feature_set::SVMFeatureSet, + solana_svm_log_collector::LogCollector, + solana_svm_transaction::svm_message::SVMStaticMessage, + solana_syscalls::create_program_runtime_environment, + solana_transaction_context::transaction::TransactionContext, + std::{cell::RefCell, rc::Rc}, +}; + +/// Fields required by `InvokeContext::new`. +pub(crate) struct InvokeContextFields<'a, 'ix_data> { + pub(crate) sanitized_message: SanitizedMessage, + pub(crate) transaction_context: TransactionContext<'ix_data>, + pub(crate) environment_config: EnvironmentConfig<'a>, + pub(crate) log_collector: Rc>, + pub(crate) execution_budget: SVMTransactionExecutionBudget, + pub(crate) execution_cost: SVMTransactionExecutionCost, +} + +/// Compile a sanitized transaction message then instantiate a transaction +/// context as well as the remaining fields required by `InvokeContext::new`. +pub(crate) fn prepare_invoke_context_fields<'a, C: InvokeContextCallback>( + instr_context: &'a InstrContext, + callback: &'a C, + loader_key: &Pubkey, + sysvar_cache: &'a SysvarCache, + compute_budget: &ComputeBudget, + program_runtime_environments: &'a ProgramRuntimeEnvironments, +) -> InvokeContextFields<'a, 'a> { + let rent = sysvar_cache.get_rent().unwrap(); + + let (sanitized_message, transaction_context) = compile_transaction_context( + &instr_context.instruction, + &instr_context.accounts, + &instr_context.instruction.program_id, + loader_key, + compute_budget, + (*rent).clone(), + ); + + let (blockhash, blockhash_lamports_per_signature) = recent_blockhash(sysvar_cache); + let environment_config = EnvironmentConfig::new( + blockhash, + blockhash_lamports_per_signature, + false, + callback, + &instr_context.feature_set, + program_runtime_environments, + sysvar_cache, + ); + + let log_collector = LogCollector::new_ref(); + let execution_budget = compute_budget.to_budget(); + let execution_cost = compute_budget.to_cost(); + + InvokeContextFields { + sanitized_message, + transaction_context, + environment_config, + log_collector, + execution_budget, + execution_cost, + } +} + +// Create a compute budget from the given feature set. +pub(crate) fn compute_budget(feature_set: &SVMFeatureSet) -> ComputeBudget { + let simd_0268_active = feature_set.raise_cpi_nesting_limit_to_8; + ComputeBudget::new_with_defaults(simd_0268_active) +} + +/// The loader that owns the program account in `accounts`, used as the program +/// account's owner when compiling the transaction. `None` if the program +/// account isn't present. +#[cfg(feature = "conformance")] +pub(crate) fn program_loader_key(accounts: &[(Pubkey, Account)], program_id: &Pubkey) -> Pubkey { + accounts + .iter() + .find(|(key, _)| key == program_id) + .map(|(_, account)| account.owner) + .expect("program not found in accounts") +} + +/// Compile `instruction` into a sanitized message and a fresh transaction +/// context sized for a single top-level instruction. +pub(crate) fn compile_transaction_context( + instruction: &Instruction, + accounts: &[(Pubkey, Account)], + program_id: &Pubkey, + loader_key: &Pubkey, + compute_budget: &ComputeBudget, + rent: Rent, +) -> (SanitizedMessage, TransactionContext<'static>) { + let (sanitized_message, transaction_accounts) = + mock_compile_message(instruction, accounts, program_id, loader_key); + let transaction_context = TransactionContext::new( + transaction_accounts, + rent, + compute_budget.max_instruction_stack_depth, + compute_budget.max_instruction_trace_length, + sanitized_message.num_instructions(), + ); + (sanitized_message, transaction_context) +} + +/// The paired (execution + deployment) program runtime environments for a +/// harness invocation. Both halves share one environment. +pub(crate) fn program_runtime_environments( + feature_set: &SVMFeatureSet, + compute_budget: &ComputeBudget, +) -> ProgramRuntimeEnvironments { + let environment = create_program_runtime_environment( + feature_set, + &compute_budget.to_budget(), + false, /* deployment */ + false, /* debugging_features */ + ) + .unwrap(); + ProgramRuntimeEnvironments::new(ProgramRuntimeEnvironment::clone(&environment), environment) +} + +/// The most recent blockhash and its lamports-per-signature from the sysvar +/// cache, or defaults when unavailable. +pub(crate) fn recent_blockhash(sysvar_cache: &SysvarCache) -> (Hash, u64) { + #[expect(deprecated)] + sysvar_cache + .get_recent_blockhashes() + .ok() + .and_then(|entries| entries.last().cloned()) + .map(|entry| (entry.blockhash, entry.fee_calculator.lamports_per_signature)) + .unwrap_or_default() +} + +/// Build a sysvar cache populated from any sysvar accounts present in the +/// input account set. +#[cfg(feature = "conformance")] +pub(crate) fn sysvar_cache_from_accounts(accounts: &[(Pubkey, Account)]) -> SysvarCache { + let mut cache = SysvarCache::default(); + cache.fill_missing_entries(|pubkey, set_sysvar| { + if let Some(account) = accounts.iter().find(|(key, _)| key == pubkey) + && account.1.lamports() > 0 + { + set_sysvar(account.1.data()); + } + }); + cache +} diff --git a/svm/src/conformance/syscall.rs b/svm/src/conformance/syscall.rs new file mode 100644 index 00000000000..ad183fe52e1 --- /dev/null +++ b/svm/src/conformance/syscall.rs @@ -0,0 +1,471 @@ +//! VM syscall conformance harness. + +use { + crate::conformance::{ + callback::DefaultCallback, + err::{UnpackedResult, unpack_stable_result}, + instr::context::InstrContext, + programs::{fill_program_cache_from_accounts, new_program_cache_with_builtins}, + serialization::{SerializedParameters, push_and_serialize_parameters}, + setup::{ + InvokeContextFields, compute_budget, prepare_invoke_context_fields, program_loader_key, + program_runtime_environments, sysvar_cache_from_accounts, + }, + }, + prost::Message, + protosol::protos::{ + InputDataRegion as ProtoInputDataRegion, SyscallContext as ProtoSyscallContext, + SyscallEffects as ProtoSyscallEffects, SyscallInvocation as ProtoSyscallInvocation, + VmContext as ProtoVmContext, + }, + solana_program_runtime::{ + invoke_context::{BpfAllocator, InvokeContext}, + loaded_programs::ProgramCacheForTxBatch, + memory_context::MemoryContext, + solana_sbpf::{ + aligned_memory::AlignedMemory, + ebpf::{HOST_ALIGN, MM_BYTECODE_START, MM_HEAP_START, MM_INPUT_START, MM_STACK_START}, + error::{ProgramResult, StableResult}, + memory_region::{AccessViolationHandler, MemoryMapping, MemoryRegion}, + program::{BuiltinProgram, SBPFVersion}, + vm::{Config, ContextObject, EbpfVm}, + }, + }, + solana_pubkey::Pubkey, + std::{ffi::c_int, sync::Arc}, +}; + +const STACK_GAP_SIZE: u64 = 4_096; +const STACK_SIZE: usize = 64 * STACK_GAP_SIZE as usize; +/// Upper bound on `vm_context.heap_max` — matches Firedancer's cap so the same +/// fuzzer inputs run on either implementation. +const HEAP_MAX: usize = 256 * 1024; +const SBPF_VERSION: SBPFVersion = SBPFVersion::V0; + +pub fn execute_vm_syscall(input: ProtoSyscallContext) -> ProtoSyscallEffects { + let instr_context = InstrContext::from(input.instr_ctx.expect("missing instr context")); + let mut vm_context = input.vm_ctx.expect("missing vm context"); + let syscall_invocation = input.syscall_invocation.unwrap_or_default(); + let registers = get_registers(&vm_context); + + let feature_set = instr_context.feature_set; + let virtual_address_space_adjustments = feature_set.virtual_address_space_adjustments; + let account_data_direct_mapping = feature_set.account_data_direct_mapping; + + let mut compute_budget = compute_budget(&feature_set); + compute_budget.compute_unit_limit = instr_context.cu_avail; // Clamp budget for execution by cu_avail + + let sysvar_cache = sysvar_cache_from_accounts(&instr_context.accounts); + + let program_id = instr_context.instruction.program_id; + let loader_key = program_loader_key(&instr_context.accounts, &program_id); + + let program_runtime_environments = program_runtime_environments(&feature_set, &compute_budget); + let deployment_environment = program_runtime_environments.get_env_for_deployment(); + let execution_environment = program_runtime_environments.get_env_for_execution(); + let config = execution_environment.get_config().clone(); + + // Only build out the program cache if the syscall is CPI. + let mut program_cache = if contains_cpi(&syscall_invocation) { + let slot = sysvar_cache + .get_clock() + .map(|clock| clock.slot) + .unwrap_or_default(); + let mut cache = new_program_cache_with_builtins(slot); + fill_program_cache_from_accounts( + &mut cache, + deployment_environment, + &instr_context.accounts, + slot, + ); + cache + } else { + ProgramCacheForTxBatch::default() + }; + + let InvokeContextFields { + sanitized_message, + mut transaction_context, + environment_config, + execution_budget, + execution_cost, + .. + } = prepare_invoke_context_fields( + &instr_context, + &DefaultCallback, + &loader_key, + &sysvar_cache, + &compute_budget, + &program_runtime_environments, + ); + + // Replay any prior return data the fuzzer wants in scope before the syscall. + if let Some(return_data) = vm_context.return_data.take() { + let return_program_id = + Pubkey::try_from(return_data.program_id).expect("invalid return data program id"); + transaction_context + .set_return_data(return_program_id, return_data.data) + .expect("failed to set return data"); + } + + let access_violation_handler = transaction_context.access_violation_handler( + virtual_address_space_adjustments, + account_data_direct_mapping, + ); + + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache, + environment_config, + None, + execution_budget, + execution_cost, + ); + + let SerializedParameters { + aligned_memory: _input_memory, // <-- Keep bound + input_memory_regions, + account_metadata, + } = push_and_serialize_parameters(&mut invoke_context, &sanitized_message, &feature_set) + .expect("failed to serialize parameters"); + + let [rodata, mut stack, mut heap] = allocate_memory(&vm_context, &syscall_invocation); + let memory_mapping = unsafe { + create_memory_mapping( + &rodata, + &mut stack, + &mut heap, + input_memory_regions, + &config, + access_violation_handler, + ) + }; + let memory_context = MemoryContext::new( + BpfAllocator::new(vm_context.heap_max), + account_metadata, + memory_mapping, + ); + invoke_context + .memory_contexts + .set_memory_context_abi_v1(memory_context) + .expect("failed to set memory context"); + + let syscall_function = execution_environment + .get_function_registry() + .lookup_by_name(&syscall_invocation.function_name) + .expect("syscall function not registered") + .1 + .0; + + let (program_result, call_depth) = { + // Invoke the syscall with a `&'static` InvokeContext, then take the + // result, dropping the VM. Avoids dangling memory. + let loader = Arc::new(BuiltinProgram::new_loader(config)); + let invoke_context_static: &mut InvokeContext<'static, 'static> = + unsafe { std::mem::transmute(&mut invoke_context) }; + + let mut vm = EbpfVm::new(loader, SBPF_VERSION, invoke_context_static, STACK_SIZE); + vm.registers = registers; + + vm.invoke_function(syscall_function); + + let program_result = std::mem::replace(&mut vm.program_result, ProgramResult::Ok(0)); + let call_depth = vm.call_depth; + (program_result, call_depth) + }; + + let input_data_regions = extract_input_data_regions( + &invoke_context, + &program_result, + virtual_address_space_adjustments, + ); + + let UnpackedResult { + error, + error_kind, + r0, + } = unpack_stable_result(program_result); + let cu_avail = invoke_context.get_remaining(); + invoke_context + .pop() + .expect("failed to pop instruction context"); + + ProtoSyscallEffects { + error, + error_kind, + r0, + cu_avail, + heap: heap.as_slice().to_vec(), + stack: stack.as_slice().to_vec(), + input_data_regions, + frame_count: call_depth, + rodata: rodata.as_slice().to_vec(), + pc: 0, + ..Default::default() + } +} + +fn get_registers(vm_context: &ProtoVmContext) -> [u64; 12] { + [ + vm_context.r0, + vm_context.r1, + vm_context.r2, + vm_context.r3, + vm_context.r4, + vm_context.r5, + vm_context.r6, + vm_context.r7, + vm_context.r8, + vm_context.r9, + vm_context.r10, + vm_context.r11, + ] +} + +fn contains_cpi(syscall_invocation: &ProtoSyscallInvocation) -> bool { + syscall_invocation.function_name == b"sol_invoke_signed_c" + || syscall_invocation.function_name == b"sol_invoke_signed_rust" +} + +fn allocate_memory( + vm_context: &ProtoVmContext, + syscall_invocation: &ProtoSyscallInvocation, +) -> [AlignedMemory; 3] { + assert!( + vm_context.heap_max as usize <= HEAP_MAX, + "vm_context.heap_max ({}) exceeds HEAP_MAX ({HEAP_MAX})", + vm_context.heap_max, + ); + let rodata = AlignedMemory::from(&vm_context.rodata); + let mut stack = AlignedMemory::from(&vec![0; STACK_SIZE]); + let mut heap = AlignedMemory::from(&vec![0; vm_context.heap_max as usize]); + + copy_memory_prefix(heap.as_slice_mut(), &syscall_invocation.heap_prefix); + copy_memory_prefix(stack.as_slice_mut(), &syscall_invocation.stack_prefix); + + [rodata, stack, heap] +} + +fn copy_memory_prefix(dst: &mut [u8], src: &[u8]) { + let size = dst.len().min(src.len()); + dst[..size].copy_from_slice(&src[..size]); +} + +// SAFETY: The backing memory for rodata/stack/heap should live at least as +// long as this function's returned `MemoryMapping`. +unsafe fn create_memory_mapping( + rodata: &AlignedMemory, + stack: &mut AlignedMemory, + heap: &mut AlignedMemory, + input_memory_regions: Vec, + config: &Config, + acces_violation_handler: AccessViolationHandler, +) -> MemoryMapping { + let stack_frame_gap = if SBPF_VERSION.stack_frame_gaps() && config.enable_stack_frame_gaps { + config.stack_frame_size as u64 + } else { + 0 + }; + let regions = [ + MemoryRegion::new(rodata.as_slice() as *const [u8], MM_BYTECODE_START), + MemoryRegion::new_gapped( + stack.as_slice_mut() as *mut [u8], + MM_STACK_START, + stack_frame_gap, + ), + MemoryRegion::new(heap.as_slice_mut() as *mut [u8], MM_HEAP_START), + ] + .into_iter() + .chain(input_memory_regions) + .collect(); + unsafe { + MemoryMapping::new_with_access_violation_handler( + regions, + config, + SBPF_VERSION, + acces_violation_handler, + ) + .expect("failed to create memory mapping") + } +} + +fn extract_input_data_regions( + invoke_context: &InvokeContext, + program_result: &ProgramResult, + virtual_address_space_adjustments: bool, +) -> Vec { + // When virtual_address_space_adjustments is enabled, Agave calls + // update_caller_account_region only after a _successful_ CPI execution, so + // on failure the input regions can hold stale data — return empty instead. + if virtual_address_space_adjustments && matches!(program_result, StableResult::Err(_)) { + return Vec::new(); + } + invoke_context + .memory_contexts + .memory_mapping() + .ok() + .map(|mapping| { + let mut regions: Vec = mapping + .get_regions() + .iter() + .filter(|region| region.vm_addr >= MM_INPUT_START) + .map(mem_region_to_input_data_region) + .collect(); + regions.sort_by_key(|region| region.offset); + regions + }) + .unwrap_or_default() +} + +fn mem_region_to_input_data_region(region: &MemoryRegion) -> ProtoInputDataRegion { + ProtoInputDataRegion { + content: unsafe { + std::slice::from_raw_parts(region.host_addr as *const u8, region.len as usize).to_vec() + }, + offset: region.vm_addr.saturating_sub(MM_INPUT_START), + is_writable: region.writable, + } +} + +/// # Safety +/// +/// `in_ptr` must point to `in_sz` initialized bytes. `out_ptr` must point +/// to a writable buffer of at least `*out_psz` bytes. On return, `*out_psz` +/// is updated to the number of bytes written. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sol_compat_vm_syscall_execute_v1( + out_ptr: *mut u8, + out_psz: *mut u64, + in_ptr: *mut u8, + in_sz: u64, +) -> c_int { + let in_slice = unsafe { std::slice::from_raw_parts(in_ptr, in_sz as usize) }; + let Ok(syscall_context) = ProtoSyscallContext::decode(in_slice) else { + return 0; + }; + + let syscall_effects = execute_vm_syscall(syscall_context); + let out_slice = unsafe { std::slice::from_raw_parts_mut(out_ptr, (*out_psz) as usize) }; + let out_vec = syscall_effects.encode_to_vec(); + if out_vec.len() > out_slice.len() { + return 0; + } + out_slice[..out_vec.len()].copy_from_slice(&out_vec); + unsafe { *out_psz = out_vec.len() as u64 }; + + 1 +} + +#[cfg(test)] +mod tests { + use { + super::*, + protosol::protos::{ + AcctState as ProtoAcctState, InstrContext as ProtoInstrContext, + SyscallInvocation as ProtoSyscallInvocation, VmContext as ProtoVmContext, + }, + solana_rent::Rent, + solana_sdk_ids::sysvar, + }; + + const PROGRAM_ID: [u8; 32] = [7; 32]; + + fn syscall_context( + function_name: &[u8], + r1: u64, + r2: u64, + r3: u64, + r4: u64, + heap_prefix: Vec, + ) -> ProtoSyscallContext { + let program_account = ProtoAcctState { + address: PROGRAM_ID.to_vec(), + lamports: 0, + data: vec![], + executable: true, + owner: Pubkey::default().to_bytes().to_vec(), + }; + let rent_sysvar = ProtoAcctState { + address: sysvar::rent::id().to_bytes().to_vec(), + lamports: 1, + data: bincode::serialize(&Rent::default()).unwrap(), + executable: false, + owner: sysvar::id().to_bytes().to_vec(), + }; + ProtoSyscallContext { + instr_ctx: Some(ProtoInstrContext { + program_id: PROGRAM_ID.to_vec(), + accounts: vec![program_account, rent_sysvar], + instr_accounts: vec![], + data: vec![], + cu_avail: 200_000, + features: None, + }), + vm_ctx: Some(ProtoVmContext { + heap_max: 1024, + r1, + r2, + r3, + r4, + ..Default::default() + }), + syscall_invocation: Some(ProtoSyscallInvocation { + function_name: function_name.to_vec(), + heap_prefix, + stack_prefix: vec![], + }), + } + } + + #[test] + fn test_sol_log() { + let msg = b"hello"; + let effects = execute_vm_syscall(syscall_context( + b"sol_log_", + MM_HEAP_START, // r1: msg address in heap + msg.len() as u64, // r2: length + 0, + 0, + msg.to_vec(), + )); + + assert_eq!(effects.error, 0); + // Logs are no longer collected (the harness runs without a log + // collector), so the syscall succeeding is all we assert here. + assert!(effects.cu_avail < 200_000, "syscall should consume compute"); + } + + #[test] + fn test_sol_memset() { + let effects = execute_vm_syscall(syscall_context( + b"sol_memset_", + MM_HEAP_START, // r1: dst + 0x42, // r2: byte + 8, // r3: count + 0, + vec![0u8; 16], + )); + + assert_eq!(effects.error, 0); + assert_eq!(&effects.heap[..8], &[0x42; 8]); + assert_eq!( + &effects.heap[8..16], + &[0u8; 8], + "must not write past length" + ); + } + + #[test] + fn test_sol_panic_surfaces_error() { + let effects = execute_vm_syscall(syscall_context( + b"sol_panic_", + MM_HEAP_START, // r1: file address + 1, // r2: file length + 10, // r3: line + 5, // r4: column + b"x".to_vec(), + )); + + assert_ne!(effects.error, 0); + } +} diff --git a/svm/src/lib.rs b/svm/src/lib.rs index cdca592f5c1..461454a72c0 100644 --- a/svm/src/lib.rs +++ b/svm/src/lib.rs @@ -4,6 +4,8 @@ pub mod account_loader; pub mod account_overrides; +#[cfg(any(feature = "conformance", feature = "dev-context-only-utils"))] +pub mod conformance; pub mod message_processor; pub mod nonce_info; pub mod program_loader; diff --git a/svm/src/program_loader.rs b/svm/src/program_loader.rs index edbba40abb2..f2bd553bc68 100644 --- a/svm/src/program_loader.rs +++ b/svm/src/program_loader.rs @@ -7,7 +7,10 @@ use { solana_loader_v3_interface::state::UpgradeableLoaderState, solana_loader_v4_interface::state::{LoaderV4State, LoaderV4Status}, solana_program_runtime::{ - loaded_programs::ProgramRuntimeEnvironment, + loaded_programs::{ + ProgramCacheForTxBatch, ProgramCacheMatchCriteria, ProgramRuntimeEnvironment, + ProgramToLoad, + }, program_cache_entry::{ DELAY_VISIBILITY_SLOT_OFFSET, ProgramCacheEntry, ProgramCacheEntryOwner, ProgramCacheEntryType, @@ -19,6 +22,7 @@ use { solana_svm_timings::ExecuteTimings, solana_svm_type_overrides::sync::Arc, solana_transaction_error::{TransactionError, TransactionResult}, + std::sync::atomic::Ordering, }; #[derive(Debug)] @@ -208,35 +212,81 @@ pub fn load_program_with_pubkey( /// Returns an error if the program's account state can not be found or parsed. pub(crate) fn get_program_deployment_slot( callbacks: &CB, - pubkey: &Pubkey, + program: &AccountSharedData, + loader: ProgramCacheEntryOwner, ) -> TransactionResult { - let (program, _slot) = callbacks - .get_account_shared_data(pubkey) - .ok_or(TransactionError::ProgramAccountNotFound)?; - if bpf_loader_upgradeable::check_id(program.owner()) { - if let Ok(UpgradeableLoaderState::Program { - programdata_address, - }) = program.state() - { - let (programdata, _slot) = callbacks - .get_account_shared_data(&programdata_address) - .ok_or(TransactionError::ProgramAccountNotFound)?; - if let Ok(UpgradeableLoaderState::ProgramData { - slot, - upgrade_authority_address: _, - }) = programdata.state() + match loader { + ProgramCacheEntryOwner::LoaderV1 | ProgramCacheEntryOwner::LoaderV2 => Ok(0), + ProgramCacheEntryOwner::LoaderV3 => { + if let Ok(UpgradeableLoaderState::Program { + programdata_address, + }) = program.state() { - return Ok(slot); + let (programdata, _slot) = callbacks + .get_account_shared_data(&programdata_address) + .ok_or(TransactionError::ProgramAccountNotFound)?; + if let Ok(UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address: _, + }) = programdata.state() + { + return Ok(slot); + } } + Err(TransactionError::ProgramAccountNotFound) + } + ProgramCacheEntryOwner::LoaderV4 => { + let state = loader_v4_get_state(program.data()) + .map_err(|_| TransactionError::ProgramAccountNotFound)?; + Ok(state.slot) + } + ProgramCacheEntryOwner::NativeLoader => unreachable!(), + } +} + +/// Appends to a set of executable program accounts (all accounts owned by any loader) +/// for transactions with a valid blockhash or nonce. +pub fn filter_executable_program_accounts<'a, CB: TransactionProcessingCallback>( + callbacks: &CB, + program_cache_for_tx_batch: &ProgramCacheForTxBatch, + keys: impl Iterator, + check_program_deployment_slot: bool, +) -> Vec> { + let mut result = Vec::new(); + for account_key in keys { + if let Some(cache_entry) = program_cache_for_tx_batch.find(account_key) { + cache_entry.stats.uses.fetch_add(1, Ordering::Relaxed); + } else if let Some((account, last_modification_slot)) = + callbacks.get_account_shared_data(account_key) + { + let loader = if loader_v4::check_id(account.owner()) { + ProgramCacheEntryOwner::LoaderV4 + } else if bpf_loader_upgradeable::check_id(account.owner()) { + ProgramCacheEntryOwner::LoaderV3 + } else if bpf_loader::check_id(account.owner()) { + ProgramCacheEntryOwner::LoaderV2 + } else if bpf_loader_deprecated::check_id(account.owner()) { + ProgramCacheEntryOwner::LoaderV1 + } else { + continue; + }; + let match_criteria = if check_program_deployment_slot { + get_program_deployment_slot(callbacks, &account, loader) + .map_or(ProgramCacheMatchCriteria::Tombstone, |slot| { + ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(slot) + }) + } else { + ProgramCacheMatchCriteria::NoCriteria + }; + result.push(ProgramToLoad { + program_id: account_key, + loader, + match_criteria, + last_modification_slot, + }); } - Err(TransactionError::ProgramAccountNotFound) - } else if loader_v4::check_id(program.owner()) { - let state = loader_v4_get_state(program.data()) - .map_err(|_| TransactionError::ProgramAccountNotFound)?; - Ok(state.slot) - } else { - Ok(0) } + result } // Plucked from the now-removed Loader V4 program library. @@ -260,6 +310,9 @@ mod tests { super::*, crate::transaction_processor::TransactionBatchProcessor, solana_account::WritableAccount, + solana_hash::Hash, + solana_keypair::Keypair, + solana_message::compiled_instruction::CompiledInstruction, solana_program_runtime::{ loaded_programs::{ BlockRelation, ForkGraph, ProgramRuntimeEnvironment, @@ -267,8 +320,10 @@ mod tests { }, solana_sbpf::program::BuiltinProgram, }, - solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable}, - solana_svm_callback::InvokeContextCallback, + solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable, native_loader}, + solana_svm_transaction::svm_message::SVMMessage, + solana_svm_type_overrides::sync::atomic::AtomicU64, + solana_transaction::{Transaction, sanitized::SanitizedTransaction}, std::{ cell::RefCell, collections::HashMap, @@ -291,8 +346,6 @@ mod tests { pub(crate) account_shared_data: RefCell>, } - impl InvokeContextCallback for MockBankCallback {} - impl TransactionProcessingCallback for MockBankCallback { fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { self.account_shared_data.borrow().get(pubkey).cloned() @@ -686,19 +739,19 @@ mod tests { #[test] fn test_program_modification_slot_account_not_found() { let mock_bank = MockBankCallback::default(); - let key = Pubkey::new_unique(); - let result = get_program_deployment_slot(&mock_bank, &key); - assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); - let mut account_data = AccountSharedData::new(100, 100, &bpf_loader_upgradeable::id()); mock_bank .account_shared_data .borrow_mut() .insert(key, (account_data.clone(), 0)); - let result = get_program_deployment_slot(&mock_bank, &key); + let result = get_program_deployment_slot( + &mock_bank, + &mock_bank.get_account_shared_data(&key).unwrap().0, + ProgramCacheEntryOwner::LoaderV3, + ); assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); let state = UpgradeableLoaderState::Program { @@ -710,7 +763,11 @@ mod tests { .borrow_mut() .insert(key, (account_data.clone(), 0)); - let result = get_program_deployment_slot(&mock_bank, &key); + let result = get_program_deployment_slot( + &mock_bank, + &mock_bank.get_account_shared_data(&key).unwrap().0, + ProgramCacheEntryOwner::LoaderV3, + ); assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); } @@ -734,7 +791,7 @@ mod tests { .borrow_mut() .insert(key1, (account_data, 0)); - let mut account_data = AccountSharedData::new_data( + let account_data = AccountSharedData::new_data( 100, &UpgradeableLoaderState::ProgramData { slot: 77, @@ -748,16 +805,129 @@ mod tests { .borrow_mut() .insert(key2, (account_data.clone(), 0)); - let result = get_program_deployment_slot(&mock_bank, &key1); + let result = get_program_deployment_slot( + &mock_bank, + &mock_bank.get_account_shared_data(&key1).unwrap().0, + ProgramCacheEntryOwner::LoaderV3, + ); assert_eq!(result.unwrap(), 77); + } - account_data.set_owner(Pubkey::new_unique()); - mock_bank - .account_shared_data - .borrow_mut() - .insert(key2, (account_data, 0)); + #[test] + fn test_filter_executable_program_accounts() { + let feepayer = Keypair::new(); + let loader_ids = [ + bpf_loader_deprecated::id(), + bpf_loader::id(), + bpf_loader_upgradeable::id(), + native_loader::id(), + ]; + let program_ids = [ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + let account_ids = [ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; - let result = get_program_deployment_slot(&mock_bank, &key2); - assert_eq!(result.unwrap(), 0); + let mut loaded_programs_for_tx_batch = ProgramCacheForTxBatch::default(); + let mock_bank = MockBankCallback::default(); + for i in 0..3 { + loaded_programs_for_tx_batch.replenish( + loader_ids[i], + Arc::new(ProgramCacheEntry { + program: ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()), + account_owner: ProgramCacheEntryOwner::NativeLoader, + account_size: 0, + deployment_slot: 0, + effective_slot: 0, + stats: Arc::default(), + latest_access_slot: AtomicU64::default(), + }), + ); + mock_bank.account_shared_data.borrow_mut().insert( + loader_ids[i], + (AccountSharedData::new(1, 1, &program_ids[3]), 0), + ); + mock_bank.account_shared_data.borrow_mut().insert( + program_ids[i], + (AccountSharedData::new(1, 1, &loader_ids[i]), 0), + ); + mock_bank.account_shared_data.borrow_mut().insert( + account_ids[i], + (AccountSharedData::new(1, 1, &program_ids[i]), 0), + ); + } + + let tx = Transaction::new_with_compiled_instructions( + &[&feepayer], + &[program_ids[1], program_ids[2], loader_ids[2]], + Hash::new_unique(), + vec![ + account_ids[0], + account_ids[1], + account_ids[2], + account_ids[3], + ], + vec![ + CompiledInstruction::new(1, &(), vec![0, 1, 2, 3]), + CompiledInstruction::new(2, &(), vec![0, 1, 2, 3]), + CompiledInstruction::new(3, &(), vec![0, 1, 2, 3]), + ], + ); + let sanitized_tx = SanitizedTransaction::from_transaction_for_tests(tx); + + let missing_programs = filter_executable_program_accounts( + &mock_bank, + &loaded_programs_for_tx_batch, + sanitized_tx.account_keys().iter(), + false, + ); + assert_eq!( + missing_programs, + &[ + ProgramToLoad { + program_id: &program_ids[1], + loader: ProgramCacheEntryOwner::LoaderV2, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }, + ProgramToLoad { + program_id: &program_ids[2], + loader: ProgramCacheEntryOwner::LoaderV3, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }, + ] + ); + + let missing_programs = filter_executable_program_accounts( + &mock_bank, + &loaded_programs_for_tx_batch, + sanitized_tx.account_keys().iter(), + true, + ); + assert_eq!( + missing_programs, + &[ + ProgramToLoad { + program_id: &program_ids[1], + loader: ProgramCacheEntryOwner::LoaderV2, + match_criteria: ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(0), + last_modification_slot: 0, + }, + ProgramToLoad { + program_id: &program_ids[2], + loader: ProgramCacheEntryOwner::LoaderV3, + match_criteria: ProgramCacheMatchCriteria::Tombstone, + last_modification_slot: 0, + }, + ] + ); } } diff --git a/svm/src/transaction_processor.rs b/svm/src/transaction_processor.rs index c803fe86015..460a4d91864 100644 --- a/svm/src/transaction_processor.rs +++ b/svm/src/transaction_processor.rs @@ -1,14 +1,14 @@ use { crate::{ account_loader::{ - AccountLoader, CheckedTransactionDetails, LoadedTransaction, PROGRAM_OWNERS, - TransactionCheckResult, TransactionLoadResult, ValidatedTransactionDetails, - load_transaction, update_rent_exempt_status_for_account, validate_fee_payer, + AccountLoader, CheckedTransactionDetails, LoadedTransaction, TransactionCheckResult, + TransactionLoadResult, ValidatedTransactionDetails, load_transaction, + update_rent_exempt_status_for_account, validate_fee_payer, }, account_overrides::AccountOverrides, message_processor::process_message, nonce_info::NonceInfo, - program_loader::{get_program_deployment_slot, load_program_with_pubkey}, + program_loader::{filter_executable_program_accounts, load_program_with_pubkey}, rollback_accounts::RollbackAccounts, transaction_account_state_info::{ TransactionAccountStateInfo, get_uninitialized_accounts_size, verify_changes, @@ -35,7 +35,7 @@ use { state::{DurableNonce, State as NonceState}, versions::Versions as NonceVersions, }, - solana_nonce_account::verify_nonce_account, + solana_nonce_account::{SystemAccountKind, get_system_account_kind, verify_nonce_account}, solana_program_runtime::{ execution_budget::{ SVMTransactionExecutionAndFeeBudgetLimits, SVMTransactionExecutionCost, @@ -44,24 +44,26 @@ use { loaded_programs::{ EpochBoundaryPreparation, ForkGraph, ProgramCache, ProgramCacheForTxBatch, ProgramCacheMatchCriteria, ProgramRuntimeEnvironment, ProgramRuntimeEnvironments, + ProgramToLoad, }, - program_cache_entry::ProgramCacheEntry, + program_cache_entry::{ProgramCacheEntry, ProgramCacheEntryOwner}, + program_metrics::ProgramStatistics, solana_sbpf::{program::BuiltinProgram, vm::Config as VmConfig}, sysvar_cache::SysvarCache, }, solana_pubkey::Pubkey, solana_rent::Rent, - solana_svm_callback::TransactionProcessingCallback, + solana_svm_callback::{InvokeContextCallback, TransactionProcessingCallback}, solana_svm_feature_set::SVMFeatureSet, solana_svm_log_collector::LogCollector, solana_svm_measure::{measure::Measure, measure_us}, solana_svm_timings::{ExecuteTimingType, ExecuteTimings}, solana_svm_transaction::{svm_message::SVMMessage, svm_transaction::SVMTransaction}, - solana_svm_type_overrides::sync::{Arc, RwLock, RwLockReadGuard, atomic::Ordering}, + solana_svm_type_overrides::sync::{Arc, RwLock, RwLockReadGuard}, solana_transaction_context::transaction::{ExecutionRecord, TransactionContext}, solana_transaction_error::{TransactionError, TransactionResult}, std::{ - collections::{HashMap, HashSet}, + collections::HashSet, fmt::{Debug, Formatter}, rc::Rc, }, @@ -139,6 +141,10 @@ pub struct TransactionProcessingConfig<'a> { /// failing transactions to be committed. If both flags are set then any /// failing transaction will cause all transactions to be aborted. pub all_or_nothing: bool, + /// Strictly require durable nonce accounts to have the canonical nonce account size. + /// + /// This is a leader-side filtering policy. It must not be enabled for replay. + pub strict_nonce_size_check: bool, } /// Runtime environment for transaction batch processing. @@ -316,9 +322,14 @@ impl TransactionBatchProcessor { // Pre-populate the builtin program cache from the global cache. // This is done once per block rather than once per batch. let mut builtin_program_cache = ProgramCacheForTxBatch::new(slot); - let mut search_for: Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)> = builtin_program_ids + let mut search_for: Vec = builtin_program_ids .iter() - .map(|key| (*key, ProgramCacheMatchCriteria::NoCriteria, 0)) + .map(|program_id| ProgramToLoad { + program_id, + loader: ProgramCacheEntryOwner::NativeLoader, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }) .collect(); self.global_program_cache.read().unwrap().extract( &mut search_for, @@ -385,7 +396,9 @@ impl TransactionBatchProcessor { } /// Main entrypoint to the SVM. - pub fn load_and_execute_sanitized_transactions( + pub fn load_and_execute_sanitized_transactions< + CB: TransactionProcessingCallback + InvokeContextCallback, + >( &self, callbacks: &CB, sanitized_txs: &[impl SVMTransaction], @@ -462,6 +475,7 @@ impl TransactionBatchProcessor { environment.blockhash_lamports_per_signature, &environment.rent, environment.feature_set.relax_post_exec_min_balance_check, + config.strict_nonce_size_check, &mut error_metrics, ) })); @@ -497,11 +511,12 @@ impl TransactionBatchProcessor { } }, TransactionLoadResult::Loaded(loaded_transaction) => { - let (program_accounts_set, filter_executable_us) = - measure_us!(self.filter_executable_program_accounts( + let (missing_programs, filter_executable_us) = + measure_us!(filter_executable_program_accounts( &account_loader, - &mut program_cache_for_tx_batch, - tx, + &program_cache_for_tx_batch, + tx.account_keys().iter(), + config.check_program_deployment_slot, )); execute_timings.saturating_add_in_place( ExecuteTimingType::FilterExecutableUs, @@ -511,13 +526,12 @@ impl TransactionBatchProcessor { let ((), program_cache_us) = measure_us!({ self.replenish_program_cache( &account_loader, - &program_accounts_set, + missing_programs, environment .program_runtime_environments .get_env_for_execution(), &mut program_cache_for_tx_batch, &mut execute_timings, - config.check_program_deployment_slot, config.limit_to_load_programs, true, // increment_usage_counter ); @@ -561,6 +575,7 @@ impl TransactionBatchProcessor { account_loader.update_accounts_for_successful_tx( tx, &executed_tx.loaded_transaction.accounts, + &executed_tx.loaded_transaction.touched_flags, self.slot, ); // Also update local program cache with modifications made by the @@ -666,6 +681,7 @@ impl TransactionBatchProcessor { next_lamports_per_signature: u64, rent: &Rent, relax_post_exec_min_balance_check: bool, + strict_nonce_size_check: bool, error_counters: &mut TransactionErrorMetrics, ) -> TransactionResult { let CheckedTransactionDetails { @@ -684,6 +700,7 @@ impl TransactionBatchProcessor { nonce_address, &next_durable_nonce, next_lamports_per_signature, + strict_nonce_size_check, error_counters, )?) } else { @@ -763,6 +780,7 @@ impl TransactionBatchProcessor { nonce_address: &Pubkey, next_durable_nonce: &DurableNonce, next_lamports_per_signature: u64, + strict_nonce_size_check: bool, error_counters: &mut TransactionErrorMetrics, ) -> TransactionResult { // When SIMD83 is enabled, if the nonce has been used in this batch already, we must drop @@ -779,6 +797,13 @@ impl TransactionBatchProcessor { return Err(TransactionError::AccountNotFound); }; + if strict_nonce_size_check + && get_system_account_kind(&nonce_account) != Some(SystemAccountKind::Nonce) + { + error_counters.blockhash_not_found += 1; + return Err(TransactionError::BlockhashNotFound); + } + // This function verifies: // * Nonce account owner is SystemProgram // * Nonce account parses as State::Initialized @@ -812,56 +837,17 @@ impl TransactionBatchProcessor { } } - /// Appends to a set of executable program accounts (all accounts owned by any loader) - /// for transactions with a valid blockhash or nonce. - fn filter_executable_program_accounts( - &self, - account_loader: &AccountLoader, - program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, - tx: &impl SVMMessage, - ) -> HashMap { - let mut program_accounts_set = HashMap::default(); - for account_key in tx.account_keys().iter() { - if let Some(cache_entry) = program_cache_for_tx_batch.find(account_key) { - cache_entry.stats.uses.fetch_add(1, Ordering::Relaxed); - } else if let Some((account, last_modification_slot)) = - account_loader.get_account_shared_data(account_key) - && PROGRAM_OWNERS.contains(account.owner()) - { - program_accounts_set.insert(*account_key, last_modification_slot); - } - } - program_accounts_set - } - #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] fn replenish_program_cache( &self, account_loader: &AccountLoader, - program_accounts_set: &HashMap, + mut missing_programs: Vec, program_runtime_environment_for_execution: &ProgramRuntimeEnvironment, program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, execute_timings: &mut ExecuteTimings, - check_program_deployment_slot: bool, limit_to_load_programs: bool, increment_usage_counter: bool, ) { - let mut missing_programs: Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)> = - program_accounts_set - .iter() - .map(|(pubkey, last_modification_slot)| { - let match_criteria = if check_program_deployment_slot { - get_program_deployment_slot(account_loader, pubkey) - .map_or(ProgramCacheMatchCriteria::Tombstone, |slot| { - ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(slot) - }) - } else { - ProgramCacheMatchCriteria::NoCriteria - }; - (*pubkey, match_criteria, *last_modification_slot) - }) - .collect(); - let mut count_hits_and_misses = true; loop { // Lock the global cache. @@ -926,9 +912,65 @@ impl TransactionBatchProcessor { } } + /// Similar to replenish_program_cache() but only used in Bank::prepare_program_cache_for_upcoming_feature_set(). + pub fn prepare_one_program_for_upcoming_feature_set( + &self, + account_loader: &CB, + check_program_deployment_slot: bool, + upcoming_environment: &ProgramRuntimeEnvironment, + key: &Pubkey, + stats_of_enqueued_program: &ProgramStatistics, + ) { + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::new(self.slot); + let mut missing_programs = filter_executable_program_accounts( + account_loader, + &program_cache_for_tx_batch, + std::iter::once(key), + check_program_deployment_slot, + ); + if missing_programs.is_empty() { + // Program account was closed + return; + } + let program_to_load = { + let program_cache_guard = self.global_program_cache.read().unwrap(); + program_cache_guard.extract( + &mut missing_programs, + &mut program_cache_for_tx_batch, + upcoming_environment, + false, // increment_usage_counter + false, // count_hits_and_misses + ) + // Unlock again because load_program_with_pubkey() might take a while. + }; + // Maybe the enqueued program was already loaded and can be skipped. + if let Some(key) = program_to_load { + // Load, verify and compile one program. + let (recompiled, last_modification_slot) = load_program_with_pubkey( + account_loader, + upcoming_environment, + &key, + self.slot, + &mut ExecuteTimings::default(), + ) + .expect("called load_program_with_pubkey() with nonexistent account"); + recompiled.stats.merge_from(stats_of_enqueued_program); + // Lock the global cache as writable this time. + let mut program_cache_guard = self.global_program_cache.write().unwrap(); + // Submit our last completed loading task. + program_cache_guard.finish_cooperative_loading_task( + upcoming_environment, + self.slot, + key, + last_modification_slot, + recompiled, + ); + } + } + /// Execute a transaction using the provided loaded accounts and update /// the executors cache if the transaction was successful. - fn execute_loaded_transaction( + fn execute_loaded_transaction( &self, callback: &CB, tx: &impl SVMTransaction, @@ -1067,10 +1109,20 @@ impl TransactionBatchProcessor { let ExecutionRecord { accounts, return_data, - touched_account_count, + mut touched_flags, accounts_resize_delta, } = execution_record; + // The fee payer (account index 0) is debited during loading, outside the + // VM, so it carries no VM touch flag but must still be written back. + if let Some(fee_payer_touched) = touched_flags.first_mut() { + *fee_payer_touched = true; + } + + // changed_account_count reflects every account that will be written back, + // including the fee payer marked above. + let touched_account_count = touched_flags.iter().filter(|touched| **touched).count(); + if post_account_state_info_result.is_ok() && transaction_accounts_lamports_sum(&accounts) .filter(|lamports_after_tx| lamports_before_tx == *lamports_after_tx) @@ -1096,8 +1148,9 @@ impl TransactionBatchProcessor { .unwrap_or_else(|err| (Err(err), None)); loaded_transaction.accounts = accounts; + loaded_transaction.touched_flags = touched_flags; execute_timings.details.total_account_count += loaded_transaction.accounts.len() as u64; - execute_timings.details.changed_account_count += touched_account_count; + execute_timings.details.changed_account_count += touched_account_count as u64; let return_data = if config.recording_config.enable_return_data_recording && !return_data.data.is_empty() @@ -1218,6 +1271,22 @@ impl TransactionBatchProcessor { callbacks: &CB, ) { let mut sysvar_cache = self.sysvar_cache.write().unwrap(); + Self::fill_missing_sysvar_cache_entries_from_accounts(&mut sysvar_cache, callbacks); + } + + pub fn reset_and_fill_sysvar_cache_entries( + &self, + callbacks: &CB, + ) { + let mut sysvar_cache = self.sysvar_cache.write().unwrap(); + sysvar_cache.reset(); + Self::fill_missing_sysvar_cache_entries_from_accounts(&mut sysvar_cache, callbacks); + } + + fn fill_missing_sysvar_cache_entries_from_accounts( + sysvar_cache: &mut SysvarCache, + callbacks: &CB, + ) { sysvar_cache.fill_missing_entries(|pubkey, set_sysvar| { if let Some((account, _slot)) = callbacks.get_account_shared_data(pubkey) { set_sysvar(account.data()); @@ -1279,7 +1348,6 @@ mod tests { solana_fee_calculator::FeeCalculator, solana_fee_structure::FeeDetails, solana_hash::Hash, - solana_keypair::Keypair, solana_message::{LegacyMessage, Message, MessageHeader, SanitizedMessage}, solana_nonce as nonce, solana_program_runtime::{ @@ -1292,11 +1360,11 @@ mod tests { }, solana_rent::Rent, solana_sbpf::vm, - solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable, system_program, sysvar}, + solana_sdk_ids::{bpf_loader, system_program, sysvar}, solana_signature::Signature, solana_svm_callback::{AccountState, InvokeContextCallback}, solana_system_interface::instruction as system_instruction, - solana_transaction::{Transaction, sanitized::SanitizedTransaction}, + solana_transaction::sanitized::SanitizedTransaction, solana_transaction_context::transaction::TransactionContext, solana_transaction_error::TransactionError, std::{borrow::Cow, collections::HashMap}, @@ -1599,6 +1667,7 @@ mod tests { let loaded_transaction = LoadedTransaction { accounts: vec![(Pubkey::new_unique(), AccountSharedData::default())], + touched_flags: Box::default(), fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::default(), compute_budget: SVMTransactionExecutionBudget::default(), @@ -1693,6 +1762,7 @@ mod tests { (key1, AccountSharedData::default()), (key2, AccountSharedData::default()), ], + touched_flags: Box::default(), fee_details: FeeDetails::default(), rollback_accounts: RollbackAccounts::default(), compute_budget: SVMTransactionExecutionBudget::default(), @@ -1732,18 +1802,19 @@ mod tests { batch_processor.program_runtime_environment_for_epoch(0); let key = Pubkey::new_unique(); - let mut account_set = HashMap::new(); - account_set.insert(key, 0); - let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::new(batch_processor.slot); batch_processor.replenish_program_cache( &account_loader, - &account_set, + vec![ProgramToLoad { + program_id: &key, + loader: ProgramCacheEntryOwner::LoaderV3, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }], &program_runtime_environment_for_execution, &mut program_cache_for_tx_batch, &mut ExecuteTimings::default(), - false, true, true, ); @@ -1768,20 +1839,21 @@ mod tests { .insert(key, account_data); let account_loader = (&mock_bank).into(); - let mut account_set = HashMap::new(); - account_set.insert(key, 0); let mut loaded_missing = 0; - for limit_to_load_programs in [false, true] { let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::new(batch_processor.slot); batch_processor.replenish_program_cache( &account_loader, - &account_set, + vec![ProgramToLoad { + program_id: &key, + loader: ProgramCacheEntryOwner::LoaderV2, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }], &program_runtime_environment_for_execution, &mut program_cache_for_tx_batch, &mut ExecuteTimings::default(), - false, limit_to_load_programs, true, ); @@ -1799,164 +1871,6 @@ mod tests { assert!(loaded_missing > 0); } - #[test] - fn test_filter_executable_program_accounts() { - let mock_bank = MockBankCallback::default(); - let key1 = Pubkey::new_unique(); - let owner1 = bpf_loader::id(); - let key2 = Pubkey::new_unique(); - let owner2 = bpf_loader_upgradeable::id(); - - let mut data1 = AccountSharedData::default(); - data1.set_owner(owner1); - data1.set_lamports(93); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(key1, data1); - - let mut data2 = AccountSharedData::default(); - data2.set_owner(owner2); - data2.set_lamports(90); - mock_bank - .account_shared_data - .write() - .unwrap() - .insert(key2, data2); - let account_loader = (&mock_bank).into(); - - let message = Message { - account_keys: vec![key1, key2], - header: MessageHeader::default(), - instructions: vec![CompiledInstruction { - program_id_index: 0, - accounts: vec![], - data: vec![], - }], - recent_blockhash: Hash::default(), - }; - - let sanitized_message = new_unchecked_sanitized_message(message); - - let sanitized_transaction = SanitizedTransaction::new_for_tests( - sanitized_message, - vec![Signature::new_unique()], - false, - ); - - let batch_processor = TransactionBatchProcessor::::default(); - let program_accounts_set = batch_processor.filter_executable_program_accounts( - &account_loader, - &mut ProgramCacheForTxBatch::default(), - &sanitized_transaction, - ); - - assert_eq!(program_accounts_set.len(), 2); - assert!(program_accounts_set.contains_key(&key1)); - assert!(program_accounts_set.contains_key(&key2)); - } - - #[test] - fn test_filter_executable_program_accounts_no_errors() { - let keypair1 = Keypair::new(); - let keypair2 = Keypair::new(); - - let non_program_pubkey1 = Pubkey::new_unique(); - let non_program_pubkey2 = Pubkey::new_unique(); - let program1_pubkey = bpf_loader::id(); - let program2_pubkey = bpf_loader_upgradeable::id(); - let account1_pubkey = Pubkey::new_unique(); - let account2_pubkey = Pubkey::new_unique(); - let account3_pubkey = Pubkey::new_unique(); - let account4_pubkey = Pubkey::new_unique(); - - let account5_pubkey = Pubkey::new_unique(); - - let bank = MockBankCallback::default(); - bank.account_shared_data.write().unwrap().insert( - non_program_pubkey1, - AccountSharedData::new(1, 10, &account5_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - non_program_pubkey2, - AccountSharedData::new(1, 10, &account5_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - program1_pubkey, - AccountSharedData::new(40, 1, &account5_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - program2_pubkey, - AccountSharedData::new(40, 1, &account5_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - account1_pubkey, - AccountSharedData::new(1, 10, &non_program_pubkey1), - ); - bank.account_shared_data.write().unwrap().insert( - account2_pubkey, - AccountSharedData::new(1, 10, &non_program_pubkey2), - ); - bank.account_shared_data.write().unwrap().insert( - account3_pubkey, - AccountSharedData::new(40, 1, &program1_pubkey), - ); - bank.account_shared_data.write().unwrap().insert( - account4_pubkey, - AccountSharedData::new(40, 1, &program2_pubkey), - ); - let account_loader = (&bank).into(); - - let tx1 = Transaction::new_with_compiled_instructions( - &[&keypair1], - &[non_program_pubkey1], - Hash::new_unique(), - vec![account1_pubkey, account2_pubkey, account3_pubkey], - vec![CompiledInstruction::new(1, &(), vec![0])], - ); - let sanitized_tx1 = SanitizedTransaction::from_transaction_for_tests(tx1); - - let tx2 = Transaction::new_with_compiled_instructions( - &[&keypair2], - &[non_program_pubkey2], - Hash::new_unique(), - vec![account4_pubkey, account3_pubkey, account2_pubkey], - vec![CompiledInstruction::new(1, &(), vec![0])], - ); - let sanitized_tx2 = SanitizedTransaction::from_transaction_for_tests(tx2); - - let batch_processor = TransactionBatchProcessor::::default(); - - let tx1_programs = batch_processor.filter_executable_program_accounts( - &account_loader, - &mut ProgramCacheForTxBatch::default(), - &sanitized_tx1, - ); - - assert_eq!(tx1_programs.len(), 1); - assert!( - tx1_programs.contains_key(&account3_pubkey), - "failed to find the program account", - ); - - let tx2_programs = batch_processor.filter_executable_program_accounts( - &account_loader, - &mut ProgramCacheForTxBatch::default(), - &sanitized_tx2, - ); - - assert_eq!(tx2_programs.len(), 2); - assert!( - tx2_programs.contains_key(&account3_pubkey), - "failed to find the program account", - ); - assert!( - tx2_programs.contains_key(&account4_pubkey), - "failed to find the program account", - ); - } - #[test] #[allow(deprecated)] fn test_sysvar_cache_initialization1() { @@ -2083,6 +1997,31 @@ mod tests { let transaction_processor = TransactionBatchProcessor::::default(); // Fill the sysvar cache transaction_processor.fill_missing_sysvar_cache_entries(&mock_bank); + + let updated_clock = Clock { + slot: 6, + epoch_start_timestamp: 7, + epoch: 8, + leader_schedule_epoch: 9, + unix_timestamp: 10, + }; + let updated_clock_account = create_account_shared_data_for_test(&updated_clock); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::clock::id(), updated_clock_account); + transaction_processor.reset_and_fill_sysvar_cache_entries(&mock_bank); + { + let sysvar_cache = transaction_processor.sysvar_cache.read().unwrap(); + assert_eq!( + sysvar_cache + .get_clock() + .expect("clock sysvar missing in cache"), + updated_clock.clone().into() + ); + } + // Reset the sysvar cache transaction_processor.reset_sysvar_cache(); @@ -2108,7 +2047,7 @@ mod tests { assert_eq!( cached_clock.expect("clock sysvar missing in cache"), - clock.into() + updated_clock.into() ); assert_eq!( cached_epoch_schedule.expect("epoch_schedule sysvar missing in cache"), @@ -2154,7 +2093,12 @@ mod tests { .write() .unwrap() .extract( - &mut vec![(key, ProgramCacheMatchCriteria::NoCriteria, 0)], + &mut vec![ProgramToLoad { + program_id: &key, + loader: ProgramCacheEntryOwner::NativeLoader, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }], &mut loaded_programs_for_tx_batch, &program_runtime_environment, true, @@ -2224,6 +2168,7 @@ mod tests { lamports_per_signature, &rent, mock_bank.feature_set.relax_post_exec_min_balance_check, + false, &mut error_counters, ); @@ -2276,6 +2221,7 @@ mod tests { lamports_per_signature, &Rent::default(), mock_bank.feature_set.relax_post_exec_min_balance_check, + false, &mut error_counters, ); @@ -2317,6 +2263,7 @@ mod tests { lamports_per_signature, &Rent::default(), mock_bank.feature_set.relax_post_exec_min_balance_check, + false, &mut error_counters, ); @@ -2362,6 +2309,7 @@ mod tests { lamports_per_signature, &rent, mock_bank.feature_set.relax_post_exec_min_balance_check, + false, &mut error_counters, ); @@ -2405,6 +2353,7 @@ mod tests { lamports_per_signature, &Rent::default(), mock_bank.feature_set.relax_post_exec_min_balance_check, + false, &mut error_counters, ); @@ -2499,6 +2448,7 @@ mod tests { &nonce_address, &next_durable_nonce, lamports_per_signature, + false, &mut error_counters, ); @@ -2592,6 +2542,7 @@ mod tests { lamports_per_signature, &rent, mock_bank.feature_set.relax_post_exec_min_balance_check, + false, &mut error_counters, ); @@ -2653,6 +2604,7 @@ mod tests { lamports_per_signature, &rent, mock_bank.feature_set.relax_post_exec_min_balance_check, + false, &mut error_counters, ); @@ -2702,6 +2654,7 @@ mod tests { lamports_per_signature, &Rent::default(), mock_bank.feature_set.relax_post_exec_min_balance_check, + false, &mut TransactionErrorMetrics::default(), ) .unwrap(); diff --git a/svm/tests/concurrent_tests.rs b/svm/tests/concurrent_tests.rs index ac4b1c481ee..cab75d9a462 100644 --- a/svm/tests/concurrent_tests.rs +++ b/svm/tests/concurrent_tests.rs @@ -10,12 +10,15 @@ use { thread, }, solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, - solana_clock::Slot, solana_instruction::{AccountMeta, Instruction}, solana_program_runtime::{ execution_budget::SVMTransactionExecutionAndFeeBudgetLimits, - loaded_programs::{ProgramCacheForTxBatch, ProgramRuntimeEnvironments}, - program_cache_entry::ProgramCacheEntryType, + loaded_programs::{ + ProgramCacheForTxBatch, ProgramCacheMatchCriteria, ProgramRuntimeEnvironments, + ProgramToLoad, + }, + program_cache_entry::{ProgramCacheEntryOwner, ProgramCacheEntryType}, + program_metrics::ProgramStatistics, }, solana_pubkey::Pubkey, solana_svm::{ @@ -31,7 +34,7 @@ use { solana_svm_feature_set::SVMFeatureSet, solana_svm_timings::ExecuteTimings, solana_transaction::{Transaction, sanitized::SanitizedTransaction}, - std::collections::{HashMap, HashSet}, + std::collections::HashSet, }; mod mock_bank; @@ -42,15 +45,12 @@ fn program_cache_execution(threads: usize) { let mut mock_bank = MockBankCallback::default(); let fork_graph = Arc::new(RwLock::new(MockForkGraph {})); let batch_processor = TransactionBatchProcessor::new(5, 5, Arc::downgrade(&fork_graph), None); - - let programs = vec![ + let programs = [ deploy_program("hello-solana".to_string(), 0, &mut mock_bank), deploy_program("simple-transfer".to_string(), 0, &mut mock_bank), deploy_program("clock-sysvar".to_string(), 0, &mut mock_bank), ]; - let account_maps: HashMap = programs.iter().map(|key| (*key, 0)).collect(); - let ths: Vec<_> = (0..threads) .map(|_| { let local_bank = mock_bank.clone(); @@ -59,9 +59,16 @@ fn program_cache_execution(threads: usize) { batch_processor.slot, batch_processor.epoch, ); - let maps = account_maps.clone(); - let programs = programs.clone(); thread::spawn(move || { + let missing_programs: Vec = programs + .iter() + .map(|program_id| ProgramToLoad { + program_id, + loader: ProgramCacheEntryOwner::LoaderV3, + match_criteria: ProgramCacheMatchCriteria::NoCriteria, + last_modification_slot: 0, + }) + .collect(); let feature_set = SVMFeatureSet::all_enabled(); let account_loader = AccountLoader::new_with_loaded_accounts_capacity( None, @@ -74,15 +81,14 @@ fn program_cache_execution(threads: usize) { processor.program_runtime_environment_for_epoch(processor.epoch); processor.replenish_program_cache( &account_loader, - &maps, + missing_programs, &program_runtime_environment_for_execution, &mut result, &mut ExecuteTimings::default(), - false, true, true, ); - for key in &programs { + for key in programs.iter() { let cache_entry = result.find(key); assert!(matches!( cache_entry.unwrap().program, @@ -91,6 +97,33 @@ fn program_cache_execution(threads: usize) { } }) }) + .chain(programs.iter().map(|program| { + let program = *program; + let local_bank = mock_bank.clone(); + let processor = TransactionBatchProcessor::new_from( + &batch_processor, + batch_processor.slot, + batch_processor.epoch, + ); + thread::spawn(move || { + let feature_set = SVMFeatureSet::all_enabled(); + let account_loader = AccountLoader::new_with_loaded_accounts_capacity( + None, + &local_bank, + &feature_set, + 0, + ); + let upcoming_environment = + processor.program_runtime_environment_for_epoch(processor.epoch + 1); + processor.prepare_one_program_for_upcoming_feature_set( + &account_loader, + false, + &upcoming_environment, + &program, + &ProgramStatistics::default(), + ); + }) + })) .collect(); for th in ths { diff --git a/svm/tests/example-programs/clock-sysvar/Cargo.toml b/svm/tests/example-programs/clock-sysvar/Cargo.toml index 95d3af349f7..fad9b170bff 100644 --- a/svm/tests/example-programs/clock-sysvar/Cargo.toml +++ b/svm/tests/example-programs/clock-sysvar/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clock-sysvar-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" edition = "2021" [dependencies] diff --git a/svm/tests/example-programs/hello-solana/Cargo.toml b/svm/tests/example-programs/hello-solana/Cargo.toml index e7cd837a003..3c18f4c63c2 100644 --- a/svm/tests/example-programs/hello-solana/Cargo.toml +++ b/svm/tests/example-programs/hello-solana/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hello-solana-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" edition = "2021" [dependencies] diff --git a/svm/tests/example-programs/simple-transfer/Cargo.toml b/svm/tests/example-programs/simple-transfer/Cargo.toml index 8cfd721e869..9afd2940307 100644 --- a/svm/tests/example-programs/simple-transfer/Cargo.toml +++ b/svm/tests/example-programs/simple-transfer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simple-transfer-program" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" edition = "2021" [dependencies] diff --git a/svm/tests/example-programs/transfer-from-account/Cargo.toml b/svm/tests/example-programs/transfer-from-account/Cargo.toml index 10ad29acfb8..c8cc21bc22f 100644 --- a/svm/tests/example-programs/transfer-from-account/Cargo.toml +++ b/svm/tests/example-programs/transfer-from-account/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "transfer-from-account" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" edition = "2021" [dependencies] diff --git a/svm/tests/example-programs/write-to-account/Cargo.toml b/svm/tests/example-programs/write-to-account/Cargo.toml index 05619104f2b..87dc5e9d00e 100644 --- a/svm/tests/example-programs/write-to-account/Cargo.toml +++ b/svm/tests/example-programs/write-to-account/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "write-to-account" -version = "4.2.0-alpha.0" +version = "4.3.0-alpha.0" edition = "2021" [dependencies] diff --git a/syscalls/Cargo.toml b/syscalls/Cargo.toml index a05da9d8eb0..6ab736d24e4 100644 --- a/syscalls/Cargo.toml +++ b/syscalls/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] default = ["metrics"] @@ -35,7 +37,7 @@ solana-bn254 = { workspace = true } solana-clock = { workspace = true } solana-cpi = { workspace = true } solana-curve25519 = { workspace = true } -solana-hash = { workspace = true } +solana-hash = { workspace = true, features = ["wincode"] } solana-hash-512 = { workspace = true } solana-instruction = { workspace = true } solana-keccak-hasher = { workspace = true, features = ["sha3"] } @@ -57,6 +59,7 @@ solana-sysvar = { workspace = true } solana-sysvar-id = { workspace = true } solana-transaction-context = { workspace = true, features = ["bincode"] } thiserror = { workspace = true } +wincode = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/syscalls/gen-syscall-list/Cargo.toml b/syscalls/gen-syscall-list/Cargo.toml index 08853ff936b..2da52971626 100644 --- a/syscalls/gen-syscall-list/Cargo.toml +++ b/syscalls/gen-syscall-list/Cargo.toml @@ -13,3 +13,6 @@ agave-unstable-api = [] [build-dependencies] regex = { workspace = true } + +[lints] +workspace = true diff --git a/syscalls/src/cpi.rs b/syscalls/src/cpi.rs index fe47d7812da..9f9383d865b 100644 --- a/syscalls/src/cpi.rs +++ b/syscalls/src/cpi.rs @@ -4,7 +4,6 @@ use { solana_program_runtime::cpi::{ SyscallInvokeSigned, TranslatedAccount, cpi_common, translate_accounts_c, translate_accounts_rust, translate_instruction_c, translate_instruction_rust, - translate_signers_c, translate_signers_rust, }, }; @@ -45,20 +44,6 @@ impl SyscallInvokeSigned for SyscallInvokeSignedRust { ) -> Result>, Error> { translate_accounts_rust(account_infos_addr, account_infos_len, invoke_context) } - - fn translate_signers( - program_id: &Pubkey, - signers_seeds_addr: u64, - signers_seeds_len: u64, - invoke_context: &InvokeContext, - ) -> Result, Error> { - translate_signers_rust( - program_id, - signers_seeds_addr, - signers_seeds_len, - invoke_context, - ) - } } declare_builtin_function!( @@ -98,18 +83,4 @@ impl SyscallInvokeSigned for SyscallInvokeSignedC { ) -> Result>, Error> { translate_accounts_c(account_infos_addr, account_infos_len, invoke_context) } - - fn translate_signers( - program_id: &Pubkey, - signers_seeds_addr: u64, - signers_seeds_len: u64, - invoke_context: &InvokeContext, - ) -> Result, Error> { - translate_signers_c( - program_id, - signers_seeds_addr, - signers_seeds_len, - invoke_context, - ) - } } diff --git a/syscalls/src/lib.rs b/syscalls/src/lib.rs index 572a3c4d256..2c81cc0102c 100644 --- a/syscalls/src/lib.rs +++ b/syscalls/src/lib.rs @@ -59,7 +59,10 @@ mod mem_ops; mod sysvar; /// Error definitions +// Note: `#[repr(u64)]` is used for `Self::discriminant`, but the actual +// memory layout of this enum's variants is not depended on by the VM. #[derive(Debug, ThisError, PartialEq, Eq)] +#[repr(u64)] pub enum SyscallError { #[error("{0}: {1:?}")] InvalidString(Utf8Error, Vec), @@ -114,6 +117,15 @@ pub enum SyscallError { ArithmeticOverflow, } +impl SyscallError { + /// Returns the enum discriminant as a `u64`. + /// + /// This is sound only because of the `#[repr(u64)]` attribute on the enum. + pub fn discriminant(&self) -> u64 { + unsafe { *std::ptr::addr_of!(*self).cast::() } + } +} + impl From for SyscallError { fn from(error: MemoryTranslationError) -> Self { match error { @@ -2701,7 +2713,9 @@ mod tests { solana_sha256_hasher::hashv, solana_slot_hashes::{self as slot_hashes, SlotHashes}, solana_stable_layout::stable_instruction::StableInstruction, - solana_stake_interface::stake_history::{self, StakeHistory, StakeHistoryEntry}, + solana_stake_interface::stake_history::{ + self, SIZE as STAKE_HISTORY_ACCOUNT_SIZE, StakeHistory, StakeHistoryEntry, + }, solana_sysvar_id::SysvarId, solana_transaction_context::instruction_accounts::InstructionAccount, std::{ @@ -2712,6 +2726,14 @@ mod tests { test_case::test_case, }; + fn create_stake_history_account_for_test(stake_history: &StakeHistory) -> AccountSharedData { + let data_len = STAKE_HISTORY_ACCOUNT_SIZE + .max(bincode::serialized_size(stake_history).unwrap() as usize); + let mut account = AccountSharedData::new(1, data_len, &sysvar::id()); + account.serialize_data(stake_history).unwrap(); + account + } + macro_rules! assert_access_violation { ($result:expr, $va:expr, $len:expr) => { match $result.unwrap_err().downcast_ref::().unwrap() { @@ -4529,17 +4551,17 @@ mod tests { let src_history = src_history; - let mut src_history_buf = vec![0; StakeHistory::size_of()]; + let mut src_history_buf = vec![0; STAKE_HISTORY_ACCOUNT_SIZE]; bincode::serialize_into(&mut src_history_buf, &src_history).unwrap(); let transaction_accounts = vec![( sysvar::stake_history::id(), - create_account_shared_data_for_test(&src_history), + create_stake_history_account_for_test(&src_history), )]; with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); { - let mut got_history_buf = vec![0; StakeHistory::size_of()]; + let mut got_history_buf = vec![0; STAKE_HISTORY_ACCOUNT_SIZE]; let got_history_buf_va = 0x100000000; let history_id_va = 0x200000000; let history_id = StakeHistory::id().to_bytes(); @@ -4564,7 +4586,7 @@ mod tests { history_id_va, got_history_buf_va, 0, - StakeHistory::size_of() as u64, + STAKE_HISTORY_ACCOUNT_SIZE as u64, 0, ); assert_eq!(result.unwrap(), 0); @@ -4594,7 +4616,7 @@ mod tests { let src_hashes = src_hashes; let mut src_hashes_buf = vec![0; SlotHashes::size_of()]; - bincode::serialize_into(&mut src_hashes_buf, &src_hashes).unwrap(); + wincode::serialize_into(&mut src_hashes_buf, &src_hashes).unwrap(); let transaction_accounts = vec![( sysvar::slot_hashes::id(), @@ -4633,7 +4655,7 @@ mod tests { ); assert_eq!(result.unwrap(), 0); - let hashes_from_buf = bincode::deserialize::(&got_hashes_buf).unwrap(); + let hashes_from_buf = wincode::deserialize::(&got_hashes_buf).unwrap(); assert_eq!(hashes_from_buf, src_hashes); } } diff --git a/syscalls/src/mem_ops.rs b/syscalls/src/mem_ops.rs index 6beb79d3980..40461bfef0b 100644 --- a/syscalls/src/mem_ops.rs +++ b/syscalls/src/mem_ops.rs @@ -159,13 +159,62 @@ fn memmove( // Marked unsafe since it assumes that the slices are at least `n` bytes long. unsafe fn memcmp(s1: &[u8], s2: &[u8], n: usize) -> i32 { - for i in 0..n { + let (s1pre, s1mid, s1end) = unsafe { + // SAFETY: Caller is required to guarantee both slices are at least n-long. + s1.get_unchecked(..n).align_to::() + }; + let mut s2ptr = s2.as_ptr(); + for s1pre_byte in s1pre.iter().copied() { unsafe { - let a = *s1.get_unchecked(i); - let b = *s2.get_unchecked(i); - if a != b { - return (a as i32).saturating_sub(b as i32); + // SAFETY: we are guaranteed to stay in bounds of a slice `s2` by virtue of both slices + // containing at least `n` bytes (caller precondition.) + let s2pre_byte = *s2ptr; + if s1pre_byte != s2pre_byte { + return i32::from(s1pre_byte).wrapping_sub(s2pre_byte.into()); + } + s2ptr = s2ptr.add(1); + } + } + for s1mid_value in s1mid.iter().copied() { + let s2mid_value = unsafe { + // SAFETY: Caller is required to guarantee both slices are at least n-long. + // SAFETY: Pointer is guaranteed to be dereferenceable by virtue of being derived from + // `s2` slice. + s2ptr.cast::().read_unaligned().to_le() + }; + if s1mid_value != s2mid_value { + // It would seem that we could work with u128s directly here and leave it to LLVM to + // figure out how to split up the operations to u64s, but it seems to produce notably + // worse code than splitting the u64s out manually (even _when_ these splits result in + // the values being re-read from "memory"). + let (s1_word, s2_word) = if s1mid_value as u64 != s2mid_value as u64 { + let w1 = s1mid_value as u64; + let w2 = s2mid_value as u64; + (w1, w2) + } else { + let w1 = (s1mid_value >> 64) as u64; + let w2 = (s2mid_value >> 64) as u64; + (w1, w2) }; + let shift = (s1_word ^ s2_word).trailing_zeros() & !7; + let b1 = (s1_word >> shift) as u8; + let b2 = (s2_word >> shift) as u8; + return i32::from(b1).wrapping_sub(b2.into()); + } + unsafe { + // SAFETY: we are guaranteed to stay in bounds of a slice `s2` by virtue of both slices + // containing at least `n` bytes (caller precondition.) + s2ptr = s2ptr.add(std::mem::size_of::()); + } + } + for s1end_byte in s1end.iter().copied() { + unsafe { + // This is the same as the `pre` slice loop above. + let s2end_byte = *s2ptr; + if s1end_byte != s2end_byte { + return i32::from(s1end_byte).wrapping_sub(s2end_byte.into()); + } + s2ptr = s2ptr.add(1); } } 0 diff --git a/test-validator/Cargo.toml b/test-validator/Cargo.toml index e5ad302cc3d..cfcd28cf525 100644 --- a/test-validator/Cargo.toml +++ b/test-validator/Cargo.toml @@ -10,13 +10,21 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] +dev-context-only-utils = [ + "solana-core/dev-context-only-utils", + "solana-ledger/dev-context-only-utils", + "solana-runtime/dev-context-only-utils", +] [dependencies] agave-feature-set = { workspace = true } agave-snapshots = { workspace = true } +agave-votor-messages = { workspace = true } arc-swap.workspace = true base64 = { workspace = true } bincode = { workspace = true } @@ -25,6 +33,7 @@ log = { workspace = true } serde_json = { workspace = true } solana-account = { workspace = true } solana-accounts-db = { workspace = true } +solana-bls-signatures = { workspace = true } solana-cli-output = { workspace = true } solana-clock = { workspace = true } solana-cluster-type = { workspace = true } @@ -61,7 +70,13 @@ solana-streamer = { workspace = true } solana-syscalls = { workspace = true } solana-transaction = { workspace = true } solana-validator-exit = { workspace = true } +solana-vote-interface = { workspace = true } tokio = { workspace = true, features = ["full"] } [dev-dependencies] solana-sdk-ids = { workspace = true } +# See order-crates-for-publishing.py for using this unusual `path = "."` +solana-test-validator = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/test-validator/src/lib.rs b/test-validator/src/lib.rs index 3a6bc616520..708903533d8 100644 --- a/test-validator/src/lib.rs +++ b/test-validator/src/lib.rs @@ -1,9 +1,14 @@ +#![cfg(feature = "agave-unstable-api")] #![allow(clippy::arithmetic_side_effects)] use { - agave_feature_set::{FEATURE_NAMES, FeatureSet, alpenglow, raise_cpi_nesting_limit_to_8}, + agave_feature_set::{ + FEATURE_NAMES, FeatureSet, alpenglow, raise_cpi_nesting_limit_to_8, + validator_admission_ticket, + }, agave_snapshots::{ SnapshotInterval, paths::BANK_SNAPSHOTS_DIR, snapshot_config::SnapshotConfig, }, + agave_votor_messages::consensus_message::BLS_KEYPAIR_DERIVE_SEED, arc_swap::ArcSwap, base64::{Engine, prelude::BASE64_STANDARD}, crossbeam_channel::Receiver, @@ -14,6 +19,7 @@ use { accounts_index::{AccountsIndexConfig, ScanFilter}, utils::create_accounts_run_and_snapshot_dirs, }, + solana_bls_signatures::keypair::Keypair as BLSKeypair, solana_cli_output::CliAccount, solana_clock::{DEFAULT_MS_PER_SLOT, Slot}, solana_commitment_config::CommitmentConfig, @@ -57,7 +63,8 @@ use { client_error::Error as RpcClientError, request::MAX_MULTIPLE_ACCOUNTS, }, solana_runtime::{ - bank_forks::BankForks, genesis_utils::create_genesis_config_with_leader_ex, + bank_forks::BankForks, + genesis_utils::{activate_alpenglow_at_genesis, create_genesis_config_with_leader_ex}, runtime_config::RuntimeConfig, }, solana_sbpf::{elf::Executable, verifier::RequisiteVerifier}, @@ -67,6 +74,7 @@ use { solana_syscalls::create_program_runtime_environment, solana_transaction::{Transaction, TransactionError}, solana_validator_exit::Exit, + solana_vote_interface::state::BLS_PUBLIC_KEY_COMPRESSED_SIZE, std::{ collections::{HashMap, HashSet}, ffi::OsStr, @@ -107,9 +115,21 @@ pub struct TestValidatorNodeConfig { impl Default for TestValidatorNodeConfig { fn default() -> Self { let bind_ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST); - #[cfg(not(debug_assertions))] let port_range = solana_net_utils::VALIDATOR_PORT_RANGE; - #[cfg(debug_assertions)] + Self { + gossip_addr: SocketAddr::new(bind_ip_addr, port_range.0), + port_range, + bind_ip_addr, + } + } +} + +#[cfg(feature = "dev-context-only-utils")] +impl TestValidatorNodeConfig { + /// Defaults suitable for unit tests; a disjoint port range will be used to + /// avoid "port already in use" errors for tests running in parallel + pub fn default_for_tests() -> Self { + let bind_ip_addr = IpAddr::V4(Ipv4Addr::LOCALHOST); let port_range = solana_net_utils::sockets::localhost_port_range_for_tests(); Self { gossip_addr: SocketAddr::new(bind_ip_addr, port_range.0), @@ -188,6 +208,27 @@ impl Default for TestValidatorGenesis { } } +fn derive_bls_pubkey_from_authorized_voter_keypair( + authorized_voter_keypair: &Keypair, +) -> [u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE] { + BLSKeypair::derive_from_signer(authorized_voter_keypair, BLS_KEYPAIR_DERIVE_SEED) + .unwrap() + .public + .to_bytes_compressed() +} + +#[cfg(feature = "dev-context-only-utils")] +impl TestValidatorGenesis { + /// Defaults suitable for unit tests; a disjoint port range will be used to + /// avoid "port already in use" errors for tests running in parallel + pub fn default_for_tests() -> Self { + Self { + node_config: TestValidatorNodeConfig::default_for_tests(), + ..Self::default() + } + } +} + fn try_transform_program_data( address: &Pubkey, account: &mut AccountSharedData, @@ -232,6 +273,14 @@ impl TestValidatorGenesis { self.deactivate_feature_set.extend(deactivate_list); self } + + pub fn activate_alpenglow(&mut self) -> &mut Self { + self.deactivate_feature_set.remove(&alpenglow::id()); + self.deactivate_feature_set + .remove(&validator_admission_ticket::id()); + self + } + pub fn ledger_path>(&mut self, ledger_path: P) -> &mut Self { self.ledger_path = Some(ledger_path.into()); self @@ -512,6 +561,8 @@ impl TestValidatorGenesis { .is_none() { self.deactivate_feature_set.insert(*feature_id); + } else { + self.deactivate_feature_set.remove(feature_id); } }); } @@ -565,7 +616,7 @@ impl TestValidatorGenesis { for dir in dirs { let matched_files = match fs::read_dir(&dir) { Ok(dir) => dir, - Err(e) => return Err(format!("Cannot read directory {}: {}", &dir, e)), + Err(e) => return Err(format!("Cannot read directory {dir}: {e}")), } .flatten() .map(|entry| entry.path()) @@ -807,12 +858,13 @@ pub struct TestValidator { impl TestValidator { /// Create a configured genesis and start validator /// Sync only; calling from a tokio runtime will panic due to nested runtimes. + #[cfg(feature = "dev-context-only-utils")] pub fn start_with_config( mint_address: Pubkey, faucet_addr: Option, socket_addr_space: SocketAddrSpace, ) -> Self { - TestValidatorGenesis::default() + TestValidatorGenesis::default_for_tests() .rent(Rent { lamports_per_byte: 1, ..Rent::default() @@ -823,12 +875,13 @@ impl TestValidator { } /// Create a configured genesis and start validator (async version) + #[cfg(feature = "dev-context-only-utils")] pub async fn async_start_with_config( mint_keypair: &Keypair, faucet_addr: Option, socket_addr_space: SocketAddrSpace, ) -> Self { - TestValidatorGenesis::default() + TestValidatorGenesis::default_for_tests() .rent(Rent { lamports_per_byte: 1, ..Rent::default() @@ -866,6 +919,12 @@ impl TestValidator { warn!("Feature {feature:?} set for deactivation is not a known Feature public key",) } } + let is_alpenglow_active = feature_set.is_active(&alpenglow::id()); + if is_alpenglow_active && !feature_set.is_active(&validator_admission_ticket::id()) { + return Err( + "Alpenglow requires the validator_admission_ticket feature to be active".into(), + ); + } let runtime_features = feature_set.runtime_features(); let program_runtime_environment = create_program_runtime_environment( @@ -936,13 +995,17 @@ impl TestValidator { ); } + // Test validator genesis uses the vote account pubkey as the authorized voter, + // so the Alpenglow BLS pubkey is derived from the vote account keypair. let mut genesis_config = create_genesis_config_with_leader_ex( mint_lamports, &mint_address, &validator_identity.pubkey(), &validator_vote_account.pubkey(), &validator_stake_account.pubkey(), - None, + Some(derive_bls_pubkey_from_authorized_voter_keypair( + &validator_vote_account, + )), validator_stake_lamports, validator_identity_lamports, config.fee_rate_governor.clone(), @@ -951,6 +1014,9 @@ impl TestValidator { &feature_set, accounts.into_iter().collect(), ); + if is_alpenglow_active { + activate_alpenglow_at_genesis(&mut genesis_config); + } genesis_config.epoch_schedule = config .epoch_schedule .as_ref() @@ -1079,7 +1145,8 @@ impl TestValidator { .iter() .any(|x| x.pubkey() == vote_account_address) { - authorized_voter_keypairs.push(Arc::new(validator_vote_account)) + // Test validator genesis uses the vote account pubkey as the authorized voter. + authorized_voter_keypairs.push(Arc::new(validator_vote_account)); } } @@ -1362,24 +1429,124 @@ impl Drop for TestValidator { mod test { use {super::*, solana_feature_gate_interface::Feature}; + async fn assert_feature_accounts( + rpc_client: &nonblocking::rpc_client::RpcClient, + active_features: &[Pubkey], + inactive_features: &[Pubkey], + ) { + for chunk in active_features.chunks(100) { + let active_feature_accounts = rpc_client.get_multiple_accounts(chunk).await.unwrap(); + for feature_account in active_feature_accounts { + let account = feature_account.unwrap(); + let feature_state: Feature = bincode::deserialize(account.data()).unwrap(); + assert!(feature_state.activated_at.is_some()); + } + } + + if !inactive_features.is_empty() { + let inactive_feature_accounts = rpc_client + .get_multiple_accounts(inactive_features) + .await + .unwrap(); + for feature_account in inactive_feature_accounts { + assert!(feature_account.is_none()); + } + } + } + + async fn wait_for_alpenglow_enabled(test_validator: &TestValidator) { + for _ in 0..240 { + let migration_status = test_validator + .bank_forks() + .read() + .unwrap() + .migration_status(); + if migration_status.is_alpenglow_enabled() { + return; + } + sleep(Duration::from_millis(250)).await; + } + let bank_forks = test_validator.bank_forks(); + let bank_forks = bank_forks.read().unwrap(); + let root_bank = bank_forks.root_bank(); + let migration_status = bank_forks.migration_status(); + panic!( + "Timed out waiting for Alpenglow migration: migration_status={migration_status:?}, \ + root_slot={}, working_slot={}, feature_activation_slot={:?}, \ + eligible_genesis_block={:?}, genesis_certificate={:?}", + root_bank.slot(), + bank_forks.working_bank().slot(), + root_bank.feature_set.activated_slot(&alpenglow::id()), + migration_status.eligible_genesis_block(), + migration_status.genesis_certificate(), + ); + } + #[test] fn get_health() { - let (test_validator, _payer) = TestValidatorGenesis::default().start(); + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests().start(); let rpc_client = test_validator.get_rpc_client(); rpc_client.get_health().expect("health"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn nonblocking_get_health() { - let (test_validator, _payer) = TestValidatorGenesis::default().start_async().await; + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests() + .start_async() + .await; let rpc_client = test_validator.get_async_rpc_client(); rpc_client.get_health().await.expect("health"); } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_all_features_active_except_alpenglow_by_default() { + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests() + .start_async() + .await; + let rpc_client = test_validator.get_async_rpc_client(); + + let active_features = FEATURE_NAMES + .keys() + .copied() + .filter(|feature| *feature != alpenglow::id()) + .collect::>(); + assert_feature_accounts(&rpc_client, &active_features, &[alpenglow::id()]).await; + assert!( + test_validator + .bank_forks() + .read() + .unwrap() + .migration_status() + .is_pre_feature_activation() + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_all_features_active_with_alpenglow_at_genesis() { + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests() + .activate_alpenglow() + .start_async() + .await; + let rpc_client = test_validator.get_async_rpc_client(); + + let active_features = FEATURE_NAMES.keys().copied().collect::>(); + assert_feature_accounts(&rpc_client, &active_features, &[]).await; + assert!( + test_validator + .bank_forks() + .read() + .unwrap() + .root_bank() + .get_alpenglow_genesis_certificate() + .is_some() + ); + wait_for_alpenglow_enabled(&test_validator).await; + } + #[test] fn test_upgradeable_program_deploayment() { let program_id = Pubkey::new_unique(); - let (test_validator, payer) = TestValidatorGenesis::default() + let (test_validator, payer) = TestValidatorGenesis::default_for_tests() .add_program("../programs/bpf-loader-tests/noop", program_id) .start(); let rpc_client = test_validator.get_rpc_client(); @@ -1406,7 +1573,7 @@ mod test { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_nonblocking_upgradeable_program_deploayment() { let program_id = Pubkey::new_unique(); - let (test_validator, payer) = TestValidatorGenesis::default() + let (test_validator, payer) = TestValidatorGenesis::default_for_tests() .add_program("../programs/bpf-loader-tests/noop", program_id) .start_async() .await; @@ -1436,7 +1603,7 @@ mod test { #[should_panic] async fn document_tokio_panic() { // `start()` blows up when run within tokio - let (_test_validator, _payer) = TestValidatorGenesis::default().start(); + let (_test_validator, _payer) = TestValidatorGenesis::default_for_tests().start(); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -1460,7 +1627,7 @@ mod test { // Convert to `Vec` so we can get a slice. let control: Vec = control.into_iter().collect(); - let (test_validator, _payer) = TestValidatorGenesis::default() + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests() .deactivate_features(&deactivate_features) .start_async() .await; @@ -1495,7 +1662,7 @@ mod test { let owner = Pubkey::new_unique(); let account = || AccountSharedData::new(100_000, 0, &owner); - let (test_validator, _payer) = TestValidatorGenesis::default() + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests() .deactivate_features(&[with_deactivate_flag]) // Just deactivate one feature. .add_accounts([ (with_deactivate_flag, account()), // But add both accounts. @@ -1527,7 +1694,9 @@ mod test { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_core_bpf_programs() { - let (test_validator, _payer) = TestValidatorGenesis::default().start_async().await; + let (test_validator, _payer) = TestValidatorGenesis::default_for_tests() + .start_async() + .await; let rpc_client = test_validator.get_async_rpc_client(); @@ -1565,7 +1734,7 @@ mod test { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_wait_for_program_with_unfunded_payer() { let program_id = Pubkey::new_unique(); - let (test_validator, _mint_keypair) = TestValidatorGenesis::default() + let (test_validator, _mint_keypair) = TestValidatorGenesis::default_for_tests() .add_program("../programs/bpf-loader-tests/noop", program_id) .start_async() .await; diff --git a/tls-utils/Cargo.toml b/tls-utils/Cargo.toml index 559c5f9295c..6754782b669 100644 --- a/tls-utils/Cargo.toml +++ b/tls-utils/Cargo.toml @@ -7,12 +7,18 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] [dependencies] +quinn = { workspace = true } rustls = { workspace = true, features = ["ring"] } solana-keypair = { workspace = true } solana-pubkey = { workspace = true } diff --git a/tls-utils/src/tls_certificates.rs b/tls-utils/src/tls_certificates.rs index 4963003e406..3f8544f86a7 100644 --- a/tls-utils/src/tls_certificates.rs +++ b/tls-utils/src/tls_certificates.rs @@ -127,6 +127,22 @@ pub fn get_pubkey_from_tls_certificate( } } +/// Recover the peer's Solana pubkey from a `quinn::Connection`. Accepts +/// only a self-signed cert chain of length 1 (matches the cert shape +/// produced by [`new_dummy_x509_certificate`]). +/// +/// Single source of truth for anything doing QUIC and using +/// Solana validator identities for auth. +pub fn get_remote_pubkey(connection: &quinn::Connection) -> Option { + connection + .peer_identity()? + .downcast::>() + .ok() + .filter(|certs| certs.len() == 1)? + .first() + .and_then(get_pubkey_from_tls_certificate) +} + /// Translate a SocketAddr into a valid SNI for the purposes of QUIC connection /// /// We do not actually check if the server holds a cert for this server_name diff --git a/tokens/Cargo.toml b/tokens/Cargo.toml index 1cb78b22e6c..0d416d43651 100644 --- a/tokens/Cargo.toml +++ b/tokens/Cargo.toml @@ -9,6 +9,14 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +# `all-features` can't be used: remote-wallet-hidraw and remote-wallet-libusb +# forward to hidapi's mutually-exclusive linux backends. Document the default +# (hidraw) backend plus the unstable API. +features = ["agave-unstable-api"] +rustdoc-args = ["--cfg=docsrs"] + [features] default = ["remote-wallet-hidraw"] agave-unstable-api = [] @@ -61,5 +69,8 @@ thiserror = { workspace = true } agave-logger = { path = "../logger", features = ["agave-unstable-api"] } assert_matches = { workspace = true } bincode = { workspace = true } -solana-test-validator = { path = "../test-validator" } +solana-test-validator = { path = "../test-validator", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-transaction-error = { workspace = true } + +[lints] +workspace = true diff --git a/tokens/src/commands.rs b/tokens/src/commands.rs index 9691ae9ad0c..b49626b73f1 100644 --- a/tokens/src/commands.rs +++ b/tokens/src/commands.rs @@ -684,10 +684,10 @@ pub fn process_allocations( let opt_confirmations = finalize_transactions(client, &mut db, args.dry_run, exit)?; - if !args.dry_run { - if let Some(output_path) = &args.output_path { - db::write_transaction_log(&db, &output_path)?; - } + if !args.dry_run + && let Some(output_path) = &args.output_path + { + db::write_transaction_log(&db, &output_path)?; } Ok(opt_confirmations) diff --git a/tokens/src/stake.rs b/tokens/src/stake.rs index c98a70c6a22..e20f53dae41 100644 --- a/tokens/src/stake.rs +++ b/tokens/src/stake.rs @@ -5,11 +5,11 @@ use { }; pub fn update_stake_args(client: &RpcClient, args: &mut Option) -> Result<(), Error> { - if let Some(stake_args) = args { - if let Some(sender_args) = &mut stake_args.sender_stake_args { - let rent = client.get_minimum_balance_for_rent_exemption(StakeStateV2::size_of())?; - sender_args.rent_exempt_reserve = Some(rent); - } + if let Some(stake_args) = args + && let Some(sender_args) = &mut stake_args.sender_stake_args + { + let rent = client.get_minimum_balance_for_rent_exemption(StakeStateV2::size_of())?; + sender_args.rent_exempt_reserve = Some(rent); } Ok(()) } diff --git a/tps-client/Cargo.toml b/tps-client/Cargo.toml deleted file mode 100644 index 81cd812360f..00000000000 --- a/tps-client/Cargo.toml +++ /dev/null @@ -1,49 +0,0 @@ -[package] -name = "solana-tps-client" -version = { workspace = true } -authors = { workspace = true } -description = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = { workspace = true } - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] -all-features = true -rustdoc-args = ["--cfg=docsrs"] - -[features] -agave-unstable-api = [] -bank-client = ["dep:solana-client-traits", "dep:solana-runtime"] -dev-context-only-utils = [] - -[dependencies] -log = { workspace = true } -solana-account = { workspace = true } -solana-client = { workspace = true } -solana-client-traits = { workspace = true, optional = true } -solana-clock = { workspace = true } -solana-commitment-config = { workspace = true } -solana-connection-cache = { workspace = true } -solana-epoch-info = { workspace = true } -solana-hash = { workspace = true } -solana-keypair = { workspace = true } -solana-message = { workspace = true } -solana-net-utils = { workspace = true } -solana-pubkey = { workspace = true } -solana-rpc-client = { workspace = true } -solana-rpc-client-api = { workspace = true } -solana-runtime = { workspace = true, optional = true } -solana-signature = { workspace = true } -solana-signer = { workspace = true } -solana-streamer = { workspace = true } -solana-tpu-client = { workspace = true } -solana-transaction = { workspace = true } -solana-transaction-error = { workspace = true } -solana-transaction-status = { workspace = true } -thiserror = { workspace = true } - -[dev-dependencies] -solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } -tempfile = { workspace = true } diff --git a/tps-client/src/bank_client.rs b/tps-client/src/bank_client.rs deleted file mode 100644 index f94a45820e8..00000000000 --- a/tps-client/src/bank_client.rs +++ /dev/null @@ -1,155 +0,0 @@ -use { - crate::{TpsClient, TpsClientError, TpsClientResult}, - solana_account::Account, - solana_client_traits::{AsyncClient, SyncClient}, - solana_commitment_config::CommitmentConfig, - solana_epoch_info::EpochInfo, - solana_hash::Hash, - solana_message::Message, - solana_pubkey::Pubkey, - solana_rpc_client_api::config::RpcBlockConfig, - solana_runtime::bank_client::BankClient, - solana_signature::Signature, - solana_transaction::versioned::VersionedTransaction, - solana_transaction_error::TransactionResult as Result, - solana_transaction_status::UiConfirmedBlock, -}; - -impl TpsClient for BankClient { - fn send_transaction(&self, transaction: VersionedTransaction) -> TpsClientResult { - AsyncClient::async_send_versioned_transaction(self, transaction).map_err(|err| err.into()) - } - fn send_batch(&self, transactions: Vec) -> TpsClientResult<()> { - AsyncClient::async_send_versioned_transaction_batch(self, transactions) - .map_err(|err| err.into()) - } - fn get_latest_blockhash(&self) -> TpsClientResult { - SyncClient::get_latest_blockhash(self).map_err(|err| err.into()) - } - - fn get_latest_blockhash_with_commitment( - &self, - commitment_config: CommitmentConfig, - ) -> TpsClientResult<(Hash, u64)> { - SyncClient::get_latest_blockhash_with_commitment(self, commitment_config) - .map_err(|err| err.into()) - } - - fn get_transaction_count(&self) -> TpsClientResult { - SyncClient::get_transaction_count(self).map_err(|err| err.into()) - } - - fn get_signature_status(&self, signature: &Signature) -> TpsClientResult>> { - SyncClient::get_signature_status(self, signature).map_err(|err| err.into()) - } - - fn get_transaction_count_with_commitment( - &self, - commitment_config: CommitmentConfig, - ) -> TpsClientResult { - SyncClient::get_transaction_count_with_commitment(self, commitment_config) - .map_err(|err| err.into()) - } - - fn get_epoch_info(&self) -> TpsClientResult { - SyncClient::get_epoch_info(self).map_err(|err| err.into()) - } - - fn get_balance(&self, pubkey: &Pubkey) -> TpsClientResult { - SyncClient::get_balance(self, pubkey).map_err(|err| err.into()) - } - - fn get_balance_with_commitment( - &self, - pubkey: &Pubkey, - commitment_config: CommitmentConfig, - ) -> TpsClientResult { - SyncClient::get_balance_with_commitment(self, pubkey, commitment_config) - .map_err(|err| err.into()) - } - - fn get_fee_for_message(&self, message: &Message) -> TpsClientResult { - SyncClient::get_fee_for_message(self, message).map_err(|err| err.into()) - } - - fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> TpsClientResult { - SyncClient::get_minimum_balance_for_rent_exemption(self, data_len).map_err(|err| err.into()) - } - - fn addr(&self) -> String { - "Local BankClient".to_string() - } - - fn request_airdrop_with_blockhash( - &self, - _pubkey: &Pubkey, - _lamports: u64, - _recent_blockhash: &Hash, - ) -> TpsClientResult { - // BankClient doesn't support airdrops - Err(TpsClientError::AirdropFailure) - } - - fn get_account(&self, pubkey: &Pubkey) -> TpsClientResult { - SyncClient::get_account(self, pubkey) - .map_err(|err| err.into()) - .and_then(|account| { - account.ok_or_else(|| { - TpsClientError::Custom(format!("AccountNotFound: pubkey={pubkey}")) - }) - }) - } - - fn get_account_with_commitment( - &self, - pubkey: &Pubkey, - commitment_config: CommitmentConfig, - ) -> TpsClientResult { - SyncClient::get_account_with_commitment(self, pubkey, commitment_config) - .map_err(|err| err.into()) - .and_then(|account| { - account.ok_or_else(|| { - TpsClientError::Custom(format!("AccountNotFound: pubkey={pubkey}")) - }) - }) - } - - fn get_multiple_accounts(&self, pubkeys: &[Pubkey]) -> TpsClientResult>> { - let mut accounts = Vec::with_capacity(pubkeys.len()); - for pubkey in pubkeys { - let account = SyncClient::get_account(self, pubkey) - .map_err(|err| TpsClientError::Custom(format!("{err:?}")))?; - if account.is_none() { - return Err(TpsClientError::Custom(format!( - "AccountNotFound: pubkey={pubkey}" - ))); - } - accounts.push(account); - } - Ok(accounts) - } - - fn get_slot_with_commitment( - &self, - commitment_config: CommitmentConfig, - ) -> TpsClientResult { - SyncClient::get_slot_with_commitment(self, commitment_config).map_err(|err| err.into()) - } - - fn get_blocks_with_commitment( - &self, - _start_slot: u64, - _end_slot: Option, - _commitment_config: CommitmentConfig, - ) -> TpsClientResult> { - unimplemented!("BankClient doesn't support get_blocks"); - } - - fn get_block_with_config( - &self, - _slot: u64, - _rpc_block_config: RpcBlockConfig, - ) -> TpsClientResult { - unimplemented!("BankClient doesn't support get_block_with_config"); - } -} diff --git a/tps-client/src/lib.rs b/tps-client/src/lib.rs deleted file mode 100644 index bc7885abafe..00000000000 --- a/tps-client/src/lib.rs +++ /dev/null @@ -1,148 +0,0 @@ -#![cfg(feature = "agave-unstable-api")] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] -use { - log::debug, - solana_account::Account, - solana_clock::DEFAULT_MS_PER_SLOT, - solana_commitment_config::CommitmentConfig, - solana_epoch_info::EpochInfo, - solana_hash::Hash, - solana_message::Message, - solana_pubkey::Pubkey, - solana_rpc_client_api::{client_error::Error as ClientError, config::RpcBlockConfig}, - solana_signature::Signature, - solana_tpu_client::tpu_client::TpuSenderError, - solana_transaction::versioned::VersionedTransaction, - solana_transaction_error::{TransactionResult as Result, TransportError}, - solana_transaction_status::UiConfirmedBlock, - std::{ - thread::sleep, - time::{Duration, Instant}, - }, - thiserror::Error, -}; - -#[derive(Error, Debug)] -pub enum TpsClientError { - #[error("Airdrop failure")] - AirdropFailure, - #[error("IO error: {0:?}")] - IoError(#[from] std::io::Error), - #[error("Client error: {0:?}")] - ClientError(#[from] ClientError), - #[error("TpuClient error: {0:?}")] - TpuSenderError(#[from] TpuSenderError), - #[error("Transport error: {0:?}")] - TransportError(#[from] TransportError), - #[error("Custom error: {0}")] - Custom(String), -} - -pub type TpsClientResult = std::result::Result; - -pub trait TpsClient { - /// Send a signed transaction without confirmation - fn send_transaction(&self, transaction: VersionedTransaction) -> TpsClientResult; - - /// Send a batch of signed transactions without confirmation. - fn send_batch(&self, transactions: Vec) -> TpsClientResult<()>; - - /// Get latest blockhash - fn get_latest_blockhash(&self) -> TpsClientResult; - - /// Get latest blockhash and its last valid block height, using explicit commitment - fn get_latest_blockhash_with_commitment( - &self, - commitment_config: CommitmentConfig, - ) -> TpsClientResult<(Hash, u64)>; - - fn get_new_latest_blockhash(&self, blockhash: &Hash) -> TpsClientResult { - let start = Instant::now(); - while start.elapsed().as_secs() < 5 { - if let Ok(new_blockhash) = self.get_latest_blockhash() { - if new_blockhash != *blockhash { - return Ok(new_blockhash); - } - } - debug!("Got same blockhash ({blockhash:?}), will retry..."); - - // Retry ~twice during a slot - sleep(Duration::from_millis(DEFAULT_MS_PER_SLOT / 2)); - } - Err(TpsClientError::Custom("Timeout".to_string())) - } - - fn get_signature_status(&self, signature: &Signature) -> TpsClientResult>>; - - /// Get transaction count - fn get_transaction_count(&self) -> TpsClientResult; - - /// Get transaction count, using explicit commitment - fn get_transaction_count_with_commitment( - &self, - commitment_config: CommitmentConfig, - ) -> TpsClientResult; - - /// Get epoch info - fn get_epoch_info(&self) -> TpsClientResult; - - /// Get account balance - fn get_balance(&self, pubkey: &Pubkey) -> TpsClientResult; - - /// Get account balance, using explicit commitment - fn get_balance_with_commitment( - &self, - pubkey: &Pubkey, - commitment_config: CommitmentConfig, - ) -> TpsClientResult; - - /// Calculate the fee for a `Message` - fn get_fee_for_message(&self, message: &Message) -> TpsClientResult; - - /// Get the rent-exempt minimum for an account - fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> TpsClientResult; - - /// Return the address of client - fn addr(&self) -> String; - - /// Request, submit, and confirm an airdrop transaction - fn request_airdrop_with_blockhash( - &self, - pubkey: &Pubkey, - lamports: u64, - recent_blockhash: &Hash, - ) -> TpsClientResult; - - /// Returns all information associated with the account of the provided pubkey - fn get_account(&self, pubkey: &Pubkey) -> TpsClientResult; - - /// Returns all information associated with the account of the provided pubkey, using explicit commitment - fn get_account_with_commitment( - &self, - pubkey: &Pubkey, - commitment_config: CommitmentConfig, - ) -> TpsClientResult; - - fn get_multiple_accounts(&self, pubkeys: &[Pubkey]) -> TpsClientResult>>; - - fn get_slot_with_commitment(&self, commitment_config: CommitmentConfig) - -> TpsClientResult; - - fn get_blocks_with_commitment( - &self, - start_slot: u64, - end_slot: Option, - commitment_config: CommitmentConfig, - ) -> TpsClientResult>; - - fn get_block_with_config( - &self, - slot: u64, - rpc_block_config: RpcBlockConfig, - ) -> TpsClientResult; -} - -#[cfg(feature = "bank-client")] -mod bank_client; -mod rpc_client; -mod tpu_client; diff --git a/tps-client/src/rpc_client.rs b/tps-client/src/rpc_client.rs deleted file mode 100644 index 6d2e868fe8e..00000000000 --- a/tps-client/src/rpc_client.rs +++ /dev/null @@ -1,156 +0,0 @@ -use { - crate::{TpsClient, TpsClientError, TpsClientResult}, - solana_account::Account, - solana_client::rpc_config::RpcSendTransactionConfig, - solana_commitment_config::CommitmentConfig, - solana_epoch_info::EpochInfo, - solana_hash::Hash, - solana_message::Message, - solana_pubkey::Pubkey, - solana_rpc_client::rpc_client::RpcClient, - solana_rpc_client_api::config::RpcBlockConfig, - solana_signature::Signature, - solana_transaction::versioned::VersionedTransaction, - solana_transaction_error::TransactionResult as Result, - solana_transaction_status::UiConfirmedBlock, -}; - -impl TpsClient for RpcClient { - fn send_transaction(&self, transaction: VersionedTransaction) -> TpsClientResult { - RpcClient::send_transaction_with_config( - self, - &transaction, - RpcSendTransactionConfig { - skip_preflight: true, - ..Default::default() - }, - ) - .map_err(|err| err.into()) - } - - fn send_batch(&self, transactions: Vec) -> TpsClientResult<()> { - for transaction in transactions { - TpsClient::send_transaction(self, transaction)?; - } - Ok(()) - } - fn get_latest_blockhash(&self) -> TpsClientResult { - RpcClient::get_latest_blockhash(self).map_err(|err| err.into()) - } - - fn get_latest_blockhash_with_commitment( - &self, - commitment_config: CommitmentConfig, - ) -> TpsClientResult<(Hash, u64)> { - RpcClient::get_latest_blockhash_with_commitment(self, commitment_config) - .map_err(|err| err.into()) - } - - fn get_new_latest_blockhash(&self, blockhash: &Hash) -> TpsClientResult { - RpcClient::get_new_latest_blockhash(self, blockhash).map_err(|err| err.into()) - } - - fn get_signature_status(&self, signature: &Signature) -> TpsClientResult>> { - RpcClient::get_signature_status(self, signature).map_err(|err| err.into()) - } - - fn get_transaction_count(&self) -> TpsClientResult { - RpcClient::get_transaction_count(self).map_err(|err| err.into()) - } - - fn get_transaction_count_with_commitment( - &self, - commitment_config: CommitmentConfig, - ) -> TpsClientResult { - RpcClient::get_transaction_count_with_commitment(self, commitment_config) - .map_err(|err| err.into()) - } - - fn get_epoch_info(&self) -> TpsClientResult { - RpcClient::get_epoch_info(self).map_err(|err| err.into()) - } - - fn get_balance(&self, pubkey: &Pubkey) -> TpsClientResult { - RpcClient::get_balance(self, pubkey).map_err(|err| err.into()) - } - - fn get_balance_with_commitment( - &self, - pubkey: &Pubkey, - commitment_config: CommitmentConfig, - ) -> TpsClientResult { - RpcClient::get_balance_with_commitment(self, pubkey, commitment_config) - .map(|res| res.value) - .map_err(|err| err.into()) - } - - fn get_fee_for_message(&self, message: &Message) -> TpsClientResult { - RpcClient::get_fee_for_message(self, message).map_err(|err| err.into()) - } - - fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> TpsClientResult { - RpcClient::get_minimum_balance_for_rent_exemption(self, data_len).map_err(|err| err.into()) - } - - fn addr(&self) -> String { - self.url() - } - - fn request_airdrop_with_blockhash( - &self, - pubkey: &Pubkey, - lamports: u64, - recent_blockhash: &Hash, - ) -> TpsClientResult { - RpcClient::request_airdrop_with_blockhash(self, pubkey, lamports, recent_blockhash) - .map_err(|err| err.into()) - } - - fn get_account(&self, pubkey: &Pubkey) -> TpsClientResult { - RpcClient::get_account(self, pubkey).map_err(|err| err.into()) - } - - fn get_account_with_commitment( - &self, - pubkey: &Pubkey, - commitment_config: CommitmentConfig, - ) -> TpsClientResult { - RpcClient::get_account_with_commitment(self, pubkey, commitment_config) - .map(|res| res.value) - .map_err(|err| err.into()) - .and_then(|account| { - account.ok_or_else(|| { - TpsClientError::Custom(format!("AccountNotFound: pubkey={pubkey}")) - }) - }) - } - - fn get_multiple_accounts(&self, pubkeys: &[Pubkey]) -> TpsClientResult>> { - RpcClient::get_multiple_accounts(self, pubkeys).map_err(|err| err.into()) - } - - fn get_slot_with_commitment( - &self, - commitment_config: CommitmentConfig, - ) -> TpsClientResult { - RpcClient::get_slot_with_commitment(self, commitment_config).map_err(|err| err.into()) - } - - fn get_blocks_with_commitment( - &self, - start_slot: u64, - end_slot: Option, - commitment_config: CommitmentConfig, - ) -> TpsClientResult> { - RpcClient::get_blocks_with_commitment(self, start_slot, end_slot, commitment_config) - .map_err(|err| err.into()) - } - - fn get_block_with_config( - &self, - slot: u64, - rpc_block_config: RpcBlockConfig, - ) -> TpsClientResult { - RpcClient::get_block_with_config(self, slot, rpc_block_config).map_err(|err| err.into()) - } -} diff --git a/tps-client/src/tpu_client.rs b/tps-client/src/tpu_client.rs deleted file mode 100644 index f2661651896..00000000000 --- a/tps-client/src/tpu_client.rs +++ /dev/null @@ -1,182 +0,0 @@ -use { - crate::{TpsClient, TpsClientError, TpsClientResult}, - solana_account::Account, - solana_commitment_config::CommitmentConfig, - solana_connection_cache::connection_cache::{ - ConnectionManager, ConnectionPool, NewConnectionConfig, - }, - solana_epoch_info::EpochInfo, - solana_hash::Hash, - solana_message::Message, - solana_pubkey::Pubkey, - solana_rpc_client_api::config::RpcBlockConfig, - solana_signature::Signature, - solana_tpu_client::tpu_client::TpuClient, - solana_transaction::versioned::VersionedTransaction, - solana_transaction_error::TransactionResult as Result, - solana_transaction_status::UiConfirmedBlock, -}; - -impl TpsClient for TpuClient -where - P: ConnectionPool, - M: ConnectionManager, - C: NewConnectionConfig, -{ - fn send_transaction(&self, transaction: VersionedTransaction) -> TpsClientResult { - let signature = transaction.signatures[0]; - self.try_send_transaction(&transaction)?; - Ok(signature) - } - fn send_batch(&self, transactions: Vec) -> TpsClientResult<()> { - self.try_send_transaction_batch(&transactions)?; - Ok(()) - } - fn get_latest_blockhash(&self) -> TpsClientResult { - self.rpc_client() - .get_latest_blockhash() - .map_err(|err| err.into()) - } - - fn get_latest_blockhash_with_commitment( - &self, - commitment_config: CommitmentConfig, - ) -> TpsClientResult<(Hash, u64)> { - self.rpc_client() - .get_latest_blockhash_with_commitment(commitment_config) - .map_err(|err| err.into()) - } - - fn get_new_latest_blockhash(&self, blockhash: &Hash) -> TpsClientResult { - self.rpc_client() - .get_new_latest_blockhash(blockhash) - .map_err(|err| err.into()) - } - - fn get_signature_status(&self, signature: &Signature) -> TpsClientResult>> { - self.rpc_client() - .get_signature_status(signature) - .map_err(|err| err.into()) - } - - fn get_transaction_count(&self) -> TpsClientResult { - self.rpc_client() - .get_transaction_count() - .map_err(|err| err.into()) - } - - fn get_transaction_count_with_commitment( - &self, - commitment_config: CommitmentConfig, - ) -> TpsClientResult { - self.rpc_client() - .get_transaction_count_with_commitment(commitment_config) - .map_err(|err| err.into()) - } - - fn get_epoch_info(&self) -> TpsClientResult { - self.rpc_client().get_epoch_info().map_err(|err| err.into()) - } - - fn get_balance(&self, pubkey: &Pubkey) -> TpsClientResult { - self.rpc_client() - .get_balance(pubkey) - .map_err(|err| err.into()) - } - - fn get_balance_with_commitment( - &self, - pubkey: &Pubkey, - commitment_config: CommitmentConfig, - ) -> TpsClientResult { - self.rpc_client() - .get_balance_with_commitment(pubkey, commitment_config) - .map(|res| res.value) - .map_err(|err| err.into()) - } - - fn get_fee_for_message(&self, message: &Message) -> TpsClientResult { - self.rpc_client() - .get_fee_for_message(message) - .map_err(|err| err.into()) - } - - fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> TpsClientResult { - self.rpc_client() - .get_minimum_balance_for_rent_exemption(data_len) - .map_err(|err| err.into()) - } - - fn addr(&self) -> String { - self.rpc_client().url() - } - - fn request_airdrop_with_blockhash( - &self, - pubkey: &Pubkey, - lamports: u64, - recent_blockhash: &Hash, - ) -> TpsClientResult { - self.rpc_client() - .request_airdrop_with_blockhash(pubkey, lamports, recent_blockhash) - .map_err(|err| err.into()) - } - - fn get_account(&self, pubkey: &Pubkey) -> TpsClientResult { - self.rpc_client() - .get_account(pubkey) - .map_err(|err| err.into()) - } - - fn get_account_with_commitment( - &self, - pubkey: &Pubkey, - commitment_config: CommitmentConfig, - ) -> TpsClientResult { - self.rpc_client() - .get_account_with_commitment(pubkey, commitment_config) - .map(|res| res.value) - .map_err(|err| err.into()) - .and_then(|account| { - account.ok_or_else(|| { - TpsClientError::Custom(format!("AccountNotFound: pubkey={pubkey}")) - }) - }) - } - - fn get_multiple_accounts(&self, pubkeys: &[Pubkey]) -> TpsClientResult>> { - self.rpc_client() - .get_multiple_accounts(pubkeys) - .map_err(|err| err.into()) - } - - fn get_slot_with_commitment( - &self, - commitment_config: CommitmentConfig, - ) -> TpsClientResult { - self.rpc_client() - .get_slot_with_commitment(commitment_config) - .map_err(|err| err.into()) - } - - fn get_blocks_with_commitment( - &self, - start_slot: u64, - end_slot: Option, - commitment_config: CommitmentConfig, - ) -> TpsClientResult> { - self.rpc_client() - .get_blocks_with_commitment(start_slot, end_slot, commitment_config) - .map_err(|err| err.into()) - } - - fn get_block_with_config( - &self, - slot: u64, - rpc_block_config: RpcBlockConfig, - ) -> TpsClientResult { - self.rpc_client() - .get_block_with_config(slot, rpc_block_config) - .map_err(|err| err.into()) - } -} diff --git a/tpu-client-next/Cargo.toml b/tpu-client-next/Cargo.toml index a931995aabd..d0ce3d31249 100644 --- a/tpu-client-next/Cargo.toml +++ b/tpu-client-next/Cargo.toml @@ -10,6 +10,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -29,6 +31,7 @@ rustls = { workspace = true } solana-clock = { workspace = true } solana-commitment-config = { workspace = true } solana-keypair = { workspace = true } +solana-leader-schedule = { workspace = true } solana-measure = { workspace = true } solana-metrics = { workspace = true, optional = true } solana-packet = { workspace = true } @@ -56,3 +59,6 @@ solana-pubkey = { workspace = true } solana-signer = { workspace = true } solana-streamer = { path = "../streamer", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-tpu-client-next = { path = ".", features = ["agave-unstable-api", "websocket-node-address-service", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/tpu-client-next/src/connection_worker.rs b/tpu-client-next/src/connection_worker.rs index 17f516f7e2e..20c7748e0f3 100644 --- a/tpu-client-next/src/connection_worker.rs +++ b/tpu-client-next/src/connection_worker.rs @@ -11,7 +11,8 @@ use { transaction_batch::TransactionBatch, }, quinn::{ConnectError, Connection, ConnectionError, Endpoint}, - solana_clock::{DEFAULT_MS_PER_SLOT, MAX_PROCESSING_AGE, NUM_CONSECUTIVE_LEADER_SLOTS}, + solana_clock::{DEFAULT_MS_PER_SLOT, MAX_PROCESSING_AGE}, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_measure::measure::Measure, solana_time_utils::timestamp, solana_tls_utils::socket_addr_to_quic_server_name, @@ -34,7 +35,7 @@ pub(crate) const DEFAULT_MAX_CONNECTION_HANDSHAKE_TIMEOUT: Duration = Duration:: /// Interval between retry attempts for creating a new connection. This value is /// a best-effort estimate, based on current network conditions. const RETRY_SLEEP_INTERVAL: Duration = - Duration::from_millis(NUM_CONSECUTIVE_LEADER_SLOTS * DEFAULT_MS_PER_SLOT); + Duration::from_millis(NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64 * DEFAULT_MS_PER_SLOT); /// Maximum age (in milliseconds) of a blockhash, beyond which transaction /// batches are dropped. diff --git a/tpu-client-next/src/node_address_service/leader_tpu_cache_service.rs b/tpu-client-next/src/node_address_service/leader_tpu_cache_service.rs index d399c4c733e..0795c7f2ccb 100644 --- a/tpu-client-next/src/node_address_service/leader_tpu_cache_service.rs +++ b/tpu-client-next/src/node_address_service/leader_tpu_cache_service.rs @@ -9,8 +9,9 @@ use { node_address_service::SlotReceiver, }, async_trait::async_trait, - solana_clock::{NUM_CONSECUTIVE_LEADER_SLOTS, Slot}, + solana_clock::Slot, solana_commitment_config::CommitmentConfig, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_pubkey::Pubkey, solana_rpc_client::nonblocking::rpc_client::RpcClient, solana_rpc_client_api::{client_error::Error as ClientError, response::RpcContactInfo}, @@ -292,13 +293,14 @@ fn leader_sockets( slot_leaders: &SlotLeaders, leader_tpu_map: &LeaderTpuMap, ) -> Vec { - let fanout_slots = (lookahead_leaders as u64).saturating_mul(NUM_CONSECUTIVE_LEADER_SLOTS); - let mut leader_sockets = Vec::with_capacity(lookahead_leaders as usize); + let lookahead_leaders = lookahead_leaders as usize; + let fanout_slots = lookahead_leaders.saturating_mul(NUM_CONSECUTIVE_LEADER_SLOTS.get()) as u64; + let mut leader_sockets = Vec::with_capacity(lookahead_leaders); // `slot_leaders.first_slot` might have been advanced since caller last read it. Take the // greater of the two values to ensure we are reading from the latest leader schedule. let current_slot = std::cmp::max(first_slot, slot_leaders.first_slot); for leader_slot in - (current_slot..current_slot + fanout_slots).step_by(NUM_CONSECUTIVE_LEADER_SLOTS as usize) + (current_slot..current_slot + fanout_slots).step_by(NUM_CONSECUTIVE_LEADER_SLOTS.get()) { if let Some(leader) = slot_leaders.slot_leader(leader_slot) { if let Some(tpu_socket) = leader_tpu_map.get(leader) { @@ -343,16 +345,16 @@ async fn initialize_state( .ok(); } - if let Some(epoch_info) = &epoch_info { - if slot_leaders.is_none() { - slot_leaders = SlotLeaders::new( - cluster_info, - slot_receiver.slot(), - epoch_info.slots_in_epoch, - ) - .await - .ok(); - } + if let Some(epoch_info) = &epoch_info + && slot_leaders.is_none() + { + slot_leaders = SlotLeaders::new( + cluster_info, + slot_receiver.slot(), + epoch_info.slots_in_epoch, + ) + .await + .ok(); } if leader_tpu_map.is_some() && epoch_info.is_some() && slot_leaders.is_some() { return Ok(( diff --git a/tpu-client/Cargo.toml b/tpu-client/Cargo.toml index a07486ee207..9362a68c540 100644 --- a/tpu-client/Cargo.toml +++ b/tpu-client/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] default = ["spinner"] @@ -29,6 +31,7 @@ solana-clock = { workspace = true } solana-commitment-config = { workspace = true } solana-connection-cache = { workspace = true } solana-epoch-schedule = { workspace = true } +solana-leader-schedule = { workspace = true } solana-message = { workspace = true, optional = true } solana-pubkey = { workspace = true } solana-pubsub-client = { workspace = true } @@ -41,3 +44,6 @@ solana-transaction-error = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } wincode = { workspace = true } + +[lints] +workspace = true diff --git a/tpu-client/src/nonblocking/tpu_client.rs b/tpu-client/src/nonblocking/tpu_client.rs index 1236e3bc2a9..53dc64ba3be 100644 --- a/tpu-client/src/nonblocking/tpu_client.rs +++ b/tpu-client/src/nonblocking/tpu_client.rs @@ -3,7 +3,7 @@ use { crate::tpu_client::{MAX_FANOUT_SLOTS, RecentLeaderSlots, TpuClientConfig}, futures_util::{future::join_all, stream::StreamExt}, log::*, - solana_clock::{DEFAULT_MS_PER_SLOT, NUM_CONSECUTIVE_LEADER_SLOTS, Slot}, + solana_clock::{DEFAULT_MS_PER_SLOT, Slot}, solana_commitment_config::CommitmentConfig, solana_connection_cache::{ connection_cache::{ @@ -13,6 +13,7 @@ use { nonblocking::client_connection::ClientConnection, }, solana_epoch_schedule::EpochSchedule, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_pubkey::Pubkey, solana_pubsub_client::nonblocking::pubsub_client::{PubsubClient, PubsubClientError}, solana_rpc_client::nonblocking::rpc_client::RpcClient, @@ -153,8 +154,8 @@ impl LeaderTpuCache { // value. Take the greater of the two values to ensure we are reading from the latest // leader schedule. let current_slot = std::cmp::max(estimated_current_slot, self.first_slot); - for leader_slot in (current_slot..current_slot + fanout_slots) - .step_by(NUM_CONSECUTIVE_LEADER_SLOTS as usize) + for leader_slot in + (current_slot..current_slot + fanout_slots).step_by(NUM_CONSECUTIVE_LEADER_SLOTS.get()) { if let Some(leader) = self.get_slot_leader(leader_slot) { if let Some(tpu_socket) = self.leader_tpu_map.get(leader) { @@ -650,17 +651,15 @@ where { let statuses = result.value; for (signature, status) in pending_signatures_chunk.iter().zip(statuses) { - if let Some(status) = status { - if status.satisfies_commitment(self.rpc_client.commitment()) { - if let Some((i, _)) = pending_transactions.remove(signature) { - progress.confirmed_transactions += 1; - if status.err.is_some() { - progress_bar - .println(format!("Failed transaction: {status:?}")); - } - transaction_errors[i] = status.err; - } + if let Some(status) = status + && status.satisfies_commitment(self.rpc_client.commitment()) + && let Some((i, _)) = pending_transactions.remove(signature) + { + progress.confirmed_transactions += 1; + if status.err.is_some() { + progress_bar.println(format!("Failed transaction: {status:?}")); } + transaction_errors[i] = status.err; } } } diff --git a/transaction-context/Cargo.toml b/transaction-context/Cargo.toml index c74318e27ca..b559c5077a1 100644 --- a/transaction-context/Cargo.toml +++ b/transaction-context/Cargo.toml @@ -7,7 +7,7 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] @@ -40,6 +40,7 @@ solana-signature = { workspace = true, optional = true } [dev-dependencies] solana-account-info = { workspace = true } solana-program-entrypoint = { workspace = true } +solana-pubkey = { workspace = true, features = ["rand"] } solana-system-interface = { workspace = true } solana-transaction-context = { path = ".", features = [ "agave-unstable-api", diff --git a/transaction-context/src/instruction_accounts.rs b/transaction-context/src/instruction_accounts.rs index 00ac4119f2d..a84788a26de 100644 --- a/transaction-context/src/instruction_accounts.rs +++ b/transaction-context/src/instruction_accounts.rs @@ -350,10 +350,6 @@ impl BorrowedInstructionAccount<'_, '_> { /// Returns an error if the account data can not be resized to the given length pub fn can_data_be_resized(&self, new_len: usize) -> Result<(), InstructionError> { let old_len = self.get_data().len(); - // Only the owner can change the length of the data - if new_len != old_len && !self.is_owned_by_current_program() { - return Err(InstructionError::AccountDataSizeChanged); - } self.transaction_context .accounts .can_data_be_resized(old_len, new_len)?; diff --git a/transaction-context/src/lib.rs b/transaction-context/src/lib.rs index 537d4a56f89..7a0e3a37ecd 100644 --- a/transaction-context/src/lib.rs +++ b/transaction-context/src/lib.rs @@ -1,7 +1,7 @@ #![cfg(feature = "agave-unstable-api")] //! Data shared between program runtime and built-in programs as well as SBF programs. #![deny(clippy::indexing_slicing)] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] pub mod instruction; pub mod instruction_accounts; diff --git a/transaction-context/src/transaction.rs b/transaction-context/src/transaction.rs index 0ea8f81efe2..cfb4fe20684 100644 --- a/transaction-context/src/transaction.rs +++ b/transaction-context/src/transaction.rs @@ -1,23 +1,26 @@ +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] use { crate::{ IndexOfAccount, MAX_ACCOUNT_DATA_GROWTH_PER_TRANSACTION, MAX_ACCOUNT_DATA_LEN, MAX_ACCOUNTS_PER_TRANSACTION, instruction::{InstructionContext, InstructionFrame}, - instruction_accounts::InstructionAccount, transaction_accounts::{KeyedAccountSharedData, TransactionAccounts}, vm_addresses::{ - GUEST_INSTRUCTION_DATA_BASE_ADDRESS, GUEST_REGION_SIZE, RETURN_DATA_SCRATCHPAD, + GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS, GUEST_INSTRUCTION_DATA_BASE_ADDRESS, + GUEST_REGION_SIZE, RETURN_DATA_SCRATCHPAD, }, - vm_slice::VmSlice, }, solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, solana_instruction::error::InstructionError, solana_instructions_sysvar as instructions, - solana_pubkey::Pubkey, solana_rent::Rent, solana_sbpf::memory_region::{AccessType, AccessViolationHandler, MemoryRegion}, std::{borrow::Cow, cell::Cell, rc::Rc}, }; +use { + crate::{instruction_accounts::InstructionAccount, vm_slice::VmSlice}, + solana_pubkey::Pubkey, +}; /// Used only in fn `take_instruction_trace` for deconstructing TransactionContext pub type InstructionTrace<'ix_data> = ( @@ -37,7 +40,9 @@ struct TransactionFrame { return_data_pubkey: Pubkey, return_data_scratchpad: VmSlice, /// Scratchpad for programs to write CPI instruction data - cpi_scratchpad: VmSlice, + pub cpi_data_scratchpad: VmSlice, + /// Scratchpad for programs to write CPI accounts + pub cpi_accounts_scratchpad: VmSlice, /// Index of current executing instruction current_executing_instruction: u16, /// Number of instructions in the instruction trace (including top level and CPIs) @@ -88,12 +93,18 @@ impl<'ix_data> TransactionContext<'ix_data> { let transaction_frame = TransactionFrame { return_data_pubkey: Pubkey::default(), return_data_scratchpad: VmSlice::new(RETURN_DATA_SCRATCHPAD, 0), - cpi_scratchpad: VmSlice::new( + cpi_data_scratchpad: VmSlice::new( GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add( GUEST_REGION_SIZE.saturating_mul(number_of_top_level_instructions as u64), ), 0, ), + cpi_accounts_scratchpad: VmSlice::new( + GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS.saturating_add( + GUEST_REGION_SIZE.saturating_mul(number_of_top_level_instructions as u64), + ), + 0, + ), current_executing_instruction: 0, total_number_of_instructions_in_trace: number_of_top_level_instructions as u16, number_of_cpis_in_trace: 0, @@ -297,12 +308,18 @@ impl<'ix_data> TransactionContext<'ix_data> { .total_number_of_instructions_in_trace .saturating_add(1); instruction.index_of_caller_instruction = caller_index; - let next_ptr = self + let next_data_ptr = self .transaction_frame - .cpi_scratchpad + .cpi_data_scratchpad .ptr() .saturating_add(GUEST_REGION_SIZE); - self.transaction_frame.cpi_scratchpad = VmSlice::new(next_ptr, 0); + self.transaction_frame.cpi_data_scratchpad = VmSlice::new(next_data_ptr, 0); + let next_accounts_ptr = self + .transaction_frame + .cpi_accounts_scratchpad + .ptr() + .saturating_add(GUEST_REGION_SIZE); + self.transaction_frame.cpi_accounts_scratchpad = VmSlice::new(next_accounts_ptr, 0); } instruction.program_account_index_in_tx = program_index; @@ -612,7 +629,8 @@ pub struct TransactionReturnData { pub struct ExecutionRecord { pub accounts: Vec, pub return_data: TransactionReturnData, - pub touched_account_count: u64, + /// Parallel to `accounts`: whether each account was modified by the VM. + pub touched_flags: Box<[bool]>, pub accounts_resize_delta: i64, } @@ -623,11 +641,16 @@ impl From> for ExecutionRecord { let (accounts, touched_flags, resize_delta) = Rc::try_unwrap(context.accounts) .expect("transaction_context.accounts has unexpected outstanding refs") .take(); - let touched_account_count = touched_flags - .iter() - .fold(0usize, |accumulator, was_touched| { - accumulator.saturating_add(was_touched.get() as usize) - }) as u64; + + // The flags only needed interior mutability while the VM was running. + // Now that we own them, unwrap the per-element `Cell`s into a plain + // `Box<[bool]>`. `Vec::from` reuses the box's allocation and the mapped + // collect reuses that same buffer in place (`Cell` and `bool` have + // identical layout), so no reallocation occurs. + let touched_flags: Box<[bool]> = Vec::from(touched_flags) + .into_iter() + .map(|flag| flag.into_inner()) + .collect(); let return_data = TransactionReturnData { program_id: context.transaction_frame.return_data_pubkey, @@ -637,7 +660,7 @@ impl From> for ExecutionRecord { Self { accounts, return_data, - touched_account_count, + touched_flags, accounts_resize_delta: Cell::into_inner(resize_delta), } } @@ -909,13 +932,35 @@ mod tests { ); assert_eq!( - transaction_context.transaction_frame.cpi_scratchpad.ptr(), + transaction_context + .transaction_frame + .cpi_data_scratchpad + .ptr(), GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(2)) ); assert_eq!( - transaction_context.transaction_frame.cpi_scratchpad.len(), + transaction_context + .transaction_frame + .cpi_data_scratchpad + .len(), 0, ); + assert_eq!( + transaction_context + .transaction_frame + .cpi_accounts_scratchpad + .ptr(), + GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS + .saturating_add(GUEST_REGION_SIZE.saturating_mul(2)) + ); + assert_eq!( + transaction_context + .transaction_frame + .cpi_data_scratchpad + .len(), + 0, + ); + assert_eq!( transaction_context.number_of_called_instructions_in_trace(), 1 @@ -956,9 +1001,20 @@ mod tests { ); assert_eq!( - transaction_context.transaction_frame.cpi_scratchpad.ptr(), + transaction_context + .transaction_frame + .cpi_data_scratchpad + .ptr(), GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(3)) ); + assert_eq!( + transaction_context + .transaction_frame + .cpi_accounts_scratchpad + .ptr(), + GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS + .saturating_add(GUEST_REGION_SIZE.saturating_mul(3)) + ); // A nested CPI transaction_context @@ -985,9 +1041,20 @@ mod tests { ); assert_eq!( - transaction_context.transaction_frame.cpi_scratchpad.ptr(), + transaction_context + .transaction_frame + .cpi_data_scratchpad + .ptr(), GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(4)) ); + assert_eq!( + transaction_context + .transaction_frame + .cpi_accounts_scratchpad + .ptr(), + GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS + .saturating_add(GUEST_REGION_SIZE.saturating_mul(4)) + ); assert_eq!( transaction_context @@ -1051,9 +1118,21 @@ mod tests { ); assert_eq!( - transaction_context.transaction_frame.cpi_scratchpad.ptr(), + transaction_context + .transaction_frame + .cpi_data_scratchpad + .ptr(), GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(5)) ); + assert_eq!( + transaction_context + .transaction_frame + .cpi_accounts_scratchpad + .ptr(), + GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS + .saturating_add(GUEST_REGION_SIZE.saturating_mul(5)) + ); + assert_eq!( transaction_context .transaction_frame @@ -1083,9 +1162,20 @@ mod tests { ); assert_eq!( - transaction_context.transaction_frame.cpi_scratchpad.ptr(), + transaction_context + .transaction_frame + .cpi_data_scratchpad + .ptr(), GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(5)) ); + assert_eq!( + transaction_context + .transaction_frame + .cpi_accounts_scratchpad + .ptr(), + GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS + .saturating_add(GUEST_REGION_SIZE.saturating_mul(5)) + ); assert_eq!( transaction_context @@ -1116,9 +1206,20 @@ mod tests { ); assert_eq!( - transaction_context.transaction_frame.cpi_scratchpad.ptr(), + transaction_context + .transaction_frame + .cpi_data_scratchpad + .ptr(), GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(5)) ); + assert_eq!( + transaction_context + .transaction_frame + .cpi_accounts_scratchpad + .ptr(), + GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS + .saturating_add(GUEST_REGION_SIZE.saturating_mul(5)) + ); assert_eq!( transaction_context @@ -1169,9 +1270,21 @@ mod tests { ); assert_eq!( - transaction_context.transaction_frame.cpi_scratchpad.ptr(), + transaction_context + .transaction_frame + .cpi_data_scratchpad + .ptr(), GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(6)) ); + assert_eq!( + transaction_context + .transaction_frame + .cpi_accounts_scratchpad + .ptr(), + GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS + .saturating_add(GUEST_REGION_SIZE.saturating_mul(6)) + ); + assert_eq!( transaction_context .transaction_frame diff --git a/transaction-context/src/vm_slice.rs b/transaction-context/src/vm_slice.rs index b973c5c2e73..a9445c9c336 100644 --- a/transaction-context/src/vm_slice.rs +++ b/transaction-context/src/vm_slice.rs @@ -11,6 +11,8 @@ use std::marker::PhantomData; +/// VmSlice serves as a stable layout for slices shared between the guest and the host. +/// It is also the layout for SolSignerSeedC and SolSignerSeedsC. #[repr(C)] #[derive(Copy, Clone, Debug, PartialEq)] pub struct VmSlice { diff --git a/transaction-status-client-types/Cargo.toml b/transaction-status-client-types/Cargo.toml index c6607054c66..4bff6a5edb7 100644 --- a/transaction-status-client-types/Cargo.toml +++ b/transaction-status-client-types/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -35,3 +37,6 @@ wincode = { workspace = true } [dev-dependencies] test-case = { workspace = true } + +[lints] +workspace = true diff --git a/transaction-status-client-types/src/lib.rs b/transaction-status-client-types/src/lib.rs index 75317d7b8c3..e3ee8da3f40 100644 --- a/transaction-status-client-types/src/lib.rs +++ b/transaction-status-client-types/src/lib.rs @@ -182,11 +182,11 @@ impl EncodedTransaction { TransactionBinaryEncoding::Base58 => bs58::decode(blob) .into_vec() .ok() - .and_then(|bytes| bincode::deserialize(&bytes).ok()), + .and_then(|bytes| wincode::deserialize(&bytes).ok()), TransactionBinaryEncoding::Base64 => BASE64_STANDARD .decode(blob) .ok() - .and_then(|bytes| bincode::deserialize(&bytes).ok()), + .and_then(|bytes| wincode::deserialize(&bytes).ok()), }; transaction.filter(|transaction| transaction.sanitize().is_ok()) @@ -202,7 +202,7 @@ pub struct EncodedTransactionWithStatusMeta { pub version: Option, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SchemaRead, SchemaWrite)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Reward { pub pubkey: String, @@ -311,35 +311,34 @@ impl<'de> DeserializeTrait<'de> for UiTransactionError { D: Deserializer<'de>, { let value = serde_json::Value::deserialize(deserializer)?; - if let Some(obj) = value.as_object() { - if let Some(arr) = obj.get("InstructionError").and_then(|v| v.as_array()) { - let outer_instruction_index: u8 = arr - .first() - .ok_or_else(|| { - DeserializeError::invalid_length(0, &"Expected the first element to exist") - })? - .as_u64() - .ok_or_else(|| { - DeserializeError::custom("Expected the first element to be a u64") - })? as u8; - let instruction_error = arr.get(1).ok_or_else(|| { - DeserializeError::invalid_length(1, &"Expected there to be at least 2 elements") - })?; - - // Handle SDK version compatibility: if it's a v2-style - // {"BorshIoError": "Unknown"}, convert it to a v3-style - // "BorshIoError" - let err: InstructionError = if instruction_error.get("BorshIoError").is_some() { - from_value(serde_json::json!("BorshIoError")) - } else { - from_value(instruction_error.clone()) - } - .map_err(|e| DeserializeError::custom(e.to_string()))?; - return Ok(UiTransactionError(TransactionError::InstructionError( - outer_instruction_index, - err, - ))); + if let Some(obj) = value.as_object() + && let Some(arr) = obj.get("InstructionError").and_then(|v| v.as_array()) + { + let outer_instruction_index: u8 = arr + .first() + .ok_or_else(|| { + DeserializeError::invalid_length(0, &"Expected the first element to exist") + })? + .as_u64() + .ok_or_else(|| DeserializeError::custom("Expected the first element to be a u64"))? + as u8; + let instruction_error = arr.get(1).ok_or_else(|| { + DeserializeError::invalid_length(1, &"Expected there to be at least 2 elements") + })?; + + // Handle SDK version compatibility: if it's a v2-style + // {"BorshIoError": "Unknown"}, convert it to a v3-style + // "BorshIoError" + let err: InstructionError = if instruction_error.get("BorshIoError").is_some() { + from_value(serde_json::json!("BorshIoError")) + } else { + from_value(instruction_error.clone()) } + .map_err(|e| DeserializeError::custom(e.to_string()))?; + return Ok(UiTransactionError(TransactionError::InstructionError( + outer_instruction_index, + err, + ))); } let err = TransactionError::deserialize(value).map_err(de::Error::custom)?; Ok(UiTransactionError(err)) @@ -806,6 +805,30 @@ mod test { assert!(unsanitary_transaction.decode().is_none()); } + #[test] + fn test_decode_v1_wire_transaction() { + let transaction = VersionedTransaction { + signatures: vec![solana_signature::Signature::default()], + message: solana_message::VersionedMessage::V1(solana_message::v1::Message::new( + solana_message::MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + solana_message::v1::TransactionConfig::empty(), + solana_message::Hash::default(), + vec![solana_message::Address::default()], + vec![], + )), + }; + let encoded = EncodedTransaction::Binary( + BASE64_STANDARD.encode(wincode::serialize(&transaction).unwrap()), + TransactionBinaryEncoding::Base64, + ); + + assert_eq!(encoded.decode(), Some(transaction)); + } + #[test] fn test_satisfies_commitment() { let status = TransactionStatus { diff --git a/transaction-status-client-types/src/option_serializer.rs b/transaction-status-client-types/src/option_serializer.rs index d16c6b102d6..32918d63eed 100644 --- a/transaction-status-client-types/src/option_serializer.rs +++ b/transaction-status-client-types/src/option_serializer.rs @@ -125,10 +125,10 @@ impl OptionSerializer { where P: FnOnce(&T) -> bool, { - if let OptionSerializer::Some(x) = self { - if predicate(&x) { - return OptionSerializer::Some(x); - } + if let OptionSerializer::Some(x) = self + && predicate(&x) + { + return OptionSerializer::Some(x); } OptionSerializer::None } diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index 171a01870c0..64408839362 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -43,6 +45,7 @@ solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } solana-transaction-status-client-types = { workspace = true } solana-vote-interface = { workspace = true } +solana-zk-sdk-pod = { workspace = true } spl-associated-token-account-interface = { workspace = true, features = ["borsh"] } spl-memo-interface = { workspace = true } spl-token-2022-interface = { workspace = true } @@ -50,6 +53,7 @@ spl-token-group-interface = { workspace = true } spl-token-interface = { workspace = true } spl-token-metadata-interface = { workspace = true } thiserror = { workspace = true } +wincode = { workspace = true } [dev-dependencies] bencher = { workspace = true } @@ -61,3 +65,6 @@ spl-token-confidential-transfer-proof-extraction = { workspace = true } [[bench]] name = "extract_memos" harness = false + +[lints] +workspace = true diff --git a/transaction-status/benches/extract_memos.rs b/transaction-status/benches/extract_memos.rs index 9166291c78c..8d92fe9191d 100644 --- a/transaction-status/benches/extract_memos.rs +++ b/transaction-status/benches/extract_memos.rs @@ -8,13 +8,14 @@ use { #[allow(clippy::arithmetic_side_effects)] fn bench_extract_memos(b: &mut Bencher) { let mut account_keys: Vec = (0..64).map(|_| Pubkey::new_unique()).collect(); - account_keys[62] = spl_memo_interface::v1::id(); - account_keys[63] = spl_memo_interface::v3::id(); + account_keys[61] = spl_memo_interface::v1::id(); + account_keys[62] = spl_memo_interface::v3::id(); + account_keys[63] = spl_memo_interface::v4::id(); let memo = "Test memo"; let instructions: Vec<_> = (0..20) .map(|i| CompiledInstruction { - program_id_index: 62 + (i % 2), + program_id_index: 61 + (i % 3), accounts: vec![], data: memo.as_bytes().to_vec(), }) diff --git a/transaction-status/src/extract_memos.rs b/transaction-status/src/extract_memos.rs index 81480b01c85..a77fd5ff729 100644 --- a/transaction-status/src/extract_memos.rs +++ b/transaction-status/src/extract_memos.rs @@ -71,6 +71,7 @@ fn extract_memos_inner( KeyType::Unknown(program_id) => { if **program_id == spl_memo_interface::v1::id() || **program_id == spl_memo_interface::v3::id() + || **program_id == spl_memo_interface::v4::id() { account_keys[index] = KeyType::MemoProgram; Some(&ix.data) @@ -95,9 +96,11 @@ mod test { let another_program_id = Pubkey::new_unique(); let memo0 = "Test memo"; let memo1 = "🦖"; + let memo2 = "Yet another memo"; let expected_memos = vec![ format!("[{}] {}", memo0.len(), memo0), format!("[{}] {}", memo1.len(), memo1), + format!("[{}] {}", memo2.len(), memo2), ]; let memo_instructions = vec![ CompiledInstruction { @@ -115,12 +118,18 @@ mod test { accounts: vec![], data: memo1.as_bytes().to_vec(), }, + CompiledInstruction { + program_id_index: 4, + accounts: vec![], + data: memo2.as_bytes().to_vec(), + }, ]; let static_keys = vec![ fee_payer, spl_memo_interface::v1::id(), another_program_id, spl_memo_interface::v3::id(), + spl_memo_interface::v4::id(), ]; let account_keys = AccountKeys::new(&static_keys, None); diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index d7fd8f7802f..f5c6cbe8ef1 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -60,6 +60,10 @@ pub mod parse_token; pub mod parse_vote; pub mod token_balances; +fn serialize_versioned_transaction(transaction: &VersionedTransaction) -> Vec { + wincode::serialize(transaction).expect("serialize versioned transaction") +} + pub struct BlockEncodingOptions { pub transaction_details: TransactionDetails, pub show_rewards: bool, @@ -398,6 +402,7 @@ pub struct VersionedConfirmedBlockWithEntries { // Data needed to reconstruct an Entry, given an ordered list of transactions in // a block. Used for uploading to BigTable. +#[derive(Clone, Copy)] pub struct EntrySummary { pub num_hashes: u64, pub hash: Hash, @@ -634,14 +639,14 @@ impl EncodableWithMeta for VersionedTransaction { ) -> Self::Encoded { match encoding { UiTransactionEncoding::Binary => EncodedTransaction::LegacyBinary( - bs58::encode(bincode::serialize(self).unwrap()).into_string(), + bs58::encode(serialize_versioned_transaction(self)).into_string(), ), UiTransactionEncoding::Base58 => EncodedTransaction::Binary( - bs58::encode(bincode::serialize(self).unwrap()).into_string(), + bs58::encode(serialize_versioned_transaction(self)).into_string(), TransactionBinaryEncoding::Base58, ), UiTransactionEncoding::Base64 => EncodedTransaction::Binary( - BASE64_STANDARD.encode(bincode::serialize(self).unwrap()), + BASE64_STANDARD.encode(serialize_versioned_transaction(self)), TransactionBinaryEncoding::Base64, ), UiTransactionEncoding::Json => self.json_encode(), @@ -678,14 +683,14 @@ impl Encodable for VersionedTransaction { fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded { match encoding { UiTransactionEncoding::Binary => EncodedTransaction::LegacyBinary( - bs58::encode(bincode::serialize(self).unwrap()).into_string(), + bs58::encode(serialize_versioned_transaction(self)).into_string(), ), UiTransactionEncoding::Base58 => EncodedTransaction::Binary( - bs58::encode(bincode::serialize(self).unwrap()).into_string(), + bs58::encode(serialize_versioned_transaction(self)).into_string(), TransactionBinaryEncoding::Base58, ), UiTransactionEncoding::Base64 => EncodedTransaction::Binary( - BASE64_STANDARD.encode(bincode::serialize(self).unwrap()), + BASE64_STANDARD.encode(serialize_versioned_transaction(self)), TransactionBinaryEncoding::Base64, ), UiTransactionEncoding::Json | UiTransactionEncoding::JsonParsed => { @@ -1075,4 +1080,40 @@ mod test { assert_eq!(encoded.block_time, Some(1234567890)); assert_eq!(encoded.transaction_index, Some(7)); } + + #[test] + fn test_v1_binary_encoding_uses_wire_format() { + let message = solana_message::v1::Message::new( + solana_message::MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + solana_message::v1::TransactionConfig::empty(), + solana_hash::Hash::new_from_array([7; solana_hash::HASH_BYTES]), + vec![ + solana_pubkey::Pubkey::new_unique(), + solana_pubkey::Pubkey::new_unique(), + ], + vec![], + ); + let transaction = VersionedTransaction { + signatures: vec![solana_signature::Signature::default()], + message: solana_message::VersionedMessage::V1(message), + }; + + let EncodedTransaction::Binary(encoded, TransactionBinaryEncoding::Base64) = + transaction.encode(UiTransactionEncoding::Base64) + else { + panic!("expected binary base64 output"); + }; + let bytes = BASE64_STANDARD.decode(&encoded).unwrap(); + + assert_eq!(bytes[0], solana_message::v1::V1_PREFIX); + assert_eq!(bytes, wincode::serialize(&transaction).unwrap()); + assert_eq!( + EncodedTransaction::Binary(encoded, TransactionBinaryEncoding::Base64).decode(), + Some(transaction) + ); + } } diff --git a/transaction-status/src/parse_instruction.rs b/transaction-status/src/parse_instruction.rs index db9aa47a353..ed3bc3996df 100644 --- a/transaction-status/src/parse_instruction.rs +++ b/transaction-status/src/parse_instruction.rs @@ -36,6 +36,7 @@ static PARSABLE_PROGRAM_IDS: std::sync::LazyLock "mint", + | AuthorityType::Pause + | AuthorityType::PermissionedBurn => "mint", AuthorityType::AccountOwner | AuthorityType::CloseAccount => "account", }; let mut value = json!({ @@ -705,6 +706,40 @@ pub fn parse_token( &instruction.accounts, account_keys, ), + TokenInstruction::UnwrapLamports { amount } => { + check_num_token_accounts(&instruction.accounts, 3)?; + let mut value = json!({ + "source": account_keys[instruction.accounts[0] as usize].to_string(), + "destination": account_keys[instruction.accounts[1] as usize].to_string(), + }); + let map = value.as_object_mut().unwrap(); + if let COption::Some(amount) = amount { + map.insert("amount".to_string(), json!(amount.to_string())); + } + parse_signers( + map, + 2, + account_keys, + &instruction.accounts, + "authority", + "multisigAuthority", + ); + Ok(ParsedInstructionEnum { + instruction_type: "unwrapLamports".to_string(), + info: value, + }) + } + TokenInstruction::PermissionedBurnExtension => parse_permissioned_burn_instruction( + &instruction.data[1..], + &instruction.accounts, + account_keys, + ), + TokenInstruction::Batch { data } => parse_batch_instruction( + &data, + instruction.program_id_index, + &instruction.accounts, + account_keys, + ), } } else if let Ok(token_group_instruction) = TokenGroupInstruction::unpack(&instruction.data) { parse_token_group_instruction( @@ -727,6 +762,52 @@ pub fn parse_token( } } +// Parses a `Batch` instruction (shared by spl-token and spl-token-2022). The +// instruction data holds, for each inner instruction, a `u8` account count, a +// `u8` data length, then that many data bytes; the outer accounts are each inner +// instruction's accounts concatenated in the same order. +fn parse_batch_instruction( + data: &[u8], + program_id_index: u8, + accounts: &[u8], + account_keys: &AccountKeys, +) -> Result { + let not_parsable = || ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken); + let mut data_cursor: usize = 0; + let mut account_cursor: usize = 0; + let mut instructions = vec![]; + while data_cursor < data.len() { + let num_accounts = *data.get(data_cursor).ok_or_else(not_parsable)? as usize; + let data_len = *data.get(data_cursor + 1).ok_or_else(not_parsable)? as usize; + data_cursor += 2; + let data_end = data_cursor.checked_add(data_len).ok_or_else(not_parsable)?; + let inner_data = data.get(data_cursor..data_end).ok_or_else(not_parsable)?; + data_cursor = data_end; + let account_end = account_cursor + .checked_add(num_accounts) + .ok_or_else(not_parsable)?; + let inner_accounts = accounts + .get(account_cursor..account_end) + .ok_or_else(not_parsable)?; + account_cursor = account_end; + // Nested batches are not permitted by the program. + if inner_data.first() == Some(&255) { + return Err(not_parsable()); + } + let inner_instruction = CompiledInstruction { + program_id_index, + accounts: inner_accounts.to_vec(), + data: inner_data.to_vec(), + }; + let parsed = parse_token(&inner_instruction, account_keys)?; + instructions.push(serde_json::to_value(parsed).map_err(|_| not_parsable())?); + } + Ok(ParsedInstructionEnum { + instruction_type: "batch".to_string(), + info: json!({ "instructions": instructions }), + }) +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub enum UiAuthorityType { @@ -747,6 +828,7 @@ pub enum UiAuthorityType { GroupMemberPointer, ScaledUiAmount, Pause, + PermissionedBurn, } impl From for UiAuthorityType { @@ -771,6 +853,7 @@ impl From for UiAuthorityType { AuthorityType::GroupMemberPointer => UiAuthorityType::GroupMemberPointer, AuthorityType::ScaledUiAmount => UiAuthorityType::ScaledUiAmount, AuthorityType::Pause => UiAuthorityType::Pause, + AuthorityType::PermissionedBurn => UiAuthorityType::PermissionedBurn, } } } @@ -806,6 +889,7 @@ pub enum UiExtensionType { ScaledUiAmount, Pausable, PausableAccount, + PermissionedBurn, } impl From for UiExtensionType { @@ -845,6 +929,7 @@ impl From for UiExtensionType { ExtensionType::ScaledUiAmount => UiExtensionType::ScaledUiAmount, ExtensionType::Pausable => UiExtensionType::Pausable, ExtensionType::PausableAccount => UiExtensionType::PausableAccount, + ExtensionType::PermissionedBurn => UiExtensionType::PermissionedBurn, } } } @@ -889,7 +974,7 @@ fn map_coption_pubkey(pubkey: COption) -> Option { #[cfg(test)] mod test { use { - super::*, solana_message::Message, solana_pubkey::Pubkey, + super::*, solana_instruction::Instruction, solana_message::Message, solana_pubkey::Pubkey, spl_token_2022_interface::instruction::*, std::iter::repeat_with, }; @@ -2160,4 +2245,50 @@ mod test { fn test_not_enough_keys_token_2022() { test_token_ix_not_enough_keys(&spl_token_2022_interface::id()); } + + fn test_parse_batch(program_id: &Pubkey) { + let source = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let transfer_ix = + transfer_checked(program_id, &source, &mint, &destination, &owner, &[], 10, 2).unwrap(); + let burn_ix = burn(program_id, &source, &mint, &owner, &[], 5).unwrap(); + + let mut data = TokenInstruction::Batch { data: vec![] }.pack(); + let mut accounts = vec![]; + for ix in [&transfer_ix, &burn_ix] { + data.push(ix.accounts.len() as u8); + data.push(ix.data.len() as u8); + data.extend_from_slice(&ix.data); + accounts.extend_from_slice(&ix.accounts); + } + let batch_ix = Instruction { + program_id: *program_id, + accounts, + data, + }; + let message = Message::new(&[batch_ix], None); + let compiled_instruction = &message.instructions[0]; + let parsed = parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None), + ) + .unwrap(); + assert_eq!(parsed.instruction_type, "batch"); + let inner = parsed.info["instructions"].as_array().unwrap(); + assert_eq!(inner.len(), 2); + assert_eq!(inner[0]["type"], "transferChecked"); + assert_eq!(inner[1]["type"], "burn"); + } + + #[test] + fn test_parse_batch_token() { + test_parse_batch(&spl_token_interface::id()); + } + + #[test] + fn test_parse_batch_token_2022() { + test_parse_batch(&spl_token_2022_interface::id()); + } } diff --git a/transaction-status/src/parse_token/extension/confidential_mint_burn.rs b/transaction-status/src/parse_token/extension/confidential_mint_burn.rs index 1da9059cf21..d1e84e6bb68 100644 --- a/transaction-status/src/parse_token/extension/confidential_mint_burn.rs +++ b/transaction-status/src/parse_token/extension/confidential_mint_burn.rs @@ -284,20 +284,18 @@ mod test { solana_instruction::{AccountMeta, Instruction}, solana_message::Message, solana_pubkey::Pubkey, + solana_zk_sdk_pod::encryption::{ + auth_encryption::PodAeCiphertext, + elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, + }, spl_token_2022_interface::{ extension::confidential_mint_burn::instruction::{ confidential_burn_with_split_proofs, confidential_mint_with_split_proofs, initialize_mint, }, - solana_zk_sdk::{ - encryption::pod::{ - auth_encryption::PodAeCiphertext, - elgamal::{PodElGamalCiphertext, PodElGamalPubkey}, - }, - zk_elgamal_proof_program::proof_data::{ - BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, - CiphertextCiphertextEqualityProofData, CiphertextCommitmentEqualityProofData, - }, + solana_zk_elgamal_proof_interface::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, + CiphertextCiphertextEqualityProofData, CiphertextCommitmentEqualityProofData, }, }, spl_token_confidential_transfer_proof_extraction::instruction::ProofLocation, diff --git a/transaction-status/src/parse_token/extension/confidential_transfer.rs b/transaction-status/src/parse_token/extension/confidential_transfer.rs index dd4d47c41b5..a938487f21e 100644 --- a/transaction-status/src/parse_token/extension/confidential_transfer.rs +++ b/transaction-status/src/parse_token/extension/confidential_transfer.rs @@ -1,9 +1,9 @@ use { super::*, + solana_zk_sdk_pod::encryption::elgamal::PodElGamalPubkey, spl_token_2022_interface::{ extension::confidential_transfer::instruction::*, instruction::{decode_instruction_data, decode_instruction_type}, - solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, }, }; @@ -167,16 +167,15 @@ pub(in crate::parse_token) fn parse_confidential_transfer_instruction( }) } ConfidentialTransferInstruction::Deposit => { - check_num_token_accounts(account_indexes, 4)?; + check_num_token_accounts(account_indexes, 3)?; let deposit_data: DepositInstructionData = *decode_instruction_data(instruction_data) .map_err(|_| { ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) })?; let amount: u64 = deposit_data.amount.into(); let mut value = json!({ - "source": account_keys[account_indexes[0] as usize].to_string(), - "destination": account_keys[account_indexes[1] as usize].to_string(), - "mint": account_keys[account_indexes[2] as usize].to_string(), + "account": account_keys[account_indexes[0] as usize].to_string(), + "mint": account_keys[account_indexes[1] as usize].to_string(), "amount": amount, "decimals": deposit_data.decimals, @@ -184,7 +183,7 @@ pub(in crate::parse_token) fn parse_confidential_transfer_instruction( let map = value.as_object_mut().unwrap(); parse_signers( map, - 3, + 2, account_keys, account_indexes, "owner", @@ -196,16 +195,15 @@ pub(in crate::parse_token) fn parse_confidential_transfer_instruction( }) } ConfidentialTransferInstruction::Withdraw => { - check_num_token_accounts(account_indexes, 5)?; + check_num_token_accounts(account_indexes, 4)?; let withdrawal_data: WithdrawInstructionData = *decode_instruction_data(instruction_data).map_err(|_| { ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) })?; let amount: u64 = withdrawal_data.amount.into(); let mut value = json!({ - "source": account_keys[account_indexes[0] as usize].to_string(), - "destination": account_keys[account_indexes[1] as usize].to_string(), - "mint": account_keys[account_indexes[2] as usize].to_string(), + "account": account_keys[account_indexes[0] as usize].to_string(), + "mint": account_keys[account_indexes[1] as usize].to_string(), "amount": amount, "decimals": withdrawal_data.decimals, "newDecryptableAvailableBalance": format!("{}", withdrawal_data.new_decryptable_available_balance), @@ -213,7 +211,7 @@ pub(in crate::parse_token) fn parse_confidential_transfer_instruction( "rangeProofInstructionOffset": withdrawal_data.range_proof_instruction_offset, }); - let mut offset = 3; + let mut offset = 2; let map = value.as_object_mut().unwrap(); if offset < account_indexes.len() - 1 && (withdrawal_data.equality_proof_instruction_offset != 0 @@ -600,18 +598,16 @@ mod test { solana_instruction::{AccountMeta, Instruction}, solana_message::Message, solana_pubkey::Pubkey, + solana_zk_sdk_pod::encryption::{ + auth_encryption::PodAeCiphertext, elgamal::PodElGamalCiphertext, + }, spl_token_2022_interface::{ extension::confidential_transfer::instruction::{ initialize_mint, inner_configure_account, inner_empty_account, update_mint, }, - solana_zk_sdk::{ - encryption::pod::{ - auth_encryption::PodAeCiphertext, elgamal::PodElGamalCiphertext, - }, - zk_elgamal_proof_program::proof_data::{ - BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, - CiphertextCommitmentEqualityProofData, ZeroCiphertextProofData, - }, + solana_zk_elgamal_proof_interface::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, + CiphertextCommitmentEqualityProofData, ZeroCiphertextProofData, }, }, spl_token_confidential_transfer_proof_extraction::instruction::ProofLocation, @@ -716,6 +712,85 @@ mod test { } } + #[test] + fn test_deposit() { + let token_account = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let instruction = deposit( + &spl_token_2022_interface::id(), + &token_account, + &mint, + 42, + 9, + &owner, + &[], + ) + .unwrap(); + let message = Message::new(&[instruction], None); + let compiled_instruction = &message.instructions[0]; + assert_eq!( + parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "depositConfidentialTransfer".to_string(), + info: json!({ + "account": token_account.to_string(), + "mint": mint.to_string(), + "amount": 42, + "decimals": 9, + "owner": owner.to_string(), + }), + } + ); + } + + #[test] + fn test_withdraw_account_mapping() { + let token_account = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let equality_context = Pubkey::new_unique(); + let range_context = Pubkey::new_unique(); + let instruction = inner_withdraw( + &spl_token_2022_interface::id(), + &token_account, + &mint, + 1, + 2, + &PodAeCiphertext::default(), + &owner, + &[], + ProofLocation::ContextStateAccount(&equality_context), + ProofLocation::ContextStateAccount(&range_context), + ) + .unwrap(); + let message = Message::new(&[instruction], None); + let compiled_instruction = &message.instructions[0]; + let parsed = parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None), + ) + .unwrap(); + assert_eq!(parsed.instruction_type, "withdrawConfidentialTransfer"); + assert_eq!(parsed.info["account"], json!(token_account.to_string())); + assert_eq!(parsed.info["mint"], json!(mint.to_string())); + assert_eq!(parsed.info["owner"], json!(owner.to_string())); + assert_eq!( + parsed.info["equalityProofContextStateAccount"], + json!(equality_context.to_string()) + ); + assert_eq!( + parsed.info["rangeProofContextStateAccount"], + json!(range_context.to_string()) + ); + assert!(parsed.info.get("destination").is_none()); + assert_ne!(parsed.info["mint"], parsed.info["owner"]); + } + #[test] fn test_withdraw() { for (equality_proof_location, range_proof_location) in [ diff --git a/transaction-status/src/parse_token/extension/confidential_transfer_fee.rs b/transaction-status/src/parse_token/extension/confidential_transfer_fee.rs index a2dd71feb0b..6081ff027a2 100644 --- a/transaction-status/src/parse_token/extension/confidential_transfer_fee.rs +++ b/transaction-status/src/parse_token/extension/confidential_transfer_fee.rs @@ -1,9 +1,9 @@ use { super::*, + solana_zk_sdk_pod::encryption::elgamal::PodElGamalPubkey, spl_token_2022_interface::{ extension::confidential_transfer_fee::instruction::*, instruction::{decode_instruction_data, decode_instruction_type}, - solana_zk_sdk::encryption::pod::elgamal::PodElGamalPubkey, }, }; @@ -212,15 +212,13 @@ mod test { solana_instruction::{AccountMeta, Instruction}, solana_message::Message, solana_pubkey::Pubkey, + solana_zk_sdk_pod::encryption::auth_encryption::PodAeCiphertext, spl_token_2022_interface::{ extension::confidential_transfer_fee::instruction::{ inner_withdraw_withheld_tokens_from_accounts, inner_withdraw_withheld_tokens_from_mint, }, - solana_zk_sdk::{ - encryption::pod::auth_encryption::PodAeCiphertext, - zk_elgamal_proof_program::proof_data::CiphertextCiphertextEqualityProofData, - }, + solana_zk_elgamal_proof_interface::proof_data::CiphertextCiphertextEqualityProofData, }, spl_token_confidential_transfer_proof_extraction::instruction::ProofLocation, std::num::NonZero, diff --git a/transaction-status/src/parse_token/extension/mod.rs b/transaction-status/src/parse_token/extension/mod.rs index 39864a43581..1d2850de2ee 100644 --- a/transaction-status/src/parse_token/extension/mod.rs +++ b/transaction-status/src/parse_token/extension/mod.rs @@ -13,6 +13,7 @@ pub(super) mod metadata_pointer; pub(super) mod mint_close_authority; pub(super) mod pausable; pub(super) mod permanent_delegate; +pub(super) mod permissioned_burn; pub(super) mod reallocate; pub(super) mod scaled_ui_amount; pub(super) mod token_group; diff --git a/transaction-status/src/parse_token/extension/permissioned_burn.rs b/transaction-status/src/parse_token/extension/permissioned_burn.rs new file mode 100644 index 00000000000..d54d84b1c30 --- /dev/null +++ b/transaction-status/src/parse_token/extension/permissioned_burn.rs @@ -0,0 +1,317 @@ +use { + super::*, + spl_token_2022_interface::{ + extension::{ + confidential_mint_burn::instruction::BurnInstructionData as ConfidentialBurnInstructionData, + permissioned_burn::instruction::{ + BurnCheckedInstructionData, BurnInstructionData, InitializeInstructionData, + PermissionedBurnInstruction, + }, + }, + instruction::{decode_instruction_data, decode_instruction_type}, + }, +}; + +pub(in crate::parse_token) fn parse_permissioned_burn_instruction( + instruction_data: &[u8], + account_indexes: &[u8], + account_keys: &AccountKeys, +) -> Result { + match decode_instruction_type(instruction_data) + .map_err(|_| ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken))? + { + PermissionedBurnInstruction::Initialize => { + check_num_token_accounts(account_indexes, 1)?; + let InitializeInstructionData { authority } = + *decode_instruction_data(instruction_data).map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + Ok(ParsedInstructionEnum { + instruction_type: "initializePermissionedBurnConfig".to_string(), + info: json!({ + "mint": account_keys[account_indexes[0] as usize].to_string(), + "authority": authority.to_string(), + }), + }) + } + PermissionedBurnInstruction::Burn => { + check_num_token_accounts(account_indexes, 4)?; + let BurnInstructionData { amount } = *decode_instruction_data(instruction_data) + .map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + let mut value = json!({ + "account": account_keys[account_indexes[0] as usize].to_string(), + "mint": account_keys[account_indexes[1] as usize].to_string(), + "permissionedBurnAuthority": account_keys[account_indexes[2] as usize].to_string(), + "amount": u64::from(amount).to_string(), + }); + let map = value.as_object_mut().unwrap(); + parse_signers( + map, + 3, + account_keys, + account_indexes, + "authority", + "multisigAuthority", + ); + Ok(ParsedInstructionEnum { + instruction_type: "permissionedBurn".to_string(), + info: value, + }) + } + PermissionedBurnInstruction::BurnChecked => { + check_num_token_accounts(account_indexes, 4)?; + let BurnCheckedInstructionData { amount, decimals } = + *decode_instruction_data(instruction_data).map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + let additional_data = SplTokenAdditionalDataV2::with_decimals(decimals); + let mut value = json!({ + "account": account_keys[account_indexes[0] as usize].to_string(), + "mint": account_keys[account_indexes[1] as usize].to_string(), + "permissionedBurnAuthority": account_keys[account_indexes[2] as usize].to_string(), + "tokenAmount": token_amount_to_ui_amount_v3(u64::from(amount), &additional_data), + }); + let map = value.as_object_mut().unwrap(); + parse_signers( + map, + 3, + account_keys, + account_indexes, + "authority", + "multisigAuthority", + ); + Ok(ParsedInstructionEnum { + instruction_type: "permissionedBurnChecked".to_string(), + info: value, + }) + } + PermissionedBurnInstruction::ConfidentialBurn => { + check_num_token_accounts(account_indexes, 4)?; + let burn_data: ConfidentialBurnInstructionData = + *decode_instruction_data(instruction_data).map_err(|_| { + ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken) + })?; + let mut value = json!({ + "account": account_keys[account_indexes[0] as usize].to_string(), + "mint": account_keys[account_indexes[1] as usize].to_string(), + "newDecryptableAvailableBalance": burn_data.new_decryptable_available_balance.to_string(), + "equalityProofInstructionOffset": burn_data.equality_proof_instruction_offset, + "ciphertextValidityProofInstructionOffset": burn_data.ciphertext_validity_proof_instruction_offset, + "rangeProofInstructionOffset": burn_data.range_proof_instruction_offset, + }); + let map = value.as_object_mut().unwrap(); + // The permissioned burn authority and the owner/delegate are the + // trailing accounts; everything between the mint and them is optional + // proof material. Reserve those two when walking the proof accounts. + let mut offset = 2; + if offset < account_indexes.len() - 2 + && (burn_data.equality_proof_instruction_offset != 0 + || burn_data.ciphertext_validity_proof_instruction_offset != 0 + || burn_data.range_proof_instruction_offset != 0) + { + map.insert( + "instructionsSysvar".to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + // Assume that extra accounts are proof accounts and not multisig + // signers. This might be wrong, but it's the best possible option. + if offset < account_indexes.len() - 2 { + let label = if burn_data.equality_proof_instruction_offset == 0 { + "equalityProofContextStateAccount" + } else { + "equalityProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + if offset < account_indexes.len() - 2 { + let label = if burn_data.ciphertext_validity_proof_instruction_offset == 0 { + "ciphertextValidityProofContextStateAccount" + } else { + "ciphertextValidityProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + if offset < account_indexes.len() - 2 { + let label = if burn_data.range_proof_instruction_offset == 0 { + "rangeProofContextStateAccount" + } else { + "rangeProofRecordAccount" + }; + map.insert( + label.to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + } + map.insert( + "permissionedBurnAuthority".to_string(), + json!(account_keys[account_indexes[offset] as usize].to_string()), + ); + offset += 1; + parse_signers( + map, + offset, + account_keys, + account_indexes, + "authority", + "multisigAuthority", + ); + Ok(ParsedInstructionEnum { + instruction_type: "permissionedConfidentialBurn".to_string(), + info: value, + }) + } + } +} + +#[cfg(test)] +mod test { + use { + super::*, + bytemuck::Zeroable, + solana_instruction::{AccountMeta, Instruction}, + solana_message::Message, + solana_pubkey::Pubkey, + solana_zk_sdk_pod::encryption::{ + auth_encryption::PodAeCiphertext, elgamal::PodElGamalCiphertext, + }, + spl_token_2022_interface::{ + extension::permissioned_burn::instruction::{ + burn, confidential_burn_with_split_proofs, initialize, + }, + solana_zk_elgamal_proof_interface::proof_data::{ + BatchedGroupedCiphertext3HandlesValidityProofData, BatchedRangeProofU128Data, + CiphertextCommitmentEqualityProofData, + }, + }, + spl_token_confidential_transfer_proof_extraction::instruction::ProofLocation, + std::num::NonZero, + }; + + fn check_no_panic(mut instruction: Instruction) { + let account_meta = AccountMeta::new_readonly(Pubkey::new_unique(), false); + for i in 0..20 { + instruction.accounts = vec![account_meta.clone(); i]; + let message = Message::new(&[instruction.clone()], None); + let compiled_instruction = &message.instructions[0]; + let _ = parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None), + ); + } + } + + #[test] + fn test_parse_initialize_permissioned_burn_instruction() { + let mint = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let ix = initialize(&spl_token_2022_interface::id(), &mint, &authority).unwrap(); + let message = Message::new(&[ix], None); + let compiled_instruction = &message.instructions[0]; + assert_eq!( + parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "initializePermissionedBurnConfig".to_string(), + info: json!({ + "mint": mint.to_string(), + "authority": authority.to_string(), + }) + } + ); + } + + #[test] + fn test_parse_permissioned_burn_instruction() { + let account = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let permissioned_burn_authority = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let ix = burn( + &spl_token_2022_interface::id(), + &account, + &mint, + &permissioned_burn_authority, + &authority, + &[], + 42, + ) + .unwrap(); + let message = Message::new(&[ix], None); + let compiled_instruction = &message.instructions[0]; + assert_eq!( + parse_token( + compiled_instruction, + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "permissionedBurn".to_string(), + info: json!({ + "account": account.to_string(), + "mint": mint.to_string(), + "permissionedBurnAuthority": permissioned_burn_authority.to_string(), + "amount": "42", + "authority": authority.to_string(), + }) + } + ); + } + + #[test] + fn test_parse_permissioned_confidential_burn_instruction() { + for (equality_proof_location, ciphertext_validity_proof_location, range_proof_location) in [ + ( + ProofLocation::InstructionOffset( + NonZero::new(1).unwrap(), + &CiphertextCommitmentEqualityProofData::zeroed(), + ), + ProofLocation::InstructionOffset( + NonZero::new(2).unwrap(), + &BatchedGroupedCiphertext3HandlesValidityProofData::zeroed(), + ), + ProofLocation::InstructionOffset( + NonZero::new(3).unwrap(), + &BatchedRangeProofU128Data::zeroed(), + ), + ), + ( + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ProofLocation::ContextStateAccount(&Pubkey::new_unique()), + ), + ] { + let instructions = confidential_burn_with_split_proofs( + &spl_token_2022_interface::id(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &PodAeCiphertext::default(), + &PodElGamalCiphertext::default(), + &PodElGamalCiphertext::default(), + &Pubkey::new_unique(), + &[], + equality_proof_location, + ciphertext_validity_proof_location, + range_proof_location, + ) + .unwrap(); + check_no_panic(instructions[0].clone()); + } + } +} diff --git a/transaction-view/Cargo.toml b/transaction-view/Cargo.toml index cebbfeff6a0..0c278922281 100644 --- a/transaction-view/Cargo.toml +++ b/transaction-view/Cargo.toml @@ -7,7 +7,12 @@ authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -17,14 +22,12 @@ dev-context-only-utils = [] solana-hash = { workspace = true } solana-message = { workspace = true } solana-packet = { workspace = true } -solana-program-runtime = { workspace = true } solana-pubkey = { workspace = true } solana-sdk-ids = { workspace = true } solana-short-vec = { workspace = true } solana-signature = { workspace = true } solana-svm-transaction = { workspace = true } solana-transaction = { workspace = true } -solana-transaction-context = { workspace = true } [dev-dependencies] # See order-crates-for-publishing.py for using this unusual `path = "."` @@ -34,6 +37,7 @@ criterion = { workspace = true } solana-instruction = { workspace = true } solana-keypair = { workspace = true } solana-message = { workspace = true, features = ["serde"] } +solana-pubkey = { workspace = true, features = ["rand"] } solana-signature = { workspace = true, features = ["serde"] } solana-signer = { workspace = true } solana-system-interface = { workspace = true, features = ["wincode"] } @@ -47,3 +51,6 @@ harness = false [[bench]] name = "transaction_view" harness = false + +[lints] +workspace = true diff --git a/transaction-view/benches/transaction_view.rs b/transaction-view/benches/transaction_view.rs index 262c5ee84b6..d4c0b74f287 100644 --- a/transaction-view/benches/transaction_view.rs +++ b/transaction-view/benches/transaction_view.rs @@ -1,5 +1,5 @@ use { - agave_transaction_view::transaction_view::TransactionView, + agave_transaction_view::{sanitize::SanitizeConfig, transaction_view::TransactionView}, criterion::{ BenchmarkGroup, Criterion, Throughput, criterion_group, criterion_main, measurement::Measurement, @@ -22,6 +22,14 @@ use { const NUM_TRANSACTIONS: usize = 1024; +// Current protocol values; production callers supply these from agave. +const SANITIZE_CONFIG: SanitizeConfig = SanitizeConfig { + min_requested_heap_size: 32 * 1024, + max_requested_heap_size: 256 * 1024, + max_instructions: 64, + max_accounts_per_instruction: Some(255), +}; + fn serialize_transactions(transactions: Vec) -> Vec> { transactions .into_iter() @@ -66,7 +74,8 @@ fn bench_transactions_parsing( c.iter(|| { for bytes in serialized_transactions.iter() { let _ = - TransactionView::try_new_sanitized(black_box(bytes.as_ref()), true).unwrap(); + TransactionView::try_new_sanitized(black_box(bytes.as_ref()), &SANITIZE_CONFIG) + .unwrap(); } }); }); diff --git a/transaction-view/src/instructions_frame.rs b/transaction-view/src/instructions_frame.rs index 12fdf5f8c5f..4a91437a1f1 100644 --- a/transaction-view/src/instructions_frame.rs +++ b/transaction-view/src/instructions_frame.rs @@ -558,9 +558,9 @@ mod tests { ..solana_message::v1::Message::default() }; - let serialized = solana_message::v1::serialize(&message); + let serialized = solana_message::v1::Message::serialize(&message); - let mut offset = 41; // instruction headers starts at offset 41 for no config, 0 addresses, per spec + let mut offset = 42; // 1-byte V1_PREFIX + 41-byte header (no config, 0 addresses), per spec let instructions_frame = InstructionsFrame::try_new_for_v1(&serialized, &mut offset, 2).unwrap(); diff --git a/transaction-view/src/lib.rs b/transaction-view/src/lib.rs index 6882958f92e..83ce9626487 100644 --- a/transaction-view/src/lib.rs +++ b/transaction-view/src/lib.rs @@ -10,7 +10,7 @@ mod instructions_frame; mod message_header_frame; pub mod resolved_transaction_view; pub mod result; -mod sanitize; +pub mod sanitize; mod signature_frame; mod static_account_keys_frame; mod transaction_config_frame; diff --git a/transaction-view/src/resolved_transaction_view.rs b/transaction-view/src/resolved_transaction_view.rs index 5157d1a888c..c8f945e270a 100644 --- a/transaction-view/src/resolved_transaction_view.rs +++ b/transaction-view/src/resolved_transaction_view.rs @@ -260,7 +260,7 @@ impl Debug for ResolvedTransactionView { mod tests { use { super::*, - crate::transaction_view::SanitizedTransactionView, + crate::{sanitize::SanitizeConfig, transaction_view::SanitizedTransactionView}, solana_message::{ MessageHeader, VersionedMessage, compiled_instruction::CompiledInstruction, @@ -271,6 +271,16 @@ mod tests { solana_transaction::versioned::VersionedTransaction, }; + // Current protocol values; production callers supply these from agave. + fn test_config() -> SanitizeConfig { + SanitizeConfig { + min_requested_heap_size: 32 * 1024, + max_requested_heap_size: 256 * 1024, + max_instructions: 64, + max_accounts_per_instruction: Some(255), + } + } + #[test] fn test_expected_loaded_addresses() { // Expected addresses passed in, but `None` was passed. @@ -294,7 +304,8 @@ mod tests { }), }; let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), true).unwrap(); + let view = + SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()).unwrap(); let result = ResolvedTransactionView::try_new(view, None, &HashSet::default()); assert!(matches!( result, @@ -325,7 +336,8 @@ mod tests { }), }; let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), true).unwrap(); + let view = + SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()).unwrap(); let result = ResolvedTransactionView::try_new(view, Some(loaded_addresses), &HashSet::default()); assert!(matches!( @@ -362,7 +374,8 @@ mod tests { }), }; let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), true).unwrap(); + let view = + SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()).unwrap(); let result = ResolvedTransactionView::try_new(view, Some(loaded_addresses), &HashSet::default()); assert!(matches!( @@ -412,7 +425,8 @@ mod tests { }; let transaction = create_transaction_with_keys(static_keys, &loaded_addresses); let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), true).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()) + .unwrap(); let resolved_view = ResolvedTransactionView::try_new( view, Some(loaded_addresses), @@ -435,7 +449,8 @@ mod tests { }; let transaction = create_transaction_with_keys(static_keys, &loaded_addresses); let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), true).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()) + .unwrap(); let resolved_view = ResolvedTransactionView::try_new( view, Some(loaded_addresses), @@ -458,7 +473,8 @@ mod tests { }; let transaction = create_transaction_with_keys(static_keys, &loaded_addresses); let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), true).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()) + .unwrap(); let resolved_view = ResolvedTransactionView::try_new( view, Some(loaded_addresses), @@ -519,7 +535,8 @@ mod tests { let static_keys = vec![key0, key1, key2]; let transaction = create_transaction_with_static_keys(static_keys, &loaded_addresses); let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), true).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()) + .unwrap(); let resolved_view = ResolvedTransactionView::try_new( view, Some(loaded_addresses.clone()), @@ -538,7 +555,8 @@ mod tests { let static_keys = vec![key0, key1, bpf_loader_upgradeable::ID]; let transaction = create_transaction_with_static_keys(static_keys, &loaded_addresses); let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), true).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()) + .unwrap(); let resolved_view = ResolvedTransactionView::try_new( view, Some(loaded_addresses.clone()), @@ -561,7 +579,8 @@ mod tests { }; let transaction = create_transaction_with_static_keys(static_keys, &loaded_addresses); let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), true).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()) + .unwrap(); let resolved_view = ResolvedTransactionView::try_new( view, diff --git a/transaction-view/src/sanitize.rs b/transaction-view/src/sanitize.rs index 6c5d98e8649..88d4eef8978 100644 --- a/transaction-view/src/sanitize.rs +++ b/transaction-view/src/sanitize.rs @@ -1,24 +1,38 @@ -use { - crate::{ - result::{Result, TransactionViewError}, - signature_frame::MAX_SIGNATURES_PER_PACKET, - transaction_data::TransactionData, - transaction_version::TransactionVersion, - transaction_view::UnsanitizedTransactionView, - }, - solana_program_runtime::execution_budget::{MAX_HEAP_FRAME_BYTES, MIN_HEAP_FRAME_BYTES}, +use crate::{ + result::{Result, TransactionViewError}, + signature_frame::MAX_SIGNATURES_PER_PACKET, + transaction_data::TransactionData, + transaction_version::TransactionVersion, + transaction_view::UnsanitizedTransactionView, }; +/// Protocol limits enforced during sanitization. +/// +/// These values are consensus parameters owned by the caller; this crate +/// intentionally does not define defaults for them. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct SanitizeConfig { + /// Inclusive lower bound for a V1 requested heap size, in bytes. + pub min_requested_heap_size: u32, + /// Inclusive upper bound for a V1 requested heap size, in bytes. + pub max_requested_heap_size: u32, + /// SIMD-160: maximum number of top-level instructions. + pub max_instructions: usize, + /// SIMD-406: maximum number of accounts per instruction. + /// `None` means the limit is not enforced. + pub max_accounts_per_instruction: Option, +} + pub(crate) fn sanitize( view: &UnsanitizedTransactionView, - enable_instruction_accounts_limit: bool, + config: &SanitizeConfig, ) -> Result<()> { sanitize_transaction_size(view)?; sanitize_message_header(view)?; - sanitize_config(view)?; + sanitize_config(view, config)?; sanitize_signatures(view)?; sanitize_account_access(view)?; - sanitize_instructions(view, enable_instruction_accounts_limit)?; + sanitize_instructions(view, config)?; sanitize_address_table_lookups(view) } @@ -67,17 +81,18 @@ fn sanitize_message_header(view: &UnsanitizedTransactionView) -> Result<()> { - #[allow(clippy::collapsible_if)] +fn sanitize_config( + view: &UnsanitizedTransactionView, + config: &SanitizeConfig, +) -> Result<()> { if let Some(requested_heap_bytes) = view .transaction_config() .and_then(|config| config.requested_heap_size()) + && (!(config.min_requested_heap_size..=config.max_requested_heap_size) + .contains(&requested_heap_bytes) + || !requested_heap_bytes.is_multiple_of(1024)) { - if !(MIN_HEAP_FRAME_BYTES..=MAX_HEAP_FRAME_BYTES).contains(&requested_heap_bytes) - || !requested_heap_bytes.is_multiple_of(1024) - { - return Err(TransactionViewError::SanitizeError); - } + return Err(TransactionViewError::SanitizeError); } Ok(()) @@ -133,12 +148,10 @@ fn sanitize_account_access(view: &UnsanitizedTransactionView, - enable_instruction_accounts_limit: bool, + config: &SanitizeConfig, ) -> Result<()> { // SIMD-160: transaction can not have more than 64 top level instructions - if usize::from(view.num_instructions()) - > solana_transaction_context::MAX_INSTRUCTION_TRACE_LENGTH - { + if usize::from(view.num_instructions()) > config.max_instructions { return Err(TransactionViewError::SanitizeError); } @@ -165,8 +178,8 @@ fn sanitize_instructions( } } - if enable_instruction_accounts_limit - && instruction.accounts.len() > solana_transaction_context::MAX_ACCOUNTS_PER_INSTRUCTION + if let Some(max_accounts_per_instruction) = config.max_accounts_per_instruction + && instruction.accounts.len() > max_accounts_per_instruction { return Err(TransactionViewError::SanitizeError); } @@ -214,6 +227,16 @@ mod tests { solana_transaction::versioned::VersionedTransaction, }; + // Current protocol values; production callers supply these from agave. + fn test_config() -> SanitizeConfig { + SanitizeConfig { + min_requested_heap_size: 32 * 1024, + max_requested_heap_size: 256 * 1024, + max_instructions: 64, + max_accounts_per_instruction: Some(255), + } + } + fn create_legacy_transaction( num_signatures: u8, header: MessageHeader, @@ -288,7 +311,7 @@ mod tests { let transaction = multiple_transfers(); let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert!(view.sanitize(true).is_ok()); + assert!(view.sanitize(&test_config()).is_ok()); } #[test] @@ -638,7 +661,7 @@ mod tests { ); let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert!(sanitize_instructions(&view, true).is_ok()); + assert!(sanitize_instructions(&view, &test_config()).is_ok()); let transaction = create_v0_transaction( num_signatures, @@ -649,7 +672,7 @@ mod tests { ); let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert!(sanitize_instructions(&view, true).is_ok()); + assert!(sanitize_instructions(&view, &test_config()).is_ok()); } for instruction_index in 0..valid_instructions.len() { @@ -666,7 +689,7 @@ mod tests { let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); assert_eq!( - sanitize_instructions(&view, true), + sanitize_instructions(&view, &test_config()), Err(TransactionViewError::SanitizeError) ); } @@ -685,7 +708,7 @@ mod tests { let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); assert_eq!( - sanitize_instructions(&view, true), + sanitize_instructions(&view, &test_config()), Err(TransactionViewError::SanitizeError) ); } @@ -703,7 +726,7 @@ mod tests { let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); assert_eq!( - sanitize_instructions(&view, true), + sanitize_instructions(&view, &test_config()), Err(TransactionViewError::SanitizeError) ); } @@ -723,7 +746,7 @@ mod tests { let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); assert_eq!( - sanitize_instructions(&view, true), + sanitize_instructions(&view, &test_config()), Err(TransactionViewError::SanitizeError) ); } @@ -747,7 +770,7 @@ mod tests { let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); assert_eq!( - sanitize_instructions(&view, true), + sanitize_instructions(&view, &test_config()), Err(TransactionViewError::SanitizeError) ); } @@ -770,7 +793,7 @@ mod tests { let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); assert_eq!( - sanitize_instructions(&view, true), + sanitize_instructions(&view, &test_config()), Err(TransactionViewError::SanitizeError) ); @@ -784,7 +807,7 @@ mod tests { let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); assert_eq!( - sanitize_instructions(&view, true), + sanitize_instructions(&view, &test_config()), Err(TransactionViewError::SanitizeError) ); } @@ -804,7 +827,7 @@ mod tests { let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); assert_eq!( - sanitize_instructions(&view, true), + sanitize_instructions(&view, &test_config()), Err(TransactionViewError::SanitizeError) ); } @@ -823,7 +846,7 @@ mod tests { let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); // Exactly 255 accounts must pass sanitization. - assert!(sanitize_instructions(&view, true).is_ok()); + assert!(sanitize_instructions(&view, &test_config()).is_ok()); } } @@ -885,11 +908,11 @@ mod tests { }, (0..2).map(|_| Pubkey::new_unique()).collect(), vec![], - TransactionConfig::empty().with_heap_size(MIN_HEAP_FRAME_BYTES), + TransactionConfig::empty().with_heap_size(test_config().min_requested_heap_size), ); let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert!(sanitize_config(&view).is_ok()); + assert!(sanitize_config(&view, &test_config()).is_ok()); } // Valid max heap size. @@ -903,11 +926,11 @@ mod tests { }, (0..2).map(|_| Pubkey::new_unique()).collect(), vec![], - TransactionConfig::empty().with_heap_size(MAX_HEAP_FRAME_BYTES), + TransactionConfig::empty().with_heap_size(test_config().max_requested_heap_size), ); let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert!(sanitize_config(&view).is_ok()); + assert!(sanitize_config(&view, &test_config()).is_ok()); } // Heap size below min. @@ -921,12 +944,13 @@ mod tests { }, (0..2).map(|_| Pubkey::new_unique()).collect(), vec![], - TransactionConfig::empty().with_heap_size(MIN_HEAP_FRAME_BYTES - 1), + TransactionConfig::empty() + .with_heap_size(test_config().min_requested_heap_size - 1), ); let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); assert_eq!( - sanitize_config(&view), + sanitize_config(&view, &test_config()), Err(TransactionViewError::SanitizeError) ); } @@ -942,12 +966,13 @@ mod tests { }, (0..2).map(|_| Pubkey::new_unique()).collect(), vec![], - TransactionConfig::empty().with_heap_size(MAX_HEAP_FRAME_BYTES + 1), + TransactionConfig::empty() + .with_heap_size(test_config().max_requested_heap_size + 1), ); let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); assert_eq!( - sanitize_config(&view), + sanitize_config(&view, &test_config()), Err(TransactionViewError::SanitizeError) ); } @@ -963,12 +988,13 @@ mod tests { }, (0..2).map(|_| Pubkey::new_unique()).collect(), vec![], - TransactionConfig::empty().with_heap_size(MIN_HEAP_FRAME_BYTES + 1), + TransactionConfig::empty() + .with_heap_size(test_config().min_requested_heap_size + 1), ); let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); assert_eq!( - sanitize_config(&view), + sanitize_config(&view, &test_config()), Err(TransactionViewError::SanitizeError) ); } @@ -988,7 +1014,7 @@ mod tests { ); let data = wincode::serialize(&transaction).unwrap(); let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert!(sanitize_config(&view).is_ok()); + assert!(sanitize_config(&view, &test_config()).is_ok()); } } } diff --git a/transaction-view/src/transaction_view.rs b/transaction-view/src/transaction_view.rs index 5d7e9d3d8bc..503743b9069 100644 --- a/transaction-view/src/transaction_view.rs +++ b/transaction-view/src/transaction_view.rs @@ -1,9 +1,13 @@ use { crate::{ address_table_lookup_frame::AddressTableLookupIterator, - instructions_frame::InstructionsIterator, result::Result, sanitize::sanitize, - transaction_config_frame::TransactionConfigView, transaction_data::TransactionData, - transaction_frame::TransactionFrame, transaction_version::TransactionVersion, + instructions_frame::InstructionsIterator, + result::Result, + sanitize::{SanitizeConfig, sanitize}, + transaction_config_frame::TransactionConfigView, + transaction_data::TransactionData, + transaction_frame::TransactionFrame, + transaction_version::TransactionVersion, }, core::fmt::{Debug, Formatter}, solana_hash::Hash, @@ -39,11 +43,8 @@ impl TransactionView { } /// Sanitizes the transaction view, returning a sanitized view on success. - pub fn sanitize( - self, - enable_instruction_accounts_limit: bool, - ) -> Result> { - sanitize(&self, enable_instruction_accounts_limit)?; + pub fn sanitize(self, config: &SanitizeConfig) -> Result> { + sanitize(&self, config)?; Ok(SanitizedTransactionView { data: self.data, frame: self.frame, @@ -53,9 +54,9 @@ impl TransactionView { impl TransactionView { /// Creates a new `TransactionView`, running sanitization checks. - pub fn try_new_sanitized(data: D, enable_instruction_accounts_limit: bool) -> Result { + pub fn try_new_sanitized(data: D, config: &SanitizeConfig) -> Result { let unsanitized_view = TransactionView::try_new_unsanitized(data)?; - unsanitized_view.sanitize(enable_instruction_accounts_limit) + unsanitized_view.sanitize(config) } } diff --git a/turbine/Cargo.toml b/turbine/Cargo.toml index a39d4da9181..58500b78282 100644 --- a/turbine/Cargo.toml +++ b/turbine/Cargo.toml @@ -7,7 +7,12 @@ description = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } -edition = "2024" +edition = { workspace = true } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] @@ -16,7 +21,6 @@ agave-unstable-api = [] agave-feature-set = { workspace = true } agave-votor = { workspace = true } agave-votor-messages = { workspace = true } -agave-xdp = { workspace = true } bytes = { workspace = true } crossbeam-channel = { workspace = true } itertools = { workspace = true } @@ -33,6 +37,7 @@ solana-entry = { workspace = true } solana-gossip = { workspace = true } solana-hash = { workspace = true } solana-keypair = { workspace = true } +solana-leader-schedule = { workspace = true } solana-ledger = { workspace = true } solana-measure = { workspace = true } solana-metrics = { workspace = true } @@ -62,7 +67,6 @@ bencher = { workspace = true } bs58 = { workspace = true } solana-genesis-config = { workspace = true } solana-gossip = { path = "../gossip", features = ["agave-unstable-api", "dev-context-only-utils"] } -solana-leader-schedule = { workspace = true } solana-ledger = { path = "../ledger", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-signature = { workspace = true, features = ["rand"] } @@ -77,3 +81,6 @@ harness = false [[bench]] name = "cluster_nodes" harness = false + +[lints] +workspace = true diff --git a/turbine/src/broadcast_stage.rs b/turbine/src/broadcast_stage.rs index e899e68ed2c..a8a6b6e7dff 100644 --- a/turbine/src/broadcast_stage.rs +++ b/turbine/src/broadcast_stage.rs @@ -11,16 +11,21 @@ use { cluster_nodes::{ClusterNodes, ClusterNodesCache}, }, agave_votor::event::VotorEventSender, - crossbeam_channel::{Receiver, RecvError, RecvTimeoutError, Sender, unbounded}, + crossbeam_channel::{ + Receiver, RecvError, RecvTimeoutError, SendError, Sender, TrySendError, bounded, + }, itertools::Itertools, - solana_clock::{NUM_CONSECUTIVE_LEADER_SLOTS, Slot}, + solana_clock::Slot, solana_gossip::{ cluster_info::{ClusterInfo, ClusterInfoError}, contact_info::Protocol, }, solana_keypair::Keypair, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_ledger::{ - blockstore::Blockstore, leader_schedule_cache::LeaderScheduleCache, shred::Shred, + blockstore::Blockstore, + leader_schedule_cache::LeaderScheduleCache, + shred::{MAX_FEC_SETS_PER_SLOT, Shred}, }, solana_measure::measure::Measure, solana_metrics::inc_new_counter_error, @@ -59,9 +64,54 @@ const _: () = const { const CLUSTER_NODES_CACHE_NUM_EPOCH_CAP: usize = MAX_LEADER_SCHEDULE_STAKES as usize; const CLUSTER_NODES_CACHE_TTL: Duration = Duration::from_secs(5); +// Capacity in batches. One batch typically packs 1-2 FEC sets worth of bytes +// (see get_target_batch_bytes_default), so this is at least 4 slots' worth of +// broadcast traffic. Filling the channel means the consumer (blockstore record +// or socket transmit) has fallen at least 4 slots behind, which means we've +// been skipped already. +const BROADCAST_CHANNEL_CAPACITY: usize = MAX_FEC_SETS_PER_SLOT as usize * 4; + pub(crate) type RecordReceiver = Receiver<(Arc>, Option)>; pub(crate) type TransmitReceiver = Receiver<(Arc>, Option)>; +/// Try to send into a bounded channel. If the channel is full, +/// log an error and fall back to a blocking send so shreds are not dropped. +/// A full channel means there is something very wrong with our node, +/// so we want a loud signal but must preserve shreds so the validator +/// does not start repairing its own blocks. +fn try_send_or_block( + sender: &Sender, + msg: T, + channel_name: &str, +) -> std::result::Result<(), SendError> { + match sender.try_send(msg) { + Ok(()) => Ok(()), + Err(TrySendError::Full(msg)) => { + error!("The channel to {channel_name} is full, node is resource starved!"); + sender.send(msg) + } + Err(TrySendError::Disconnected(msg)) => Err(SendError(msg)), + } +} + +/// Fan a batch of shreds out to both the blockstore record path and the +/// network transmit path. Each leg uses [`try_send_or_block`] so any full +/// channel is logged loudly but never drops shreds. +#[allow(clippy::type_complexity)] +pub(crate) fn dispatch_shreds( + blockstore_sender: &Sender<(Arc>, Option)>, + socket_sender: &Sender<(Arc>, Option)>, + shreds: Arc>, + batch_info: Option, +) -> std::result::Result<(), SendError<(Arc>, Option)>> { + try_send_or_block( + blockstore_sender, + (shreds.clone(), batch_info.clone()), + "blockstore", + )?; + try_send_or_block(socket_sender, (shreds, batch_info), "network egress") +} + #[derive(Debug, Error)] pub enum Error { #[error(transparent)] @@ -274,8 +324,8 @@ impl BroadcastStage { mut broadcast_stage_run: impl BroadcastRun + Send + 'static + Clone, xdp_sender: Option, ) -> Self { - let (socket_sender, socket_receiver) = unbounded(); - let (blockstore_sender, blockstore_receiver) = unbounded(); + let (socket_sender, socket_receiver) = bounded(BROADCAST_CHANNEL_CAPACITY); + let (blockstore_sender, blockstore_receiver) = bounded(BROADCAST_CHANNEL_CAPACITY); let bs_run = broadcast_stage_run.clone(); let socket_sender_ = socket_sender.clone(); @@ -418,7 +468,7 @@ impl BroadcastStage { .all(|shred| shred.slot() == new_retransmit_slot) ); if !data_shreds.is_empty() { - socket_sender.send((data_shreds, None))?; + try_send_or_block(socket_sender, (data_shreds, None), "network egress")?; } let coding_shreds = Arc::new( @@ -433,7 +483,7 @@ impl BroadcastStage { .all(|shred| shred.slot() == new_retransmit_slot) ); if !coding_shreds.is_empty() { - socket_sender.send((coding_shreds, None))?; + try_send_or_block(socket_sender, (coding_shreds, None), "network egress")?; } } @@ -470,7 +520,7 @@ fn next_broadcast_leader_pubkey( my_pubkey: &Pubkey, slot: Slot, ) -> Option { - let next_leader_slot = slot.saturating_add(NUM_CONSECUTIVE_LEADER_SLOTS); + let next_leader_slot = slot.saturating_add(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot); leader_schedule_cache .slot_leader_at(next_leader_slot, Some(working_bank)) .map(|next_leader| next_leader.id) @@ -582,7 +632,7 @@ pub mod test { use { super::*, agave_votor_messages::migration::MigrationStatus, - crossbeam_channel::{bounded, unbounded}, + crossbeam_channel::bounded, rand::Rng, solana_entry::{entry::create_ticks, entry_or_marker::EntryOrMarker}, solana_gossip::{cluster_info::ClusterInfo, node::Node}, @@ -706,7 +756,7 @@ pub mod test { vote_address: Pubkey::new_unique(), }, ], - NUM_CONSECUTIVE_LEADER_SLOTS as usize, + NUM_CONSECUTIVE_LEADER_SLOTS.get(), &bank, ); @@ -719,7 +769,7 @@ pub mod test { &leader_schedule_cache, &bank, &leader_b.pubkey(), - NUM_CONSECUTIVE_LEADER_SLOTS, + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot, ), Some(leader_a.pubkey()) ); @@ -728,7 +778,7 @@ pub mod test { &leader_schedule_cache, &bank, &leader_a.pubkey(), - NUM_CONSECUTIVE_LEADER_SLOTS, + NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot, ), None, ); @@ -739,8 +789,8 @@ pub mod test { // Setup let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Arc::new(Blockstore::open(ledger_path.path()).unwrap()); - let (transmit_sender, transmit_receiver) = unbounded(); - let (retransmit_slots_sender, retransmit_slots_receiver) = unbounded(); + let (transmit_sender, transmit_receiver) = bounded(1024); + let (retransmit_slots_sender, retransmit_slots_receiver) = bounded(1024); // Make some shreds let updated_slot = 0; @@ -751,12 +801,8 @@ pub mod test { assert!(num_data_shreds >= 10); // Insert all the shreds - blockstore - .insert_shreds(all_data_shreds, None, true) - .unwrap(); - blockstore - .insert_shreds(all_coding_shreds, None, true) - .unwrap(); + blockstore.insert_shreds(all_data_shreds, true).unwrap(); + blockstore.insert_shreds(all_coding_shreds, true).unwrap(); // Insert duplicate retransmit signal, blocks should // only be retransmitted once @@ -864,8 +910,8 @@ pub mod test { // Create the leader scheduler let leader_keypair = Arc::new(Keypair::new()); - let (entry_sender, entry_receiver) = unbounded(); - let (retransmit_slots_sender, retransmit_slots_receiver) = unbounded(); + let (entry_sender, entry_receiver) = bounded(1024); + let (retransmit_slots_sender, retransmit_slots_receiver) = bounded(1024); let broadcast_service = setup_dummy_broadcast_service( leader_keypair, ledger_path.path(), diff --git a/turbine/src/broadcast_stage/broadcast_duplicates_run.rs b/turbine/src/broadcast_stage/broadcast_duplicates_run.rs index dec8495b624..77ce6de9840 100644 --- a/turbine/src/broadcast_stage/broadcast_duplicates_run.rs +++ b/turbine/src/broadcast_stage/broadcast_duplicates_run.rs @@ -263,8 +263,6 @@ impl BroadcastRun for BroadcastDuplicatesRun { if !data_shreds.is_empty() { let data_shreds = Arc::new(data_shreds); - blockstore_sender.send((data_shreds.clone(), None))?; - // 3) Start broadcast step info!( "{} Sending good shreds for slot {} to network", @@ -272,7 +270,7 @@ impl BroadcastRun for BroadcastDuplicatesRun { data_shreds.first().unwrap().slot() ); assert!(data_shreds.iter().all(|shred| shred.slot() == bank.slot())); - socket_sender.send((data_shreds, None))?; + dispatch_shreds(blockstore_sender, socket_sender, data_shreds, None)?; } // Special handling of last shred to cause partition @@ -294,9 +292,6 @@ impl BroadcastRun for BroadcastDuplicatesRun { let original_last_data_shred = Arc::new(original_last_data_shred); let partition_last_data_shred = Arc::new(partition_last_data_shred); - // Store the original shreds that this node replayed - blockstore_sender.send((original_last_data_shred.clone(), None))?; - assert!( original_last_data_shred .iter() @@ -311,7 +306,15 @@ impl BroadcastRun for BroadcastDuplicatesRun { if let Some(duplicate_slot_sender) = &self.config.duplicate_slot_sender { let _ = duplicate_slot_sender.send(bank.slot()); } - socket_sender.send((original_last_data_shred, None))?; + // Store the original shreds that this node replayed; + dispatch_shreds( + blockstore_sender, + socket_sender, + original_last_data_shred, + None, + )?; + // the partition shreds go only over the wire to create the duplicate slot. + // blocking here is ok since this is only ever used in tests. socket_sender.send((partition_last_data_shred, None))?; } @@ -442,7 +445,7 @@ impl BroadcastRun for BroadcastDuplicatesRun { fn record(&mut self, receiver: &RecordReceiver, blockstore: &Blockstore) -> Result<()> { let (all_shreds, _) = receiver.recv()?; blockstore - .insert_shreds(all_shreds.to_vec(), None, true) + .insert_shreds(all_shreds.to_vec(), true) .expect("Failed to insert shreds in blockstore"); Ok(()) } diff --git a/turbine/src/broadcast_stage/broadcast_metrics.rs b/turbine/src/broadcast_stage/broadcast_metrics.rs index 57926b400e8..eab86ffd02e 100644 --- a/turbine/src/broadcast_stage/broadcast_metrics.rs +++ b/turbine/src/broadcast_stage/broadcast_metrics.rs @@ -307,7 +307,7 @@ mod test { let slot_broadcast_stats = Arc::new(Mutex::new(SlotBroadcastStats::default())); let num_threads = 5; let slot = 0; - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let thread_handles: Vec<_> = (0..num_threads) .map(|i| { let slot_broadcast_stats = slot_broadcast_stats.clone(); diff --git a/turbine/src/broadcast_stage/broadcast_utils.rs b/turbine/src/broadcast_stage/broadcast_utils.rs index 1ea9a50a982..d90adaaf772 100644 --- a/turbine/src/broadcast_stage/broadcast_utils.rs +++ b/turbine/src/broadcast_stage/broadcast_utils.rs @@ -252,7 +252,7 @@ pub(super) fn set_block_id_and_send( mod tests { use { super::*, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_entry::entry_or_marker::EntryOrMarker, solana_genesis_config::GenesisConfig, solana_ledger::genesis_utils::{GenesisConfigInfo, create_genesis_config}, @@ -309,7 +309,7 @@ mod tests { SlotLeader::default(), 1, ); - let (s, r) = unbounded(); + let (s, r) = bounded(1024); let mut last_hash = genesis_config.hash(); assert!(bank1.max_tick_height() > 1); @@ -342,7 +342,7 @@ mod tests { fn test_recv_slot_components_does_not_pre_drain_queue() { let (genesis_config, bank0, _bank_forks, tx) = setup_test(); let bank1 = Arc::new(Bank::new_from_parent(bank0, SlotLeader::default(), 1)); - let (s, r) = unbounded(); + let (s, r) = bounded(1024); let mut last_hash = genesis_config.hash(); let entries: Vec<_> = (1..=3) .map(|tick_height| { @@ -384,7 +384,7 @@ mod tests { SlotLeader::default(), 2, ); - let (s, r) = unbounded(); + let (s, r) = bounded(1024); let mut last_hash = genesis_config.hash(); assert!(bank1.max_tick_height() > 1); @@ -433,7 +433,7 @@ mod tests { fn test_marker_carryover_does_not_advance_last_tick_height() { let (genesis_config, bank0, _bank_forks, tx) = setup_test(); let bank1 = Arc::new(Bank::new_from_parent(bank0, SlotLeader::default(), 1)); - let (s, r) = unbounded(); + let (s, r) = bounded(1024); let max_tick = bank1.max_tick_height(); let mut last_hash = genesis_config.hash(); @@ -444,7 +444,7 @@ mod tests { .unwrap(); } - let marker = solana_entry::block_component::VersionedBlockMarker::new_block_header( + let marker = solana_entry::block_component::VersionedBlockMarker::from_block_header( solana_entry::block_component::BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), @@ -471,7 +471,7 @@ mod tests { fn test_marker_preserves_entry_ordering() { let (genesis_config, bank0, _bank_forks, tx) = setup_test(); let bank1 = Arc::new(Bank::new_from_parent(bank0, SlotLeader::default(), 1)); - let (s, r) = unbounded(); + let (s, r) = bounded(1024); let mut last_hash = genesis_config.hash(); @@ -480,7 +480,7 @@ mod tests { s.send((bank1.clone(), (EntryOrMarker::Entry(entry1.clone()), 1))) .unwrap(); - let marker = solana_entry::block_component::VersionedBlockMarker::new_block_header( + let marker = solana_entry::block_component::VersionedBlockMarker::from_block_header( solana_entry::block_component::BlockHeaderV1 { parent_slot: 0, parent_block_id: Hash::default(), @@ -533,7 +533,7 @@ mod tests { SlotLeader::default(), 2, )); - let (s, r) = unbounded(); + let (s, r) = bounded(1024); let mut last_hash = genesis_config.hash(); @@ -546,7 +546,7 @@ mod tests { // Bank changes to bank2, but the next item is a marker with tick_height=3 — this should // leave entries empty after the clear. The stale last_tick_height (5, from bank1) must not // leak into subsequent results. - let marker = solana_entry::block_component::VersionedBlockMarker::new_block_header( + let marker = solana_entry::block_component::VersionedBlockMarker::from_block_header( solana_entry::block_component::BlockHeaderV1 { parent_slot: 1, parent_block_id: Hash::default(), diff --git a/turbine/src/broadcast_stage/standard_broadcast_run.rs b/turbine/src/broadcast_stage/standard_broadcast_run.rs index 5df7a2289ac..4d20cc047f1 100644 --- a/turbine/src/broadcast_stage/standard_broadcast_run.rs +++ b/turbine/src/broadcast_stage/standard_broadcast_run.rs @@ -24,9 +24,14 @@ use { solana_runtime::bank::Bank, solana_sha256_hasher::hashv, solana_time_utils::AtomicInterval, - std::{borrow::Cow, sync::RwLock}, + std::{borrow::Cow, collections::VecDeque, sync::RwLock}, }; +// Expect blacklist events to be extremely rare, so we can tightly bound the +// data structure. Need to be able to hold a full leader span, and we'll go +// ahead and overprovision a bit just in case. +const MAX_BROADCAST_BLACKLIST_SIZE: usize = 16; + #[derive(Clone)] pub struct StandardBroadcastRun { slot: Slot, @@ -59,6 +64,7 @@ pub struct StandardBroadcastRun { votor_event_sender: VotorEventSender, max_data_shreds_per_slot: u32, max_code_shreds_per_slot: u32, + broadcast_blacklist: VecDeque, } #[derive(Debug)] @@ -81,7 +87,10 @@ impl StandardBroadcastRun { slot: Slot::MAX, parent: Slot::MAX, parent_block_id: Hash::default(), - parent_for_double_merkle: (Slot::MAX, Hash::default()), + parent_for_double_merkle: Block { + slot: Slot::MAX, + block_id: Hash::default(), + }, chained_merkle_root: Hash::default(), double_merkle_leaves: vec![], carryover_entry: None, @@ -102,6 +111,22 @@ impl StandardBroadcastRun { votor_event_sender, max_data_shreds_per_slot: DEFAULT_MAX_DATA_SHREDS_PER_SLOT, max_code_shreds_per_slot: DEFAULT_MAX_CODE_SHREDS_PER_SLOT, + broadcast_blacklist: VecDeque::new(), + } + } + + fn is_broadcast_blacklisted(&self, slot: Slot) -> bool { + self.broadcast_blacklist.contains(&slot) + } + + fn blacklist_broadcast_slot(&mut self, slot: Slot) { + if self.is_broadcast_blacklisted(slot) { + return; + } + + self.broadcast_blacklist.push_back(slot); + while self.broadcast_blacklist.len() > MAX_BROADCAST_BLACKLIST_SIZE { + self.broadcast_blacklist.pop_front(); } } @@ -140,7 +165,10 @@ impl StandardBroadcastRun { self.slot = bank.slot(); self.parent = bank.parent_slot(); self.parent_block_id = parent_block_id; - self.parent_for_double_merkle = (bank.parent_slot(), parent_block_id); + self.parent_for_double_merkle = Block { + slot: bank.parent_slot(), + block_id: parent_block_id, + }; self.chained_merkle_root = chained_merkle_root; self.double_merkle_leaves.clear(); self.next_shred_index = 0u32; @@ -257,19 +285,20 @@ impl StandardBroadcastRun { return; }; let parent_for_double_merkle = match update_parent { - VersionedUpdateParent::V1(update_parent) => ( - update_parent.new_parent_slot, - update_parent.new_parent_block_id, - ), + VersionedUpdateParent::V1(update_parent) => Block { + slot: update_parent.new_parent_slot, + block_id: update_parent.new_parent_block_id, + }, }; self.parent_for_double_merkle = parent_for_double_merkle; } /// Leaf that binds the current replay parent into the double-merkle block id. - fn parent_info_leaf(&self) -> Hash { + fn parent_info_leaf(&self, fec_set_count: u32) -> Hash { hashv(&[ - &self.parent_for_double_merkle.0.to_le_bytes(), - self.parent_for_double_merkle.1.as_bytes(), + &self.parent_for_double_merkle.slot.to_le_bytes(), + self.parent_for_double_merkle.block_id.as_bytes(), + &fec_set_count.to_le_bytes(), ]) } @@ -283,8 +312,8 @@ impl StandardBroadcastRun { receive_results: ReceiveResults, bank_forks: &RwLock, ) -> Result<()> { - let (bsend, brecv) = unbounded(); - let (ssend, srecv) = unbounded(); + let (bsend, brecv) = bounded(BROADCAST_CHANNEL_CAPACITY); + let (ssend, srecv) = bounded(BROADCAST_CHANNEL_CAPACITY); self.process_receive_results( keypair, blockstore, @@ -314,6 +343,15 @@ impl StandardBroadcastRun { last_tick_height, } = receive_results; + if self.is_broadcast_blacklisted(bank.slot()) { + return Ok(()); + } + + if self.is_broadcast_blacklisted(bank.parent_slot()) { + self.blacklist_broadcast_slot(bank.slot()); + return Err(Error::DuplicateSlotBroadcast(bank.slot())); + } + let mut to_shreds_time = Measure::start("broadcast_to_shreds"); self.max_data_shreds_per_slot = bank.max_data_shreds_per_slot(); self.max_code_shreds_per_slot = bank.max_code_shreds_per_slot(); @@ -331,8 +369,7 @@ impl StandardBroadcastRun { was_interrupted: true, }); let shreds = Arc::new(shreds); - socket_sender.send((shreds.clone(), batch_info.clone()))?; - blockstore_sender.send((shreds, batch_info))?; + dispatch_shreds(blockstore_sender, socket_sender, shreds, batch_info)?; } // If blockstore already has shreds for this slot, // it should not recreate the slot: @@ -346,6 +383,7 @@ impl StandardBroadcastRun { process_stats.num_extant_slots += 1; // This is a faulty situation that should not happen. // Refrain from generating shreds for the slot. + self.blacklist_broadcast_slot(bank.slot()); return Err(Error::DuplicateSlotBroadcast(bank.slot())); } @@ -408,7 +446,6 @@ impl StandardBroadcastRun { blockstore .insert_cow_shreds( [Cow::Borrowed(shred)], - None, // leader_schedule true, // is_trusted ) .expect("Failed to insert shreds in blockstore"); @@ -431,8 +468,7 @@ impl StandardBroadcastRun { let shreds = Arc::new(shreds); debug_assert!(shreds.iter().all(|shred| shred.slot() == bank.slot())); - socket_sender.send((shreds.clone(), batch_info.clone()))?; - blockstore_sender.send((shreds, batch_info))?; + dispatch_shreds(blockstore_sender, socket_sender, shreds, batch_info)?; coding_send_time.stop(); @@ -452,15 +488,16 @@ impl StandardBroadcastRun { .should_use_double_merkle_block_id(bank.slot()) { // Block id is the double merkle root - let fec_set_count = self.double_merkle_leaves.len(); + let fec_set_count = u32::try_from(self.double_merkle_leaves.len()).unwrap(); // Add the final leaf (parent info) - self.double_merkle_leaves.push(self.parent_info_leaf()); + self.double_merkle_leaves + .push(self.parent_info_leaf(fec_set_count)); // Compute the double merkle root // Blockstore population of the DoubleMerkleMeta happens asynchronously when shreds are inserted let merkle_tree = MerkleTree::try_new_with_len( self.double_merkle_leaves.drain(..).map(Ok), - fec_set_count + 1, + fec_set_count as usize + 1, ) .expect("Double merkle tree construction cannot fail"); *merkle_tree.root() @@ -498,9 +535,7 @@ impl StandardBroadcastRun { let num_shreds = shreds.len(); let shreds = shreds.iter().skip(offset).map(Cow::Borrowed); blockstore - .insert_cow_shreds( - shreds, /*leader_schedule:*/ None, /*is_trusted:*/ true, - ) + .insert_cow_shreds(shreds, /*is_trusted:*/ true) .expect("Failed to insert shreds in blockstore"); let insert_shreds_elapsed = insert_shreds_start.elapsed(); let new_insert_shreds_stats = InsertShredsStats { @@ -629,7 +664,6 @@ mod test { use { super::*, assert_matches::assert_matches, - crossbeam_channel::unbounded, rand::Rng, solana_entry::entry::create_ticks, solana_genesis_config::GenesisConfig, @@ -638,12 +672,18 @@ mod test { solana_keypair::Keypair, solana_ledger::{ blockstore::Blockstore, + blockstore_meta::SlotMeta, genesis_utils::create_genesis_config, get_tmp_ledger_path, shred::{DATA_SHREDS_PER_FEC_BLOCK, max_ticks_per_n_shreds}, }, solana_net_utils::{SocketAddrSpace, sockets::bind_to_localhost_unique}, - solana_runtime::bank::Bank, + solana_pubkey::Pubkey, + solana_runtime::{ + bank::{Bank, SlotLeader}, + genesis_utils::{activate_feature, deactivate_features}, + slot_params::{slot_time_feature_gates, slot_time_feature_ids}, + }, solana_signer::Signer, std::{ops::Deref, sync::Arc, time::Duration}, test_case::test_case, @@ -707,10 +747,69 @@ mod test { Arc::new(LeaderScheduleCache::new_from_bank(bank)) } + fn broadcast_shred_limits_for_slot_time_features( + feature_ids: impl IntoIterator, + ) -> (u32, u32) { + let ledger_path = get_tmp_ledger_path!(); + let blockstore = Arc::new( + Blockstore::open(&ledger_path).expect("Expected to be able to open database ledger"), + ); + let mut genesis_config = create_genesis_config(10_000).genesis_config; + let slot_time_features = slot_time_feature_ids().to_vec(); + deactivate_features(&mut genesis_config, &slot_time_features); + for feature_id in feature_ids { + activate_feature(&mut genesis_config, feature_id); + } + + let (parent_bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + parent_bank.set_tick_height(parent_bank.max_tick_height()); + Bank::calculate_and_set_block_id_for_dcou(&parent_bank); + + let effective_slot = parent_bank.epoch_schedule().get_first_slot_in_epoch(1); + let bank = Bank::new_from_parent_with_bank_forks( + &bank_forks, + parent_bank, + SlotLeader::default(), + effective_slot, + ); + let ticks = create_ticks(1, 0, genesis_config.hash()); + let receive_results = ReceiveResults { + component: BlockComponent::EntryBatch(ticks.clone()), + bank: bank.clone(), + last_tick_height: bank.tick_height() + ticks.len() as u64, + }; + let (socket_sender, _socket_receiver) = bounded(1024); + let (blockstore_sender, _blockstore_receiver) = bounded(1024); + let (votor_event_sender, _votor_event_receiver) = bounded(1024); + let mut standard_broadcast_run = StandardBroadcastRun::new( + 0, + Arc::new(MigrationStatus::default()), + votor_event_sender, + test_leader_schedule_cache(&bank), + ); + + standard_broadcast_run + .process_receive_results( + &Keypair::new(), + &blockstore, + &socket_sender, + &blockstore_sender, + receive_results, + &mut ProcessShredsStats::default(), + ) + .unwrap(); + + ( + standard_broadcast_run.max_data_shreds_per_slot, + standard_broadcast_run.max_code_shreds_per_slot, + ) + } + #[test] fn test_interrupted_slot_last_shred() { let keypair = Arc::new(Keypair::new()); - let (votor_event_sender, _votor_event_receiver) = unbounded(); + let (votor_event_sender, _votor_event_receiver) = bounded(1024); let bank = Bank::new_for_tests(&create_genesis_config(10_000).genesis_config); let mut run = StandardBroadcastRun::new( 0, @@ -767,7 +866,7 @@ mod test { }; // Step 1: Make an incomplete transmission for slot 1 - let (votor_event_sender, _votor_event_receiver) = unbounded(); + let (votor_event_sender, _votor_event_receiver) = bounded(1024); let mut standard_broadcast_run = StandardBroadcastRun::new( 0, Arc::new(migration_status), @@ -895,17 +994,11 @@ mod test { .is_none() ); - // Try to fetch the incomplete ticks from blockstore, should succeed - assert_eq!( - blockstore.get_slot_entries(1, header_shred_offset).unwrap(), - ticks0 - ); - assert_eq!( - blockstore - .get_slot_entries(1, shred_multiplier * num_shreds_per_slot) - .unwrap(), - vec![], - ); + // Try to fetch the incomplete ticks from blockstore, will fail because + // broadcast interruption serializes an empty entry batch into shreds + // which is an invalid block + assert!(blockstore.get_slot_entries(1, header_shred_offset).is_err()); + assert!(blockstore.get_slot_entries(1, 0).is_err()); } #[test] @@ -921,9 +1014,9 @@ mod test { _bank_forks, ) = setup(num_shreds_per_slot); let bank = new_child_bank(&parent_bank, 1); - let (bsend, brecv) = unbounded(); - let (ssend, _srecv) = unbounded(); - let (votor_event_sender, _votor_event_receiver) = unbounded(); + let (bsend, brecv) = bounded(1024); + let (ssend, _srecv) = bounded(1024); + let (votor_event_sender, _votor_event_receiver) = bounded(1024); let mut last_tick_height = bank.tick_height(); let mut standard_broadcast_run = StandardBroadcastRun::new( 0, @@ -997,7 +1090,7 @@ mod test { last_tick_height: bank.tick_height() + ticks.len() as u64, }; - let (votor_event_sender, _votor_event_receiver) = unbounded(); + let (votor_event_sender, _votor_event_receiver) = bounded(1024); let mut standard_broadcast_run = StandardBroadcastRun::new( 0, Arc::new(MigrationStatus::default()), @@ -1018,53 +1111,135 @@ mod test { } #[test] - fn test_update_shred_limits_from_bank() { - agave_logger::setup(); + fn test_broadcast_blacklist() { let num_shreds_per_slot = 2; let ( blockstore, genesis_config, - cluster_info, + _cluster_info, parent_bank, leader_keypair, - socket, - bank_forks, + _socket, + _bank_forks, ) = setup(num_shreds_per_slot); - let bank = new_child_bank(&parent_bank, 1); - let ticks = create_ticks(1, 0, genesis_config.hash()); - let receive_results = ReceiveResults { - component: BlockComponent::EntryBatch(ticks), - bank: bank.clone(), - last_tick_height: bank.tick_height() + 1, - }; + let bank1 = new_child_bank(&parent_bank, 1); + + blockstore + .put_meta( + bank1.slot(), + &SlotMeta { + slot: bank1.slot(), + parent_slot: Some(bank1.parent_slot()), + received: 1, + ..SlotMeta::default() + }, + ) + .unwrap(); - let (votor_event_sender, _votor_event_receiver) = unbounded(); + let (votor_event_sender, _votor_event_receiver) = bounded(1024); let mut standard_broadcast_run = StandardBroadcastRun::new( 0, Arc::new(MigrationStatus::default()), votor_event_sender, - test_leader_schedule_cache(&bank), + test_leader_schedule_cache(&bank1), ); - standard_broadcast_run.max_data_shreds_per_slot = 1; - standard_broadcast_run.max_code_shreds_per_slot = 1; + let (bsend, brecv) = bounded(1024); + let (ssend, srecv) = bounded(1024); + + let ticks = create_ticks(1, 0, genesis_config.hash()); + let err = standard_broadcast_run + .process_receive_results( + &leader_keypair, + &blockstore, + &ssend, + &bsend, + ReceiveResults { + component: BlockComponent::EntryBatch(ticks.clone()), + bank: bank1.clone(), + last_tick_height: bank1.tick_height() + ticks.len() as u64, + }, + &mut ProcessShredsStats::default(), + ) + .unwrap_err(); + assert_matches!(err, Error::DuplicateSlotBroadcast(1)); + assert!(standard_broadcast_run.is_broadcast_blacklisted(1)); + standard_broadcast_run - .test_process_receive_results( + .process_receive_results( &leader_keypair, - &cluster_info, - &socket, &blockstore, - receive_results, - &bank_forks, + &ssend, + &bsend, + ReceiveResults { + component: BlockComponent::EntryBatch(ticks.clone()), + bank: bank1.clone(), + last_tick_height: bank1.tick_height() + ticks.len() as u64, + }, + &mut ProcessShredsStats::default(), ) .unwrap(); + assert!(brecv.try_recv().is_err()); + assert!(srecv.try_recv().is_err()); + + let bank2 = Arc::new(Bank::new_from_parent( + bank1.clone(), + *bank1.leader(), + bank1.slot() + 1, + )); + let err = standard_broadcast_run + .process_receive_results( + &leader_keypair, + &blockstore, + &ssend, + &bsend, + ReceiveResults { + component: BlockComponent::EntryBatch(ticks.clone()), + bank: bank2, + last_tick_height: bank1.tick_height() + ticks.len() as u64, + }, + &mut ProcessShredsStats::default(), + ) + .unwrap_err(); + assert_matches!(err, Error::DuplicateSlotBroadcast(2)); + assert!(standard_broadcast_run.is_broadcast_blacklisted(2)); + for slot in 3..=18 { + standard_broadcast_run.blacklist_broadcast_slot(slot); + } assert_eq!( - standard_broadcast_run.max_data_shreds_per_slot, - bank.max_data_shreds_per_slot() + standard_broadcast_run.broadcast_blacklist.len(), + MAX_BROADCAST_BLACKLIST_SIZE + ); + assert!(!standard_broadcast_run.is_broadcast_blacklisted(1)); + assert!(!standard_broadcast_run.is_broadcast_blacklisted(2)); + assert!(standard_broadcast_run.is_broadcast_blacklisted(3)); + assert!(standard_broadcast_run.is_broadcast_blacklisted(18)); + } + + #[test] + fn test_update_shred_limits_from_bank() { + assert_eq!( + broadcast_shred_limits_for_slot_time_features(std::iter::empty()), + ( + DEFAULT_MAX_DATA_SHREDS_PER_SLOT, + DEFAULT_MAX_CODE_SHREDS_PER_SLOT + ) ); + + for ((feature_id, _), expected_shreds_per_slot) in slot_time_feature_gates() + .into_iter() + .zip([28_672, 24_576, 20_480, 16_384]) + { + assert_eq!( + broadcast_shred_limits_for_slot_time_features([feature_id]), + (expected_shreds_per_slot, expected_shreds_per_slot) + ); + } + + let [reduce_to_350ms, _, _, reduce_to_200ms] = slot_time_feature_ids(); assert_eq!( - standard_broadcast_run.max_code_shreds_per_slot, - bank.max_code_shreds_per_slot() + broadcast_shred_limits_for_slot_time_features([reduce_to_350ms, reduce_to_200ms]), + (16_384, 16_384) ); } @@ -1072,7 +1247,7 @@ mod test { fn test_component_to_shreds_max() { agave_logger::setup(); let keypair = Keypair::new(); - let (votor_event_sender, _votor_event_receiver) = unbounded(); + let (votor_event_sender, _votor_event_receiver) = bounded(1024); let bank = Bank::new_for_tests(&create_genesis_config(10_000).genesis_config); let mut bs = StandardBroadcastRun::new( 0, @@ -1109,7 +1284,7 @@ mod test { #[test] fn test_update_parent() { let keypair = Keypair::new(); - let (votor_event_sender, _votor_event_receiver) = unbounded(); + let (votor_event_sender, _votor_event_receiver) = bounded(1024); let bank = Bank::new_for_tests(&create_genesis_config(10_000).genesis_config); let mut bs = StandardBroadcastRun::new( 0, @@ -1121,11 +1296,14 @@ mod test { bs.parent = 10; let original_parent_block_id = Hash::new_unique(); bs.parent_block_id = original_parent_block_id; - bs.parent_for_double_merkle = (bs.parent, bs.parent_block_id); + bs.parent_for_double_merkle = Block { + slot: bs.parent, + block_id: bs.parent_block_id, + }; let new_parent_slot = 7; let new_parent_block_id = Hash::new_unique(); - let component = BlockComponent::new_block_marker(VersionedBlockMarker::new_update_parent( + let component = BlockComponent::new_block_marker(VersionedBlockMarker::from_update_parent( solana_entry::block_component::UpdateParentV1 { new_parent_slot, new_parent_block_id, @@ -1147,13 +1325,18 @@ mod test { assert_eq!(bs.parent_block_id, original_parent_block_id); assert_eq!( bs.parent_for_double_merkle, - (new_parent_slot, new_parent_block_id) + Block { + slot: new_parent_slot, + block_id: new_parent_block_id + } ); + let fec_set_count = 3u32; assert_eq!( - bs.parent_info_leaf(), + bs.parent_info_leaf(fec_set_count), hashv(&[ &new_parent_slot.to_le_bytes(), - new_parent_block_id.as_bytes() + new_parent_block_id.as_bytes(), + &fec_set_count.to_le_bytes(), ]) ); diff --git a/turbine/src/lib.rs b/turbine/src/lib.rs index 19fe5b7e514..186149df020 100644 --- a/turbine/src/lib.rs +++ b/turbine/src/lib.rs @@ -11,9 +11,7 @@ pub mod retransmit_stage; pub mod sigverify_shreds; -pub mod xdp_sender; - -pub use crate::xdp_sender::XdpSender; +pub use solana_net_utils::PinnedXdpSender as XdpSender; #[macro_use] extern crate log; diff --git a/turbine/src/retransmit_stage.rs b/turbine/src/retransmit_stage.rs index 25c64edec34..bd159210839 100644 --- a/turbine/src/retransmit_stage.rs +++ b/turbine/src/retransmit_stage.rs @@ -16,6 +16,7 @@ use { rayon::{ThreadPool, ThreadPoolBuilder, prelude::*}, solana_clock::Slot, solana_gossip::cluster_info::ClusterInfo, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_ledger::{ leader_schedule_cache::LeaderScheduleCache, shred::{self, ShredFlags, ShredId, ShredType}, @@ -119,6 +120,45 @@ struct RetransmitStats { unknown_shred_slot_leader: usize, } +struct RetransmitState { + stats: RetransmitStats, + addr_cache: AddrCache, + shred_buf: Vec>, + pending_first_shred_event: Option, +} + +struct RetransmitNotifiers { + rpc_subscriptions: Option>, + slot_status_notifier: Option, + migration_status: Arc, + votor_event_sender: Sender, +} + +struct RetransmitContext { + thread_pool: ThreadPool, + bank_forks: Arc>, + leader_schedule_cache: Arc, + cluster_info: Arc, + retransmit_receiver: Receiver>, + retransmit_sockets: Arc>, + xdp_sender: Option, + cluster_nodes_cache: ClusterNodesCache, + shred_deduper: ShredDeduper, + max_slots: Arc, + notifiers: RetransmitNotifiers, +} + +impl RetransmitState { + fn new(now: Instant) -> Self { + Self { + stats: RetransmitStats::new(now), + addr_cache: AddrCache::with_capacity(/*capacity:*/ 4), + shred_buf: Vec::with_capacity(RETRANSMIT_BATCH_SIZE), + pending_first_shred_event: None, + } + } +} + impl RetransmitStats { fn maybe_submit( &mut self, @@ -279,26 +319,32 @@ const RETRANSMIT_BATCH_SIZE: usize = 4096; // pull the shreds from the shreds_receiver until empty, then retransmit them. // uses a thread_pool to parallelize work if there are enough shreds to justify that -#[allow(clippy::too_many_arguments)] -fn retransmit( - thread_pool: &ThreadPool, - bank_forks: &RwLock, - leader_schedule_cache: &LeaderScheduleCache, - cluster_info: &ClusterInfo, - retransmit_receiver: &Receiver>, - retransmit_sockets: &[UdpSocket], - xdp_sender: Option<&XdpSender>, - stats: &mut RetransmitStats, - cluster_nodes_cache: &ClusterNodesCache, - addr_cache: &mut AddrCache, - shred_deduper: &ShredDeduper, - max_slots: &MaxSlots, - rpc_subscriptions: Option<&RpcSubscriptions>, - slot_status_notifier: Option<&SlotStatusNotifier>, - shred_buf: &mut Vec>, - votor_event_sender: &Sender, - migration_status: &MigrationStatus, -) -> Result<(), ()> { +fn retransmit(context: &RetransmitContext, state: &mut RetransmitState) -> Result<(), ()> { + let thread_pool = &context.thread_pool; + let bank_forks = context.bank_forks.as_ref(); + let leader_schedule_cache = context.leader_schedule_cache.as_ref(); + let cluster_info = context.cluster_info.as_ref(); + let retransmit_receiver = &context.retransmit_receiver; + let retransmit_sockets = context.retransmit_sockets.as_slice(); + let xdp_sender = context.xdp_sender.as_ref(); + let cluster_nodes_cache = &context.cluster_nodes_cache; + let shred_deduper = &context.shred_deduper; + let max_slots = context.max_slots.as_ref(); + let RetransmitState { + stats, + addr_cache, + shred_buf, + pending_first_shred_event, + } = state; + + // Attempt to resend a pending first shred event to votor + if let Some(event) = pending_first_shred_event.take() + && let Err(TrySendError::Full(event)) = context.notifiers.votor_event_sender.try_send(event) + { + // Failed again, requeue + *pending_first_shred_event = Some(event); + } + // Try to receive shreds from the channel without blocking. If the channel // is empty precompute turbine trees speculatively. If no cache updates are // made then block on the channel until some shreds are received. @@ -429,11 +475,9 @@ fn retransmit( slot_stats, root_bank.slot(), addr_cache, - rpc_subscriptions, - slot_status_notifier, - migration_status, - votor_event_sender, - )?; + &context.notifiers, + pending_first_shred_event, + ); timer_start.stop(); stats.total_time += timer_start.as_us(); stats.maybe_submit( @@ -645,8 +689,6 @@ impl RetransmitStage { CLUSTER_NODES_CACHE_TTL, ); let mut rng = rand::rng(); - let mut stats = RetransmitStats::new(Instant::now()); - let mut addr_cache = AddrCache::with_capacity(/*capacity:*/ 4); let shred_deduper = ShredDeduper::new(&mut rng, DEDUPER_NUM_BITS); let thread_pool = { @@ -658,32 +700,31 @@ impl RetransmitStage { .unwrap() }; + let retransmit_context = RetransmitContext { + thread_pool, + bank_forks, + leader_schedule_cache, + cluster_info, + retransmit_receiver, + retransmit_sockets, + xdp_sender, + cluster_nodes_cache, + shred_deduper, + max_slots, + notifiers: RetransmitNotifiers { + rpc_subscriptions, + slot_status_notifier, + migration_status, + votor_event_sender, + }, + }; + let retransmit_thread_handle = Builder::new() .name("solRetransmittr".to_string()) .spawn({ move || { - let mut shred_buf = Vec::with_capacity(RETRANSMIT_BATCH_SIZE); - while retransmit( - &thread_pool, - &bank_forks, - &leader_schedule_cache, - &cluster_info, - &retransmit_receiver, - &retransmit_sockets, - xdp_sender.as_ref(), - &mut stats, - &cluster_nodes_cache, - &mut addr_cache, - &shred_deduper, - &max_slots, - rpc_subscriptions.as_deref(), - slot_status_notifier.as_ref(), - &mut shred_buf, - &votor_event_sender, - &migration_status, - ) - .is_ok() - {} + let mut retransmit_state = RetransmitState::new(Instant::now()); + while retransmit(&retransmit_context, &mut retransmit_state).is_ok() {} } }) .unwrap(); @@ -762,11 +803,9 @@ impl RetransmitStats { feed: impl IntoIterator, root: Slot, addr_cache: &mut AddrCache, - rpc_subscriptions: Option<&RpcSubscriptions>, - slot_status_notifier: Option<&SlotStatusNotifier>, - migration_status: &MigrationStatus, - votor_event_sender: &Sender, - ) -> Result<(), ()> { + notifiers: &RetransmitNotifiers, + pending_first_shred_event: &mut Option, + ) { for (slot, mut slot_stats) in feed { addr_cache.record(slot, &mut slot_stats); match self.slot_stats.get_mut(&slot) { @@ -775,11 +814,9 @@ impl RetransmitStats { notify_subscribers( slot, slot_stats.outset, - rpc_subscriptions, - slot_status_notifier, - migration_status, - votor_event_sender, - )?; + notifiers, + pending_first_shred_event, + ); } self.slot_stats.put(slot, slot_stats); } @@ -797,7 +834,6 @@ impl RetransmitStats { None => break, } } - Ok(()) } } @@ -871,40 +907,42 @@ impl RetransmitSlotStats { fn notify_subscribers( slot: Slot, timestamp: u64, // When the first shred in the slot was received. - rpc_subscriptions: Option<&RpcSubscriptions>, - slot_status_notifier: Option<&SlotStatusNotifier>, - migration_status: &MigrationStatus, - votor_event_sender: &Sender, -) -> Result<(), ()> { - if let Some(rpc_subscriptions) = rpc_subscriptions { + notifiers: &RetransmitNotifiers, + pending_first_shred_event: &mut Option, +) { + if let Some(rpc_subscriptions) = notifiers.rpc_subscriptions.as_ref() { let slot_update = SlotUpdate::FirstShredReceived { slot, timestamp }; rpc_subscriptions.notify_slot_update(slot_update); datapoint_info!("retransmit-first-shred", ("slot", slot, i64)); } - if let Some(slot_status_notifier) = slot_status_notifier { + if let Some(slot_status_notifier) = notifiers.slot_status_notifier.as_ref() { slot_status_notifier .read() .unwrap() .notify_first_shred_received(slot); } - if migration_status.should_send_votor_event(slot) { - match votor_event_sender.try_send(VotorEvent::FirstShred(slot)) { + if notifiers.migration_status.should_send_votor_event(slot) + && slot.is_multiple_of(NUM_CONSECUTIVE_LEADER_SLOTS.get() as u64) + { + match notifiers + .votor_event_sender + .try_send(VotorEvent::FirstShred(slot)) + { Ok(()) => (), Err(TrySendError::Full(event)) => { error!( - "Votor event channel is backed up len {}, something is wrong, blocking", - votor_event_sender.len(), + "Votor event channel is backed up len {}, something is wrong", + notifiers.votor_event_sender.len(), ); - let _ = votor_event_sender.send(event); + // Only the latest first shred notification matters, requeue + pending_first_shred_event.replace(event); } Err(TrySendError::Disconnected(_)) => { - info!("Votor event channel disconnectioned, we are shutting down") + info!("Votor event channel disconnected, we are shutting down") } } } - - Ok(()) } #[cfg(test)] diff --git a/udp-client/Cargo.toml b/udp-client/Cargo.toml index 5a7ebdb4890..18b323f0a63 100644 --- a/udp-client/Cargo.toml +++ b/udp-client/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] dev-context-only-utils = [] @@ -25,3 +30,6 @@ solana-transaction-error = { workspace = true } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-packet = { workspace = true } solana-streamer = { path = "../streamer", features = ["agave-unstable-api", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/unified-scheduler-logic/Cargo.toml b/unified-scheduler-logic/Cargo.toml index f4f5fd38777..ba1deb33cc3 100644 --- a/unified-scheduler-logic/Cargo.toml +++ b/unified-scheduler-logic/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] @@ -25,5 +30,9 @@ unwrap_none = { workspace = true } [dev-dependencies] solana-instruction = { workspace = true } solana-message = { workspace = true } +solana-pubkey = { workspace = true, features = ["rand"] } solana-runtime-transaction = { path = "../runtime-transaction", features = ["agave-unstable-api", "dev-context-only-utils"] } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/unified-scheduler-pool/Cargo.toml b/unified-scheduler-pool/Cargo.toml index 0ee1c522215..c81163fcddc 100644 --- a/unified-scheduler-pool/Cargo.toml +++ b/unified-scheduler-pool/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] dev-context-only-utils = [] @@ -49,5 +54,8 @@ solana-keypair = { workspace = true } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-system-transaction = { workspace = true } # See order-crates-for-publishing.py for using this unusual `path = "."` -solana-unified-scheduler-pool = { path = ".", features = ["dev-context-only-utils"] } +solana-unified-scheduler-pool = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 71797e023c1..2b8895239f4 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -13,19 +13,23 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] [dependencies] +agave-cpu-utils = { workspace = true } +agave-feature-set = { workspace = true } agave-logger = { workspace = true } agave-snapshots = { workspace = true } agave-votor = { workspace = true } agave-xdp = { workspace = true } +bytesize = { workspace = true } chrono = { workspace = true, features = ["default", "serde"] } clap = { workspace = true } console = { workspace = true } -core_affinity = { workspace = true } crossbeam-channel = { workspace = true } fd-lock = { workspace = true } indicatif = { workspace = true } @@ -57,7 +61,7 @@ solana-epoch-schedule = { workspace = true } solana-faucet = { workspace = true } solana-genesis-utils = { workspace = true } solana-geyser-plugin-manager = { workspace = true } -solana-gossip = { workspace = true, features = ["agave-unstable-api"] } +solana-gossip = { workspace = true } solana-hash = { workspace = true } solana-inflation = { workspace = true } solana-keypair = { workspace = true } @@ -115,3 +119,6 @@ solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-c solana-time-utils = { workspace = true } tempfile = { workspace = true } test-case = { workspace = true } + +[lints] +workspace = true diff --git a/validator/src/admin_rpc_service.rs b/validator/src/admin_rpc_service.rs index 7833618c1f8..522388a7ada 100644 --- a/validator/src/admin_rpc_service.rs +++ b/validator/src/admin_rpc_service.rs @@ -1,5 +1,7 @@ use { - agave_votor::event::VotorEvent, + agave_votor::{ + event::VotorEvent, vote_history::VoteHistory, vote_history_storage::VoteHistoryStorage, + }, crossbeam_channel::Sender, jsonrpc_core::{BoxFuture, ErrorCode, MetaIoHandler, Metadata, Result}, jsonrpc_core_client::{RpcError, transports::ipc}, @@ -9,7 +11,7 @@ use { }, log::*, serde::{Deserialize, Serialize, de::Deserializer}, - solana_clock::Slot, + solana_clock::{Epoch, Slot}, solana_core::{ admin_rpc_post_init::AdminRpcRequestMetadataPostInit, banking_stage::{ @@ -20,6 +22,7 @@ use { repair::repair_service, validator::{ BlockProductionMethod, SchedulerPacing, TransactionStructure, ValidatorStartProgress, + should_require_vote_history_file, }, }, solana_geyser_plugin_manager::GeyserPluginManagerRequest, @@ -27,7 +30,7 @@ use { solana_keypair::{Keypair, read_keypair_file}, solana_metrics::{datapoint_info, datapoint_warn}, solana_pubkey::Pubkey, - solana_runtime::snapshot_controller::SnapshotController, + solana_runtime::{bank::VATHealthError, snapshot_controller::SnapshotController}, solana_signer::Signer, solana_validator_exit::Exit, std::{ @@ -56,6 +59,7 @@ pub struct AdminRpcRequestMetadata { pub validator_exit_backpressure: HashMap>, pub authorized_voter_keypairs: Arc>>>, pub tower_storage: Arc, + pub vote_history_storage: Arc, pub staked_nodes_overrides: Arc>>, pub post_init: Arc>>, pub rpc_to_plugin_manager_sender: Option>, @@ -108,6 +112,18 @@ pub struct AdminRpcRepairWhitelist { pub whitelist: Vec, } +#[derive(Debug, Deserialize, Serialize)] +pub struct AdminRpcValidatorAdmissionTicketStatus { + pub vote_account: Pubkey, + pub voting_enabled: bool, + pub current_epoch: Epoch, + pub vat_active: bool, + pub in_current_epoch_vat: bool, + pub current_epoch_vote_account_stake: u64, + pub current_epoch_vote_accounts: usize, + pub next_epoch_vat_failure_reason: Option, +} + impl From for AdminRpcContactInfo { fn from(node: ContactInfo) -> Self { macro_rules! unwrap_socket { @@ -155,7 +171,7 @@ impl solana_cli_output::QuietDisplay for AdminRpcContactInfo {} impl Display for AdminRpcRepairWhitelist { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "Repair whitelist: {:?}", &self.whitelist) + writeln!(f, "Repair whitelist: {:?}", self.whitelist) } } impl solana_cli_output::VerboseDisplay for AdminRpcRepairWhitelist {} @@ -219,6 +235,7 @@ pub trait AdminRpc { meta: Self::Metadata, keypair_file: String, require_tower: bool, + require_vote_history: bool, ) -> Result<()>; #[rpc(meta, name = "setIdentityFromBytes")] @@ -227,6 +244,7 @@ pub trait AdminRpc { meta: Self::Metadata, identity_keypair: Vec, require_tower: bool, + require_vote_history: bool, ) -> Result<()>; #[rpc(meta, name = "setStakedNodesOverrides")] @@ -235,6 +253,9 @@ pub trait AdminRpc { #[rpc(meta, name = "contactInfo")] fn contact_info(&self, meta: Self::Metadata) -> Result; + #[rpc(meta, name = "validatorAdmissionTicketStatus")] + fn vat_status(&self, meta: Self::Metadata) -> Result; + #[rpc(meta, name = "selectActiveInterface")] fn select_active_interface(&self, meta: Self::Metadata, interface: IpAddr) -> Result<()>; @@ -559,6 +580,7 @@ impl AdminRpc for AdminRpcImpl { meta: Self::Metadata, keypair_file: String, require_tower: bool, + require_vote_history: bool, ) -> Result<()> { debug!("set_identity request received"); @@ -568,7 +590,12 @@ impl AdminRpc for AdminRpcImpl { )) })?; - AdminRpcImpl::set_identity_keypair(meta, identity_keypair, require_tower) + AdminRpcImpl::set_identity_keypair( + meta, + identity_keypair, + require_tower, + require_vote_history, + ) } fn set_identity_from_bytes( @@ -576,6 +603,7 @@ impl AdminRpc for AdminRpcImpl { meta: Self::Metadata, identity_keypair: Vec, require_tower: bool, + require_vote_history: bool, ) -> Result<()> { debug!("set_identity_from_bytes request received"); @@ -585,16 +613,18 @@ impl AdminRpc for AdminRpcImpl { )) })?; - AdminRpcImpl::set_identity_keypair(meta, identity_keypair, require_tower) + AdminRpcImpl::set_identity_keypair( + meta, + identity_keypair, + require_tower, + require_vote_history, + ) } fn set_staked_nodes_overrides(&self, meta: Self::Metadata, path: String) -> Result<()> { let loaded_config = load_staked_nodes_overrides(&path) .map_err(|err| { - error!( - "Failed to load staked nodes overrides from {}: {}", - &path, err - ); + error!("Failed to load staked nodes overrides from {path}: {err}"); jsonrpc_core::error::Error::internal_error() })? .staked_map_id; @@ -610,6 +640,40 @@ impl AdminRpc for AdminRpcImpl { meta.with_post_init(|post_init| Ok(post_init.cluster_info.my_contact_info().into())) } + fn vat_status(&self, meta: Self::Metadata) -> Result { + debug!("vat_status request received"); + meta.with_post_init(|post_init| { + let bank = post_init.bank_forks.read().unwrap().root_bank(); + let current_epoch = bank.epoch(); + let vat_active = bank.feature_set.snapshot().validator_admission_ticket; + let epoch_vote_accounts = bank + .epoch_vote_accounts(current_epoch) + .ok_or_else(jsonrpc_core::Error::internal_error)?; + let current_epoch_vote_account_stake = epoch_vote_accounts + .get(&post_init.vote_account) + .map(|(stake, _vote_account)| *stake) + .unwrap_or_default(); + let in_current_epoch_vote_accounts = + epoch_vote_accounts.contains_key(&post_init.vote_account); + + let next_epoch_vat_failure_reason = bank + .get_vat_health_for_next_epoch(&post_init.vote_account) + .err(); + + let voting_enabled = !meta.authorized_voter_keypairs.read().unwrap().is_empty(); + Ok(AdminRpcValidatorAdmissionTicketStatus { + vote_account: post_init.vote_account, + voting_enabled, + current_epoch, + vat_active, + in_current_epoch_vat: vat_active && in_current_epoch_vote_accounts, + current_epoch_vote_account_stake, + current_epoch_vote_accounts: epoch_vote_accounts.len(), + next_epoch_vat_failure_reason, + }) + }) + } + fn select_active_interface(&self, meta: Self::Metadata, interface: IpAddr) -> Result<()> { debug!("select_active_interface received: {interface}"); meta.with_post_init(|post_init| { @@ -674,7 +738,7 @@ impl AdminRpc for AdminRpcImpl { *post_init.repair_whitelist.write().unwrap() = whitelist; warn!( "Repair whitelist set to {:?}", - &post_init.repair_whitelist.read().unwrap() + post_init.repair_whitelist.read().unwrap() ); Ok(()) }) @@ -880,6 +944,7 @@ impl AdminRpcImpl { meta: AdminRpcRequestMetadata, identity_keypair: Keypair, require_tower: bool, + require_vote_history: bool, ) -> Result<()> { meta.with_post_init(|post_init| { if require_tower { @@ -893,6 +958,34 @@ impl AdminRpcImpl { })?; } + if require_vote_history { + let should_require_vote_history = { + let bank_forks = post_init.bank_forks.read().unwrap(); + should_require_vote_history_file( + &bank_forks.working_bank(), + &post_init.vote_account, + &identity_keypair.pubkey(), + ) + }; + if should_require_vote_history { + let _ = VoteHistory::restore( + meta.vote_history_storage.as_ref(), + &identity_keypair.pubkey(), + ) + .map_err(|err| { + jsonrpc_core::error::Error::invalid_params(format!( + "Unable to load vote history file for identity {}: {}. The vote \ + account {} has prior Alpenglow votes. Ensure the vote history file \ + is present or (dangerous) use --do-not-require-vote-history if you \ + know what you're doing", + identity_keypair.pubkey(), + err, + post_init.vote_account + )) + })?; + } + } + for (key, notifier) in &*post_init.notifies.read().unwrap() { if let Err(err) = notifier.update_key(&identity_keypair) { error!("Error updating network layer keypair: {err} on {key:?}"); @@ -1056,7 +1149,7 @@ mod tests { agave_snapshots::snapshot_config::SnapshotConfig, agave_votor::event::VotorEventSender, assert_matches::assert_matches, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, serde_json::Value, solana_accounts_db::{ accounts_db::{ACCOUNTS_DB_CONFIG_FOR_TESTING, AccountsDbConfig}, @@ -1122,7 +1215,7 @@ mod tests { }, }); - let (snapshot_request_sender, _) = unbounded(); + let (snapshot_request_sender, _) = bounded(1024); let snapshot_controller = Arc::new(SnapshotController::new( snapshot_request_sender.clone(), SnapshotConfig::default(), @@ -1136,7 +1229,7 @@ mod tests { let start_progress = Arc::new(RwLock::new(ValidatorStartProgress::default())); let repair_whitelist = Arc::new(RwLock::new(HashSet::new())); let votor_event_sender = config.votor_event_sender.unwrap_or_else(|| { - let (votor_event_sender, _) = unbounded(); + let (votor_event_sender, _) = bounded(1024); votor_event_sender }); let meta = AdminRpcRequestMetadata { @@ -1147,6 +1240,9 @@ mod tests { validator_exit_backpressure: HashMap::default(), authorized_voter_keypairs: Arc::new(RwLock::new(vec![vote_keypair])), tower_storage: Arc::new(NullTowerStorage {}), + vote_history_storage: Arc::new( + agave_votor::vote_history_storage::NullVoteHistoryStorage::default(), + ), post_init: Arc::new(RwLock::new(Some(AdminRpcRequestMetadataPostInit { cluster_info, bank_forks, @@ -1193,7 +1289,7 @@ mod tests { // Bank but without validator. #[test] fn test_set_identity() { - let (votor_event_sender, votor_event_receiver) = unbounded(); + let (votor_event_sender, votor_event_receiver) = bounded(1024); let rpc = RpcHandler::start_with_config(TestConfig { account_indexes: AccountSecondaryIndexes::default(), votor_event_sender: Some(votor_event_sender), @@ -1205,7 +1301,7 @@ mod tests { let validator_id_bytes = format!("{:?}", expected_validator_id.to_bytes()); let set_id_request = format!( - r#"{{"jsonrpc":"2.0","id":1,"method":"setIdentityFromBytes","params":[{validator_id_bytes}, false]}}"#, + r#"{{"jsonrpc":"2.0","id":1,"method":"setIdentityFromBytes","params":[{validator_id_bytes}, false, false]}}"#, ); let response = io.handle_request_sync(&set_id_request, meta.clone()); let actual_parsed_response: Value = @@ -1240,6 +1336,42 @@ mod tests { assert_matches!(event, VotorEvent::SetIdentity); } + #[test] + fn test_vat_status() { + let rpc = RpcHandler::_start(); + let RpcHandler { io, meta, .. } = rpc; + let post_init = meta.post_init.read().unwrap().clone().unwrap(); + let bank = post_init.bank_forks.read().unwrap().root_bank(); + let current_epoch = bank.epoch(); + let epoch_vote_accounts = bank.epoch_vote_accounts(current_epoch).unwrap(); + let expected_vat_active = bank.feature_set.snapshot().validator_admission_ticket; + let expected_in_vat = + expected_vat_active && epoch_vote_accounts.contains_key(&post_init.vote_account); + let expected_stake = epoch_vote_accounts + .get(&post_init.vote_account) + .map(|(stake, _vote_account)| *stake) + .unwrap_or_default(); + + let request = + r#"{"jsonrpc":"2.0","id":1,"method":"validatorAdmissionTicketStatus","params":[]}"#; + let response = io.handle_request_sync(request, meta.clone()); + let result: Value = serde_json::from_str(&response.expect("actual response")) + .expect("actual response deserialization"); + let status: AdminRpcValidatorAdmissionTicketStatus = + serde_json::from_value(result["result"].clone()).unwrap(); + + assert_eq!(status.vote_account, post_init.vote_account); + assert!(status.voting_enabled); + assert_eq!(status.current_epoch, current_epoch); + assert_eq!(status.vat_active, expected_vat_active); + assert_eq!(status.in_current_epoch_vat, expected_in_vat); + assert_eq!(status.current_epoch_vote_account_stake, expected_stake); + assert_eq!( + status.current_epoch_vote_accounts, + epoch_vote_accounts.len() + ); + } + struct TestValidatorWithAdminRpc { meta: AdminRpcRequestMetadata, io: MetaIoHandler, @@ -1279,6 +1411,9 @@ mod tests { validator_exit_backpressure: HashMap::default(), authorized_voter_keypairs: authorized_voter_keypairs.clone(), tower_storage: Arc::new(NullTowerStorage {}), + vote_history_storage: Arc::new( + agave_votor::vote_history_storage::NullVoteHistoryStorage::default(), + ), post_init: post_init.clone(), staked_nodes_overrides: Arc::new(RwLock::new(HashMap::new())), rpc_to_plugin_manager_sender: None, @@ -1359,6 +1494,9 @@ mod tests { validator_exit_backpressure: HashMap::default(), authorized_voter_keypairs: authorized_voter_keypairs.clone(), tower_storage: Arc::new(NullTowerStorage {}), + vote_history_storage: Arc::new( + agave_votor::vote_history_storage::NullVoteHistoryStorage::default(), + ), post_init: post_init.clone(), staked_nodes_overrides: Arc::new(RwLock::new(HashMap::new())), rpc_to_plugin_manager_sender: None, @@ -1376,7 +1514,7 @@ mod tests { let validator_id_bytes = format!("{:?}", expected_validator_id.to_bytes()); let set_id_request = format!( - r#"{{"jsonrpc":"2.0","id":1,"method":"setIdentityFromBytes","params":[{validator_id_bytes}, false]}}"#, + r#"{{"jsonrpc":"2.0","id":1,"method":"setIdentityFromBytes","params":[{validator_id_bytes}, false, false]}}"#, ); let response = test_validator.handle_request(&set_id_request); let actual_parsed_response: Value = @@ -1452,6 +1590,9 @@ mod tests { validator_exit_backpressure: HashMap::default(), authorized_voter_keypairs, tower_storage: Arc::new(NullTowerStorage {}), + vote_history_storage: Arc::new( + agave_votor::vote_history_storage::NullVoteHistoryStorage::default(), + ), post_init: Arc::new(RwLock::new(None)), staked_nodes_overrides: Arc::new(RwLock::new(HashMap::new())), rpc_to_plugin_manager_sender: None, diff --git a/validator/src/bin/solana-test-validator.rs b/validator/src/bin/solana-test-validator.rs index 70ffe055595..3a39c5ab6a0 100644 --- a/validator/src/bin/solana-test-validator.rs +++ b/validator/src/bin/solana-test-validator.rs @@ -3,6 +3,7 @@ use { admin_rpc_service, cli, commands::FromClapArgMatches, dashboard::Dashboard, ledger_lockfile, lock_ledger, println_name_value, }, + agave_votor::vote_history_storage::FileVoteHistoryStorage, clap::{crate_name, value_t, value_t_or_exit, values_t_or_exit}, crossbeam_channel::unbounded, itertools::Itertools, @@ -385,6 +386,18 @@ fn main() { }); let features_to_deactivate = pubkeys_of(&matches, "deactivate_feature").unwrap_or_default(); + if matches.is_present("alpenglow") + && features_to_deactivate.iter().any(|feature| { + *feature == agave_feature_set::alpenglow::id() + || *feature == agave_feature_set::validator_admission_ticket::id() + }) + { + println!( + "Error: --alpenglow requires both the alpenglow and validator_admission_ticket \ + features to be active" + ); + exit(1); + } if TestValidatorGenesis::ledger_exists(&ledger_path) { for (name, long) in &[ @@ -396,6 +409,7 @@ fn main() { ("slots_per_epoch", "--slots-per-epoch"), ("inflation_fixed", "--inflation-fixed"), ("faucet_sol", "--faucet-sol"), + ("alpenglow", "--alpenglow"), ("deactivate_feature", "--deactivate-feature"), ] { if matches.is_present(name) { @@ -418,6 +432,7 @@ fn main() { genesis.enable_scheduler_bindings = matches.is_present("enable_scheduler_bindings"); let tower_storage = Arc::new(FileTowerStorage::new(ledger_path.clone())); + let vote_history_storage = Arc::new(FileVoteHistoryStorage::new(ledger_path.clone())); let admin_service_post_init = Arc::new(RwLock::new(None)); // If geyser_plugin_config value is invalid, the validator will exit when the values are extracted below @@ -440,6 +455,7 @@ fn main() { staked_nodes_overrides: genesis.staked_nodes_overrides.clone(), post_init: admin_service_post_init, tower_storage: tower_storage.clone(), + vote_history_storage: vote_history_storage.clone(), rpc_to_plugin_manager_sender, }, ); @@ -490,8 +506,7 @@ fn main() { .unwrap_or_else(|e| { println!("Error: add_accounts_from_directories failed: {e}"); exit(1); - }) - .deactivate_features(&features_to_deactivate); + }); genesis.rpc_config(JsonRpcConfig { enable_rpc_transaction_history: true, @@ -502,67 +517,73 @@ fn main() { ..JsonRpcConfig::default_for_test() }); - if !accounts_to_clone.is_empty() { - if let Err(e) = genesis.clone_accounts( + if !accounts_to_clone.is_empty() + && let Err(e) = genesis.clone_accounts( accounts_to_clone, cluster_rpc_client .as_ref() .expect("--clone-account requires --json-rpc-url argument"), false, - ) { - println!("Error: clone_accounts failed: {e}"); - exit(1); - } + ) + { + println!("Error: clone_accounts failed: {e}"); + exit(1); } - if !alt_accounts_to_clone.is_empty() { - if let Err(e) = genesis.deep_clone_address_lookup_table_accounts( + if !alt_accounts_to_clone.is_empty() + && let Err(e) = genesis.deep_clone_address_lookup_table_accounts( alt_accounts_to_clone, cluster_rpc_client .as_ref() .expect("--deep-clone-address-lookup-table requires --json-rpc-url argument"), - ) { - println!("Error: alt_accounts_to_clone failed: {e}"); - exit(1); - } + ) + { + println!("Error: alt_accounts_to_clone failed: {e}"); + exit(1); } - if !accounts_to_maybe_clone.is_empty() { - if let Err(e) = genesis.clone_accounts( + if !accounts_to_maybe_clone.is_empty() + && let Err(e) = genesis.clone_accounts( accounts_to_maybe_clone, cluster_rpc_client .as_ref() .expect("--maybe-clone requires --json-rpc-url argument"), true, - ) { - println!("Error: clone_accounts failed: {e}"); - exit(1); - } + ) + { + println!("Error: clone_accounts failed: {e}"); + exit(1); } - if !upgradeable_programs_to_clone.is_empty() { - if let Err(e) = genesis.clone_upgradeable_programs( + if !upgradeable_programs_to_clone.is_empty() + && let Err(e) = genesis.clone_upgradeable_programs( upgradeable_programs_to_clone, cluster_rpc_client .as_ref() .expect("--clone-upgradeable-program requires --json-rpc-url argument"), - ) { - println!("Error: clone_upgradeable_programs failed: {e}"); - exit(1); - } + ) + { + println!("Error: clone_upgradeable_programs failed: {e}"); + exit(1); } - if clone_feature_set { - if let Err(e) = genesis.clone_feature_set( + if clone_feature_set + && let Err(e) = genesis.clone_feature_set( cluster_rpc_client .as_ref() .expect("--clone-feature-set requires --json-rpc-url argument"), - ) { - println!("Error: clone_feature_set failed: {e}"); - exit(1); - } + ) + { + println!("Error: clone_feature_set failed: {e}"); + exit(1); + } + + if matches.is_present("alpenglow") { + genesis.activate_alpenglow(); } + genesis.deactivate_features(&features_to_deactivate); + if let Some(warp_slot) = warp_slot { genesis.warp_slot(warp_slot); } diff --git a/validator/src/bootstrap.rs b/validator/src/bootstrap.rs index 544d87b2a71..e72635e5506 100644 --- a/validator/src/bootstrap.rs +++ b/validator/src/bootstrap.rs @@ -166,6 +166,7 @@ fn start_gossip_node( &cluster_info, None, gossip_sockets, + None, gossip_validators, should_check_duplicate_instance, None, @@ -339,10 +340,10 @@ pub fn fail_rpc_node( blacklisted_rpc_nodes: &mut HashSet, ) { warn!("{err}"); - if let Some(known_validators) = known_validators { - if known_validators.contains(rpc_id) { - return; - } + if let Some(known_validators) = known_validators + && known_validators.contains(rpc_id) + { + return; } info!("Excluding {rpc_id} as a future RPC candidate"); @@ -1243,23 +1244,22 @@ fn download_snapshot( && download_progress.estimated_remaining_time > 60_f32 && *download_abort_count < maximum_snapshot_download_abort { - if let Some(ref known_validators) = validator_config.known_validators { - if known_validators.contains(rpc_contact_info.pubkey()) - && known_validators.len() == 1 - && bootstrap_config.only_known_rpc - { - warn!( - "The snapshot download is too slow, throughput: {} < min speed {} \ - bytes/sec, but will NOT abort and try a different node as it is the \ - only known validator and the --only-known-rpc flag is set. Abort \ - count: {}, Progress detail: {:?}", - download_progress.last_throughput, - minimal_snapshot_download_speed, - download_abort_count, - download_progress, - ); - return true; // Do not abort download from the one-and-only known validator - } + if let Some(ref known_validators) = validator_config.known_validators + && known_validators.contains(rpc_contact_info.pubkey()) + && known_validators.len() == 1 + && bootstrap_config.only_known_rpc + { + warn!( + "The snapshot download is too slow, throughput: {} < min speed {} \ + bytes/sec, but will NOT abort and try a different node as it is the only \ + known validator and the --only-known-rpc flag is set. Abort count: {}, \ + Progress detail: {:?}", + download_progress.last_throughput, + minimal_snapshot_download_speed, + download_abort_count, + download_progress, + ); + return true; // Do not abort download from the one-and-only known validator } warn!( "The snapshot download is too slow, throughput: {} < min speed {} bytes/sec, \ diff --git a/validator/src/cli.rs b/validator/src/cli.rs index c063bd4c115..f1cf2ef0904 100644 --- a/validator/src/cli.rs +++ b/validator/src/cli.rs @@ -137,6 +137,7 @@ fn deprecated_arguments() -> Vec { .takes_value(true) .multiple(true) .help("Path to accounts shrink path which can hold a compacted account set."), + usage_warning: "Shrink paths are no longer used.", ); add_arg!( // deprecated in v4.2.0; the `mmap` value was deprecated in v4.0.0, and now mmap mode has @@ -149,6 +150,20 @@ fn deprecated_arguments() -> Vec { .possible_values(&["mmap", "file"]) .help("No-op; account storages are always accessed via file I/O"), ); + add_arg!( + // deprecated in v4.2.0 + Arg::with_name("accounts_db_cache_limit_mb") + .long("accounts-db-cache-limit-mb") + .value_name("MEGABYTES") + .validator(is_parsable::) + .takes_value(true) + .help( + "How large the write cache for account data can become. If this is exceeded, the \ + cache is flushed more aggressively.", + ) + .conflicts_with("accounts_db_write_cache_limit"), + replaced_by: "accounts-db-write-cache-limit", + ); add_arg!( // deprecated in v4.0.0 Arg::with_name("enable_accounts_disk_index") @@ -157,6 +172,17 @@ fn deprecated_arguments() -> Vec { .conflicts_with("accounts_index_limit"), replaced_by: "accounts-index-limit", ); + add_arg!( + // deprecated in v4.2.0 + Arg::with_name("experimental_poh_pinned_cpu_core") + .long("experimental-poh-pinned-cpu-core") + .takes_value(true) + .value_name("CPU_ID") + .conflicts_with("poh_pinned_cpu_core") + .validator(is_parsable::) + .help("Specify which CPU core PoH is pinned to. Use --poh-pinned-cpu-core instead"), + replaced_by: "poh-pinned-cpu-core", + ); add_arg!( // deprecated in v4.1.0 Arg::with_name("experimental_retransmit_xdp_cpu_cores") @@ -164,12 +190,11 @@ fn deprecated_arguments() -> Vec { .takes_value(true) .value_name("CPU_LIST") .conflicts_with("xdp_cpu_cores") + .conflicts_with("no_xdp") .validator(|value| { validate_cpu_ranges(value, "--experimental-retransmit-xdp-cpu-cores") }) - .help( - "Enable XDP retransmit on the specified CPU cores. Use --xdp-cpu-cores instead", - ), + .help("CPU cores to reserve for XDP. Use --xdp-cpu-cores instead"), replaced_by: "xdp-cpu-cores", ); add_arg!( @@ -179,8 +204,8 @@ fn deprecated_arguments() -> Vec { .takes_value(true) .value_name("INTERFACE") .conflicts_with("xdp_interface") - .requires("experimental_retransmit_xdp_cpu_cores") - .help("Network interface to use for XDP retransmit. Use --xdp-interface instead"), + .conflicts_with("no_xdp") + .help("Network interface to use for XDP. Use --xdp-interface instead"), replaced_by: "xdp-interface", ); add_arg!( @@ -189,7 +214,7 @@ fn deprecated_arguments() -> Vec { .long("experimental-retransmit-xdp-zero-copy") .takes_value(false) .conflicts_with("xdp_zero_copy") - .requires("experimental_retransmit_xdp_cpu_cores") + .conflicts_with("no_xdp") .help("Enable XDP zero copy. Use --xdp-zero-copy instead"), replaced_by: "xdp-zero-copy", ); @@ -353,6 +378,7 @@ pub fn port_validator(port: String) -> Result<(), String> { pub fn port_range_validator(port_range: String) -> Result<(), String> { if let Some((start, end)) = solana_net_utils::parse_port_range(&port_range) { + // The port range is half-open: [start, end) if end - start < MINIMUM_VALIDATOR_PORT_RANGE_WIDTH { Err(format!( "Port range is too small. Try --dynamic-port-range {}-{}", @@ -654,8 +680,12 @@ pub fn test_app<'a>(version: &'a str, default_args: &'a DefaultTestArgs) -> App< .long("dynamic-port-range") .value_name("MIN_PORT-MAX_PORT") .takes_value(true) + .default_value(&default_args.dynamic_port_range) .validator(port_range_validator) - .help("Range to use for dynamically assigned ports [default: 1024-65535]"), + .help( + "Range to use for dynamically assigned ports. MIN_PORT-MAX_PORT yields the \ + range [MIN_PORT, MAX_PORT)", + ), ) .arg( Arg::with_name("bind_address") @@ -826,6 +856,15 @@ pub fn test_app<'a>(version: &'a str, default_args: &'a DefaultTestArgs) -> App< .takes_value(false) .help("Enables external processes to connect and manage block production"), ) + .arg( + Arg::with_name("alpenglow") + .long("alpenglow") + .takes_value(false) + .help( + "Activate Alpenglow at genesis. The validator_admission_ticket feature must \ + remain active", + ), + ) .arg( Arg::with_name("deactivate_feature") .long("deactivate-feature") @@ -877,6 +916,7 @@ pub fn test_app<'a>(version: &'a str, default_args: &'a DefaultTestArgs) -> App< pub struct DefaultTestArgs { pub rpc_port: String, pub faucet_port: String, + pub dynamic_port_range: String, pub limit_ledger_size: String, pub faucet_sol: String, pub faucet_time_slice_secs: String, @@ -887,6 +927,7 @@ impl DefaultTestArgs { DefaultTestArgs { rpc_port: 8899.to_string(), faucet_port: FAUCET_PORT.to_string(), + dynamic_port_range: format!("{}-{}", VALIDATOR_PORT_RANGE.0, VALIDATOR_PORT_RANGE.1), /* 10,000 was derived empirically by watching the size * of the rocksdb/ directory self-limit itself to the * 40MB-150MB range when running `solana-test-validator` diff --git a/validator/src/commands/run/args.rs b/validator/src/commands/run/args.rs index 22845086a82..a1683a415df 100644 --- a/validator/src/commands/run/args.rs +++ b/validator/src/commands/run/args.rs @@ -5,6 +5,7 @@ use { commands::{FromClapArgMatches, Result}, }, agave_snapshots::{SUPPORTED_ARCHIVE_COMPRESSION, SnapshotVersion}, + bytesize::ByteSize, clap::{App, Arg, ArgMatches, values_t}, solana_accounts_db::utils::create_and_canonicalize_directory, solana_clap_utils::{ @@ -30,7 +31,7 @@ use { solana_send_transaction_service::send_transaction_service::Config as SendTransactionServiceConfig, solana_signer::Signer, solana_unified_scheduler_pool::DefaultSchedulerPool, - std::{collections::HashSet, net::SocketAddr, path::PathBuf, str::FromStr}, + std::{collections::HashSet, net::SocketAddr, path::PathBuf}, }; const EXCLUDE_KEY: &str = "account-index-exclude-key"; @@ -368,7 +369,10 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, .takes_value(true) .default_value(&default_args.dynamic_port_range) .validator(port_range_validator) - .help("Range to use for dynamically assigned ports"), + .help( + "Range to use for dynamically assigned ports. MIN_PORT-MAX_PORT yields the range \ + [MIN_PORT, MAX_PORT)", + ), ) .arg( Arg::with_name("maximum_local_snapshot_age") @@ -546,6 +550,12 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, .takes_value(false) .help("Refuse to start if saved tower state is not found"), ) + .arg( + clap::Arg::with_name("do_not_require_vote_history") + .long("do-not-require-vote-history") + .takes_value(false) + .help("Do not require saved vote history state for startup"), + ) .arg( Arg::with_name("expected_genesis_hash") .long("expected-genesis-hash") @@ -855,21 +865,11 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, ) .arg( Arg::with_name("poh_pinned_cpu_core") - .hidden(hidden_unless_forced()) - .long("experimental-poh-pinned-cpu-core") - .takes_value(true) - .value_name("CPU_CORE_INDEX") - .validator(|s| { - let core_index = usize::from_str(&s).map_err(|e| e.to_string())?; - let max_index = core_affinity::get_core_ids() - .map(|cids| cids.len() - 1) - .unwrap_or(0); - if core_index > max_index { - return Err(format!("core index must be in the range [0, {max_index}]")); - } - Ok(()) - }) - .help("EXPERIMENTAL: Specify which CPU core PoH is pinned to"), + .long("poh-pinned-cpu-core") + .takes_value(true) + .value_name("CPU_ID") + .validator(is_parsable::) + .help("Specify which CPU core PoH is pinned to. Defaults to CPU 0 on Linux"), ) .arg( Arg::with_name("poh_hashes_per_batch") @@ -979,14 +979,16 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, .hidden(hidden_unless_forced()), ) .arg( - Arg::with_name("accounts_db_cache_limit_mb") - .long("accounts-db-cache-limit-mb") - .value_name("MEGABYTES") - .validator(is_parsable::) + Arg::with_name("accounts_db_write_cache_limit") + .long("accounts-db-write-cache-limit") + .value_name("BYTES") + .validator(is_parsable::) .takes_value(true) - .help( - "How large the write cache for account data can become. If this is exceeded, the \ - cache is flushed more aggressively.", + .help("How large the write cache for account data can become, in bytes") + .long_help( + "How large the write cache for account data can become, in bytes. If this is \ + exceeded, the write cache is flushed more aggressively. Accepts SI and IEC \ + prefixes, e.g. 17.1GB or 18Gi.", ), ) .arg( @@ -998,11 +1000,13 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, .max_values(2) .multiple(false) .require_delimiter(true) + .validator(is_parsable::) .help("How large the read cache for account data can become, in bytes") .long_help( "How large the read cache for account data can become, in bytes. The values will \ be the low and high watermarks for the cache. When the cache exceeds the high \ - watermark, entries will be evicted until the size reaches the low watermark.", + watermark, entries will be evicted until the size reaches the low watermark. \ + Accepts SI and IEC prefixes, e.g. 8.1GB,8.3GiB. LOW must be <= HIGH.", ) .hidden(hidden_unless_forced()), ) @@ -1100,6 +1104,7 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, Arg::with_name("allow_private_addr") .long("allow-private-addr") .takes_value(false) + .requires("no_xdp") .help("Allow contacting private ip addresses") .hidden(hidden_unless_forced()), ) @@ -1139,17 +1144,17 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, .help("Disables the banking trace"), ) .arg( - Arg::with_name("delay_leader_block_for_pending_fork") + Arg::with_name("no_delay_leader_block_for_pending_fork") .hidden(hidden_unless_forced()) - .long("delay-leader-block-for-pending-fork") + .long("no-delay-leader-block-for-pending-fork") .takes_value(false) .help( - "Delay leader block creation while replaying a block which descends from the \ - current fork and has a lower slot than our next leader slot. If we don't delay \ - here, our new leader block will be on a different fork from the block we are \ - replaying and there is a high chance that the cluster will confirm that block's \ - fork rather than our leader block's fork because it was created before we \ - started creating ours.", + "Disable delaying leader block creation while replaying a block which descends \ + from the current fork and has a lower slot than our next leader slot. If we \ + don't delay here, our new leader block will be on a different fork from the \ + block we are replaying and there is a high chance that the cluster will confirm \ + that block's fork rather than our leader block's fork because it was created \ + before we started creating ours.", ), ) .arg( @@ -1208,28 +1213,41 @@ pub fn add_args<'a>(app: App<'a, 'a>, default_args: &'a DefaultArgs) -> App<'a, .validator(|s| is_within_range(s, 1..)) .help(DefaultSchedulerPool::cli_message()), ) + .arg( + Arg::with_name("no_xdp") + .long("no-xdp") + .takes_value(false) + .help("Disable XDP transmit and fall back to UDP sockets"), + ) .arg( Arg::with_name("xdp_interface") .long("xdp-interface") .takes_value(true) .value_name("INTERFACE") - .requires("xdp_cpu_cores") - .help("Network interface to use for XDP"), + .conflicts_with("no_xdp") + .help( + "Network interface to use for XDP transmit. Auto-detected from default route if \ + not specified", + ), ) .arg( Arg::with_name("xdp_cpu_cores") .long("xdp-cpu-cores") .takes_value(true) .value_name("CPU_LIST") + .conflicts_with("no_xdp") .validator(|value| validate_cpu_ranges(value, "--xdp-cpu-cores")) - .help("Use the specified CPU cores for XDP"), + .help( + "CPU cores to reserve for XDP transmit (e.g. \"2-4,7\"). Defaults to 1 \ + auto-selected core", + ), ) .arg( Arg::with_name("xdp_zero_copy") .long("xdp-zero-copy") .takes_value(false) - .requires("xdp_cpu_cores") - .help("Enable XDP zero copy. Requires hardware support"), + .conflicts_with("no_xdp") + .help("Enable XDP zero copy mode. Requires hardware and driver support"), ) .args(&pub_sub_config::args(/*test_validator:*/ false)) .args(&json_rpc_config::args()) @@ -1249,14 +1267,14 @@ fn validators_set( let validators_set: Option> = values_t!(matches, matches_name, Pubkey) .ok() .map(|validators| validators.into_iter().collect()); - if let Some(validators_set) = &validators_set { - if validators_set.contains(identity_pubkey) { - return Err(crate::commands::Error::Dynamic( - Box::::from(format!( - "the validator's identity pubkey cannot be a {arg_name}: {identity_pubkey}" - )), - )); - } + if let Some(validators_set) = &validators_set + && validators_set.contains(identity_pubkey) + { + return Err(crate::commands::Error::Dynamic( + Box::::from(format!( + "the validator's identity pubkey cannot be a {arg_name}: {identity_pubkey}" + )), + )); } Ok(validators_set) } else { @@ -1832,7 +1850,7 @@ mod tests { }; verify_args_struct_by_command_run_with_identity_setup( default_run_args, - vec!["--allow-private-addr"], + vec!["--allow-private-addr", "--no-xdp"], expected_args, ); } diff --git a/validator/src/commands/run/execute.rs b/validator/src/commands/run/execute.rs index 4506aea6110..683eebbc7f1 100644 --- a/validator/src/commands/run/execute.rs +++ b/validator/src/commands/run/execute.rs @@ -12,7 +12,7 @@ use { snapshot_config::{SnapshotConfig, SnapshotUsage}, }, agave_votor::vote_history_storage, - agave_xdp::{set_cpu_affinity, transmitter::XdpConfig}, + bytesize::ByteSize, clap::{ArgMatches, crate_name, value_t, value_t_or_exit, values_t, values_t_or_exit}, crossbeam_channel::unbounded, log::*, @@ -29,9 +29,7 @@ use { create_and_canonicalize_directory, }, }, - solana_clap_utils::input_parsers::{ - keypair_of, keypairs_of, parse_cpu_ranges, pubkey_of, value_of, values_of, - }, + solana_clap_utils::input_parsers::{keypair_of, keypairs_of, pubkey_of, value_of, values_of}, solana_clock::{DEFAULT_SLOTS_PER_EPOCH, Slot}, solana_core::{ banking_stage::transaction_scheduler::scheduler_controller::SchedulerConfig, @@ -84,6 +82,12 @@ use { sync::{Arc, RwLock, atomic::AtomicBool}, }, }; +#[cfg(target_os = "linux")] +use { + agave_cpu_utils::cpu_affinity, + agave_xdp::transmitter::{QueueCpuBinding, XdpConfig}, + solana_clap_utils::input_parsers::parse_cpu_ranges, +}; #[derive(Debug, PartialEq, Eq)] pub enum Operation { @@ -148,7 +152,6 @@ pub fn execute( solana_metrics::set_host_id(identity_keypair.pubkey().to_string()); solana_metrics::set_panic_hook("validator", Some(String::from(solana_version))); - solana_entry::entry::init_poh(); let bind_addresses = { let parsed = matches @@ -165,27 +168,11 @@ pub fn execute( Err(format!("invalid entrypoint address: {addr}"))?; } } - - let xdp_interface = matches - .value_of("xdp_interface") - .or_else(|| matches.value_of("experimental_retransmit_xdp_interface")); - let xdp_zero_copy = matches.is_present("xdp_zero_copy") - || matches.is_present("experimental_retransmit_xdp_zero_copy"); - let retransmit_xdp = matches - .value_of("xdp_cpu_cores") - .or_else(|| matches.value_of("experimental_retransmit_xdp_cpu_cores")) - .map(|cpus| { - XdpConfig::new( - xdp_interface, - parse_cpu_ranges(cpus).unwrap(), - xdp_zero_copy, - ) - }); - if bind_addresses.len() > 1 && retransmit_xdp.is_some() { - Err(String::from( - "--xdp-cpu-cores cannot be used in a multihoming context", - ))?; - } + // XDP is not needed for init — it only initializes the ledger and exits. + // Also, init drops all Linux capabilities in main() so XDP setup would fail. + #[cfg(target_os = "linux")] + let xdp_transmit_config: Option = + build_xdp_config(matches, &operation, &bind_addresses)?; let dynamic_port_range = solana_net_utils::parse_port_range(matches.value_of("dynamic_port_range").unwrap()) @@ -288,13 +275,14 @@ pub fn execute( let _ = config; #[cfg(target_os = "linux")] - let xdp_builder_with_src_addr = { + let (xdp_transmit_setup, xdp_network_config_report) = { use { agave_xdp::transmitter::TransmitterBuilder, caps::{ CapSet, Capability::{CAP_BPF, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON, CAP_SYS_NICE}, }, + solana_core::system_monitor_service::XdpNetworkConfigReport, }; let super::Config { primordial_caps } = config; @@ -314,7 +302,7 @@ pub fn execute( required_caps.extend(primordial_caps.clone()); retained_caps.extend(primordial_caps.clone()); - if let Some(xdp_config) = retransmit_xdp.as_ref() { + if let Some(xdp_config) = xdp_transmit_config.as_ref() { required_caps.insert(CAP_NET_ADMIN); required_caps.insert(CAP_NET_RAW); if xdp_config.zero_copy { @@ -370,37 +358,46 @@ pub fn execute( // XDP _MUST_ be setup _BEFORE_ the app spawns any threads to ensure linux // capabilities do not leak, leaving the process in a state where it could // potentially be used as a privilege escalation gadget - let xdp_builder_with_src_addr = retransmit_xdp.clone().map(|xdp_config| { - use { - agave_xdp::{default_device_ipv4, interface_ipv4}, - std::net::SocketAddrV4, - }; - - let src_port = node.sockets.retransmit_sockets[0] - .local_addr() - .expect("failed to get local address") - .port(); - let src_ip = match node.bind_ip_addrs.active() { - IpAddr::V4(ip) if !ip.is_unspecified() => ip, - IpAddr::V4(_unspecified) => { - if let Some(interface) = xdp_config.interface.as_ref() { - interface_ipv4(interface).expect( - "configured interface should exist and have an IPv4 address assigned", - ) - } else { - default_device_ipv4().expect( - "default route device should exist and have an IPv4 address assigned", - ) - } - } - _ => panic!("IPv6 not supported"), - }; - ( - TransmitterBuilder::new(xdp_config, exit.clone()) - .expect("failed to create xdp transmitter"), - SocketAddrV4::new(src_ip, src_port), - ) - }); + let (xdp_transmit_setup, report) = xdp_transmit_config + .clone() + .map(|mut xdp_config| { + use { + agave_xdp::{device::NetworkDevice, interface_ipv4}, + solana_core::validator::XdpTransmitSetup, + }; + + let device = if let Some(interface) = xdp_config.interface.as_ref() { + NetworkDevice::new(interface).expect("configured interface should exist") + } else { + NetworkDevice::new_from_default_route() + .expect("default route device should exist") + }; + + let xdp_interface = device.name().to_string(); + // Keep the transmitter and metrics on the selected XDP device. Source IP lookup + // uses the same interface name, with bond-master fallback. + xdp_config.interface = Some(xdp_interface.clone()); + let zero_copy = xdp_config.zero_copy; + let src_ip = match node.bind_ip_addrs.active() { + IpAddr::V4(ip) if !ip.is_unspecified() => ip, + IpAddr::V4(_unspecified) => interface_ipv4(&xdp_interface).expect( + "selected interface should exist and have an IPv4 address assigned", + ), + _ => panic!("IPv6 not supported"), + }; + ( + XdpTransmitSetup { + transmitter_builder: TransmitterBuilder::new(xdp_config, exit.clone()) + .expect("failed to create xdp transmitter"), + src_ip, + }, + XdpNetworkConfigReport { + zero_copy, + interface: xdp_interface, + }, + ) + }) + .map_or((None, None), |(setup, report)| (Some(setup), Some(report))); // we're done with caps needed to init xdp now. remove them from our process caps::set(None, CapSet::Effective, &retained_caps) @@ -408,27 +405,19 @@ pub fn execute( caps::set(None, CapSet::Permitted, &retained_caps) .expect("linux allows permitted capset to be set"); - xdp_builder_with_src_addr + (xdp_transmit_setup, report) }; #[cfg(not(target_os = "linux"))] - let xdp_builder_with_src_addr = None; + let (xdp_transmit_setup, xdp_network_config_report) = (None, None); - let reserved = retransmit_xdp - .map(|xdp| xdp.cpus.clone()) - .unwrap_or_default() - .iter() - .cloned() - .collect::>(); - if !reserved.is_empty() { - let available = core_affinity::get_core_ids() - .unwrap_or_default() - .into_iter() - .map(|core_id| core_id.id) - .collect::>(); - let available = available.difference(&reserved); - set_cpu_affinity(available.into_iter().copied()).unwrap(); - } + #[cfg(target_os = "linux")] + let poh_pinned_cpu_core = value_of(matches, "poh_pinned_cpu_core") + .or_else(|| value_of(matches, "experimental_poh_pinned_cpu_core")) + .or(poh_service::DEFAULT_PINNED_CPU_CORE); + + #[cfg(not(target_os = "linux"))] + let poh_pinned_cpu_core = None; solana_core::validator::report_target_features(); @@ -656,36 +645,37 @@ pub fn execute( const MB: usize = 1_024 * 1_024; - let account_shrink_paths: Option> = - values_t!(matches, "account_shrink_path", String) - .map(|shrink_paths| shrink_paths.into_iter().map(PathBuf::from).collect()) - .ok(); - let account_shrink_paths = account_shrink_paths - .as_ref() - .map(|paths| { - create_and_canonicalize_directories(paths) - .map_err(|err| format!("unable to access account shrink path: {err}")) - }) - .transpose()?; - - let (account_shrink_run_paths, account_shrink_snapshot_paths) = account_shrink_paths - .map(|paths| { - create_all_accounts_run_and_snapshot_dirs(&paths) - .map_err(|err| format!("unable to create account subdirectories: {err}")) - }) - .transpose()? - .unzip(); - - let read_cache_limit_bytes = - values_of::(matches, "accounts_db_read_cache_limit").map(|limits| { - match limits.len() { - 2 => (limits[0], limits[1]), - _ => { - // clap will enforce two values are given - unreachable!("invalid number of values given to accounts-db-read-cache-limit") + let read_cache_limit_bytes = if let Some(limits) = + values_of::(matches, "accounts_db_read_cache_limit") + { + match limits.as_slice() { + [lo, hi] => { + let lo = usize::try_from(lo.0)?; + let hi = usize::try_from(hi.0)?; + if lo > hi { + Err(format!( + "invalid --accounts-db-read-cache-limit: LOW ({lo}) must be <= HIGH ({hi})", + ))?; } + Some((lo, hi)) + } + _ => { + // clap will enforce two values are given + unreachable!("invalid number of values given to accounts-db-read-cache-limit") } - }); + } + } else { + None + }; + + let write_cache_limit_bytes = + value_of::(matches, "accounts_db_write_cache_limit").map(|limit| limit.0); + // accounts-db-write-cache-limit-mb was deprecated in v4.2.0 + let write_cache_limit_mb = value_t!(matches, "accounts_db_cache_limit_mb", u64) + .ok() + .map(|mb| mb * MB as u64); + // clap will enforce only one cli arg is provided, so pick whichever is Some + let write_cache_limit_bytes = write_cache_limit_bytes.or(write_cache_limit_mb); let scan_filter_for_shrinking = matches .value_of("accounts_db_scan_filter_for_shrinking") @@ -704,14 +694,11 @@ pub fn execute( index: Some(accounts_index_config), account_indexes: Some(account_indexes.clone()), bank_hash_details_dir: ledger_path.clone(), - shrink_paths: account_shrink_run_paths, shrink_ratio, read_cache_limit_bytes, read_cache_evict_sample_size: None, read_cache_num_shards: None, - write_cache_limit_bytes: value_t!(matches, "accounts_db_cache_limit_mb", u64) - .ok() - .map(|mb| mb * MB as u64), + write_cache_limit_bytes, ancient_append_vec_offset: value_t!(matches, "accounts_db_ancient_append_vecs", i64).ok(), ancient_storage_ideal_size: value_t!( matches, @@ -759,17 +746,6 @@ pub fn execute( create_all_accounts_run_and_snapshot_dirs(&account_paths) .map_err(|err| format!("unable to create account directories: {err}"))?; - // These snapshot paths are only used for initial clean up, add in shrink paths if they exist. - let account_snapshot_paths = - if let Some(account_shrink_snapshot_paths) = account_shrink_snapshot_paths { - account_snapshot_paths - .into_iter() - .chain(account_shrink_snapshot_paths) - .collect() - } else { - account_snapshot_paths - }; - let snapshot_config = new_snapshot_config( matches, &ledger_path, @@ -786,6 +762,7 @@ pub fn execute( let mut validator_config = ValidatorConfig { log_config, require_tower: matches.is_present("require_tower"), + require_vote_history: !matches.is_present("do_not_require_vote_history"), tower_storage, vote_history_storage, max_genesis_archive_unpacked_size: MAX_GENESIS_ARCHIVE_UNPACKED_SIZE, @@ -832,13 +809,13 @@ pub fn execute( no_poh_speed_test: matches.is_present("no_poh_speed_test"), no_os_memory_stats_reporting: matches.is_present("no_os_memory_stats_reporting"), no_os_network_stats_reporting: matches.is_present("no_os_network_stats_reporting"), + xdp_network_config_report, no_os_cpu_stats_reporting: matches.is_present("no_os_cpu_stats_reporting"), no_os_disk_stats_reporting: matches.is_present("no_os_disk_stats_reporting"), // The validator needs to open many files, check that the process has // permission to do so in order to fail quickly and give a direct error enforce_ulimit_nofile: true, - poh_pinned_cpu_core: value_of(matches, "poh_pinned_cpu_core") - .unwrap_or(poh_service::DEFAULT_PINNED_CPU_CORE), + poh_pinned_cpu_core, poh_hashes_per_batch: value_of(matches, "poh_hashes_per_batch") .unwrap_or(poh_service::DEFAULT_HASHES_PER_BATCH), process_ledger_before_services: matches.is_present("process_ledger_before_services"), @@ -862,8 +839,8 @@ pub fn execute( replay_transactions_threads, tvu_shred_sigverify_threads: tvu_sigverify_threads, tvu_bls_sigverify_threads, - delay_leader_block_for_pending_fork: matches - .is_present("delay_leader_block_for_pending_fork"), + delay_leader_block_for_pending_fork: !matches + .is_present("no_delay_leader_block_for_pending_fork"), turbine_mode: TurbineMode::default(), broadcast_stage_type: BroadcastStageType::Standard, block_verification_method: value_t_or_exit!( @@ -965,6 +942,7 @@ pub fn execute( authorized_voter_keypairs: authorized_voter_keypairs.clone(), post_init: admin_service_post_init.clone(), tower_storage: validator_config.tower_storage.clone(), + vote_history_storage: validator_config.vote_history_storage.clone(), staked_nodes_overrides, rpc_to_plugin_manager_sender, }, @@ -1148,7 +1126,7 @@ pub fn execute( sigverify_threads: tpu_sigverify_threads, }, admin_service_post_init, - xdp_builder_with_src_addr, + xdp_transmit_setup, exit, ) .map_err(|err| format!("{err:?}"))?; @@ -1406,3 +1384,150 @@ fn new_snapshot_config( Ok(snapshot_config) } + +#[cfg(target_os = "linux")] +fn build_xdp_config( + matches: &ArgMatches, + operation: &Operation, + bind_addresses: &BindIpAddrs, +) -> Result, String> { + if matches.is_present("no_xdp") || *operation == Operation::Initialize { + return Ok(None); + } + if bind_addresses.len() > 1 { + return Err( + "XDP cannot be used in a multihoming context; pass --no-xdp to disable XDP".to_string(), + ); + } + let xdp_interface = matches + .value_of("xdp_interface") + .or_else(|| matches.value_of("experimental_retransmit_xdp_interface")); + let xdp_zero_copy = matches.is_present("xdp_zero_copy") + || matches.is_present("experimental_retransmit_xdp_zero_copy"); + let poh_pinned_cpu_core = value_of(matches, "poh_pinned_cpu_core") + .or_else(|| value_of(matches, "experimental_poh_pinned_cpu_core")) + .or(poh_service::DEFAULT_PINNED_CPU_CORE); + let xdp_cpu_cores = matches + .value_of("xdp_cpu_cores") + .or_else(|| matches.value_of("experimental_retransmit_xdp_cpu_cores")); + let cpus = if let Some(cpu_str) = xdp_cpu_cores { + let parsed = + parse_cpu_ranges(cpu_str).expect("clap validator already accepted this CPU list"); + if let Some(poh_core) = poh_pinned_cpu_core + && parsed.contains(&poh_core) + { + return Err(format!( + "--xdp-cpu-cores includes PoH core {poh_core}; XDP and PoH must not share a CPU \ + core" + )); + } + Some(parsed) + } else { + // Auto-select a single core, avoiding the PoH core. + match cpu_affinity(None) { + Ok(allowed) => { + match allowed + .iter() + .rev() + .map(|cpu| **cpu) + .find(|cpu| Some(*cpu) != poh_pinned_cpu_core) + { + Some(cpu) => Some(vec![cpu]), + None => { + return Err(format!( + "XDP requires a dedicated CPU core separate from PoH (core \ + {poh_pinned_cpu_core:?}), but none is available. Pass --no-xdp to \ + disable XDP." + )); + } + } + } + Err(e) => { + return Err(format!( + "failed to query CPU affinity: {e}. Pass --no-xdp to disable XDP, or provide \ + --xdp-cpu-cores explicitly." + )); + } + } + }; + Ok(cpus.map(|cpus| { + info!("XDP enabled on CPU cores: {cpus:?}"); + // Map the CPU list onto hardware queues sequentially (queue i -> cpus[i]). + let queues = cpus + .into_iter() + .enumerate() + .map(|(queue, cpu)| QueueCpuBinding { + queue: queue as u32, + cpu, + }) + .collect(); + XdpConfig::new(xdp_interface, queues, xdp_zero_copy) + })) +} + +#[cfg(all(target_os = "linux", test))] +mod xdp_tests { + use { + super::*, + crate::{cli::DefaultArgs, commands::run::args::add_args}, + solana_net_utils::multihomed_sockets::BindIpAddrs, + std::net::{IpAddr, Ipv4Addr}, + }; + + fn single_ip_bind() -> BindIpAddrs { + BindIpAddrs::new(vec![Ipv4Addr::UNSPECIFIED.into()]).unwrap() + } + + fn multihoming_bind() -> BindIpAddrs { + BindIpAddrs::new(vec![ + IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), + IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2)), + ]) + .unwrap() + } + + #[test] + fn test_no_xdp_flag_disables_xdp() { + let default_args = DefaultArgs::default(); + let app = add_args(clap::App::new("agave-validator"), &default_args); + let matches = app.get_matches_from(vec!["agave-validator", "--no-xdp"]); + let result = build_xdp_config(&matches, &Operation::Run, &single_ip_bind()); + assert!(result.unwrap().is_none(), "--no-xdp must disable XDP"); + } + + #[test] + fn test_init_disables_xdp() { + let default_args = DefaultArgs::default(); + let app = add_args(clap::App::new("agave-validator"), &default_args); + let matches = app.get_matches_from(vec!["agave-validator"]); + let result = build_xdp_config(&matches, &Operation::Initialize, &single_ip_bind()); + assert!(result.unwrap().is_none(), "init operation must disable XDP"); + } + + #[test] + fn test_multihoming_is_error() { + let default_args = DefaultArgs::default(); + let app = add_args(clap::App::new("agave-validator"), &default_args); + let matches = app.get_matches_from(vec!["agave-validator"]); + let result = build_xdp_config(&matches, &Operation::Run, &multihoming_bind()); + assert!( + result.unwrap_err().contains("multihoming"), + "multihoming context must produce an error" + ); + } + + #[test] + fn test_explicit_xdp_core_conflicts_with_poh_core_is_error() { + let default_args = DefaultArgs::default(); + let app = add_args(clap::App::new("agave-validator"), &default_args); + let poh_core = solana_poh::poh_service::DEFAULT_PINNED_CPU_CORE + .unwrap_or(0) + .to_string(); + let matches = app.get_matches_from(vec!["agave-validator", "--xdp-cpu-cores", &poh_core]); + let result = build_xdp_config(&matches, &Operation::Run, &single_ip_bind()); + assert!( + result.unwrap_err().contains("PoH core"), + "XDP core overlapping PoH core must produce an error" + ); + } +} diff --git a/validator/src/commands/set_identity/mod.rs b/validator/src/commands/set_identity/mod.rs index 0e2c9d7e250..9c3a834095c 100644 --- a/validator/src/commands/set_identity/mod.rs +++ b/validator/src/commands/set_identity/mod.rs @@ -13,10 +13,20 @@ use { const COMMAND: &str = "set-identity"; #[derive(Debug, PartialEq)] -#[cfg_attr(test, derive(Default))] pub struct SetIdentityArgs { pub identity: Option, pub require_tower: bool, + pub require_vote_history: bool, +} + +impl Default for SetIdentityArgs { + fn default() -> Self { + Self { + identity: None, + require_tower: false, + require_vote_history: true, + } + } } impl FromClapArgMatches for SetIdentityArgs { @@ -24,6 +34,7 @@ impl FromClapArgMatches for SetIdentityArgs { Ok(SetIdentityArgs { identity: value_t!(matches, "identity", String).ok(), require_tower: matches.is_present("require_tower"), + require_vote_history: !matches.is_present("do_not_require_vote_history"), }) } } @@ -45,6 +56,12 @@ pub fn command<'a>() -> App<'a, 'a> { .takes_value(false) .help("Refuse to set the validator identity if saved tower state is not found"), ) + .arg( + clap::Arg::with_name("do_not_require_vote_history") + .long("do-not-require-vote-history") + .takes_value(false) + .help("Do not require saved vote history state for identity change"), + ) .after_help( "Note: the new identity only applies to the currently running validator instance", ) @@ -54,6 +71,7 @@ pub fn execute(matches: &ArgMatches, ledger_path: &Path) -> Result<()> { let SetIdentityArgs { identity, require_tower, + require_vote_history, } = SetIdentityArgs::from_clap_arg_match(matches)?; if let Some(identity_keypair) = identity { @@ -68,7 +86,11 @@ pub fn execute(matches: &ArgMatches, ledger_path: &Path) -> Result<()> { admin_rpc_service::runtime().block_on(async move { admin_client .await? - .set_identity(identity_keypair.display().to_string(), require_tower) + .set_identity( + identity_keypair.display().to_string(), + require_tower, + require_vote_history, + ) .await })?; } else { @@ -81,7 +103,11 @@ pub fn execute(matches: &ArgMatches, ledger_path: &Path) -> Result<()> { admin_rpc_service::runtime().block_on(async move { admin_client .await? - .set_identity_from_bytes(Vec::from(identity_keypair.to_bytes()), require_tower) + .set_identity_from_bytes( + Vec::from(identity_keypair.to_bytes()), + require_tower, + require_vote_history, + ) .await })?; } @@ -129,4 +155,16 @@ mod tests { }, ); } + + #[test] + fn verify_args_struct_by_command_set_identity_do_not_require_vote_history() { + verify_args_struct_by_command( + command(), + vec![COMMAND, "--do-not-require-vote-history"], + SetIdentityArgs { + require_vote_history: false, + ..SetIdentityArgs::default() + }, + ); + } } diff --git a/validator/src/dashboard.rs b/validator/src/dashboard.rs index 5f8d3c42fe9..f68093a39c4 100644 --- a/validator/src/dashboard.rs +++ b/validator/src/dashboard.rs @@ -71,12 +71,9 @@ impl Dashboard { let progress_bar = new_spinner_progress_bar(); progress_bar.set_message("Connecting..."); - let Some((rpc_addr, start_time)) = runtime.block_on(wait_for_validator_startup( - &ledger_path, - &exit, - progress_bar, - refresh_interval, - )) else { + let Some((rpc_addr, start_time, mut vat_status)) = runtime.block_on( + wait_for_validator_startup(&ledger_path, &exit, progress_bar, refresh_interval), + ) else { continue; }; @@ -89,6 +86,11 @@ impl Dashboard { } }; println_name_value("Identity:", &identity.to_string()); + if let Some(status) = vat_status.as_ref() + && status.voting_enabled + { + println_name_value("Vote Account:", &status.vote_account.to_string()); + } if let Ok(genesis_hash) = rpc_client.get_genesis_hash() { println_name_value("Genesis Hash:", &genesis_hash.to_string()); @@ -118,6 +120,7 @@ impl Dashboard { let progress_bar = new_spinner_progress_bar(); let mut snapshot_slot_info = None; + let mut admin_client = None; for i in 0.. { if exit.load(Ordering::Relaxed) { break; @@ -130,6 +133,34 @@ impl Dashboard { if identity != new_identity { identity = new_identity; progress_bar.println(format_name_value("Identity:", &identity.to_string())); + if let Some(status) = vat_status.as_ref() + && status.voting_enabled + { + progress_bar.println(format_name_value( + "Vote Account:", + &status.vote_account.to_string(), + )); + } + } + + if i > 0 && i % 30 == 0 { + if admin_client.is_none() { + admin_client = runtime + .block_on(admin_rpc_service::connect(&ledger_path)) + .ok(); + } + + let vat_status_result = admin_client + .as_ref() + .map(|admin_client| runtime.block_on(admin_client.vat_status())); + vat_status = match vat_status_result { + Some(Ok(status)) => Some(status), + Some(Err(_err)) => { + admin_client = None; + None + } + None => None, + }; } match get_validator_stats(&rpc_client, &identity) { @@ -153,10 +184,12 @@ impl Dashboard { ) }; + let vat_status_formatted = format_vat_status(vat_status.as_ref()); + progress_bar.set_message(format!( "{}{}| Processed Slot: {} | Confirmed Slot: {} | Finalized Slot: {} | \ Full Snapshot Slot: {} | Incremental Snapshot Slot: {} | \ - Transactions: {} | {}", + Transactions: {} | {}\n{}", uptime, if health == "ok" { "".to_string() @@ -177,7 +210,8 @@ impl Dashboard { .map(|incremental| incremental.to_string())) .unwrap_or_else(|| '-'.to_string()), transaction_count, - identity_balance + identity_balance, + vat_status_formatted, )); thread::sleep(refresh_interval); } @@ -191,12 +225,59 @@ impl Dashboard { } } +fn format_vat_status( + status: Option<&admin_rpc_service::AdminRpcValidatorAdmissionTicketStatus>, +) -> String { + let Some(status) = status else { + return "VAT: failed to connect to admin RPC".to_string(); + }; + + if !status.vat_active { + return "VAT: inactive".to_string(); + } + + format!( + "{}{}", + format_current_vat_status(status), + format_effective_epoch_vat_status(status) + ) +} + +fn format_current_vat_status( + status: &admin_rpc_service::AdminRpcValidatorAdmissionTicketStatus, +) -> String { + if status.in_current_epoch_vat { + format!( + "VAT: epoch {} in (stake: {})", + status.current_epoch, + Sol(status.current_epoch_vote_account_stake) + ) + } else { + format!("VAT: epoch {} out", status.current_epoch) + } +} + +fn format_effective_epoch_vat_status( + status: &admin_rpc_service::AdminRpcValidatorAdmissionTicketStatus, +) -> String { + let vat_effective_epoch = status.current_epoch.saturating_add(2); + if let Some(vat_failure_reason) = &status.next_epoch_vat_failure_reason { + format!(", epoch {vat_effective_epoch}: {vat_failure_reason}") + } else { + format!(", epoch {vat_effective_epoch}: eligible if staked") + } +} + async fn wait_for_validator_startup( ledger_path: &Path, exit: &AtomicBool, progress_bar: ProgressBar, refresh_interval: Duration, -) -> Option<(SocketAddr, SystemTime)> { +) -> Option<( + SocketAddr, + SystemTime, + Option, +)> { let mut admin_client = None; loop { if exit.load(Ordering::Relaxed) { @@ -222,12 +303,19 @@ async fn wait_for_validator_startup( let validator_info = async move { let rpc_addr = admin_client.rpc_addr().await?; let start_time = admin_client.start_time().await?; - Ok::<_, jsonrpc_core_client::RpcError>((rpc_addr, start_time)) + let vat_status = if rpc_addr.is_some() { + admin_client.vat_status().await.ok() + } else { + None + }; + Ok::<_, jsonrpc_core_client::RpcError>((rpc_addr, start_time, vat_status)) } .await; match validator_info { - Ok((None, _)) => progress_bar.set_message("RPC service not available"), - Ok((Some(rpc_addr), start_time)) => return Some((rpc_addr, start_time)), + Ok((None, _, _)) => progress_bar.set_message("RPC service not available"), + Ok((Some(rpc_addr), start_time, vat_status)) => { + return Some((rpc_addr, start_time, vat_status)); + } Err(err) => { progress_bar .set_message(format!("Failed to get validator info: {err}")); diff --git a/validator/tests/cli.rs b/validator/tests/cli.rs index 1ee5acda6e1..bf1478b5a02 100644 --- a/validator/tests/cli.rs +++ b/validator/tests/cli.rs @@ -25,6 +25,7 @@ fn test_use_the_same_path_for_accounts_and_snapshots() { "--log", "-", "--no-voting", + "--no-xdp", "--accounts", temp_dir_str, "--snapshots", diff --git a/version/Cargo.toml b/version/Cargo.toml index 71aeaf4cd80..fbf410a9280 100644 --- a/version/Cargo.toml +++ b/version/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] name = "solana_version" @@ -33,6 +35,8 @@ solana-frozen-abi-macro = { workspace = true, optional = true, features = [ ] } solana-sanitize = { workspace = true } solana-serde-varint = { workspace = true } +solana-wincode-varint = { workspace = true } +wincode = { workspace = true } [dev-dependencies] bincode = { workspace = true } diff --git a/version/build.rs b/version/build.rs index 7a03c0a8542..fb35bb186b0 100644 --- a/version/build.rs +++ b/version/build.rs @@ -1,12 +1,11 @@ use std::process::Command; fn main() { - if let Ok(git_output) = Command::new("git").args(["rev-parse", "HEAD"]).output() { - if git_output.status.success() { - if let Ok(git_commit_hash) = String::from_utf8(git_output.stdout) { - let trimmed_hash = git_commit_hash.trim().to_string(); - println!("cargo:rustc-env=AGAVE_GIT_COMMIT_HASH={trimmed_hash}"); - } - } + if let Ok(git_output) = Command::new("git").args(["rev-parse", "HEAD"]).output() + && git_output.status.success() + && let Ok(git_commit_hash) = String::from_utf8(git_output.stdout) + { + let trimmed_hash = git_commit_hash.trim().to_string(); + println!("cargo:rustc-env=AGAVE_GIT_COMMIT_HASH={trimmed_hash}"); } } diff --git a/version/src/v4.rs b/version/src/v4.rs index 4b79e94b99f..c89797e2ae8 100644 --- a/version/src/v4.rs +++ b/version/src/v4.rs @@ -4,7 +4,13 @@ use { serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _, ser::Error as _}, solana_sanitize::Sanitize, solana_serde_varint as serde_varint, - std::{convert::TryInto, fmt, str::FromStr}, + solana_wincode_varint::Leb128Int, + std::{convert::TryInto, fmt, mem::MaybeUninit, str::FromStr}, + wincode::{ + ReadError, ReadResult, SchemaRead, SchemaWrite, WriteError, WriteResult, + config::Config, + io::{Reader, Writer}, + }, }; #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] @@ -80,9 +86,13 @@ impl FromStr for Prerelease { } #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, SchemaRead, SchemaWrite)] #[serde(transparent)] -struct PackedMinor(#[serde(with = "serde_varint")] u16); +struct PackedMinor( + #[serde(with = "serde_varint")] + #[wincode(with = "Leb128Int")] + u16, +); #[derive(Clone, Debug, PartialEq)] pub enum PackedMinorPackError { @@ -146,6 +156,7 @@ impl PackedMinor { } } +#[cfg_attr(feature = "frozen-abi", derive(StableAbi))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct Version { major: u16, @@ -271,18 +282,81 @@ impl fmt::Display for Version { } impl Sanitize for Version {} + +unsafe impl SchemaWrite for Version { + type Src = Version; + + fn size_of(src: &Version) -> WriteResult { + let serialized = Self::to_serialized(src)?; + >::size_of(&serialized) + } + + fn write(writer: impl Writer, src: &Version) -> WriteResult<()> { + let serialized = Self::to_serialized(src)?; + >::write(writer, &serialized) + } +} + +impl Version { + fn to_serialized(src: &Version) -> WriteResult { + let (packed_minor, patch) = PackedMinor::try_pack(src.minor, src.patch, &src.prerelease) + .map_err(|_| WriteError::Custom("Version: invalid minor/patch/prerelease"))?; + let client = u16::try_from(src.client.clone()) + .map_err(|_| WriteError::Custom("Version: invalid client ID"))?; + Ok(SerializedVersion { + major: src.major, + packed_minor, + patch, + commit: src.commit, + feature_set: src.feature_set, + client, + }) + } +} + +unsafe impl<'de, C: Config> SchemaRead<'de, C> for Version { + type Dst = Version; + + fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit) -> ReadResult<()> { + let SerializedVersion { + major, + packed_minor, + patch, + commit, + feature_set, + client, + } = >::get(reader)?; + let (minor, patch, prerelease) = packed_minor + .try_unpack(patch) + .map_err(|_| ReadError::Custom("Version: invalid packed minor"))?; + dst.write(Version { + major, + minor, + patch, + commit, + feature_set, + client: ClientId::from(client), + prerelease, + }); + Ok(()) + } +} + #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, SchemaRead, SchemaWrite)] struct SerializedVersion { #[serde(with = "serde_varint")] + #[wincode(with = "Leb128Int")] major: u16, #[serde(rename = "minor")] packed_minor: PackedMinor, #[serde(with = "serde_varint")] + #[wincode(with = "Leb128Int")] patch: u16, commit: u32, feature_set: u32, #[serde(with = "serde_varint")] + #[wincode(with = "Leb128Int")] client: u16, } @@ -349,10 +423,65 @@ impl<'de> Deserialize<'de> for Version { } } +// Generates a random, always-serializable `Version`. `Version` has a packed +// wire format with cross-field invariants (`minor` fits in 14 bits, non-stable +// prereleases force `patch == 0`; see `PackedMinor::try_pack`), so the fields +// cannot be sampled independently. Used by tests and as the `StableAbi` sampler. +#[cfg(any(test, feature = "frozen-abi"))] +fn random_version(rng: &mut R) -> Version { + let minor = rng.random::() & PackedMinor::PRERELEASE_MINOR_MAX; + let (prerelease, patch) = match rng.random::() % 4 { + 0 => (Prerelease::Stable, rng.random::()), + 1 => (Prerelease::ReleaseCandidate(rng.random()), 0), + 2 => (Prerelease::Beta(rng.random()), 0), + _ => (Prerelease::Alpha(rng.random()), 0), + }; + Version::new_from_parts( + rng.random(), + minor, + patch, + rng.random(), + rng.random(), + ClientId::from(rng.random::()), + prerelease, + ) +} + +// `StableAbiSample` cannot be derived here because it samples fields +// independently; `random_version` upholds the cross-field invariants instead. +#[cfg(feature = "frozen-abi")] +impl solana_frozen_abi::rand::distr::Distribution + for solana_frozen_abi::rand::distr::StandardUniform +{ + fn sample(&self, rng: &mut R) -> Version { + random_version(rng) + } +} + #[cfg(test)] mod tests { use {super::*, crate::v3}; + #[test] + fn test_wincode_compatibility() { + let mut rng = rand::rng(); + for _ in 0..1000 { + let version = random_version(&mut rng); + + // Serialize with bincode, deserialize with wincode, check results agree. + let bincode_bytes = bincode::serialize(&version).unwrap(); + let wincode_decoded: Version = wincode::deserialize(&bincode_bytes).unwrap(); + assert_eq!(version, wincode_decoded); + + // Serialize with wincode, deserialize with bincode, check results agree. + let wincode_bytes = wincode::serialize(&version).unwrap(); + let bincode_decoded: Version = bincode::deserialize(&wincode_bytes).unwrap(); + assert_eq!(version, bincode_decoded); + + assert_eq!(bincode_bytes, wincode_bytes); + } + } + #[test] fn test_prerelease_patch_is_valid() { assert!(Prerelease::Alpha(0).patch_is_valid(0)); diff --git a/vote/Cargo.toml b/vote/Cargo.toml index 2b71a638192..7d993034d30 100644 --- a/vote/Cargo.toml +++ b/vote/Cargo.toml @@ -11,6 +11,8 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [lib] crate-type = ["lib"] @@ -28,6 +30,8 @@ dev-context-only-utils = [ frozen-abi = [ "dep:solana-frozen-abi", "dep:solana-frozen-abi-macro", + "solana-account/frozen-abi", + "solana-pubkey/frozen-abi", "solana-vote-interface/frozen-abi", ] diff --git a/vote/src/vote_account.rs b/vote/src/vote_account.rs index d4191fb900d..6e303769e4a 100644 --- a/vote/src/vote_account.rs +++ b/vote/src/vote_account.rs @@ -32,7 +32,7 @@ use { }, }; -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Clone, Debug, PartialEq)] pub struct VoteAccount(Arc); @@ -44,15 +44,20 @@ pub enum Error { InvalidOwner(/*owner:*/ Pubkey), } -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Debug)] struct VoteAccountInner { account: AccountSharedData, + // Skipped by the custom serializer, and hard to instantiate other than via `AbiExample`. + #[cfg_attr( + feature = "frozen-abi", + stable_abi_sample(with = "solana_frozen_abi::abi_example::AbiExample::example()") + )] vote_state_view: VoteStateView, } pub type VoteAccountsHashMap = HashMap; -#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive(Debug, Serialize, Deserialize)] #[cfg_attr( feature = "dev-context-only-utils", @@ -62,6 +67,7 @@ pub struct VoteAccounts { #[serde(deserialize_with = "deserialize_accounts_hash_map")] vote_accounts: Arc, // Inner Arc is meant to implement copy-on-write semantics. + #[cfg_attr(feature = "frozen-abi", stable_abi_sample(with = "Default::default()"))] #[serde(skip)] staked_nodes: OnceLock< Arc< @@ -312,7 +318,8 @@ impl VoteAccounts { Some(mem::replace(old_vote_account, new_vote_account)) } Entry::Vacant(entry) => { - // This is a new vote account. We don't know the stake yet, so we need to compute it. + // This is a new vote account. Use the caller-provided stake for the initial + // cache entry. let (stake, vote_account) = entry.insert((calculate_stake(), new_vote_account)); if let Some(staked_nodes) = self.staked_nodes.get_mut() { Self::do_add_node_stake(staked_nodes, *stake, *vote_account.node_pubkey()); diff --git a/vote/src/vote_transaction.rs b/vote/src/vote_transaction.rs index 816c3a8846e..9056f72ebde 100644 --- a/vote/src/vote_transaction.rs +++ b/vote/src/vote_transaction.rs @@ -10,8 +10,11 @@ use { #[cfg_attr( feature = "frozen-abi", - derive(AbiExample, AbiEnumVisitor), - frozen_abi(digest = "BBjevJKZyxpH1c6op3oD7xpYwVuMxDR5MJhHN8W6z1u6") + derive(AbiExample, AbiEnumVisitor, StableAbi, StableAbiSample), + frozen_abi( + api_digest = "s77hb8RD8Hc9fsXn6uo6by1Kk3ByFF5bcBGA7qr2bLn", + abi_digest = "BZedcpj1BV5iCF1n7Tt62rbyfY4gm6D5Fjtj5dDtGSh5" + ) )] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub enum VoteTransaction { diff --git a/votor-messages/Cargo.toml b/votor-messages/Cargo.toml index 77ed23ecc76..308afcacd26 100644 --- a/votor-messages/Cargo.toml +++ b/votor-messages/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] dev-context-only-utils = [] @@ -21,6 +26,7 @@ frozen-abi = [ [dependencies] agave-feature-set = { workspace = true } +crossbeam-channel = { workspace = true } log = { workspace = true } serde = { workspace = true } solana-address = { workspace = true, features = ["curve25519"] } @@ -44,7 +50,7 @@ thiserror = { workspace = true } wincode = { workspace = true } [dev-dependencies] -agave-votor-messages = { path = ".", features = ["dev-context-only-utils"] } +agave-votor-messages = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } tempfile = { workspace = true } [lints] diff --git a/votor-messages/src/certificate.rs b/votor-messages/src/certificate.rs new file mode 100644 index 00000000000..64fc68218b8 --- /dev/null +++ b/votor-messages/src/certificate.rs @@ -0,0 +1,254 @@ +//! Definitions of Alpenglow consensus certificates + +use { + crate::{ + consensus_message::Block, + fraction::Fraction, + migration::GENESIS_VOTE_THRESHOLD, + vote::{Vote, VoteType}, + wire::get_vote_payload_to_sign, + }, + solana_bls_signatures::Signature as BLSSignature, + solana_clock::Slot, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +/// A cert signature +pub struct CertSignature { + /// The aggregate signature. + pub signature: BLSSignature, + /// A rank bitmap for validators' signatures included in the aggregate. + /// See solana-signer-store for encoding format. + pub bitmap: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// Genesis cert +pub struct GenesisCert { + /// Block the cert is for. + pub block: Block, + /// the signature on the cert + pub signature: CertSignature, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// A notarize cert +pub struct NotarCert { + /// Block the cert is for. + pub block: Block, + /// the signature on the cert + pub signature: CertSignature, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// A slow finalized cert +pub struct FinalizeCert { + /// Slot the cert is for. + pub slot: Slot, + /// the signature on the cert + pub signature: CertSignature, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// A fast finalized cert +pub struct FastFinalizeCert { + /// Block the cert is for. + pub block: Block, + /// the signature on the cert + pub signature: CertSignature, +} + +/// The actual certificate with the aggregate signature and bitmap for which validators are included in the aggregate. +/// BLS vote message, we need rank to look up pubkey +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Certificate { + /// The certificate type. + pub cert_type: CertificateType, + /// The aggregate signature. + pub signature: BLSSignature, + /// A rank bitmap for validators' signatures included in the aggregate. + /// See solana-signer-store for encoding format. + pub bitmap: Vec, +} + +/// The different types of certificates and their relevant state. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CertificateType { + /// Finalize certificate + Finalize(Slot), + /// Fast finalize certificate + FinalizeFast(Block), + /// Notarize certificate + Notarize(Block), + /// Notarize fallback certificate + NotarizeFallback(Block), + /// Skip certificate + Skip(Slot), + /// Genesis certificate + Genesis(Block), +} + +impl CertificateType { + /// Get the slot of the certificate + pub fn slot(&self) -> Slot { + match self { + CertificateType::Finalize(slot) + | CertificateType::FinalizeFast(Block { slot, block_id: _ }) + | CertificateType::NotarizeFallback(Block { slot, block_id: _ }) + | CertificateType::Notarize(Block { slot, block_id: _ }) + | CertificateType::Genesis(Block { slot, block_id: _ }) + | CertificateType::Skip(slot) => *slot, + } + } + + /// Returns the serialized vote payloads needed to verify signature on the cert + pub fn get_vote_payload(&self, shred_version: u16) -> (Vec, Option>) { + match self { + Self::Notarize(block) | Self::FinalizeFast(block) => { + let vote = Vote::new_notarization_vote(*block); + (get_vote_payload_to_sign(vote, shred_version), None) + } + Self::Genesis(block) => { + let vote = Vote::new_genesis_vote(*block); + (get_vote_payload_to_sign(vote, shred_version), None) + } + Self::Finalize(slot) => { + let vote = Vote::new_finalization_vote(*slot); + (get_vote_payload_to_sign(vote, shred_version), None) + } + Self::Skip(slot) => { + let skip_vote = Vote::new_skip_vote(*slot); + let skip_fallback_vote = Vote::new_skip_fallback_vote(*slot); + ( + get_vote_payload_to_sign(skip_vote, shred_version), + Some(get_vote_payload_to_sign(skip_fallback_vote, shred_version)), + ) + } + Self::NotarizeFallback(block) => { + let notar_vote = Vote::new_notarization_vote(*block); + let notar_fallback_vote = Vote::new_notarization_fallback_vote(*block); + ( + get_vote_payload_to_sign(notar_vote, shred_version), + Some(get_vote_payload_to_sign(notar_fallback_vote, shred_version)), + ) + } + } + } + + /// Is this a fast finalize certificate? + pub fn is_fast_finalization(&self) -> bool { + matches!(self, Self::FinalizeFast(_)) + } + + /// Is this a finalize / fast finalize certificate? + pub fn is_finalization(&self) -> bool { + matches!(self, Self::Finalize(_) | Self::FinalizeFast(_)) + } + + /// Is this a slow finalize certificate? + pub fn is_slow_finalization(&self) -> bool { + matches!(self, Self::Finalize(_)) + } + + /// Is this a notarize certificate? + pub fn is_notarize(&self) -> bool { + matches!(self, Self::Notarize(_)) + } + + /// Is this a notarize fallback certificate? + pub fn is_notarize_fallback(&self) -> bool { + matches!(self, Self::NotarizeFallback(_)) + } + + /// Is this a skip certificate? + pub fn is_skip(&self) -> bool { + matches!(self, Self::Skip(_)) + } + + /// Is this a genesis certificate? + pub fn is_genesis(&self) -> bool { + matches!(self, Self::Genesis(_)) + } + + /// Gets the block associated with this certificate, if present + pub fn to_block(self) -> Option { + match self { + CertificateType::Finalize(_) | CertificateType::Skip(_) => None, + CertificateType::Notarize(block) + | CertificateType::NotarizeFallback(block) + | CertificateType::Genesis(block) + | CertificateType::FinalizeFast(block) => Some(block), + } + } + + /// Reconstructs the single source `Vote` payload for this certificate. + /// + /// This method is used primarily by the signature verifier. For + /// certificates formed by aggregating a single type of vote + /// (e.g., a `Notarize` certificate from `Notarize` votes), this function + /// reconstructs the canonical message payload that was signed by validators. + /// + /// For `NotarizeFallback` and `Skip` certificates, this function returns the + /// appropriate payload *only* if the certificate was formed from a single + /// vote type (e.g., exclusively from `Notarize` or `Skip` votes). For + /// certificates formed from a mix of two vote types, use the `to_source_votes` + /// function. + pub fn to_source_vote(self) -> Vote { + match self { + Self::Notarize(block) | Self::FinalizeFast(block) | Self::NotarizeFallback(block) => { + Vote::new_notarization_vote(block) + } + Self::Finalize(slot) => Vote::new_finalization_vote(slot), + Self::Skip(slot) => Vote::new_skip_vote(slot), + Self::Genesis(block) => Vote::new_genesis_vote(block), + } + } + + /// Reconstructs the two distinct source `Vote` payloads for this certificate. + /// + /// This method is primarily used by the signature verifier for certificates that + /// can be formed by aggregating two different types of votes. For example, a + /// `NotarizeFallback` certificate accepts both `Notarize` and `NotarizeFallback`. + /// + /// It reconstructs both potential message payloads that were signed by validators, which + /// the verifier uses to check the single aggregate signature. + pub fn to_source_votes(self) -> Option<(Vote, Vote)> { + match self { + Self::NotarizeFallback(block) => { + let vote1 = Vote::new_notarization_vote(block); + let vote2 = Vote::new_notarization_fallback_vote(block); + Some((vote1, vote2)) + } + Self::Skip(slot) => { + let vote1 = Vote::new_skip_vote(slot); + let vote2 = Vote::new_skip_fallback_vote(slot); + Some((vote1, vote2)) + } + // Other certificate types do not use Base3 encoding. + _ => None, + } + } + + /// Returns the stake fraction required for certificate completion and the + /// `VoteType`s that contribute to this certificate. + /// + /// Must be in sync with `Vote::to_cert_types` + pub const fn limits_and_vote_types(&self) -> (Fraction, &'static [VoteType]) { + match self { + CertificateType::Notarize(_) => (Fraction::from_percentage(60), &[VoteType::Notarize]), + CertificateType::NotarizeFallback(_) => ( + Fraction::from_percentage(60), + &[VoteType::Notarize, VoteType::NotarizeFallback], + ), + CertificateType::FinalizeFast(_) => { + (Fraction::from_percentage(80), &[VoteType::Notarize]) + } + CertificateType::Finalize(_) => (Fraction::from_percentage(60), &[VoteType::Finalize]), + CertificateType::Skip(_) => ( + Fraction::from_percentage(60), + &[VoteType::Skip, VoteType::SkipFallback], + ), + CertificateType::Genesis(_) => (GENESIS_VOTE_THRESHOLD, &[VoteType::Genesis]), + } + } +} diff --git a/votor-messages/src/consensus_message.rs b/votor-messages/src/consensus_message.rs index c82ed3a4a1e..4c0487cd259 100644 --- a/votor-messages/src/consensus_message.rs +++ b/votor-messages/src/consensus_message.rs @@ -1,9 +1,8 @@ //! Put Alpenglow consensus messages here so all clients can agree on the format. use { crate::{ - fraction::Fraction, - migration::GENESIS_VOTE_THRESHOLD, - vote::{Vote, VoteType}, + certificate::{Certificate, CertificateType}, + vote::Vote, }, serde::{Deserialize, Serialize}, solana_bls_signatures::Signature as BLSSignature, @@ -21,219 +20,50 @@ pod_wrapper! { /// The seed used to derive the BLS keypair pub const BLS_KEYPAIR_DERIVE_SEED: &[u8; 9] = b"alpenglow"; -/// Block, a (slot, hash) tuple -pub type Block = (Slot, Hash); -/// A consensus vote. -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "5eorzdc18a1sNEUDLAKPgrHCqHmA8ssuTwKSGsZLwBqR") -)] -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, SchemaWrite, SchemaRead)] -pub struct VoteMessage { - /// The type of the vote. - pub vote: Vote, - /// The signature. - #[wincode(with = "PodBLSSignature")] - pub signature: BLSSignature, - /// The rank of the validator. - pub rank: u16, +#[cfg(feature = "frozen-abi")] +fn sample_hash(rng: &mut (impl solana_frozen_abi::rand::RngCore + ?Sized)) -> Hash { + use solana_frozen_abi::stable_abi::StableAbi; + Hash::new_from_array(<[u8; solana_hash::HASH_BYTES] as StableAbi>::random(rng)) } -/// The different types of certificates and their relevant state. -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample, AbiEnumVisitor), - frozen_abi(digest = "CazjewshYYizgQuCgBBRv6gzasJpUvFVKoSeEirWRKgA") -)] +/// An alpenglow block +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] #[derive( - Debug, - Copy, Clone, + Copy, + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, - Deserialize, + Default, Serialize, + Deserialize, SchemaWrite, SchemaRead, )] -pub enum CertificateType { - /// Finalize certificate - Finalize(Slot), - /// Fast finalize certificate - FinalizeFast(Slot, Hash), - /// Notarize certificate - Notarize(Slot, Hash), - /// Notarize fallback certificate - NotarizeFallback(Slot, Hash), - /// Skip certificate - Skip(Slot), - /// Genesis certificate - Genesis(Slot, Hash), -} - -impl CertificateType { - /// Get the slot of the certificate - pub fn slot(&self) -> Slot { - match self { - CertificateType::Finalize(slot) - | CertificateType::FinalizeFast(slot, _) - | CertificateType::Notarize(slot, _) - | CertificateType::NotarizeFallback(slot, _) - | CertificateType::Genesis(slot, _) - | CertificateType::Skip(slot) => *slot, - } - } - - /// Is this a fast finalize certificate? - pub fn is_fast_finalization(&self) -> bool { - matches!(self, Self::FinalizeFast(_, _)) - } - - /// Is this a finalize / fast finalize certificate? - pub fn is_finalization(&self) -> bool { - matches!(self, Self::Finalize(_) | Self::FinalizeFast(_, _)) - } - - /// Is this a slow finalize certificate? - pub fn is_slow_finalization(&self) -> bool { - matches!(self, Self::Finalize(_)) - } - - /// Is this a notarize certificate? - pub fn is_notarize(&self) -> bool { - matches!(self, Self::Notarize(_, _)) - } - - /// Is this a notarize fallback certificate? - pub fn is_notarize_fallback(&self) -> bool { - matches!(self, Self::NotarizeFallback(_, _)) - } - - /// Is this a skip certificate? - pub fn is_skip(&self) -> bool { - matches!(self, Self::Skip(_)) - } - - /// Is this a genesis certificate? - pub fn is_genesis(&self) -> bool { - matches!(self, Self::Genesis(_, _)) - } - - /// Gets the block associated with this certificate, if present - pub fn to_block(self) -> Option { - match self { - CertificateType::Finalize(_) | CertificateType::Skip(_) => None, - CertificateType::Notarize(slot, block_id) - | CertificateType::NotarizeFallback(slot, block_id) - | CertificateType::Genesis(slot, block_id) - | CertificateType::FinalizeFast(slot, block_id) => Some((slot, block_id)), - } - } - - /// Reconstructs the single source `Vote` payload for this certificate. - /// - /// This method is used primarily by the signature verifier. For - /// certificates formed by aggregating a single type of vote - /// (e.g., a `Notarize` certificate from `Notarize` votes), this function - /// reconstructs the canonical message payload that was signed by validators. - /// - /// For `NotarizeFallback` and `Skip` certificates, this function returns the - /// appropriate payload *only* if the certificate was formed from a single - /// vote type (e.g., exclusively from `Notarize` or `Skip` votes). For - /// certificates formed from a mix of two vote types, use the `to_source_votes` - /// function. - pub fn to_source_vote(self) -> Vote { - match self { - Self::Notarize(slot, block_id) - | Self::FinalizeFast(slot, block_id) - | Self::NotarizeFallback(slot, block_id) => Vote::new_notarization_vote(slot, block_id), - Self::Finalize(slot) => Vote::new_finalization_vote(slot), - Self::Skip(slot) => Vote::new_skip_vote(slot), - Self::Genesis(slot, block_id) => Vote::new_genesis_vote(slot, block_id), - } - } - - /// Reconstructs the two distinct source `Vote` payloads for this certificate. - /// - /// This method is primarily used by the signature verifier for certificates that - /// can be formed by aggregating two different types of votes. For example, a - /// `NotarizeFallback` certificate accepts both `Notarize` and `NotarizeFallback`. - /// - /// It reconstructs both potential message payloads that were signed by validators, which - /// the verifier uses to check the single aggregate signature. - pub fn to_source_votes(self) -> Option<(Vote, Vote)> { - match self { - Self::NotarizeFallback(slot, block_id) => { - let vote1 = Vote::new_notarization_vote(slot, block_id); - let vote2 = Vote::new_notarization_fallback_vote(slot, block_id); - Some((vote1, vote2)) - } - Self::Skip(slot) => { - let vote1 = Vote::new_skip_vote(slot); - let vote2 = Vote::new_skip_fallback_vote(slot); - Some((vote1, vote2)) - } - // Other certificate types do not use Base3 encoding. - _ => None, - } - } - - /// Returns the stake fraction required for certificate completion and the - /// `VoteType`s that contribute to this certificate. - /// - /// Must be in sync with `Vote::to_cert_types` - pub const fn limits_and_vote_types(&self) -> (Fraction, &'static [VoteType]) { - match self { - CertificateType::Notarize(_, _) => { - (Fraction::from_percentage(60), &[VoteType::Notarize]) - } - CertificateType::NotarizeFallback(_, _) => ( - Fraction::from_percentage(60), - &[VoteType::Notarize, VoteType::NotarizeFallback], - ), - CertificateType::FinalizeFast(_, _) => { - (Fraction::from_percentage(80), &[VoteType::Notarize]) - } - CertificateType::Finalize(_) => (Fraction::from_percentage(60), &[VoteType::Finalize]), - CertificateType::Skip(_) => ( - Fraction::from_percentage(60), - &[VoteType::Skip, VoteType::SkipFallback], - ), - CertificateType::Genesis(_, _) => (GENESIS_VOTE_THRESHOLD, &[VoteType::Genesis]), - } - } +pub struct Block { + /// The slot in the block. + pub slot: Slot, + /// The block_id of the block. + #[cfg_attr(feature = "frozen-abi", stable_abi_sample(with = "sample_hash(rng)"))] + pub block_id: Hash, } -/// The actual certificate with the aggregate signature and bitmap for which validators are included in the aggregate. -/// BLS vote message, we need rank to look up pubkey -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "B5NsoWZr2Lpbbjqj8udwEKvae6bA37Pm4R92udZHxwfU") -)] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SchemaWrite, SchemaRead)] -pub struct Certificate { - /// The certificate type. - pub cert_type: CertificateType, - /// The aggregate signature. - #[wincode(with = "PodBLSSignature")] +/// A consensus vote. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct VoteMessage { + /// The type of the vote. + pub vote: Vote, + /// The signature. pub signature: BLSSignature, - /// A rank bitmap for validators' signatures included in the aggregate. - /// See solana-signer-store for encoding format. - pub bitmap: Vec, + /// The rank of the validator. + pub rank: u16, } /// A consensus message sent between validators. -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample, AbiEnumVisitor), - frozen_abi(digest = "BdKT6dbkLnTeGNMS8XtQkg6HTeHSQ6Z41Btc1rJ117PB") -)] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SchemaWrite, SchemaRead)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[allow(clippy::large_enum_variant)] pub enum ConsensusMessage { /// A vote from a single party. @@ -264,6 +94,14 @@ impl ConsensusMessage { bitmap, }) } + + /// Returns the slot this message is for. + pub fn slot(&self) -> Slot { + match self { + Self::Vote(vote) => vote.vote.slot(), + Self::Certificate(certificate) => certificate.cert_type.slot(), + } + } } impl From for ConsensusMessage { diff --git a/votor-messages/src/finalized_slot.rs b/votor-messages/src/finalized_slot.rs new file mode 100644 index 00000000000..b33b794a09d --- /dev/null +++ b/votor-messages/src/finalized_slot.rs @@ -0,0 +1,68 @@ +//! A module to define FinalizedSlot +use {solana_clock::Slot, std::cmp::Ordering}; + +/// A slot can be finalized if the node has a fast finalization cert for it (fast finalization) or +/// it has a notar and a finalization cert for it (slow finalization). We prefer fast finalization +/// certs above slow finalization for the same slot. This enum helps in comparing two different +/// finalized slots while preferring fast finalization over slow for the same slot. +#[derive(Debug, PartialEq, Eq)] +pub enum FinalizedSlot { + /// The slot was slow finalized. + Slow(Slot), + /// The slot was fast finalized. + Fast(Slot), +} + +impl FinalizedSlot { + /// Returns the actual slot. + pub fn slot(&self) -> Slot { + match self { + Self::Slow(slot) | Self::Fast(slot) => *slot, + } + } +} + +impl Ord for FinalizedSlot { + /// Overloading the cmp operators to prefer fast finalization over slow for the same slot. + fn cmp(&self, other: &Self) -> Ordering { + match self.slot().cmp(&other.slot()) { + Ordering::Equal => match (self, other) { + (Self::Fast(_), Self::Slow(_)) => Ordering::Greater, + (Self::Slow(_), Self::Fast(_)) => Ordering::Less, + _ => Ordering::Equal, + }, + ordering => ordering, + } + } +} + +impl PartialOrd for FinalizedSlot { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn validation() { + assert!(FinalizedSlot::Slow(5) < FinalizedSlot::Slow(6)); + assert!(FinalizedSlot::Slow(5) == FinalizedSlot::Slow(5)); + assert!(FinalizedSlot::Slow(5) > FinalizedSlot::Slow(4)); + + assert!(FinalizedSlot::Fast(5) < FinalizedSlot::Fast(6)); + assert!(FinalizedSlot::Fast(5) == FinalizedSlot::Fast(5)); + assert!(FinalizedSlot::Fast(5) > FinalizedSlot::Fast(4)); + + assert!(FinalizedSlot::Slow(5) < FinalizedSlot::Fast(6)); + assert!(FinalizedSlot::Slow(5) < FinalizedSlot::Fast(5)); + assert!(FinalizedSlot::Slow(5) > FinalizedSlot::Fast(4)); + + assert!(FinalizedSlot::Fast(5) < FinalizedSlot::Slow(6)); + assert!(FinalizedSlot::Fast(5) > FinalizedSlot::Slow(5)); + assert!(FinalizedSlot::Fast(5) > FinalizedSlot::Slow(4)); + } +} diff --git a/votor-messages/src/lib.rs b/votor-messages/src/lib.rs index caa77e7f33b..162e6747ad2 100644 --- a/votor-messages/src/lib.rs +++ b/votor-messages/src/lib.rs @@ -3,12 +3,29 @@ #![cfg_attr(feature = "frozen-abi", feature(min_specialization))] #![deny(missing_docs)] +use { + crossbeam_channel::{Receiver, Sender}, + solana_clock::Slot, + solana_pubkey::Pubkey, +}; + +pub mod certificate; pub mod consensus_message; +pub mod finalized_slot; pub mod fraction; +pub mod metric_types; pub mod migration; pub mod reward_certificate; +pub mod unverified_vote_message; pub mod vote; +pub mod wire; #[cfg_attr(feature = "frozen-abi", macro_use)] #[cfg(feature = "frozen-abi")] extern crate solana_frozen_abi_macro; + +/// Send side of verified voter channel. +/// Each message contains the Pubkey of the voter and the slots in last verified vote. +pub type VerifiedVoterSlotsSender = Sender<(Pubkey, Vec)>; +/// Receive side of verified voter channel. +pub type VerifiedVoterSlotsReceiver = Receiver<(Pubkey, Vec)>; diff --git a/votor-messages/src/metric_types.rs b/votor-messages/src/metric_types.rs new file mode 100644 index 00000000000..487c70327c7 --- /dev/null +++ b/votor-messages/src/metric_types.rs @@ -0,0 +1,54 @@ +//! Definitions related to consensus metrics collection. + +use { + crate::vote::Vote, + crossbeam_channel::{Receiver, Sender}, + solana_clock::Slot, + solana_pubkey::Pubkey, + std::time::Instant, +}; + +/// Different types of events to notify the metrics container of. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ConsensusMetricsEvent { + /// A vote was received from the node with `id`. + Vote { + /// The validator that voted. + id: Pubkey, + /// The type of vote. + vote: Vote, + }, + /// A block hash was seen for `slot` and the `leader` is responsible for producing it. + BlockHashSeen { + /// The leader that produced the block. + leader: Pubkey, + /// The slot the block was produced for. + slot: Slot, + }, + /// Start of slot. + StartOfSlot { + /// The slot that just started. + slot: Slot, + }, + /// A slot was finalized. + SlotFinalized { + /// The slot that was finalized. + slot: Slot, + }, +} + +/// Send side of the channel to send metrics events on. +pub type ConsensusMetricsEventSender = Sender<(Instant, Vec)>; +/// Receive side of the channel to receive metrics events on. +pub type ConsensusMetricsEventReceiver = Receiver<(Instant, Vec)>; + +/// Even at 10 events per slot, this supports 1000 slots in flight +/// With 2000 active validators, we can't have more than: +/// - 1 Notarize vote +/// - 3 Notarize-fallback votes +/// - 1 Skip-fallback vote +/// - 1 Finalize vote +/// +/// Per validator, resulting in 12k vote events. +/// We overprovision this channel at 15k total events. +pub const MAX_IN_FLIGHT_CONSENSUS_EVENTS: usize = 15_000; diff --git a/votor-messages/src/migration.rs b/votor-messages/src/migration.rs index 55fd71bb841..b07d36270a7 100644 --- a/votor-messages/src/migration.rs +++ b/votor-messages/src/migration.rs @@ -42,11 +42,14 @@ //! - When in `AlpenglowEnabled` various TowerBFT threads stop processsing alpenglow slots while still processing //! TowerBFT slots pre alpenglow genesis in order to help other cluster participants catchup. //! - When in `FullAlpenglowEpoch` we completely shutdown these TowerBFT threads (AncestorHashesService and ClusterSlotsService) +#[cfg(feature = "dev-context-only-utils")] use { - crate::{ - consensus_message::{Block, Certificate, CertificateType}, - fraction::Fraction, - }, + crate::certificate::CertSignature, + solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, + solana_hash::Hash, +}; +use { + crate::{certificate::GenesisCert, consensus_message::Block, fraction::Fraction}, log::*, solana_address::Address, solana_clock::{Epoch, Slot}, @@ -60,11 +63,6 @@ use { time::Duration, }, }; -#[cfg(feature = "dev-context-only-utils")] -use { - solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, - solana_hash::Hash, -}; /// The slot offset post feature flag activation to begin the migration. /// Epoch boundaries induce heavy computation often resulting in forks. It's best to decouple the migration period @@ -76,6 +74,9 @@ pub const MIGRATION_SLOT_OFFSET: Slot = 5000; #[cfg(feature = "dev-context-only-utils")] pub const MIGRATION_SLOT_OFFSET: Slot = 32; +/// A marker for vote accounts' epoch credit to indicate migration from tower to alpenwlow +pub const AG_MIGRATION_EPOCH_CREDIT: (Epoch, u64, u64) = (Epoch::MAX, u64::MAX, u64::MAX); + /// We match Alpenglow's 20 + 20 model, by allowing a maximum of 20% malicious stake during the migration. pub const MIGRATION_MALICIOUS_THRESHOLD: f64 = 20.0 / 100.0; @@ -112,7 +113,7 @@ enum MigrationPhase { /// The block we've identified as the genesis block genesis_block: Option, /// The genesis certificate we've received - genesis_cert: Option>, + genesis_cert: Option>, }, /// The alpenglow genesis vote has succeeded, and we have frozen the genesis bank. We are ready to @@ -121,14 +122,14 @@ enum MigrationPhase { /// `AlpenglowEnabled` when it is safe to do so. ReadyToEnable { /// The genesis certificate produced by the cluster - genesis_cert: Arc, + genesis_cert: Arc, }, /// Alpenglow has been enabled, TowerBFT blocks > the alpenglow genesis have been purged, /// and all further blocks are alpenglow. AlpenglowEnabled { /// The genesis certificate produced by the cluster - genesis_cert: Arc, + genesis_cert: Arc, }, /// We have completed the mixed migration epoch and now all epochs only contain alpenglow blocks @@ -137,7 +138,7 @@ enum MigrationPhase { #[allow(dead_code)] full_alpenglow_epoch: Epoch, /// The genesis certificate produced by the cluster - genesis_cert: Arc, + genesis_cert: Arc, }, } @@ -178,9 +179,7 @@ impl MigrationPhase { MigrationPhase::PreFeatureActivation | MigrationPhase::Migration { .. } | MigrationPhase::ReadyToEnable { .. } => false, - MigrationPhase::AlpenglowEnabled { genesis_cert } => { - slot > genesis_cert.cert_type.slot() - } + MigrationPhase::AlpenglowEnabled { genesis_cert } => slot > genesis_cert.block.slot, MigrationPhase::FullAlpenglowEpoch { .. } => true, } } @@ -250,9 +249,7 @@ impl MigrationPhase { MigrationPhase::PreFeatureActivation | MigrationPhase::Migration { .. } | MigrationPhase::ReadyToEnable { .. } => true, - MigrationPhase::AlpenglowEnabled { genesis_cert } => { - slot <= genesis_cert.cert_type.slot() - } + MigrationPhase::AlpenglowEnabled { genesis_cert } => slot <= genesis_cert.block.slot, MigrationPhase::FullAlpenglowEpoch { .. } => false, } } @@ -284,11 +281,6 @@ impl MigrationPhase { fn should_use_double_merkle_block_id(&self, slot: Slot) -> bool { self.is_alpenglow_block(slot) } - - /// Should this block allow the UpdateParent marker, i.e., support fast leader handover? - fn should_allow_fast_leader_handover(&self, slot: Slot) -> bool { - self.is_alpenglow_block(slot) - } } /// Keeps track of the current migration status @@ -345,10 +337,15 @@ impl MigrationStatus { /// Creates a post migration status for use in tests #[cfg(feature = "dev-context-only-utils")] pub fn post_migration_status() -> Self { - let genesis_certificate = Certificate { - cert_type: CertificateType::Genesis(0, Hash::default()), - signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), - bitmap: vec![], + let genesis_certificate = GenesisCert { + block: Block { + slot: 0, + block_id: Hash::default(), + }, + signature: CertSignature { + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }, }; Self::new(MigrationPhase::AlpenglowEnabled { genesis_cert: Arc::new(genesis_certificate), @@ -358,22 +355,27 @@ impl MigrationStatus { /// Enable alpenglow for testing code #[cfg(feature = "dev-context-only-utils")] pub fn enable_alpenglow_for_tests(&self) { - let genesis_block = (0, Hash::new_unique()); + let block = Block { + slot: 0, + block_id: Hash::new_unique(), + }; self.record_feature_activation(0); - self.set_genesis_block(genesis_block); - self.set_genesis_certificate(Arc::new(Certificate { - cert_type: CertificateType::Genesis(genesis_block.0, genesis_block.1), - signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), - bitmap: vec![], + self.set_genesis_block(block); + self.set_genesis_certificate(Arc::new(GenesisCert { + block, + signature: CertSignature { + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }, })); - assert_eq!(self.enable_alpenglow_during_startup(), genesis_block.0); + assert_eq!(self.enable_alpenglow_during_startup(), block.slot); } /// Initialize migration status based on feature flag activation and genesis certificate pub fn initialize( root_epoch: Epoch, ff_activation_slot: Option, - genesis_cert: Option, + genesis_cert: Option, epoch_schedule: &EpochSchedule, ) -> Self { let phase = match (genesis_cert, ff_activation_slot) { @@ -408,7 +410,7 @@ impl MigrationStatus { } }; - warn!("Pre startup initializing alpenglow migration from root bank: {phase:?}"); + info!("Pre startup initializing alpenglow migration from root bank: {phase:?}"); Self::new(phase) } @@ -426,7 +428,7 @@ impl MigrationStatus { pub fn log_phase(&self) { let my_pubkey = self.my_pubkey(); let phase = self.phase.read().unwrap(); - warn!("{my_pubkey}: Alpenglow migration phase {phase:?}"); + info!("{my_pubkey}: Alpenglow migration phase {phase:?}"); } /// Record that PohService has started and must be coordinated with when enabling Alpenglow. @@ -449,7 +451,6 @@ impl MigrationStatus { dispatch!(pub fn should_respond_to_ancestor_hashes_requests(&self, slot: Slot) -> bool); dispatch!(pub fn should_have_alpenglow_ticks(&self, slot: Slot) -> bool); dispatch!(pub fn should_allow_block_markers(&self, slot: Slot) -> bool); - dispatch!(pub fn should_allow_fast_leader_handover(&self, slot: Slot) -> bool); dispatch!(pub fn should_use_double_merkle_block_id(&self, slot: Slot) -> bool); /// The alpenglow feature flag has been activated in slot `slot`. @@ -470,7 +471,7 @@ impl MigrationStatus { genesis_cert: None, }; - warn!( + info!( "{}: Alpenglow feature flag was activated in {slot}, migration will start at \ {migration_slot}", self.my_pubkey() @@ -505,7 +506,7 @@ impl MigrationStatus { /// /// Should only be used during `Migration`, and transitions to `ReadyToEnable` if we have already /// received a genesis certificate and it matches. - pub fn set_genesis_block(&self, discovered_genesis_block @ (slot, _): Block) { + pub fn set_genesis_block(&self, discovered_genesis_block: Block) { let mut phase = self.phase.write().unwrap(); if phase.is_pre_feature_activation() { unreachable!( @@ -532,10 +533,10 @@ impl MigrationStatus { } assert!( - slot < *migration_slot, + discovered_genesis_block.slot < *migration_slot, "Attempting to set a genesis block that is past the migration start" ); - warn!( + info!( "{} Setting genesis block {discovered_genesis_block:?}", self.my_pubkey() ); @@ -544,20 +545,15 @@ impl MigrationStatus { let Some(genesis_cert) = genesis_cert else { return; }; - let CertificateType::Genesis(slot, block_id) = genesis_cert.cert_type else { - unreachable!("Programmer error invalid genesis certificate"); - }; - if genesis_block - .as_ref() - .map(|b| *b != (slot, block_id)) - .unwrap_or(true) - { + + if discovered_genesis_block != genesis_cert.block { panic!( "{}: We wish to cast a genesis vote on {discovered_genesis_block:?}, however we \ - have received a genesis certificate for ({slot}, {block_id}). This means there \ - is significant malicious activity causing two distinct forks to reach the \ + have received a genesis certificate for ({:?}). This means there is significant \ + malicious activity causing two distinct forks to reach the \ {GENESIS_VOTE_THRESHOLD}. We cannot recover without operator intervention.", - self.my_pubkey() + self.my_pubkey(), + genesis_cert.block ); } @@ -571,7 +567,7 @@ impl MigrationStatus { /// This should only be called with certificates that have passed signature verification /// /// Transitions to `ReadyToEnable` if we have already received a genesis block and it matches. - pub fn set_genesis_certificate(&self, cert: Arc) { + pub fn set_genesis_certificate(&self, cert: Arc) { let mut phase = self.phase.write().unwrap(); if phase.is_pre_feature_activation() { unreachable!( @@ -590,30 +586,28 @@ impl MigrationStatus { return; }; - let CertificateType::Genesis(slot, block_id) = cert.cert_type else { - unreachable!("Programmer error adding invalid genesis certificate"); - }; - assert!( - slot < *migration_slot, + cert.block.slot < *migration_slot, "Attempting to set a genesis certificate past the migration start" ); - warn!( - "{} Setting genesis cert for ({slot},{block_id:?})", - self.my_pubkey() + info!( + "{} Setting genesis cert for ({:?})", + self.my_pubkey(), + cert.block ); *genesis_cert = Some(cert.clone()); let Some(genesis_block) = genesis_block else { return; }; - if *genesis_block != (slot, block_id) { + if *genesis_block != cert.block { panic!( "{}: We cast a genesis vote on {genesis_block:?}, however we have received a \ - genesis certificate for ({slot}, {block_id}). This means there is significant \ - malicious activity causing two distinct forks to reach the \ - {GENESIS_VOTE_THRESHOLD}. We cannot recover without operator intervention.", - self.my_pubkey() + genesis certificate for ({:?}). This means there is significant malicious \ + activity causing two distinct forks to reach the {GENESIS_VOTE_THRESHOLD}. We \ + cannot recover without operator intervention.", + self.my_pubkey(), + cert.block ); } @@ -636,14 +630,14 @@ impl MigrationStatus { self.wait_for_migration_or_exit(exit); if exit.load(Ordering::Relaxed) { - warn!( + info!( "{}: Validator shutdown before Alpenglow could be enabled", self.my_pubkey() ); return; } - warn!("{}: Alpenglow enabled!", self.my_pubkey()); + info!("{}: Alpenglow enabled!", self.my_pubkey()); } /// PohService is shutting down after being asked to by replay_stage via `enable_alpenglow`. @@ -669,7 +663,7 @@ impl MigrationStatus { /// /// Transition the phase from `ReadyToEnable` to `AlpenglowEnabled` pub fn enable_alpenglow_during_startup(&self) -> Slot { - warn!("{}: Enabling alpenglow during startup", self.my_pubkey()); + info!("{}: Enabling alpenglow during startup", self.my_pubkey()); let MigrationPhase::ReadyToEnable { genesis_cert } = self.phase.read().unwrap().clone() else { unreachable!( @@ -679,7 +673,7 @@ impl MigrationStatus { ); }; - let genesis_slot = genesis_cert.cert_type.slot(); + let genesis_slot = genesis_cert.block.slot; if self.poh_service_started.load(Ordering::Acquire) { // If `PohService` is started, use the full enable pathway // We do not respect the exit flag during startup replay @@ -694,7 +688,7 @@ impl MigrationStatus { let (is_alpenglow_enabled, _condvar) = &self.migration_wait; *is_alpenglow_enabled.lock().unwrap() = true; // No need to condvar as we're in startup and no one is waiting for us. - warn!( + info!( "{}: Alpenglow enabled during startup! Genesis slot {genesis_slot}", self.my_pubkey() ); @@ -718,7 +712,7 @@ impl MigrationStatus { full_alpenglow_epoch, }; - warn!( + info!( "{}: Migration epoch has concluded, entering full alpenglow epoch {}!", self.my_pubkey(), full_alpenglow_epoch @@ -727,15 +721,11 @@ impl MigrationStatus { /// The alpenglow genesis block. This should only be used when we are in `ReadyToEnable` or further pub fn genesis_block(&self) -> Option { - self.genesis_certificate().map(|cert| { - cert.cert_type - .to_block() - .expect("Must be a genesis certificate") - }) + self.genesis_certificate().map(|cert| cert.block) } /// The alpenglow genesis certificate. Only relevant when we are in `ReadyToEnable` or further - pub fn genesis_certificate(&self) -> Option> { + pub fn genesis_certificate(&self) -> Option> { let phase = self.phase.read().unwrap(); match &*phase { MigrationPhase::PreFeatureActivation | MigrationPhase::Migration { .. } => None, diff --git a/votor-messages/src/reward_certificate.rs b/votor-messages/src/reward_certificate.rs index c272d630999..71f5604ab27 100644 --- a/votor-messages/src/reward_certificate.rs +++ b/votor-messages/src/reward_certificate.rs @@ -5,7 +5,6 @@ use { solana_bls_signatures::SignatureCompressed as BLSSignatureCompressed, solana_clock::Slot, solana_hash::Hash, - solana_pubkey::Pubkey, solana_short_vec::ShortU16, solana_signer_store::EncodeError, thiserror::Error, @@ -117,30 +116,6 @@ impl NotarRewardCertificate { } } -/// Message to add votes to the rewards container. -#[derive(Debug)] -pub struct AddVoteMessage { - /// List of [`VoteMessage`]s. - pub votes: Vec, -} - -/// Request to build reward certificates. -pub struct BuildRewardCertsRequest { - /// The bank slot which will include the built reward certs. - pub bank_slot: Slot, -} - -/// Response when the reward certs are built successfully. -#[derive(Default)] -pub struct BuildRewardCertsRespSucc { - /// Skip reward certificate. None if no skip votes were registered. - pub skip: Option, - /// Notar reward certificate. None if no notar votes were registered. - pub notar: Option, - /// If at least one of the certs above is present, then this contains the slot for which the reward certs were built and the list of validators in the certs. - pub validators: Vec, -} - /// Error returned when build reward certs fails. #[derive(Debug, Error)] pub enum BuildRewardCertsRespError { @@ -152,10 +127,9 @@ pub enum BuildRewardCertsRespError { Encode(EncodeError), } -/// Response to a [`BuildRewardCertsRequest`]. -pub struct BuildRewardCertsResponse { - /// The bank slot from the corresponding request. - pub bank_slot: Slot, - /// The result of building reward certs for `bank_slot`. - pub result: Result, +/// Message to add votes to the rewards container. +#[derive(Debug)] +pub struct AddVoteMessage { + /// List of [`VoteMessage`]s. + pub votes: Vec, } diff --git a/votor-messages/src/unverified_vote_message.rs b/votor-messages/src/unverified_vote_message.rs new file mode 100644 index 00000000000..c6c84eb3b18 --- /dev/null +++ b/votor-messages/src/unverified_vote_message.rs @@ -0,0 +1,157 @@ +//! Defines unverified vote message and related types + +use { + crate::{ + certificate::CertificateType, + vote::Vote, + wire::{VersionedWireConsensusMessage, WireConsensusMessageKind}, + }, + solana_bls_signatures::Signature as BLSSignature, +}; + +/// An unverified vote message. +#[derive(Clone, Debug)] +pub struct UnverifiedVoteMessage { + /// The vote payload that is signed. + pub vote: Vote, + /// The signature + pub signature: BLSSignature, + /// rank of the validator that signed the payload + pub rank: u16, + /// the shred version + pub shred_version: u16, +} + +/// an unverified certificate +#[derive(Clone, Debug)] +pub struct UnverifiedCertificate { + /// The certificate type. + pub cert_type: CertificateType, + /// The aggregate signature. + pub signature: BLSSignature, + /// A rank bitmap for validators' signatures included in the aggregate. + /// See solana-signer-store for encoding format. + pub bitmap: Vec, + /// the shred version + pub shred_version: u16, +} + +/// Output of decoding a wire consensus message into unverified vote or certificate. +pub enum DecodedWireConsensusMessage { + /// Decoded to a vote + Vote(UnverifiedVoteMessage), + /// Decoded to a certificate + Certificate(UnverifiedCertificate), +} + +impl DecodedWireConsensusMessage { + /// Decodes a wire consensus message. + pub fn try_new(msg: VersionedWireConsensusMessage, shred_version: u16) -> Option { + let VersionedWireConsensusMessage::V1(msg) = msg; + if msg.shred_version != shred_version { + return None; + } + let msg = match msg.kind { + WireConsensusMessageKind::NotarVote(v) => Self::Vote(UnverifiedVoteMessage { + vote: Vote::new_notarization_vote(v.block), + signature: v.signature.signature, + rank: v.signature.rank, + shred_version: msg.shred_version, + }), + WireConsensusMessageKind::NotarFallbackVote(v) => Self::Vote(UnverifiedVoteMessage { + vote: Vote::new_notarization_fallback_vote(v.block), + signature: v.signature.signature, + rank: v.signature.rank, + shred_version: msg.shred_version, + }), + WireConsensusMessageKind::FinalizeVote(v) => Self::Vote(UnverifiedVoteMessage { + vote: Vote::new_finalization_vote(v.slot), + signature: v.signature.signature, + rank: v.signature.rank, + shred_version: msg.shred_version, + }), + WireConsensusMessageKind::SkipVote(v) => Self::Vote(UnverifiedVoteMessage { + vote: Vote::new_skip_vote(v.slot), + signature: v.signature.signature, + rank: v.signature.rank, + shred_version: msg.shred_version, + }), + WireConsensusMessageKind::SkipFallbackVote(v) => Self::Vote(UnverifiedVoteMessage { + vote: Vote::new_skip_fallback_vote(v.slot), + signature: v.signature.signature, + rank: v.signature.rank, + shred_version: msg.shred_version, + }), + WireConsensusMessageKind::GenesisVote(v) => Self::Vote(UnverifiedVoteMessage { + vote: Vote::new_genesis_vote(v.block), + signature: v.signature.signature, + rank: v.signature.rank, + shred_version: msg.shred_version, + }), + + WireConsensusMessageKind::NotarCert(c) => { + let cert_type = CertificateType::Notarize(c.block); + Self::Certificate(UnverifiedCertificate { + cert_type, + signature: c.signature.signature, + bitmap: c.signature.bitmap, + shred_version: msg.shred_version, + }) + } + WireConsensusMessageKind::FinalizeCert(c) => { + let cert_type = CertificateType::Finalize(c.slot); + Self::Certificate(UnverifiedCertificate { + cert_type, + signature: c.signature.signature, + bitmap: c.signature.bitmap, + shred_version: msg.shred_version, + }) + } + WireConsensusMessageKind::FastFinalizeCert(c) => { + let cert_type = CertificateType::FinalizeFast(c.block); + Self::Certificate(UnverifiedCertificate { + cert_type, + signature: c.signature.signature, + bitmap: c.signature.bitmap, + shred_version: msg.shred_version, + }) + } + WireConsensusMessageKind::NotarFallbackCert(c) => { + let cert_type = CertificateType::NotarizeFallback(c.block); + Self::Certificate(UnverifiedCertificate { + cert_type, + signature: c.signature.signature, + bitmap: c.signature.bitmap, + shred_version: msg.shred_version, + }) + } + WireConsensusMessageKind::SkipCert(c) => { + let cert_type = CertificateType::Skip(c.slot); + Self::Certificate(UnverifiedCertificate { + cert_type, + signature: c.signature.signature, + bitmap: c.signature.bitmap, + shred_version: msg.shred_version, + }) + } + WireConsensusMessageKind::GenesisCert(c) => { + let cert_type = CertificateType::Genesis(c.block); + Self::Certificate(UnverifiedCertificate { + cert_type, + signature: c.signature.signature, + bitmap: c.signature.bitmap, + shred_version: msg.shred_version, + }) + } + }; + Some(msg) + } + + /// returns the shred version + pub fn shred_version(&self) -> u16 { + match self { + Self::Vote(v) => v.shred_version, + Self::Certificate(c) => c.shred_version, + } + } +} diff --git a/votor-messages/src/vote.rs b/votor-messages/src/vote.rs index ec3b2dbd017..91a83ada86e 100644 --- a/votor-messages/src/vote.rs +++ b/votor-messages/src/vote.rs @@ -1,21 +1,9 @@ //! Vote data types for use by clients -use { - serde::{Deserialize, Serialize}, - solana_clock::Slot, - solana_hash::Hash, - wincode::{SchemaRead, SchemaWrite}, -}; +use {crate::consensus_message::Block, solana_clock::Slot, solana_hash::Hash}; /// Enum that clients can use to parse and create the vote /// structures expected by the program -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample, AbiEnumVisitor), - frozen_abi(digest = "AgKoR2cpjUSVCW7Cpihob5nDiPcFt1PXmoPKWJg3zuSB") -)] -#[derive( - Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, SchemaWrite, SchemaRead, -)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Vote { /// A notarization vote Notarize(NotarizationVote), @@ -50,8 +38,8 @@ pub enum VoteType { impl Vote { /// Create a new notarization vote - pub fn new_notarization_vote(slot: Slot, block_id: Hash) -> Self { - Self::from(NotarizationVote { slot, block_id }) + pub fn new_notarization_vote(block: Block) -> Self { + Self::from(NotarizationVote { block }) } /// Create a new finalization vote @@ -65,8 +53,8 @@ impl Vote { } /// Create a new notarization fallback vote - pub fn new_notarization_fallback_vote(slot: Slot, block_id: Hash) -> Self { - Self::from(NotarizationFallbackVote { slot, block_id }) + pub fn new_notarization_fallback_vote(block: Block) -> Self { + Self::from(NotarizationFallbackVote { block }) } /// Create a new skip fallback vote @@ -75,28 +63,28 @@ impl Vote { } /// Create a new genesis vote - pub fn new_genesis_vote(slot: Slot, block_id: Hash) -> Self { - Self::from(GenesisVote { slot, block_id }) + pub fn new_genesis_vote(block: Block) -> Self { + Self::from(GenesisVote { block }) } /// The slot which was voted for pub fn slot(&self) -> Slot { match self { - Self::Notarize(vote) => vote.slot, + Self::Notarize(vote) => vote.block.slot, Self::Finalize(vote) => vote.slot, Self::Skip(vote) => vote.slot, - Self::NotarizeFallback(vote) => vote.slot, + Self::NotarizeFallback(vote) => vote.block.slot, Self::SkipFallback(vote) => vote.slot, - Self::Genesis(vote) => vote.slot, + Self::Genesis(vote) => vote.block.slot, } } /// The block id associated with the block which was voted for pub fn block_id(&self) -> Option<&Hash> { match self { - Self::Notarize(vote) => Some(&vote.block_id), - Self::NotarizeFallback(vote) => Some(&vote.block_id), - Self::Genesis(vote) => Some(&vote.block_id), + Self::Notarize(vote) => Some(&vote.block.block_id), + Self::NotarizeFallback(vote) => Some(&vote.block.block_id), + Self::Genesis(vote) => Some(&vote.block.block_id), Self::Finalize(_) | Self::Skip(_) | Self::SkipFallback(_) => None, } } @@ -181,50 +169,14 @@ impl From for Vote { } /// A notarization vote -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "5AdwChAjsj5QUXLdpDnGGK2L2nA8y8EajVXi6jsmTv1m") -)] -#[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - Default, - Serialize, - Deserialize, - SchemaWrite, - SchemaRead, -)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] pub struct NotarizationVote { - /// The slot this vote is cast for. - pub slot: Slot, - /// The block id this vote is for. - pub block_id: Hash, + /// The block this vote is cast for + pub block: Block, } /// A finalization vote -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "2XQ5N6YLJjF28w7cMFFUQ9SDgKuf9JpJNtAiXSPA8vR2") -)] -#[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - Default, - Serialize, - Deserialize, - SchemaWrite, - SchemaRead, -)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] pub struct FinalizationVote { /// The slot this vote is cast for. pub slot: Slot, @@ -233,101 +185,29 @@ pub struct FinalizationVote { /// A skip vote /// Represents a range of slots to skip /// inclusive on both ends -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "G8Nrx3sMYdnLpHsCNark3BGA58BmW2sqNnqjkYhQHtN") -)] -#[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - Default, - Serialize, - Deserialize, - SchemaWrite, - SchemaRead, -)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] pub struct SkipVote { /// The slot this vote is cast for. pub slot: Slot, } /// A notarization fallback vote -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "7j5ZPwwyz1FaG3fpyQv5PVnQXicdSmqSk8NvqzkG1Eqz") -)] -#[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - Default, - Serialize, - Deserialize, - SchemaWrite, - SchemaRead, -)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] pub struct NotarizationFallbackVote { - /// The slot this vote is cast for. - pub slot: Slot, - /// The block id this vote is for. - pub block_id: Hash, + /// The block this vote is cast for + pub block: Block, } /// A skip fallback vote -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "WsUNum8V62gjRU1yAnPuBMAQui4YvMwD1RwrzHeYkeF") -)] -#[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - Default, - Serialize, - Deserialize, - SchemaWrite, - SchemaRead, -)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] pub struct SkipFallbackVote { /// The slot this vote is cast for. pub slot: Slot, } /// A genesis vote. Only used during the migration from TowerBFT -#[cfg_attr( - feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "2JAiHmnnKHCzhkyCY3Bej6rAaVkMHsXgRcz1TPCNqAJ9") -)] -#[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - Default, - Serialize, - Deserialize, - SchemaWrite, - SchemaRead, -)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] pub struct GenesisVote { - /// The slot this vote is cast for. - pub slot: Slot, - /// The block id this vote is for. - pub block_id: Hash, + /// The block this vote is cast for + pub block: Block, } diff --git a/votor-messages/src/wire.rs b/votor-messages/src/wire.rs new file mode 100644 index 00000000000..b7b16e64c00 --- /dev/null +++ b/votor-messages/src/wire.rs @@ -0,0 +1,464 @@ +//! This module defines various types relevant to the wire format of alpenglow +//! votes and certificates. +//! +//! When a validator wants to send a `Vote` to other nodes, it needs to include +//! the `shred_version` in the signed payload so that other validators can +//! ensure that they are both in the same cluster instance. To get the +//! appropriate vote payload with the shred version, the sender should use +//! `get_vote_payload_to_sign()`. The shred version does not need to be part of +//! the vote payload. The receiver can use `get_vote_payload_to_sign()` with +//! its own shred_version to get the payload that should be verified. If the +//! signature does not match, then the vote should be rejected. Once the sender +//! has the signature on the payload, it can construct a `VoteMessage` using the +//! signature. +//! +//! We do not need to include such a shred_version for certificates because they +//! are aggregating signatures on the votes. When verifying a `Certificate`, +//! the verifier would use `Certificate::get_vote_payload()` passing in its own +//! shred_version and if the signature verifies then that implies that the shred +//! versions also match. +//! +//! The actual message that the sender sends over the wire is +//! `VersionedWireConsensusMessage` which can be constructed from a +//! `VoteMessage` or a `Certificate`. This contains a message version to +//! support upgrades and unlike `ConsensusMessage` the different vote and +//! certificate variants are under a single enum tag to enable [de]coding times. +//! Generally, when a signature on a vote or a certificate does not verify, the +//! receiver would ban the sender. However, tt is possible that two validators +//! with different shred versions accidentally exchange messages and this should +//! not cause the sender to get banned. To avoid this, +//! `VersionedWireConsensusMessage` also contains a shred_version that is used +//! to filter out accidental shred_version mismatches. +//! +//! When a receiver processes a `VersionedWireConsensusMessage`, it first +//! converts it to a `DecodedWireConsensusMessage` which performs the +//! shred_version check to avoid accidental mismatches. This enum contains +//! either a `UnverifiedVoteMessage` or `UnverifiedCertificate`. Both of these +//! contain all the relevant information to verify them and once verified, the +//! receiver can convert them to `VoteMessage` and `Certificate` respectively. +//! +//! `VoteMessage` verification happens in the bls-sigverify crate and +//! `Certificate` verfication happens in the `bls-cert-verify` crate. + +use { + crate::{ + certificate::{Certificate, CertificateType}, + consensus_message::{Block, ConsensusMessage, VoteMessage}, + vote::Vote, + }, + serde::{Deserialize, Serialize}, + solana_bls_signatures::Signature as BLSSignature, + solana_clock::Slot, + wincode::{SchemaRead, SchemaWrite, pod_wrapper}, +}; + +#[cfg(feature = "frozen-abi")] +fn sample_bls_signature( + rng: &mut (impl solana_frozen_abi::rand::RngCore + ?Sized), +) -> BLSSignature { + use solana_frozen_abi::stable_abi::StableAbi; + BLSSignature(<[u8; solana_bls_signatures::BLS_SIGNATURE_AFFINE_SIZE] as StableAbi>::random(rng)) +} + +// Use `BLSSignature` directly once `BLSSignature` wincode support +// is released in solana-sdk. +pod_wrapper! { + unsafe struct PodBLSSignature(BLSSignature); +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Clone, Debug, Hash, PartialEq, Eq, SchemaRead, SchemaWrite, Serialize)] +pub(crate) struct WireVoteSignature { + #[cfg_attr( + feature = "frozen-abi", + stable_abi_sample(with = "sample_bls_signature(rng)") + )] + #[wincode(with = "PodBLSSignature")] + pub(crate) signature: BLSSignature, + pub(crate) rank: u16, +} + +impl From for WireVoteSignature { + fn from(msg: VoteMessage) -> Self { + Self { + signature: msg.signature, + rank: msg.rank, + } + } +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Clone, Debug, Hash, PartialEq, Eq, SchemaRead, SchemaWrite, Serialize)] +pub(crate) struct WireBlockVoteMessage { + pub(crate) block: Block, + pub(crate) signature: WireVoteSignature, +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Clone, Debug, Hash, PartialEq, Eq, SchemaRead, SchemaWrite, Serialize)] +pub(crate) struct WireSlotVoteMessage { + pub(crate) slot: Slot, + pub(crate) signature: WireVoteSignature, +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Clone, Debug, Hash, PartialEq, Eq, SchemaRead, SchemaWrite, Serialize, Deserialize)] +/// Signature on a wire cert message +pub struct WireCertSignature { + #[cfg_attr( + feature = "frozen-abi", + stable_abi_sample(with = "sample_bls_signature(rng)") + )] + #[wincode(with = "PodBLSSignature")] + /// the aggregate signature + pub signature: BLSSignature, + /// bitmap of ranks of validators included in the aggregate. + pub bitmap: Vec, +} + +impl From for WireCertSignature { + fn from(cert: Certificate) -> Self { + Self { + signature: cert.signature, + bitmap: cert.bitmap, + } + } +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Debug, Clone, Hash, PartialEq, Eq, SchemaRead, SchemaWrite, Serialize)] +pub(crate) struct WireSlotCertMessage { + pub(crate) slot: Slot, + pub(crate) signature: WireCertSignature, +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Debug, Clone, Hash, PartialEq, Eq, SchemaRead, SchemaWrite, Serialize, Deserialize)] +/// A wire cert message that holds a block. +pub struct WireBlockCertMessage { + /// the block the cert is certifying. + pub block: Block, + /// the signature of the cert message. + pub signature: WireCertSignature, +} + +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample, AbiEnumVisitor) +)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, SchemaWrite, SchemaRead, Serialize)] +#[wincode(tag_encoding = "u8")] +pub(crate) enum WireConsensusMessageKind { + #[wincode(tag = 1)] + NotarVote(WireBlockVoteMessage), + #[wincode(tag = 2)] + FinalizeVote(WireSlotVoteMessage), + #[wincode(tag = 3)] + SkipVote(WireSlotVoteMessage), + #[wincode(tag = 4)] + NotarFallbackVote(WireBlockVoteMessage), + #[wincode(tag = 5)] + SkipFallbackVote(WireSlotVoteMessage), + #[wincode(tag = 6)] + GenesisVote(WireBlockVoteMessage), + + #[wincode(tag = 7)] + FinalizeCert(WireSlotCertMessage), + #[wincode(tag = 8)] + FastFinalizeCert(WireBlockCertMessage), + #[wincode(tag = 9)] + NotarCert(WireBlockCertMessage), + #[wincode(tag = 10)] + NotarFallbackCert(WireBlockCertMessage), + #[wincode(tag = 11)] + SkipCert(WireSlotCertMessage), + #[wincode(tag = 12)] + GenesisCert(WireBlockCertMessage), +} + +impl WireConsensusMessageKind { + fn new(msg: ConsensusMessage) -> Self { + match msg { + ConsensusMessage::Vote(v) => Self::new_from_vote(v), + ConsensusMessage::Certificate(c) => Self::new_from_cert(c), + } + } + + fn new_from_vote(msg: VoteMessage) -> Self { + let vote = msg.vote; + let signature = WireVoteSignature::from(msg); + match vote { + Vote::Notarize(v) => Self::NotarVote(WireBlockVoteMessage { + block: v.block, + signature, + }), + Vote::NotarizeFallback(v) => Self::NotarFallbackVote(WireBlockVoteMessage { + block: v.block, + signature, + }), + Vote::Finalize(v) => Self::FinalizeVote(WireSlotVoteMessage { + slot: v.slot, + signature, + }), + Vote::Skip(v) => Self::SkipVote(WireSlotVoteMessage { + slot: v.slot, + signature, + }), + Vote::SkipFallback(v) => Self::SkipFallbackVote(WireSlotVoteMessage { + slot: v.slot, + signature, + }), + Vote::Genesis(v) => Self::GenesisVote(WireBlockVoteMessage { + block: v.block, + signature, + }), + } + } + + fn new_from_cert(cert: Certificate) -> Self { + let cert_type = cert.cert_type; + let signature = WireCertSignature::from(cert); + match cert_type { + CertificateType::Finalize(slot) => { + Self::FinalizeCert(WireSlotCertMessage { slot, signature }) + } + CertificateType::FinalizeFast(block) => { + Self::FastFinalizeCert(WireBlockCertMessage { block, signature }) + } + CertificateType::Notarize(block) => { + Self::NotarCert(WireBlockCertMessage { block, signature }) + } + CertificateType::NotarizeFallback(block) => { + Self::NotarFallbackCert(WireBlockCertMessage { block, signature }) + } + CertificateType::Skip(slot) => Self::SkipCert(WireSlotCertMessage { slot, signature }), + CertificateType::Genesis(block) => { + Self::GenesisCert(WireBlockCertMessage { block, signature }) + } + } + } + + /// Returns the slot on the message + fn slot(&self) -> Slot { + match self { + Self::NotarVote(v) | Self::NotarFallbackVote(v) | Self::GenesisVote(v) => v.block.slot, + Self::FinalizeVote(v) | Self::SkipVote(v) | Self::SkipFallbackVote(v) => v.slot, + Self::NotarCert(c) + | Self::GenesisCert(c) + | Self::FastFinalizeCert(c) + | Self::NotarFallbackCert(c) => c.block.slot, + Self::FinalizeCert(c) | Self::SkipCert(c) => c.slot, + } + } +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, SchemaWrite, SchemaRead)] +/// First version of a wire consensus message +pub struct WireConsensusMessageV1 { + pub(crate) kind: WireConsensusMessageKind, + pub(crate) shred_version: u16, +} + +impl WireConsensusMessageV1 { + fn new(msg: ConsensusMessage, shred_version: u16) -> Self { + Self { + kind: WireConsensusMessageKind::new(msg), + shred_version, + } + } + + fn new_from_vote(msg: VoteMessage, shred_version: u16) -> Self { + Self { + kind: WireConsensusMessageKind::new_from_vote(msg), + shred_version, + } + } + + fn new_from_cert(cert: Certificate, shred_version: u16) -> Self { + Self { + kind: WireConsensusMessageKind::new_from_cert(cert), + shred_version, + } + } + + fn slot(&self) -> Slot { + self.kind.slot() + } +} + +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, AbiEnumVisitor, StableAbi, StableAbiSample), + frozen_abi( + digest = "Gf8GEMXaXezQnGsqwDdpZN3WtNkMZw5wfp3nwep9j55V", + abi_digest = "AHPJsANgE3T8wfzkFZwao1PAWwd6LtS5GAhGrN9X4Xhy", + abi_serializer = "wincode", + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, SchemaWrite, SchemaRead)] +#[wincode(tag_encoding = "u8")] +/// versioned wire format of consensus message +pub enum VersionedWireConsensusMessage { + /// The first version + #[wincode(tag = 1)] + V1(WireConsensusMessageV1), +} + +impl VersionedWireConsensusMessage { + /// Constructs a new versioned wire consensus message. + pub fn new(msg: ConsensusMessage, shred_version: u16) -> Self { + let v1 = WireConsensusMessageV1::new(msg, shred_version); + Self::V1(v1) + } + + /// Constructs a new versioned wire consensus message from a vote. + pub fn new_from_vote(vote: VoteMessage, shred_version: u16) -> Self { + let v1 = WireConsensusMessageV1::new_from_vote(vote, shred_version); + Self::V1(v1) + } + + /// Constructs a new versioned wire consensus message from a cert. + pub fn new_from_cert(cert: Certificate, shred_version: u16) -> Self { + let v1 = WireConsensusMessageV1::new_from_cert(cert, shred_version); + Self::V1(v1) + } + + /// Returns the slot on the message. + pub fn slot(&self) -> Slot { + match self { + Self::V1(v1) => v1.slot(), + } + } +} + +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, AbiEnumVisitor, StableAbi, StableAbiSample), + frozen_abi( + digest = "AKMt6bqYRf1xh7tWg4eAgG7jNN1qtUvNVPb5XZn9vjtV", + abi_digest = "2aBMTuPyDgGSYeYX1aBbXURgA4qqr92Eh9yiTeHX6qZq", + abi_serializer = "wincode", + test_roundtrip = "eq_and_wire", + ) +)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, SchemaWrite, SchemaRead)] +#[wincode(tag_encoding = "u8")] +/// Vote payload that must be signed +pub enum VotePayloadToSign { + #[wincode(tag = 1)] + /// notar vote + Notar { + /// block + block: Block, + /// shred version + shred_version: u16, + }, + #[wincode(tag = 2)] + /// finalize vote + Finalize { + /// slot + slot: Slot, + /// shred version + shred_version: u16, + }, + #[wincode(tag = 3)] + /// skip vote + Skip { + /// slot + slot: Slot, + /// shred version + shred_version: u16, + }, + #[wincode(tag = 4)] + /// notar fallback vote + NotarFallback { + /// block + block: Block, + /// shred version + shred_version: u16, + }, + #[wincode(tag = 5)] + /// skip fallback vote + SkipFallback { + /// slot + slot: Slot, + /// shred version + shred_version: u16, + }, + #[wincode(tag = 6)] + /// genesis vote + Genesis { + /// block + block: Block, + /// shred version + shred_version: u16, + }, +} + +impl VotePayloadToSign { + /// Converts a `Vote` into a `VotePayloadToSign` + pub fn new_from_vote(vote: Vote, shred_version: u16) -> Self { + match vote { + Vote::Notarize(v) => Self::Notar { + block: v.block, + shred_version, + }, + Vote::NotarizeFallback(v) => Self::NotarFallback { + block: v.block, + shred_version, + }, + Vote::Genesis(v) => Self::Genesis { + block: v.block, + shred_version, + }, + Vote::Finalize(v) => Self::Finalize { + slot: v.slot, + shred_version, + }, + Vote::Skip(v) => Self::Skip { + slot: v.slot, + shred_version, + }, + Vote::SkipFallback(v) => Self::SkipFallback { + slot: v.slot, + shred_version, + }, + } + } + + /// Returns the slot the vote is for. + pub fn slot(&self) -> Slot { + match self { + Self::Notar { block, .. } + | Self::NotarFallback { block, .. } + | Self::Genesis { block, .. } => block.slot, + Self::Finalize { slot, .. } + | Self::Skip { slot, .. } + | Self::SkipFallback { slot, .. } => *slot, + } + } +} + +impl From for Vote { + /// Converts a `VotePayloadToSign` back into a `Vote`, dropping the shred version. + fn from(vote_payload: VotePayloadToSign) -> Self { + match vote_payload { + VotePayloadToSign::Notar { block, .. } => Self::new_notarization_vote(block), + VotePayloadToSign::Finalize { slot, .. } => Self::new_finalization_vote(slot), + VotePayloadToSign::Skip { slot, .. } => Self::new_skip_vote(slot), + VotePayloadToSign::NotarFallback { block, .. } => { + Self::new_notarization_fallback_vote(block) + } + VotePayloadToSign::SkipFallback { slot, .. } => Self::new_skip_fallback_vote(slot), + VotePayloadToSign::Genesis { block, .. } => Self::new_genesis_vote(block), + } + } +} + +/// Returns the appropriate vote payload to sign. +pub fn get_vote_payload_to_sign(vote: Vote, shred_version: u16) -> Vec { + let vote_to_sign = VotePayloadToSign::new_from_vote(vote, shred_version); + wincode::serialize(&vote_to_sign).unwrap() +} diff --git a/votor/Cargo.toml b/votor/Cargo.toml index 3413f3717f2..e204884d182 100644 --- a/votor/Cargo.toml +++ b/votor/Cargo.toml @@ -9,6 +9,11 @@ homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] dev-context-only-utils = [ @@ -18,32 +23,25 @@ dev-context-only-utils = [ frozen-abi = [ "dep:solana-frozen-abi", "dep:solana-frozen-abi-macro", - "solana-accounts-db/frozen-abi", - "solana-bloom/frozen-abi", "solana-ledger/frozen-abi", "solana-runtime/frozen-abi", "solana-signature/frozen-abi", - "solana-vote/frozen-abi", - "solana-vote-program/frozen-abi", "agave-votor-messages/frozen-abi", ] [dependencies] +agave-bls-sigverify = { workspace = true } agave-logger = { workspace = true } agave-math-utils = { workspace = true } agave-votor-messages = { workspace = true } -bincode = { workspace = true } bitvec = { workspace = true } crossbeam-channel = { workspace = true } itertools = { workspace = true } lazy-lru = { workspace = true } log = { workspace = true } parking_lot = { workspace = true } -qualifier_attr = { workspace = true } serde = { workspace = true } serde_bytes = { workspace = true } -solana-accounts-db = { workspace = true } -solana-bloom = { workspace = true } solana-bls-signatures = { workspace = true, features = ["solana-signer-derive"] } solana-client = { workspace = true } solana-clock = { workspace = true } @@ -58,6 +56,7 @@ solana-frozen-abi-macro = { workspace = true, optional = true, features = [ solana-gossip = { workspace = true } solana-hash = { workspace = true } solana-keypair = { workspace = true } +solana-leader-schedule = { workspace = true } solana-ledger = { workspace = true } solana-measure = { workspace = true } solana-metrics = { workspace = true } @@ -71,18 +70,14 @@ solana-streamer = { workspace = true } solana-time-utils = { workspace = true } solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } -solana-vote = { workspace = true } -solana-vote-program = { workspace = true } thiserror = { workspace = true } wincode = { workspace = true, features = ["alloc"] } [dev-dependencies] -agave-votor = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } rand = { workspace = true } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api"] } solana-perf = { path = "../perf", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } -solana-sdk-ids = { workspace = true } solana-streamer = { path = "../streamer", features = ["agave-unstable-api", "dev-context-only-utils"] } tempfile = { workspace = true } test-case = { workspace = true } diff --git a/votor/src/commitment.rs b/votor/src/commitment.rs index 8901191702a..8a49dfff09e 100644 --- a/votor/src/commitment.rs +++ b/votor/src/commitment.rs @@ -14,8 +14,8 @@ pub enum CommitmentError { pub enum CommitmentType { /// Our node has voted notarize for the slot Notarize, - /// We have observed a finalization certificate for the slot - Finalized, + /// Votor has selected the slot as root + Rooted, } #[derive(Debug, PartialEq)] @@ -45,11 +45,11 @@ pub fn update_commitment_cache( #[cfg(test)] mod tests { - use {super::*, crossbeam_channel::unbounded}; + use {super::*, crossbeam_channel::bounded}; #[test] fn test_update_commitment_cache() { - let (commitment_sender, commitment_receiver) = unbounded(); + let (commitment_sender, commitment_receiver) = bounded(1024); let slot = 3; let commitment_type = CommitmentType::Notarize; update_commitment_cache(commitment_type, slot, &commitment_sender) @@ -65,7 +65,7 @@ mod tests { } ); let slot = 5; - let commitment_type = CommitmentType::Finalized; + let commitment_type = CommitmentType::Rooted; update_commitment_cache(commitment_type, slot, &commitment_sender) .expect("Failed to send commitment data"); let received_data = commitment_receiver @@ -74,7 +74,7 @@ mod tests { assert_eq!( received_data, CommitmentAggregationData { - commitment_type: CommitmentType::Finalized, + commitment_type: CommitmentType::Rooted, slot, } ); diff --git a/votor/src/common.rs b/votor/src/common.rs index 26c37bce27f..81ddedef11d 100644 --- a/votor/src/common.rs +++ b/votor/src/common.rs @@ -1,31 +1,36 @@ use { agave_votor_messages::{ - consensus_message::CertificateType, + certificate::CertificateType, fraction::Fraction, vote::{Vote, VoteType}, }, - solana_clock::DEFAULT_MS_PER_SLOT, std::time::Duration, }; // Core consensus types and constants pub type Stake = u64; -pub const fn conflicting_types(vote_type: VoteType) -> &'static [VoteType] { +pub(crate) const fn conflicting_types(vote_type: VoteType) -> &'static [VoteType] { match vote_type { VoteType::Finalize => &[ VoteType::NotarizeFallback, VoteType::Skip, VoteType::SkipFallback, + VoteType::Genesis, ], - VoteType::Notarize => &[VoteType::Skip, VoteType::NotarizeFallback], - VoteType::NotarizeFallback => &[VoteType::Finalize, VoteType::Notarize], + VoteType::Notarize => &[ + VoteType::Skip, + VoteType::NotarizeFallback, + VoteType::Genesis, + ], + VoteType::NotarizeFallback => &[VoteType::Finalize, VoteType::Notarize, VoteType::Genesis], VoteType::Skip => &[ VoteType::Finalize, VoteType::Notarize, VoteType::SkipFallback, + VoteType::Genesis, ], - VoteType::SkipFallback => &[VoteType::Skip, VoteType::Finalize], + VoteType::SkipFallback => &[VoteType::Skip, VoteType::Finalize, VoteType::Genesis], VoteType::Genesis => &[ VoteType::Finalize, VoteType::Notarize, @@ -42,17 +47,17 @@ pub const fn conflicting_types(vote_type: VoteType) -> &'static [VoteType] { pub fn vote_to_cert_types(vote: &Vote) -> Vec { match vote { Vote::Notarize(vote) => vec![ - CertificateType::Notarize(vote.slot, vote.block_id), - CertificateType::NotarizeFallback(vote.slot, vote.block_id), - CertificateType::FinalizeFast(vote.slot, vote.block_id), + CertificateType::Notarize(vote.block), + CertificateType::NotarizeFallback(vote.block), + CertificateType::FinalizeFast(vote.block), ], Vote::NotarizeFallback(vote) => { - vec![CertificateType::NotarizeFallback(vote.slot, vote.block_id)] + vec![CertificateType::NotarizeFallback(vote.block)] } Vote::Finalize(vote) => vec![CertificateType::Finalize(vote.slot)], Vote::Skip(vote) => vec![CertificateType::Skip(vote.slot)], Vote::SkipFallback(vote) => vec![CertificateType::Skip(vote.slot)], - Vote::Genesis(vote) => vec![CertificateType::Genesis(vote.slot, vote.block_id)], + Vote::Genesis(vote) => vec![CertificateType::Genesis(vote.block)], } } @@ -69,26 +74,13 @@ pub const SAFE_TO_SKIP_THRESHOLD: Fraction = Fraction::from_percentage(40); /// Time bound assumed on network transmission delays during periods of synchrony. pub const DELTA: Duration = Duration::from_millis(250); -/// Time bound for propagation delay in the block propagation sub-protocol. For -/// Turbine this is a maximum of `3 * DELTA` for the current maximum number of -/// validators. -const DELTA_BLOCK_PROPAGATION: Duration = DELTA.checked_mul(3).unwrap(); - /// Base leader handover timeout: Time after parent-ready that a validator would -/// see a leaders first slice if that leader sent it at the very start of their +/// see a leaders first fec set if that leader sent it at the very start of their /// window. /// -/// This accounts for up to `DELTA` difference between the leader and the other -/// validator triggering the parent ready event and for block propagation delay. -pub(crate) const DELTA_TIMEOUT: Duration = DELTA.checked_add(DELTA_BLOCK_PROPAGATION).unwrap(); - -/// Time budget we allow a leader to build and send their first slice after -/// their leader window starts. `TimeoutCrashedLeader` must therefore fire -/// no earlier than `DELTA_TIMEOUT + DELTA_FIRST_SLICE` from the start of the -/// window, otherwise we may declare a correct leader crashed. -/// -/// Conservatively initialized to the slot time. -pub(crate) const DELTA_FIRST_SLICE: Duration = Duration::from_millis(DEFAULT_MS_PER_SLOT); +/// With the current 400ms slot duration, this schedules both +/// `TimeoutCrashedLeader(s)` and `Timeout(s)` at 800ms after `ParentReady`. +pub(crate) const DELTA_TIMEOUT: Duration = Duration::from_millis(400); /// Timeout for standstill detection mechanism. pub(crate) const DELTA_STANDSTILL: Duration = Duration::from_millis(10_000); diff --git a/votor/src/consensus_metrics.rs b/votor/src/consensus_metrics.rs index fc74514f1a6..9153357f8dc 100644 --- a/votor/src/consensus_metrics.rs +++ b/votor/src/consensus_metrics.rs @@ -1,7 +1,10 @@ use { agave_math_utils::welford_stats::WelfordStats, - agave_votor_messages::vote::Vote, - crossbeam_channel::{Receiver, RecvTimeoutError, Sender}, + agave_votor_messages::{ + metric_types::{ConsensusMetricsEvent, ConsensusMetricsEventReceiver}, + vote::Vote, + }, + crossbeam_channel::RecvTimeoutError, solana_clock::{Epoch, Slot}, solana_epoch_schedule::EpochSchedule, solana_metrics::datapoint_info, @@ -17,35 +20,9 @@ use { }, }; -/// Even at 10 events per slot, this supports 1000 slots in flight -/// With 2000 active validators, we can't have more than: -/// - 1 Notarize vote -/// - 3 Notarize-fallback votes -/// - 1 Skip-fallback vote -/// - 1 Finalize vote -/// -/// Per validator, resulting in 12k vote events. -/// We overprovision this channel at 15k total events. -pub const MAX_IN_FLIGHT_CONSENSUS_EVENTS: usize = 15_000; - /// Number of epochs to retain metrics for (current + previous). const EPOCHS_TO_RETAIN: u64 = 2; -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ConsensusMetricsEvent { - /// A vote was received from the node with `id`. - Vote { id: Pubkey, vote: Vote }, - /// A block hash was seen for `slot` and the `leader` is responsible for producing it. - BlockHashSeen { leader: Pubkey, slot: Slot }, - /// Start of slot - StartOfSlot { slot: Slot }, - /// A slot was finalized - SlotFinalized { slot: Slot }, -} - -pub type ConsensusMetricsEventSender = Sender<(Instant, Vec)>; -pub type ConsensusMetricsEventReceiver = Receiver<(Instant, Vec)>; - /// Tracks all [`Vote`] metrics for a given node. #[derive(Debug, Default)] struct NodeVoteMetrics { @@ -81,20 +58,6 @@ impl NodeVoteMetrics { } } -/// Errors returned from [`ConsensusMetrics::record_vote`]. -#[derive(Debug)] -pub enum RecordVoteError { - /// Could not find start of slot entry. - SlotNotFound, -} - -/// Errors returned from [`ConsensusMetrics::record_block_hash_seen`]. -#[derive(Debug)] -pub enum RecordBlockHashError { - /// Could not find start of slot entry. - SlotNotFound, -} - /// Per-epoch metrics container. #[derive(Debug, Default)] struct EpochMetrics { @@ -343,14 +306,14 @@ mod tests { use { super::*, agave_votor_messages::vote::{SkipVote, Vote}, - crossbeam_channel::unbounded, + crossbeam_channel::bounded, solana_keypair::Keypair, solana_signer::Signer, std::thread::sleep, }; fn new_metrics() -> ConsensusMetrics { - let (_, rx) = unbounded(); + let (_, rx) = bounded(1024); ConsensusMetrics::new(EpochSchedule::custom(100, 100, false), rx) // 100 slots/epoch } diff --git a/votor/src/consensus_pool.rs b/votor/src/consensus_pool.rs index fe637c5a6b7..85896f28c72 100644 --- a/votor/src/consensus_pool.rs +++ b/votor/src/consensus_pool.rs @@ -1,22 +1,26 @@ //! Defines ConsensusPool to store received and generated votes and certificates. use { crate::{ - commitment::CommitmentError, common::{ MAX_ENTRIES_PER_PUBKEY_FOR_NOTARIZE_LITE, MAX_ENTRIES_PER_PUBKEY_FOR_OTHER_TYPES, Stake, conflicting_types, vote_to_cert_types, }, consensus_pool::{ - parent_ready_tracker::ParentReadyTracker, + parent_ready_tracker::{ParentReady, ParentReadyTracker}, slot_stake_counters::SlotStakeCounters, stats::ConsensusPoolStats, vote_pool::{DuplicateBlockVotePool, SimpleVotePool, VotePool}, }, event::VotorEvent, - generated_cert_types::GeneratedCertTypes, }, + agave_bls_sigverify::generated_cert_types::GeneratedCertTypes, agave_votor_messages::{ - consensus_message::{Block, Certificate, CertificateType, ConsensusMessage, VoteMessage}, + certificate::{ + CertSignature, Certificate, CertificateType, FastFinalizeCert, FinalizeCert, + GenesisCert, NotarCert, + }, + consensus_message::{Block, ConsensusMessage, VoteMessage}, + finalized_slot::FinalizedSlot, fraction::Fraction, migration::MigrationStatus, vote::{Vote, VoteType}, @@ -28,7 +32,7 @@ use { solana_hash::Hash, solana_pubkey::Pubkey, solana_runtime::{bank::Bank, validated_block_finalization::ValidatedBlockFinalizationCert}, - std::{cmp::Ordering, collections::BTreeMap, num::NonZeroU64, sync::Arc}, + std::{collections::BTreeMap, num::NonZero, sync::Arc}, thiserror::Error, }; @@ -55,47 +59,26 @@ pub(crate) enum AddVoteError { #[error("Certificate error: {0}")] Certificate(#[from] CertificateBuildError), - #[error("{0} channel disconnected")] - ChannelDisconnected(String), - - #[error("Voting Service queue full")] - VotingServiceQueueFull, - #[error("Invalid rank: {0}")] InvalidRank(u16), } -impl From for AddVoteError { - fn from(_: CommitmentError) -> Self { - AddVoteError::ChannelDisconnected("CommitmentSender".to_string()) - } -} - fn get_key_and_stakes( root_bank: &Bank, slot: Slot, rank: u16, -) -> Result<(Pubkey, Stake, Stake), AddVoteError> { +) -> Result<(Pubkey, NonZero, NonZero), AddVoteError> { let epoch_stakes = root_bank.epoch_stakes_from_slot(slot).ok_or_else(|| { AddVoteError::EpochStakesNotFound(root_bank.epoch_schedule().get_epoch(slot)) })?; - let Some(entry) = epoch_stakes - .bls_pubkey_to_rank_map() - .get_pubkey_stake_entry(rank as usize) - else { + let rank_map = epoch_stakes.bls_pubkey_to_rank_map(); + let Some(entry) = rank_map.get_pubkey_stake_entry(rank as usize) else { return Err(AddVoteError::InvalidRank(rank)); }; - if entry.stake == 0 { - // Since we have a valid rank, this should never happen, there is no rank for zero stake. - panic!( - "Validator stake is zero for pubkey: {}", - entry.vote_account_pubkey - ); - } Ok(( entry.vote_account_pubkey, entry.stake, - epoch_stakes.total_stake(), + rank_map.total_stake(), )) } @@ -122,8 +105,8 @@ pub(crate) struct ConsensusPool { stats: ConsensusPoolStats, /// Slot stake counters, used to calculate safe_to_notar and safe_to_skip slot_stake_counters_map: BTreeMap, - /// Stores details about the genesis vote during the migration - migration_status: Option>, + /// Stores details about the genesis vote during the migration. + migration_status: Arc, /// Pending safe-to-notar blocks for intrawindow slots that need parent verification. /// These are blocks that have reached the safe-to-notar threshold but are not the /// first block in their leader window. They need to be verified that their parent @@ -134,26 +117,15 @@ pub(crate) struct ConsensusPool { } impl ConsensusPool { - pub(crate) fn new_from_root_bank_pre_migration( + pub(crate) fn new( cluster_info: Arc, - bank: &Bank, + root: &Bank, generated_cert_types: Arc, migration_status: Arc, + initial_parent_ready: ParentReady, ) -> Self { - let mut pool = Self::new_from_root_bank(cluster_info, bank, generated_cert_types); - pool.migration_status = Some(migration_status); - pool - } - - pub fn new_from_root_bank( - cluster_info: Arc, - bank: &Bank, - generated_cert_types: Arc, - ) -> Self { - // To account for genesis and snapshots we allow default block id until - // block id can be serialized as part of the snapshot - let root_block = (bank.slot(), bank.block_id().unwrap_or_default()); - let parent_ready_tracker = ParentReadyTracker::new(cluster_info.clone(), root_block); + let parent_ready_tracker = + ParentReadyTracker::new(cluster_info.clone(), root.slot(), initial_parent_ready); Self { cluster_info, @@ -163,7 +135,7 @@ impl ConsensusPool { parent_ready_tracker, stats: ConsensusPoolStats::default(), slot_stake_counters_map: BTreeMap::new(), - migration_status: None, + migration_status, generated_cert_types, pending_safe_to_notar: vec![], last_pruned_slot: 0, @@ -178,6 +150,9 @@ impl ConsensusPool { VoteType::Notarize => VotePool::DuplicateBlockVotePool(DuplicateBlockVotePool::new( MAX_ENTRIES_PER_PUBKEY_FOR_OTHER_TYPES, )), + VoteType::Genesis => VotePool::DuplicateBlockVotePool(DuplicateBlockVotePool::new( + MAX_ENTRIES_PER_PUBKEY_FOR_OTHER_TYPES, + )), _ => VotePool::SimpleVotePool(SimpleVotePool::default()), } } @@ -186,7 +161,7 @@ impl ConsensusPool { &mut self, vote: VoteMessage, validator_vote_key: Pubkey, - validator_stake: Stake, + validator_stake: NonZero, ) -> Option { let vote_type = vote.vote.get_type(); let pool = self @@ -216,7 +191,7 @@ impl ConsensusPool { vote: &Vote, block_id: Option, events: &mut Vec, - total_stake: Stake, + total_stake: NonZero, ) -> Result>, AddVoteError> { let slot = vote.slot(); let mut new_certificates_to_send = Vec::new(); @@ -243,7 +218,6 @@ impl ConsensusPool { }) }) .sum::(); - let total_stake = NonZeroU64::new(total_stake).unwrap(); if Fraction::new(accumulated_stake, total_stake) < limit { continue; } @@ -263,7 +237,7 @@ impl ConsensusPool { } }); let new_cert = Arc::new(cert_builder.build()?); - self.insert_certificate(root_bank, cert_type, new_cert.clone(), events); + self.insert_certificate(root_bank, new_cert.clone(), events); self.generated_cert_types.insert_cert(cert_type); self.stats.incr_generated_cert(&new_cert.cert_type); new_certificates_to_send.push(new_cert); @@ -308,37 +282,47 @@ impl ConsensusPool { fn insert_certificate( &mut self, root_bank: &Bank, - cert_type: CertificateType, cert: Arc, events: &mut Vec, ) { trace!( "{}: Inserting certificate {:?}", self.cluster_info.id(), - cert_type + cert.cert_type ); - self.completed_certificates.insert(cert_type, cert.clone()); - match cert_type { - CertificateType::NotarizeFallback(slot, block_id) => { - events.push(VotorEvent::BlockNotarFallback((slot, block_id))); + self.completed_certificates + .insert(cert.cert_type, cert.clone()); + match cert.cert_type { + CertificateType::NotarizeFallback(block) => { + events.push(VotorEvent::BlockNotarFallback(block)); self.parent_ready_tracker - .add_new_notar_fallback_or_stronger((slot, block_id), events); + .add_new_notar_fallback_or_stronger(block, events); } CertificateType::Skip(slot) => self.parent_ready_tracker.add_new_skip(slot, events), - CertificateType::Notarize(slot, block_id) => { - events.push(VotorEvent::BlockNotarized((slot, block_id))); + CertificateType::Notarize(block) => { + events.push(VotorEvent::BlockNotarized(block)); self.parent_ready_tracker - .add_new_notar_fallback_or_stronger((slot, block_id), events); + .add_new_notar_fallback_or_stronger(block, events); - if let Some(finalize_cert) = self.get_finalize_cert(slot) { + if let Some(finalize_cert) = self.get_finalize_cert(block.slot) { // It's fine to set FastFinalization to false here, because // we will report correctly as long as we have FastFinalization cert. - events.push(VotorEvent::Finalized((slot, block_id), false)); - if self.highest_finalized_slot().is_none_or(|s| s < slot) { + events.push(VotorEvent::Finalized(block, false)); + if self + .highest_finalized_slot() + .is_none_or(|s| s < FinalizedSlot::Slow(block.slot)) + { + let notarize_cert = NotarCert { + block, + signature: CertSignature { + signature: cert.signature, + bitmap: cert.bitmap.clone(), + }, + }; self.highest_finalized_slot_cert = Some(ValidatedBlockFinalizationCert::from_validated_slow( - Arc::unwrap_or_clone(finalize_cert.clone()), - Arc::unwrap_or_clone(cert), + finalize_cert, + notarize_cert, root_bank, )); } @@ -346,38 +330,62 @@ impl ConsensusPool { } CertificateType::Finalize(slot) => { if let Some(notarize_cert) = self.get_notarize_cert(slot) { - let (_s, block_id) = notarize_cert.cert_type.to_block().unwrap(); - events.push(VotorEvent::Finalized((slot, block_id), false)); - if self.highest_finalized_slot().is_none_or(|s| s < slot) { + let block = notarize_cert.block; + events.push(VotorEvent::Finalized(block, false)); + if self + .highest_finalized_slot() + .is_none_or(|s| s < FinalizedSlot::Slow(slot)) + { + let finalize_cert = FinalizeCert { + slot, + signature: CertSignature { + signature: cert.signature, + bitmap: cert.bitmap.clone(), + }, + }; self.highest_finalized_slot_cert = Some(ValidatedBlockFinalizationCert::from_validated_slow( - Arc::unwrap_or_clone(cert), - Arc::unwrap_or_clone(notarize_cert), + finalize_cert, + notarize_cert, root_bank, )); } } } - CertificateType::FinalizeFast(slot, block_id) => { - events.push(VotorEvent::Finalized((slot, block_id), true)); + CertificateType::FinalizeFast(block) => { + events.push(VotorEvent::Finalized(block, true)); self.parent_ready_tracker - .add_new_notar_fallback_or_stronger((slot, block_id), events); - // Use <= for FastFinalize since it supersedes standard finalization at the same slot - if self.highest_finalized_slot().is_none_or(|s| s <= slot) { + .add_new_notar_fallback_or_stronger(block, events); + if self + .highest_finalized_slot() + .is_none_or(|s| s < FinalizedSlot::Fast(block.slot)) + { + let fast_finalize_cert = FastFinalizeCert { + block, + signature: CertSignature { + signature: cert.signature, + bitmap: cert.bitmap.clone(), + }, + }; self.highest_finalized_slot_cert = Some(ValidatedBlockFinalizationCert::from_validated_fast( - Arc::unwrap_or_clone(cert), + fast_finalize_cert, root_bank, )); } } - CertificateType::Genesis(slot, block_id) => { - if let Some(ref migration_status) = self.migration_status { - migration_status.set_genesis_certificate(cert); - } + CertificateType::Genesis(block) => { + let genesis_cert = Arc::new(GenesisCert { + block, + signature: CertSignature { + signature: cert.signature, + bitmap: cert.bitmap.clone(), + }, + }); + self.migration_status.set_genesis_certificate(genesis_cert); // The genesis block is automatically certified self.parent_ready_tracker - .add_new_notar_fallback_or_stronger((slot, block_id), events); + .add_new_notar_fallback_or_stronger(block, events); } } } @@ -410,7 +418,10 @@ impl ConsensusPool { } else { None }; - Ok((new_finalized_slot, new_certficates_to_send)) + Ok(( + new_finalized_slot.map(|s| s.slot()), + new_certficates_to_send, + )) } fn add_vote( @@ -420,18 +431,12 @@ impl ConsensusPool { vote_message: VoteMessage, events: &mut Vec, ) -> Result>, AddVoteError> { - let vote = &vote_message.vote; + let vote = vote_message.vote; let rank = vote_message.rank; let vote_slot = vote.slot(); let (validator_vote_key, validator_stake, total_stake) = get_key_and_stakes(root_bank, vote_slot, rank)?; - // Since we have a valid rank, this should never happen, there is no rank for zero stake. - assert_ne!( - validator_stake, 0, - "Validator stake is zero for pubkey: {validator_vote_key}" - ); - self.stats.incoming_votes = self.stats.incoming_votes.saturating_add(1); if vote_slot < root_bank.slot() { self.stats.out_of_range_votes = self.stats.out_of_range_votes.saturating_add(1); @@ -470,7 +475,7 @@ impl ConsensusPool { .entry(vote_slot) .or_insert_with(|| SlotStakeCounters::new(total_stake)); fallback_vote_counters.add_vote( - vote, + &vote, entry_stake, my_vote_pubkey == &validator_vote_key, events, @@ -481,7 +486,7 @@ impl ConsensusPool { } self.stats.incr_ingested_vote_type(vote_type); - self.update_certificates(root_bank, vote, block_id, events, total_stake) + self.update_certificates(root_bank, &vote, block_id, events, total_stake) } fn add_certificate( @@ -501,30 +506,43 @@ impl ConsensusPool { return Ok(vec![]); } let cert = Arc::new(cert); - self.insert_certificate(root_bank, cert_type, cert.clone(), events); + self.insert_certificate(root_bank, cert.clone(), events); self.stats.incr_ingested_cert(&cert_type); Ok(vec![cert]) } /// Get the Notarize certificate for a slot - fn get_notarize_cert(&self, slot: Slot) -> Option> { + fn get_notarize_cert(&self, slot: Slot) -> Option { self.completed_certificates .iter() .find_map(|(cert_type, cert)| match cert_type { - CertificateType::Notarize(s, _) if slot == *s => Some(cert.clone()), + CertificateType::Notarize(block) if slot == block.slot => Some(NotarCert { + block: *block, + signature: CertSignature { + signature: cert.signature, + bitmap: cert.bitmap.clone(), + }, + }), _ => None, }) } /// Get the Finalize certificate for a slot - fn get_finalize_cert(&self, slot: Slot) -> Option<&Arc> { + fn get_finalize_cert(&self, slot: Slot) -> Option { self.completed_certificates .get(&CertificateType::Finalize(slot)) + .map(|c| FinalizeCert { + slot, + signature: CertSignature { + signature: c.signature, + bitmap: c.bitmap.clone(), + }, + }) } /// Get the highest finalized slot (slow or fast) - pub(crate) fn highest_finalized_slot(&self) -> Option { + pub(crate) fn highest_finalized_slot(&self) -> Option { self.highest_finalized_slot_cert.as_ref().map(|c| c.slot()) } @@ -534,12 +552,11 @@ impl ConsensusPool { self.completed_certificates .keys() .filter_map(|cert_type| match cert_type { - CertificateType::Notarize(s, _) => Some(s), - CertificateType::NotarizeFallback(s, _) => Some(s), + CertificateType::Notarize(block) => Some(block.slot), + CertificateType::NotarizeFallback(block) => Some(block.slot), _ => None, }) .max() - .copied() .unwrap_or(0) } @@ -556,12 +573,16 @@ impl ConsensusPool { .unwrap_or(0) } - /// Checks if any block in the slot `s` is finalized + /// Checks if any block in the `slot` is finalized #[cfg(test)] fn is_finalized(&self, slot: Slot) -> bool { - self.completed_certificates.keys().any(|cert_type| { - matches!(cert_type, CertificateType::Finalize(s) | CertificateType::FinalizeFast(s, _) if *s == slot) - }) + self.completed_certificates + .keys() + .any(|cert_type| match cert_type { + CertificateType::Finalize(s) => s == &slot, + CertificateType::FinalizeFast(block) => block.slot == slot, + _ => false, + }) } /// Checks if the any block in slot `slot` has received a `NotarizeFallback` certificate, if so return @@ -569,7 +590,7 @@ impl ConsensusPool { #[cfg(test)] fn slot_has_notarized_fallback(&self, slot: Slot) -> bool { self.completed_certificates.iter().any( - |(cert_type, _)| matches!(cert_type, CertificateType::NotarizeFallback(s,_) if *s == slot), + |(cert_type, _)| matches!(cert_type, CertificateType::NotarizeFallback(block) if block.slot == slot), ) } @@ -583,15 +604,8 @@ impl ConsensusPool { /// Checks if a specific block has a NotarizeFallback certificate (or stronger). /// This is used for verifying that an intrawindow block's parent has been certified. pub(crate) fn block_has_notar_fallback_or_stronger(&self, block: Block) -> bool { - let (slot, block_id) = block; - self.completed_certificates - .contains_key(&CertificateType::NotarizeFallback(slot, block_id)) - || self - .completed_certificates - .contains_key(&CertificateType::Notarize(slot, block_id)) - || self - .completed_certificates - .contains_key(&CertificateType::FinalizeFast(slot, block_id)) + self.parent_ready_tracker + .has_notar_fallback_or_stronger(block) } /// Takes the pending safe-to-notar blocks that need parent verification. @@ -657,7 +671,7 @@ impl ConsensusPool { self.slot_stake_counters_map = self.slot_stake_counters_map.split_off(&root_slot); self.parent_ready_tracker.set_root(root_slot); self.pending_safe_to_notar - .retain(|(slot, _)| *slot >= root_slot); + .retain(|block| block.slot >= root_slot); self.last_pruned_slot = root_slot; } @@ -666,33 +680,14 @@ impl ConsensusPool { } pub(crate) fn get_certs_for_standstill(&self) -> Vec> { - let (highest_slot, has_fast_finalize) = self - .highest_finalized_slot_cert - .as_ref() - .map(|certs| (certs.slot(), certs.is_fast())) - .unwrap_or((0, false)); + let highest_slot = match &self.highest_finalized_slot_cert { + Some(c) => c.slot().slot(), + None => 0, + }; self.completed_certificates .iter() .filter_map(|(cert_type, cert)| { - let cert_to_send = match ( - cert_type.slot().cmp(&highest_slot), - cert_type, - has_fast_finalize, - ) { - (Ordering::Greater, _, _) - | ( - Ordering::Equal, - CertificateType::Finalize(_) | CertificateType::Notarize(_, _), - false, - ) - | (Ordering::Equal, CertificateType::FinalizeFast(_, _), true) => { - Some(cert.clone()) - } - (Ordering::Equal, CertificateType::FinalizeFast(_, _), false) => { - panic!("Should not happen while certificate pool is single threaded") - } - _ => None, - }; + let cert_to_send = (cert_type.slot() > highest_slot).then(|| cert.clone()); if cert_to_send.is_some() { trace!( "{}: Refreshing certificate {:?}", @@ -717,7 +712,10 @@ mod tests { use { super::*, crate::tests::get_cluster_info, - agave_votor_messages::consensus_message::{BLS_KEYPAIR_DERIVE_SEED, VoteMessage}, + agave_votor_messages::{ + consensus_message::{BLS_KEYPAIR_DERIVE_SEED, VoteMessage}, + wire::get_vote_payload_to_sign, + }, bitvec::vec::BitVec, solana_bls_signatures::{ BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature, VerifiableSignature, @@ -761,12 +759,19 @@ mod tests { let root_bank = bank_forks.read().unwrap().root_bank(); let generated_cert_types = Arc::new(GeneratedCertTypes::default()); let cluster_info = get_cluster_info(Keypair::new()); + let root_block = Block { + slot: root_bank.slot(), + block_id: root_bank.block_id().unwrap_or_default(), + }; + let initial_parent_ready = (root_bank.slot().checked_add(1).unwrap(), root_block); Self { validators: validator_keypairs, - pool: ConsensusPool::new_from_root_bank( + pool: ConsensusPool::new( cluster_info, &root_bank, generated_cert_types.clone(), + Arc::new(MigrationStatus::post_migration_status()), + initial_parent_ready, ), bank_forks, generated_cert_types, @@ -785,17 +790,24 @@ mod tests { fn add_certificate(&mut self, vote: Vote) { for rank in 0..=6 { - self.add_message(dummy_vote_message(&self.validators, &vote, rank)); + self.add_message(dummy_vote_message( + &self.validators, + self.pool.cluster_info.my_shred_version(), + &vote, + rank, + )); } match vote { - Vote::Notarize(vote) => assert_eq!(self.pool.highest_notarized_slot(), vote.slot), + Vote::Notarize(vote) => { + assert_eq!(self.pool.highest_notarized_slot(), vote.block.slot) + } Vote::NotarizeFallback(vote) => { - assert_eq!(self.pool.highest_notarized_slot(), vote.slot) + assert_eq!(self.pool.highest_notarized_slot(), vote.block.slot) } Vote::Skip(vote) => assert_eq!(self.pool.highest_skip_slot(), vote.slot), Vote::SkipFallback(vote) => assert_eq!(self.pool.highest_skip_slot(), vote.slot), Vote::Finalize(vote) => assert_eq!( - self.pool.highest_finalized_slot().unwrap_or_default(), + self.pool.highest_finalized_slot().unwrap().slot(), vote.slot ), Vote::Genesis(_genesis_vote) => (), @@ -805,15 +817,15 @@ mod tests { fn dummy_vote_message( keypairs: &[ValidatorVoteKeypairs], + shred_version: u16, vote: &Vote, rank: usize, ) -> ConsensusMessage { let bls_keypair = BLSKeypair::derive_from_signer(&keypairs[rank].vote_keypair, BLS_KEYPAIR_DERIVE_SEED) .unwrap(); - let signature: BLSSignature = bls_keypair - .sign(bincode::serialize(vote).unwrap().as_slice()) - .into(); + let payload = get_vote_payload_to_sign(*vote, shred_version); + let signature: BLSSignature = bls_keypair.sign(&payload).into(); ConsensusMessage::new_vote(*vote, signature, rank as u16) } @@ -844,7 +856,7 @@ mod tests { pool.add_message( root_bank, &Pubkey::new_unique(), - dummy_vote_message(keypairs, &vote, rank), + dummy_vote_message(keypairs, pool.cluster_info.my_shred_version(), &vote, rank), &mut vec![], ) .unwrap(); @@ -1002,7 +1014,10 @@ mod tests { ctx.bank_forks.write().unwrap().insert(bank); // Notarize slot 5 - ctx.add_certificate(Vote::new_notarization_vote(5, Hash::default())); + ctx.add_certificate(Vote::new_notarization_vote(Block { + slot: 5, + block_id: Hash::default(), + })); assert_eq!(ctx.pool.highest_notarized_slot(), 5); // No skip certificate for 6-10 @@ -1023,7 +1038,10 @@ mod tests { let mut ctx = TestContext::new(); // Notarize slot 5 - ctx.add_certificate(Vote::new_notarization_vote(5, Hash::default())); + ctx.add_certificate(Vote::new_notarization_vote(Block { + slot: 5, + block_id: Hash::default(), + })); assert_eq!(ctx.pool.highest_notarized_slot(), 5); // Leader slot is just +1 from notarized slot (no skip needed) @@ -1042,7 +1060,10 @@ mod tests { let mut ctx = TestContext::new(); // Notarize slot 5 - ctx.add_certificate(Vote::new_notarization_vote(5, Hash::default())); + ctx.add_certificate(Vote::new_notarization_vote(Block { + slot: 5, + block_id: Hash::default(), + })); assert_eq!(ctx.pool.highest_notarized_slot(), 5); // Valid skip certificate for 6-9 exists @@ -1065,7 +1086,10 @@ mod tests { let mut ctx = TestContext::new(); // Notarize slot 5 - ctx.add_certificate(Vote::new_notarization_vote(5, Hash::default())); + ctx.add_certificate(Vote::new_notarization_vote(Block { + slot: 5, + block_id: Hash::default(), + })); assert_eq!(ctx.pool.highest_notarized_slot(), 5); // Valid skip certificate for 4-9 exists @@ -1086,8 +1110,8 @@ mod tests { } #[test_case(Vote::new_finalization_vote(5), vec![CertificateType::Finalize(5)])] - #[test_case(Vote::new_notarization_vote(6, Hash::default()), vec![CertificateType::Notarize(6, Hash::default()), CertificateType::NotarizeFallback(6, Hash::default())])] - #[test_case(Vote::new_notarization_fallback_vote(7, Hash::default()), vec![CertificateType::NotarizeFallback(7, Hash::default())])] + #[test_case(Vote::new_notarization_vote(Block { slot: 6, block_id: Hash::default() }), vec![CertificateType::Notarize(Block { slot: 6, block_id: Hash::default() }), CertificateType::NotarizeFallback(Block { slot: 6, block_id: Hash::default() })])] + #[test_case(Vote::new_notarization_fallback_vote(Block { slot: 7, block_id: Hash::default() }), vec![CertificateType::NotarizeFallback(Block { slot: 7, block_id: Hash::default() })])] #[test_case(Vote::new_skip_vote(8), vec![CertificateType::Skip(8)])] #[test_case(Vote::new_skip_fallback_vote(9), vec![CertificateType::Skip(9)])] fn test_add_vote_and_create_new_certificate_with_types( @@ -1101,7 +1125,10 @@ mod tests { // because finalization requires both Finalize and Notarize certificates if vote.is_finalize() { let notarize_cert = Certificate { - cert_type: CertificateType::Notarize(vote.slot(), Hash::default()), + cert_type: CertificateType::Notarize(Block { + slot: vote.slot(), + block_id: Hash::default(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1109,28 +1136,49 @@ mod tests { } let highest_slot_fn = match &vote { - Vote::Finalize(_) => { - |pool: &ConsensusPool| pool.highest_finalized_slot().unwrap_or_default() - } + Vote::Finalize(_) => |pool: &ConsensusPool| { + pool.highest_finalized_slot() + .map(|s| s.slot()) + .unwrap_or_default() + }, Vote::Notarize(_) => |pool: &ConsensusPool| pool.highest_notarized_slot(), Vote::NotarizeFallback(_) => |pool: &ConsensusPool| pool.highest_notarized_slot(), Vote::Skip(_) => |pool: &ConsensusPool| pool.highest_skip_slot(), Vote::SkipFallback(_) => |pool: &ConsensusPool| pool.highest_skip_slot(), Vote::Genesis(_genesis_vote) => |_pool: &ConsensusPool| 0, }; - ctx.add_message(dummy_vote_message(&ctx.validators, &vote, my_validator_ix)); + ctx.add_message(dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + my_validator_ix, + )); let slot = vote.slot(); assert!(highest_slot_fn(&ctx.pool) < slot); // Same key voting again shouldn't make a certificate - ctx.add_message(dummy_vote_message(&ctx.validators, &vote, my_validator_ix)); + ctx.add_message(dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + my_validator_ix, + )); assert!(highest_slot_fn(&ctx.pool) < slot); for rank in 0..4 { - ctx.add_message(dummy_vote_message(&ctx.validators, &vote, rank)); + ctx.add_message(dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + rank, + )); } assert!(highest_slot_fn(&ctx.pool) < slot); let new_validator_ix = 6; - let (new_finalized_slot, certs_to_send) = - ctx.add_message(dummy_vote_message(&ctx.validators, &vote, new_validator_ix)); + let (new_finalized_slot, certs_to_send) = ctx.add_message(dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + new_validator_ix, + )); if vote.is_finalize() { assert_eq!(new_finalized_slot, Some(slot)); } else { @@ -1154,16 +1202,16 @@ mod tests { #[test_case(CertificateType::Finalize(5), Vote::new_finalization_vote(5))] #[test_case( - CertificateType::FinalizeFast(6, Hash::default()), - Vote::new_notarization_vote(6, Hash::default()) + CertificateType::FinalizeFast(Block { slot: 6, block_id: Hash::default() }), + Vote::new_notarization_vote(Block { slot: 6, block_id: Hash::default() }) )] #[test_case( - CertificateType::Notarize(6, Hash::default()), - Vote::new_notarization_vote(6, Hash::default()) + CertificateType::Notarize(Block { slot: 6, block_id: Hash::default() }), + Vote::new_notarization_vote(Block { slot: 6, block_id: Hash::default() }) )] #[test_case( - CertificateType::NotarizeFallback(7, Hash::default()), - Vote::new_notarization_fallback_vote(7, Hash::default()) + CertificateType::NotarizeFallback(Block { slot: 7, block_id: Hash::default() }), + Vote::new_notarization_fallback_vote(Block { slot: 7, block_id: Hash::default() }) )] #[test_case(CertificateType::Skip(8), Vote::new_skip_vote(8))] fn test_add_certificate_with_types(cert_type: CertificateType, vote: Vote) { @@ -1179,7 +1227,10 @@ mod tests { // because finalization requires both certificates to be present if matches!(cert_type, CertificateType::Finalize(slot) if slot == 5) { let notarize_cert = Certificate { - cert_type: CertificateType::Notarize(5, Hash::default()), + cert_type: CertificateType::Notarize(Block { + slot: 5, + block_id: Hash::default(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1200,7 +1251,7 @@ mod tests { .unwrap(); // Because this is the first certificate of this type, it should be sent out. if matches!(cert_type, CertificateType::Finalize(_)) - || matches!(cert_type, CertificateType::FinalizeFast(_, _)) + || matches!(cert_type, CertificateType::FinalizeFast(_)) { assert_eq!(new_finalized_slot, Some(cert_type.slot())); } else { @@ -1224,7 +1275,12 @@ mod tests { .add_message( &root_bank, &Pubkey::new_unique(), - dummy_vote_message(&ctx.validators, &vote, rank), + dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + rank, + ), &mut vec![], ) .unwrap(); @@ -1276,7 +1332,12 @@ mod tests { let slot = (i as u64).saturating_add(16); let vote = Vote::new_skip_vote(slot); // These should not extend the skip range - ctx.add_message(dummy_vote_message(&ctx.validators, &vote, i)); + ctx.add_message(dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + i, + )); } assert_single_certificate_range(&ctx.pool, 15, 15); @@ -1388,20 +1449,35 @@ mod tests { } // 10% vote for skip 2 let vote = Vote::new_skip_vote(2); - ctx.add_message(dummy_vote_message(&ctx.validators, &vote, 6)); + ctx.add_message(dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + 6, + )); assert_eq!(ctx.pool.highest_skip_slot(), 2); assert_single_certificate_range(&ctx.pool, 2, 2); // 10% vote for skip 4 let vote = Vote::new_skip_vote(4); - ctx.add_message(dummy_vote_message(&ctx.validators, &vote, 7)); + ctx.add_message(dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + 7, + )); assert_eq!(ctx.pool.highest_skip_slot(), 4); assert_single_certificate_range(&ctx.pool, 2, 2); assert_single_certificate_range(&ctx.pool, 4, 4); // 10% vote for skip 3 let vote = Vote::new_skip_vote(3); - ctx.add_message(dummy_vote_message(&ctx.validators, &vote, 8)); + ctx.add_message(dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + 8, + )); assert_eq!(ctx.pool.highest_skip_slot(), 4); assert_single_certificate_range(&ctx.pool, 2, 4); assert!(ctx.pool.skip_certified(3)); @@ -1435,7 +1511,12 @@ mod tests { } // Range expansion on a singleton vote should be ok let vote = Vote::new_skip_vote(1); - ctx.add_message(dummy_vote_message(&ctx.validators, &vote, 6)); + ctx.add_message(dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + 6, + )); assert_eq!(ctx.pool.highest_skip_slot(), 1); add_skip_vote_range( &mut ctx.pool, @@ -1464,7 +1545,12 @@ mod tests { // AlreadyExists, silently fail let vote = Vote::new_skip_vote(20); - ctx.add_message(dummy_vote_message(&ctx.validators, &vote, 6)); + ctx.add_message(dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + 6, + )); } #[test] @@ -1547,7 +1633,12 @@ mod tests { .add_message( &bank, &my_vote_key, - dummy_vote_message(&ctx.validators, &vote, 0), + dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + 0, + ), &mut new_events, ) .unwrap(); @@ -1555,20 +1646,24 @@ mod tests { // 40% notarized, should succeed for rank in 1..5 { - let vote = Vote::new_notarization_vote(slot, block_id); + let vote = Vote::new_notarization_vote(Block { slot, block_id }); ctx.pool .add_message( &bank, &Pubkey::new_unique(), - dummy_vote_message(&ctx.validators, &vote, rank), + dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + rank, + ), &mut new_events, ) .unwrap(); } assert_eq!(new_events.len(), 1); - if let VotorEvent::SafeToNotar((event_slot, event_block_id)) = new_events[0] { - assert_eq!(block_id, event_block_id); - assert_eq!(slot, event_slot); + if let VotorEvent::SafeToNotar(block) = &new_events[0] { + assert_eq!(*block, Block { slot, block_id }); } else { panic!("Expected SafeToNotar event"); } @@ -1580,12 +1675,17 @@ mod tests { // Add 20% notarize, but no vote from myself, should fail for rank in 1..3 { - let vote = Vote::new_notarization_vote(slot, block_id); + let vote = Vote::new_notarization_vote(Block { slot, block_id }); ctx.pool .add_message( &bank, &Pubkey::new_unique(), - dummy_vote_message(&ctx.validators, &vote, rank), + dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + rank, + ), &mut new_events, ) .unwrap(); @@ -1593,12 +1693,20 @@ mod tests { assert!(new_events.is_empty()); // Add a notarize from myself for some other block, but still not enough notar or skip, should fail. - let vote = Vote::new_notarization_vote(slot, Hash::new_unique()); + let vote = Vote::new_notarization_vote(Block { + slot, + block_id: Hash::new_unique(), + }); ctx.pool .add_message( &bank, &my_vote_key, - dummy_vote_message(&ctx.validators, &vote, 0), + dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + 0, + ), &mut new_events, ) .unwrap(); @@ -1612,7 +1720,12 @@ mod tests { .add_message( &bank, &Pubkey::new_unique(), - dummy_vote_message(&ctx.validators, &vote, rank), + dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + rank, + ), &mut new_events, ) .unwrap(); @@ -1623,9 +1736,8 @@ mod tests { } else { panic!("Expected SafeToSkip event"); } - if let VotorEvent::SafeToNotar((event_slot, event_block_id)) = new_events[1] { - assert_eq!(block_id, event_block_id); - assert_eq!(slot, event_slot); + if let VotorEvent::SafeToNotar(block) = &new_events[1] { + assert_eq!(*block, Block { slot, block_id }); } else { panic!("Expected SafeToNotar event"); } @@ -1635,21 +1747,34 @@ mod tests { // but not on the same block_id because we already sent the event let duplicate_block_id = Hash::new_unique(); for rank in 7..9 { - let vote = Vote::new_notarization_vote(slot, duplicate_block_id); + let vote = Vote::new_notarization_vote(Block { + slot, + block_id: duplicate_block_id, + }); ctx.pool .add_message( &bank, &Pubkey::new_unique(), - dummy_vote_message(&ctx.validators, &vote, rank), + dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + rank, + ), &mut new_events, ) .unwrap(); } assert_eq!(new_events.len(), 1); - if let VotorEvent::SafeToNotar((event_slot, event_block_id)) = new_events[0] { - assert_eq!(duplicate_block_id, event_block_id); - assert_eq!(slot, event_slot); + if let VotorEvent::SafeToNotar(block) = &new_events[0] { + assert_eq!( + *block, + Block { + slot, + block_id: duplicate_block_id + } + ); } else { panic!("Expected SafeToNotar event"); } @@ -1665,12 +1790,17 @@ mod tests { // Add a notarize from myself. let block_id = Hash::new_unique(); - let vote = Vote::new_notarization_vote(2, block_id); + let vote = Vote::new_notarization_vote(Block { slot: 2, block_id }); ctx.pool .add_message( &bank, &my_vote_key, - dummy_vote_message(&ctx.validators, &vote, 0), + dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + 0, + ), &mut new_events, ) .unwrap(); @@ -1683,7 +1813,12 @@ mod tests { .add_message( &bank, &Pubkey::new_unique(), - dummy_vote_message(&ctx.validators, &vote, rank), + dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + rank, + ), &mut new_events, ) .unwrap(); @@ -1696,12 +1831,17 @@ mod tests { } new_events.clear(); // Add 10% more notarize, will not send new SafeToSkip because the event was already sent - let vote = Vote::new_notarization_vote(2, block_id); + let vote = Vote::new_notarization_vote(Block { slot: 2, block_id }); ctx.pool .add_message( &bank, &Pubkey::new_unique(), - dummy_vote_message(&ctx.validators, &vote, 6), + dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + 6, + ), &mut new_events, ) .unwrap(); @@ -1710,14 +1850,21 @@ mod tests { fn create_new_vote(vote_type: VoteType, slot: Slot) -> Vote { match vote_type { - VoteType::Notarize => Vote::new_notarization_vote(slot, Hash::default()), - VoteType::NotarizeFallback => { - Vote::new_notarization_fallback_vote(slot, Hash::default()) - } + VoteType::Notarize => Vote::new_notarization_vote(Block { + slot, + block_id: Hash::default(), + }), + VoteType::NotarizeFallback => Vote::new_notarization_fallback_vote(Block { + slot, + block_id: Hash::default(), + }), VoteType::Skip => Vote::new_skip_vote(slot), VoteType::SkipFallback => Vote::new_skip_fallback_vote(slot), VoteType::Finalize => Vote::new_finalization_vote(slot), - VoteType::Genesis => Vote::new_genesis_vote(slot, Hash::default()), + VoteType::Genesis => Vote::new_genesis_vote(Block { + slot, + block_id: Hash::default(), + }), } } @@ -1734,14 +1881,14 @@ mod tests { pool.add_message( bank, &Pubkey::new_unique(), - dummy_vote_message(validators, &vote_1, 0), + dummy_vote_message(validators, pool.cluster_info.my_shred_version(), &vote_1, 0), &mut vec![], ) .unwrap(); pool.add_message( bank, &Pubkey::new_unique(), - dummy_vote_message(validators, &vote_2, 0), + dummy_vote_message(validators, pool.cluster_info.my_shred_version(), &vote_2, 0), &mut vec![], ) .unwrap_err(); @@ -1793,7 +1940,10 @@ mod tests { ) .unwrap(); let cert_2 = Certificate { - cert_type: CertificateType::FinalizeFast(2, Hash::new_unique()), + cert_type: CertificateType::FinalizeFast(Block { + slot: 2, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1825,14 +1975,22 @@ mod tests { .add_message( &new_bank, &Pubkey::new_unique(), - dummy_vote_message(&ctx.validators, &vote, 0), + dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + 0 + ), &mut vec![] ) .is_err() ); // Send a cert on slot 2, it should be rejected - let cert_type = CertificateType::Notarize(2, Hash::new_unique()); + let cert_type = CertificateType::Notarize(Block { + slot: 2, + block_id: Hash::new_unique(), + }); let cert = ConsensusMessage::Certificate(Certificate { cert_type, signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), @@ -1854,7 +2012,10 @@ mod tests { // Add notar-fallback cert on 3 and finalize cert on 4 let cert_3 = Certificate { - cert_type: CertificateType::NotarizeFallback(3, Hash::new_unique()), + cert_type: CertificateType::NotarizeFallback(Block { + slot: 3, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1869,13 +2030,16 @@ mod tests { let certs = ctx.pool.get_certs_for_standstill(); assert_eq!(certs.len(), 2); assert!(certs.iter().any(|cert| cert.cert_type.slot() == 3 - && matches!(cert.cert_type, CertificateType::NotarizeFallback(_, _)))); + && matches!(cert.cert_type, CertificateType::NotarizeFallback(_)))); assert!(certs.iter().any(|cert| cert.cert_type.slot() == 4 && matches!(cert.cert_type, CertificateType::Finalize(_)))); // Add Notarize cert on 5 let cert_5 = Certificate { - cert_type: CertificateType::Notarize(5, Hash::new_unique()), + cert_type: CertificateType::Notarize(Block { + slot: 5, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -1891,33 +2055,34 @@ mod tests { // Add FinalizeFast cert on 5 let cert_5 = Certificate { - cert_type: CertificateType::FinalizeFast(5, Hash::new_unique()), + cert_type: CertificateType::FinalizeFast(Block { + slot: 5, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; ctx.add_message(ConsensusMessage::Certificate(cert_5)); - // Should return only FinalizeFast cert on 5 + // Slot 5 is now the highest finalized slot, so standstill cert refresh only returns + // certificates for later slots. let certs = ctx.pool.get_certs_for_standstill(); - assert_eq!(certs.len(), 1); - assert!( - certs[0].cert_type.slot() == 5 - && matches!(certs[0].cert_type, CertificateType::FinalizeFast(_, _)) - ); + assert!(certs.is_empty()); // Now add Notarize cert on 6 let cert_6 = Certificate { - cert_type: CertificateType::Notarize(6, Hash::new_unique()), + cert_type: CertificateType::Notarize(Block { + slot: 6, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; ctx.add_message(ConsensusMessage::Certificate(cert_6)); - // Should return certs on 5 and 6 + // Should return certs after highest finalized slot 5. let certs = ctx.pool.get_certs_for_standstill(); - assert_eq!(certs.len(), 2); - assert!(certs.iter().any(|cert| cert.cert_type.slot() == 5 - && matches!(cert.cert_type, CertificateType::FinalizeFast(_, _)))); + assert_eq!(certs.len(), 1); assert!(certs.iter().any(|cert| cert.cert_type.slot() == 6 - && matches!(cert.cert_type, CertificateType::Notarize(_, _)))); + && matches!(cert.cert_type, CertificateType::Notarize(_)))); // Add another Finalize cert on 6 let cert_6_finalize = Certificate { @@ -1928,19 +2093,18 @@ mod tests { ctx.add_message(ConsensusMessage::Certificate(cert_6_finalize)); // Add a NotarizeFallback cert on 6 let cert_6_notarize_fallback = Certificate { - cert_type: CertificateType::NotarizeFallback(6, Hash::new_unique()), + cert_type: CertificateType::NotarizeFallback(Block { + slot: 6, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; ctx.add_message(ConsensusMessage::Certificate(cert_6_notarize_fallback)); - // This should not be returned because 6 is the current highest finalized slot - // only Notarize/Finalze/FinalizeFast should be returned + // Slot 6 is now the current highest finalized slot, so no slot-6 certs should + // be returned by the queued refresh path. let certs = ctx.pool.get_certs_for_standstill(); - assert_eq!(certs.len(), 2); - assert!(certs.iter().any(|cert| cert.cert_type.slot() == 6 - && matches!(cert.cert_type, CertificateType::Finalize(_)))); - assert!(certs.iter().any(|cert| cert.cert_type.slot() == 6 - && matches!(cert.cert_type, CertificateType::Notarize(_, _)))); + assert!(certs.is_empty()); // Add another skip on 7 let cert_7 = Certificate { @@ -1949,13 +2113,9 @@ mod tests { bitmap: dummy_bitmap(), }; ctx.add_message(ConsensusMessage::Certificate(cert_7)); - // Should return certs on 6 and 7 + // Should return certs after highest finalized slot 6. let certs = ctx.pool.get_certs_for_standstill(); - assert_eq!(certs.len(), 3); - assert!(certs.iter().any(|cert| cert.cert_type.slot() == 6 - && matches!(cert.cert_type, CertificateType::Finalize(_)))); - assert!(certs.iter().any(|cert| cert.cert_type.slot() == 6 - && matches!(cert.cert_type, CertificateType::Notarize(_, _)))); + assert_eq!(certs.len(), 1); assert!( certs.iter().any(|cert| cert.cert_type.slot() == 7 && matches!(cert.cert_type, CertificateType::Skip(_))) @@ -1969,19 +2129,19 @@ mod tests { }; ctx.add_message(ConsensusMessage::Certificate(cert_8_finalize)); let cert_8_notarize = Certificate { - cert_type: CertificateType::Notarize(8, Hash::new_unique()), + cert_type: CertificateType::Notarize(Block { + slot: 8, + block_id: Hash::new_unique(), + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; ctx.add_message(ConsensusMessage::Certificate(cert_8_notarize)); - // Should only return certs on 8 now + // Slot 8 is now the current highest finalized slot, so latest-finalization + // certs are refreshed from highest_finalized instead of this queue. let certs = ctx.pool.get_certs_for_standstill(); - assert_eq!(certs.len(), 2); - assert!(certs.iter().any(|cert| cert.cert_type.slot() == 8 - && matches!(cert.cert_type, CertificateType::Finalize(_)))); - assert!(certs.iter().any(|cert| cert.cert_type.slot() == 8 - && matches!(cert.cert_type, CertificateType::Notarize(_, _)))); + assert!(certs.is_empty()); } #[test] @@ -1994,7 +2154,10 @@ mod tests { let hash = Hash::new_unique(); for slot in 1..=3 { let cert = Certificate { - cert_type: CertificateType::Notarize(slot, hash), + cert_type: CertificateType::Notarize(Block { + slot, + block_id: hash, + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -2014,15 +2177,18 @@ mod tests { .iter() .any(|event| matches!(event, VotorEvent::ParentReady { slot: 4, - parent_block: (3, h) - } if h == &hash)) + parent_block + } if parent_block == &Block { slot: 3, block_id: hash })) ); events.clear(); // Also works if we add FinalizeFast for slot 4 to 7 for slot in 4..=7 { let cert = Certificate { - cert_type: CertificateType::FinalizeFast(slot, hash), + cert_type: CertificateType::FinalizeFast(Block { + slot, + block_id: hash, + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -2042,22 +2208,28 @@ mod tests { .iter() .any(|event| matches!(event, VotorEvent::ParentReady { slot: 8, - parent_block: (7, h) - } if h == &hash)) + parent_block + } if parent_block == &Block { slot: 7, block_id: hash })) ); events.clear(); // NotarizeFallback on slot 8 to 10 and FinalizeFast on slot 11 for slot in 8..=10 { let cert = Certificate { - cert_type: CertificateType::NotarizeFallback(slot, hash), + cert_type: CertificateType::NotarizeFallback(Block { + slot, + block_id: hash, + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; ctx.add_message(ConsensusMessage::Certificate(cert)); } let cert = Certificate { - cert_type: CertificateType::FinalizeFast(11, hash), + cert_type: CertificateType::FinalizeFast(Block { + slot: 11, + block_id: hash, + }), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: dummy_bitmap(), }; @@ -2077,8 +2249,8 @@ mod tests { .iter() .any(|event| matches!(event, VotorEvent::ParentReady { slot: 12, - parent_block: (11, h) - } if h == &hash)) + parent_block + } if parent_block == &Block { slot: 11, block_id: hash })) ); } @@ -2086,9 +2258,17 @@ mod tests { fn test_vote_message_signature_verification() { let ctx = TestContext::new(); let rank_to_test = 3; - let vote = Vote::new_notarization_vote(42, Hash::new_unique()); + let vote = Vote::new_notarization_vote(Block { + slot: 42, + block_id: Hash::new_unique(), + }); - let consensus_message = dummy_vote_message(&ctx.validators, &vote, rank_to_test); + let consensus_message = dummy_vote_message( + &ctx.validators, + ctx.pool.cluster_info.my_shred_version(), + &vote, + rank_to_test, + ); let ConsensusMessage::Vote(vote_message) = consensus_message else { panic!("Expected Vote message") }; @@ -2098,10 +2278,10 @@ mod tests { BLSKeypair::derive_from_signer(validator_vote_keypair, BLS_KEYPAIR_DERIVE_SEED) .unwrap(); - let signed_message = bincode::serialize(&vote).unwrap(); + let payload = get_vote_payload_to_sign(vote, ctx.pool.cluster_info.my_shred_version()); vote_message .signature - .verify(&bls_keypair.public, &signed_message) + .verify(&bls_keypair.public, &payload) .expect("vote message signature should verify"); } @@ -2110,7 +2290,10 @@ mod tests { let mut ctx = TestContext::new(); let slot = ctx.bank_forks.read().unwrap().root_bank().slot() + 1; let hash = Hash::default(); - let cert_type = CertificateType::NotarizeFallback(slot, hash); + let cert_type = CertificateType::NotarizeFallback(Block { + slot, + block_id: hash, + }); let cert = Certificate { cert_type, signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), diff --git a/votor/src/consensus_pool/certificate_builder.rs b/votor/src/consensus_pool/certificate_builder.rs index 993bd9532b9..84f1caa2985 100644 --- a/votor/src/consensus_pool/certificate_builder.rs +++ b/votor/src/consensus_pool/certificate_builder.rs @@ -1,5 +1,8 @@ use { - agave_votor_messages::consensus_message::{Certificate, CertificateType, VoteMessage}, + agave_votor_messages::{ + certificate::{Certificate, CertificateType}, + consensus_message::VoteMessage, + }, bitvec::prelude::*, solana_bls_signatures::{BlsError, SignatureProjective}, solana_signer_store::{EncodeError, encode_base2, encode_base3}, @@ -13,7 +16,7 @@ use { /// conservative number 4096 for now. During build() we will cut off end /// of the bitmaps if the tail contains only zeroes, so actual bitmap /// length will be less than or equal to this number. -const MAXIMUM_VALIDATORS: usize = 4096; +pub const MAXIMUM_VALIDATORS: usize = 4096; /// Different types of errors that can be returned from the [`CertificateBuilder::aggregate()`] function. #[derive(Debug, Error, PartialEq, Eq)] @@ -95,20 +98,9 @@ enum BuilderType { signature: SignatureProjective, bitmap: BitVec, }, - /// A [`Certificate`] of type Skip will be produced. - /// - /// It can require two types of [`VoteMessage`]s. - /// In order to be able to produce certificates for reward purposes, signature aggregates for the two types are tracked separately. - Skip { - signature0: SignatureProjective, - bitmap0: BitVec, - sig_and_bitmap1: Option<(SignatureProjective, BitVec)>, - }, - /// A [`Certificate`] of type NotarFallback will be produced. - /// - /// It can require two types of [`VoteMessage`]s. - /// This certificate is not used for rewards so its signature can be aggregated in a single container. - NotarFallback { + + /// The produced [`Certificate`] will require two types of [`VoteMessage`]s. + DoubleVote { signature: SignatureProjective, bitmap0: BitVec, bitmap1: Option>, @@ -119,12 +111,7 @@ impl BuilderType { /// Creates a new instance of [`BuilderType`]. fn new(cert_type: &CertificateType) -> Self { match cert_type { - CertificateType::Skip(_) => Self::Skip { - signature0: SignatureProjective::identity(), - bitmap0: default_bitvec(), - sig_and_bitmap1: None, - }, - CertificateType::NotarizeFallback(_, _) => Self::NotarFallback { + CertificateType::Skip(_) | CertificateType::NotarizeFallback(_) => Self::DoubleVote { signature: SignatureProjective::identity(), bitmap0: default_bitvec(), bitmap1: None, @@ -144,39 +131,7 @@ impl BuilderType { ) -> Result<(), AggregateError> { let vote_types = cert_type.limits_and_vote_types().1; match self { - Self::Skip { - signature0, - bitmap0, - sig_and_bitmap1, - } => { - assert_eq!(vote_types.len(), 2); - for msg in msgs { - let vote_type = msg.vote.get_type(); - if vote_type == vote_types[0] { - try_set_bitmap(bitmap0, msg.rank)?; - } else { - assert_eq!(vote_type, vote_types[1]); - let (_, bitmap) = sig_and_bitmap1.get_or_insert_with(|| { - (SignatureProjective::identity(), default_bitvec()) - }); - try_set_bitmap(bitmap, msg.rank)?; - } - } - signature0.aggregate_with(msgs.iter().filter_map(|msg| { - (msg.vote.get_type() == vote_types[0]).then_some(&msg.signature) - }))?; - sig_and_bitmap1 - .as_mut() - .map(|(signature, _)| { - signature.aggregate_with(msgs.iter().filter_map(|msg| { - (msg.vote.get_type() == vote_types[1]).then_some(&msg.signature) - })) - }) - .unwrap_or(Ok(()))?; - Ok(()) - } - - Self::NotarFallback { + Self::DoubleVote { signature, bitmap0, bitmap1, @@ -212,20 +167,7 @@ impl BuilderType { Self::SingleVote { signature, bitmap } => { build_cert_from_bitmap(cert_type, signature, bitmap).map_err(BuildError::Encode) } - Self::Skip { - mut signature0, - bitmap0, - sig_and_bitmap1, - } => match sig_and_bitmap1 { - None => build_cert_from_bitmap(cert_type, signature0, bitmap0) - .map_err(BuildError::Encode), - Some((signature1, bitmap1)) => { - signature0.aggregate_with([signature1].iter())?; - build_cert_from_bitmaps(cert_type, signature0, bitmap0, bitmap1) - .map_err(BuildError::Encode) - } - }, - Self::NotarFallback { + Self::DoubleVote { signature, bitmap0, bitmap1, @@ -271,9 +213,12 @@ mod tests { use { super::*, agave_votor_messages::{ - consensus_message::{CertificateType, VoteMessage}, + certificate::CertificateType, + consensus_message::{Block, VoteMessage}, vote::Vote, + wire::get_vote_payload_to_sign, }, + rand::Rng, solana_bls_signatures::{ BLS_SIGNATURE_AFFINE_SIZE, Keypair as BLSKeypair, PreparedHashedMessage, PubkeyProjective as BLSPubkeyProjective, Signature as BLSSignature, @@ -285,12 +230,15 @@ mod tests { #[test] fn test_normal_build() { - let hash = Hash::new_unique(); - let cert_type = CertificateType::NotarizeFallback(1, hash); + let block = Block { + slot: 1, + block_id: Hash::new_unique(), + }; + let cert_type = CertificateType::NotarizeFallback(block); let mut builder = CertificateBuilder::new(cert_type); // Test building the certificate from Notarize and NotarizeFallback votes // Create Notarize on validator 1, 4, 6 - let vote = Vote::new_notarization_vote(1, hash); + let vote = Vote::new_notarization_vote(block); let rank_1 = [1, 4, 6]; let messages_1 = rank_1 .iter() @@ -308,7 +256,7 @@ mod tests { .aggregate(&messages_1) .expect("Failed to aggregate notarization votes"); // Create NotarizeFallback on validator 2, 3, 5, 7 - let vote = Vote::new_notarization_fallback_vote(1, hash); + let vote = Vote::new_notarization_fallback_vote(block); let rank_2 = [2, 3, 5, 7]; let messages_2 = rank_2 .iter() @@ -391,13 +339,16 @@ mod tests { #[test] fn test_builder_with_errors() { - let hash = Hash::new_unique(); - let cert_type = CertificateType::NotarizeFallback(1, hash); + let block = Block { + slot: 1, + block_id: Hash::new_unique(), + }; + let cert_type = CertificateType::NotarizeFallback(block); let mut builder = CertificateBuilder::new(cert_type); // Test with a rank that exceeds the maximum allowed - let vote = Vote::new_notarization_vote(1, hash); - let vote2 = Vote::new_notarization_fallback_vote(1, hash); + let vote = Vote::new_notarization_vote(block); + let vote2 = Vote::new_notarization_fallback_vote(block); let rank_out_of_bounds = MAXIMUM_VALIDATORS.saturating_add(1); // Exceeds MAXIMUM_VALIDATORS let keypair = BLSKeypair::new(); let signature = keypair.sign(b"fake_vote_message"); @@ -450,17 +401,20 @@ mod tests { #[test] fn test_certificate_verification_base2_encoding() { - let slot = 10; - let hash = Hash::new_unique(); - let cert_type = CertificateType::Notarize(slot, hash); + let block = Block { + slot: 10, + block_id: Hash::new_unique(), + }; + let shred_version = rand::rng().random(); + let cert_type = CertificateType::Notarize(block); // 1. Setup: Create keypairs and a single vote object. // All validators will sign the same message, resulting in a single bitmap. let num_validators = 5; let mut keypairs = Vec::new(); let mut vote_messages = Vec::new(); - let vote = Vote::new_notarization_vote(slot, hash); - let serialized_vote = bincode::serialize(&vote).unwrap(); + let vote = Vote::new_notarization_vote(block); + let serialized_vote = get_vote_payload_to_sign(vote, shred_version); for i in 0..num_validators { let keypair = BLSKeypair::new(); @@ -491,18 +445,21 @@ mod tests { #[test] fn test_certificate_verification_base3_encoding() { - let slot = 20; - let hash = Hash::new_unique(); + let block = Block { + slot: 20, + block_id: Hash::new_unique(), + }; // A NotarizeFallback certificate can be composed of both Notarize and NotarizeFallback // votes. - let cert_type = CertificateType::NotarizeFallback(slot, hash); + let cert_type = CertificateType::NotarizeFallback(block); + let shred_version = rand::rng().random(); // 1. Setup: Create two groups of validators signing two different vote types. let mut all_vote_messages = Vec::new(); let mut all_pubkeys = Vec::new(); // Group 1: Signs a Notarize vote. - let notarize_vote = Vote::new_notarization_vote(slot, hash); - let serialized_notarize_vote = bincode::serialize(¬arize_vote).unwrap(); + let notarize_vote = Vote::new_notarization_vote(block); + let serialized_notarize_vote = get_vote_payload_to_sign(notarize_vote, shred_version); for i in 0..3 { let keypair = BLSKeypair::new(); let signature = keypair.sign(&serialized_notarize_vote); @@ -515,8 +472,9 @@ mod tests { } // Group 2: Signs a NotarizeFallback vote. - let notarize_fallback_vote = Vote::new_notarization_fallback_vote(slot, hash); - let serialized_fallback_vote = bincode::serialize(¬arize_fallback_vote).unwrap(); + let notarize_fallback_vote = Vote::new_notarization_fallback_vote(block); + let serialized_fallback_vote = + get_vote_payload_to_sign(notarize_fallback_vote, shred_version); for i in 3..6 { let keypair = BLSKeypair::new(); let signature = keypair.sign(&serialized_fallback_vote); diff --git a/votor/src/consensus_pool/parent_ready_tracker.rs b/votor/src/consensus_pool/parent_ready_tracker.rs index a5221512912..d272131bd72 100644 --- a/votor/src/consensus_pool/parent_ready_tracker.rs +++ b/votor/src/consensus_pool/parent_ready_tracker.rs @@ -16,11 +16,14 @@ use { crate::{common::MAX_NOTAR_FALLBACK_BLOCKS, event::VotorEvent}, agave_votor_messages::consensus_message::Block, core::fmt, - solana_clock::{NUM_CONSECUTIVE_LEADER_SLOTS, Slot}, + solana_clock::Slot, solana_gossip::cluster_info::ClusterInfo, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, std::{collections::HashMap, sync::Arc}, }; +pub(crate) type ParentReady = (Slot, Block); + #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(crate) enum BlockProductionParent { MissedWindow, @@ -62,44 +65,63 @@ struct ParentReadyStatus { } impl ParentReadyTracker { - /// Creates a new tracker with the root bank as implicitely notarized fallback - pub(super) fn new(cluster_info: Arc, root_block @ (root_slot, _): Block) -> Self { + /// Creates a new tracker with the selected startup parent-ready state. + pub(super) fn new( + cluster_info: Arc, + root: Slot, + (slot, parent_block): ParentReady, + ) -> Self { + debug_assert!(parent_block.slot < slot); let mut slot_statuses = HashMap::new(); + // Parent block is notarize fallback slot_statuses.insert( - root_slot, + parent_block.slot, ParentReadyStatus { skip: false, - notar_fallbacks: vec![root_block], + notar_fallbacks: vec![parent_block], parents_ready: vec![], }, ); + // Intermediate blocks have skips + for s in (parent_block.slot.checked_add(1).unwrap())..slot { + slot_statuses.insert( + s, + ParentReadyStatus { + skip: true, + notar_fallbacks: vec![], + parents_ready: vec![parent_block], + }, + ); + } + // Initial slot is parent ready slot_statuses.insert( - root_slot.saturating_add(1), + slot, ParentReadyStatus { skip: false, notar_fallbacks: vec![], - parents_ready: vec![root_block], + parents_ready: vec![parent_block], }, ); + Self { cluster_info: DebugIgnore(cluster_info), slot_statuses, - root: root_slot, - highest_with_parent_ready: root_slot.saturating_add(1), + root: parent_block.slot.min(root), + highest_with_parent_ready: slot.max(root), } } /// Adds a new notarize fallback certificate, we can use Notarize/NotarizeFallback/FastFinalize pub(super) fn add_new_notar_fallback_or_stronger( &mut self, - block @ (slot, _): Block, + block: Block, events: &mut Vec, ) { - if slot <= self.root { + if block.slot <= self.root { return; } - let status = self.slot_statuses.entry(slot).or_default(); + let status = self.slot_statuses.entry(block.slot).or_default(); if status.notar_fallbacks.contains(&block) { return; } @@ -111,7 +133,7 @@ impl ParentReadyTracker { assert!(status.notar_fallbacks.len() <= MAX_NOTAR_FALLBACK_BLOCKS); // Add this block as valid parent to skip connected future blocks - for s in slot.saturating_add(1).. { + for s in block.slot.saturating_add(1).. { trace!( "{}: Adding new parent ready for {s} parent {block:?}", self.cluster_info.0.id() @@ -121,7 +143,7 @@ impl ParentReadyTracker { status.parents_ready.push(block); // Only notify for parent ready on first leader slots - if s % NUM_CONSECUTIVE_LEADER_SLOTS == 0 { + if s.is_multiple_of(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot) { events.push(VotorEvent::ParentReady { slot: s, parent_block: block, @@ -192,7 +214,7 @@ impl ParentReadyTracker { } status.parents_ready.push(block); // Only notify for parent ready on first leader slots - if s % NUM_CONSECUTIVE_LEADER_SLOTS == 0 { + if s.is_multiple_of(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot) { events.push(VotorEvent::ParentReady { slot: s, parent_block: block, @@ -230,6 +252,15 @@ impl ParentReadyTracker { } } + /// Returns whether the block is notarized or notarized-fallback + /// + /// This includes any special cases like: genesis, snapshot root, migration Genesis cert + pub(super) fn has_notar_fallback_or_stronger(&self, block: Block) -> bool { + self.slot_statuses + .get(&block.slot) + .is_some_and(|ss| ss.notar_fallbacks.contains(&block)) + } + fn highest_parent_ready(&self) -> Slot { self.highest_with_parent_ready } @@ -243,19 +274,30 @@ impl ParentReadyTracker { #[cfg(test)] mod tests { use { - super::*, crate::tests::get_cluster_info, itertools::Itertools, - solana_clock::NUM_CONSECUTIVE_LEADER_SLOTS, solana_hash::Hash, solana_keypair::Keypair, + super::*, crate::tests::get_cluster_info, itertools::Itertools, solana_hash::Hash, + solana_keypair::Keypair, }; + fn root_parent_ready(root_block: Block) -> ParentReady { + (root_block.slot.saturating_add(1), root_block) + } + + fn new_tracker(cluster_info: Arc, root_block: Block) -> ParentReadyTracker { + ParentReadyTracker::new(cluster_info, root_block.slot, root_parent_ready(root_block)) + } + #[test] fn basic() { let cluster_info = get_cluster_info(Keypair::new()); let genesis = Block::default(); - let mut tracker = ParentReadyTracker::new(cluster_info, genesis); + let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; - for i in 1..2 * NUM_CONSECUTIVE_LEADER_SLOTS { - let block = (i, Hash::new_unique()); + for i in 1..2 * NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot { + let block = Block { + slot: i, + block_id: Hash::new_unique(), + }; tracker.add_new_notar_fallback_or_stronger(block, &mut events); assert_eq!(tracker.highest_parent_ready(), i + 1); assert!(tracker.parent_ready(i + 1, block)); @@ -266,9 +308,12 @@ mod tests { fn skips() { let cluster_info = get_cluster_info(Keypair::new()); let genesis = Block::default(); - let mut tracker = ParentReadyTracker::new(cluster_info, genesis); + let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; - let block = (1, Hash::new_unique()); + let block = Block { + slot: 1, + block_id: Hash::new_unique(), + }; tracker.add_new_notar_fallback_or_stronger(block, &mut events); tracker.add_new_skip(1, &mut events); @@ -284,9 +329,12 @@ mod tests { fn out_of_order() { let cluster_info = get_cluster_info(Keypair::new()); let genesis = Block::default(); - let mut tracker = ParentReadyTracker::new(cluster_info, genesis); + let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; - let block = (1, Hash::new_unique()); + let block = Block { + slot: 1, + block_id: Hash::new_unique(), + }; tracker.add_new_skip(3, &mut events); tracker.add_new_skip(2, &mut events); @@ -304,8 +352,11 @@ mod tests { fn snapshot_wfsm() { let cluster_info = get_cluster_info(Keypair::new()); let root_slot = 2147; - let root_block = (root_slot, Hash::new_unique()); - let mut tracker = ParentReadyTracker::new(cluster_info, root_block); + let root_block = Block { + slot: root_slot, + block_id: Hash::new_unique(), + }; + let mut tracker = new_tracker(cluster_info, root_block); let mut events = vec![]; assert!(tracker.parent_ready(root_slot + 1, root_block)); @@ -322,18 +373,39 @@ mod tests { assert!(tracker.parent_ready(root_slot + 3, root_block)); assert_eq!(tracker.highest_parent_ready(), root_slot + 3); - let block = (root_slot + 4, Hash::new_unique()); + let block = Block { + slot: root_slot + 4, + block_id: Hash::new_unique(), + }; tracker.add_new_notar_fallback_or_stronger(block, &mut events); assert!(tracker.parent_ready(root_slot + 3, root_block)); assert!(tracker.parent_ready(root_slot + 5, block)); assert_eq!(tracker.highest_parent_ready(), root_slot + 5); } + #[test] + fn restored_parent_ready() { + let cluster_info = get_cluster_info(Keypair::new()); + let root_slot = 4; + let restored = Block { + slot: 13, + block_id: Hash::new_unique(), + }; + let tracker = ParentReadyTracker::new(cluster_info, root_slot, (16, restored)); + + assert!(tracker.parent_ready(16, restored)); + assert_eq!(tracker.highest_parent_ready(), 16); + assert_eq!( + tracker.block_production_parent(16), + BlockProductionParent::Parent(restored) + ); + } + #[test] fn highest_parent_ready_out_of_order() { let cluster_info = get_cluster_info(Keypair::new()); let genesis = Block::default(); - let mut tracker = ParentReadyTracker::new(cluster_info, genesis); + let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; assert_eq!(tracker.highest_parent_ready(), 1); @@ -356,7 +428,7 @@ mod tests { fn missed_window() { let cluster_info = get_cluster_info(Keypair::new()); let genesis = Block::default(); - let mut tracker = ParentReadyTracker::new(cluster_info, genesis); + let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; assert_eq!(tracker.highest_parent_ready(), 1); assert_eq!( @@ -364,7 +436,13 @@ mod tests { BlockProductionParent::ParentNotReady ); - tracker.add_new_notar_fallback_or_stronger((4, Hash::new_unique()), &mut events); + tracker.add_new_notar_fallback_or_stronger( + Block { + slot: 4, + block_id: Hash::new_unique(), + }, + &mut events, + ); assert_eq!(tracker.highest_parent_ready(), 5); assert_eq!( tracker.block_production_parent(4), @@ -375,7 +453,13 @@ mod tests { tracker.block_production_parent(8), BlockProductionParent::ParentNotReady ); - tracker.add_new_notar_fallback_or_stronger((64, Hash::new_unique()), &mut events); + tracker.add_new_notar_fallback_or_stronger( + Block { + slot: 64, + block_id: Hash::new_unique(), + }, + &mut events, + ); assert_eq!(tracker.highest_parent_ready(), 65); assert_eq!( tracker.block_production_parent(8), @@ -387,12 +471,18 @@ mod tests { fn pick_more_skips() { let cluster_info = get_cluster_info(Keypair::new()); let genesis = Block::default(); - let mut tracker = ParentReadyTracker::new(cluster_info, genesis); + let mut tracker = new_tracker(cluster_info, genesis); let mut events = vec![]; for i in 1..=10 { tracker.add_new_skip(i, &mut vec![]); - tracker.add_new_notar_fallback_or_stronger((i, Hash::new_unique()), &mut vec![]); + tracker.add_new_notar_fallback_or_stronger( + Block { + slot: i, + block_id: Hash::new_unique(), + }, + &mut vec![], + ); } tracker.add_new_skip(11, &mut events); @@ -403,7 +493,7 @@ mod tests { .map(|event| match event { VotorEvent::ParentReady { slot, parent_block } => { assert!(slot == 12); - parent_block.0 + parent_block.slot } _ => panic!("Invalid event"), }) diff --git a/votor/src/consensus_pool/slot_stake_counters.rs b/votor/src/consensus_pool/slot_stake_counters.rs index ab8ba56669e..67331e107a0 100644 --- a/votor/src/consensus_pool/slot_stake_counters.rs +++ b/votor/src/consensus_pool/slot_stake_counters.rs @@ -8,15 +8,16 @@ use { event::VotorEvent, }, agave_votor_messages::{consensus_message::Block, fraction::Fraction, vote::Vote}, - solana_clock::NUM_CONSECUTIVE_LEADER_SLOTS, + solana_clock::Slot, solana_hash::Hash, - std::{collections::BTreeMap, num::NonZeroU64}, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, + std::{collections::BTreeMap, num::NonZero}, }; -#[derive(Debug, Default)] +#[derive(Debug)] pub(crate) struct SlotStakeCounters { my_first_vote: Option, - total_stake: Stake, + total_stake: NonZero, skip_total: Stake, notarize_total: Stake, notarize_entry_total: BTreeMap, @@ -26,10 +27,16 @@ pub(crate) struct SlotStakeCounters { } impl SlotStakeCounters { - pub fn new(total_stake: Stake) -> Self { + pub fn new(total_stake: NonZero) -> Self { Self { + my_first_vote: None, total_stake, - ..Default::default() + skip_total: 0, + notarize_total: 0, + notarize_entry_total: BTreeMap::new(), + top_notarized_stake: 0, + safe_to_notar_sent: vec![], + safe_to_skip_sent: false, } } @@ -53,7 +60,7 @@ impl SlotStakeCounters { Vote::Notarize(vote) => { let old_entry_stake = self .notarize_entry_total - .insert(vote.block_id, entry_stake) + .insert(vote.block.block_id, entry_stake) .unwrap_or(0); self.notarize_total = self .notarize_total @@ -72,7 +79,7 @@ impl SlotStakeCounters { } let slot = vote.slot(); let is_first_in_leader_window = - slot.is_multiple_of(NUM_CONSECUTIVE_LEADER_SLOTS) || slot == 1; + slot.is_multiple_of(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot) || slot == 1; // Check safe to notar for (block_id, stake) in &self.notarize_entry_total { @@ -82,11 +89,17 @@ impl SlotStakeCounters { if is_first_in_leader_window { // First block in leader window - emit event immediately - events.push(VotorEvent::SafeToNotar((slot, *block_id))); + events.push(VotorEvent::SafeToNotar(Block { + slot, + block_id: *block_id, + })); stats.event_safe_to_notarize = stats.event_safe_to_notarize.saturating_add(1); } else { // Intrawindow block - add to pending for later processing - pending_safe_to_notar.push((slot, *block_id)); + pending_safe_to_notar.push(Block { + slot, + block_id: *block_id, + }); } } } @@ -102,21 +115,22 @@ impl SlotStakeCounters { // White paper v1.1 page 22: The event is only issued if the node voted in slot s already, // but not to notarize b. Moreover: // notar(b) >= 40% or (skip(s) + notar(b) >= 60% and notar(b) >= 20%) - if let Some(Vote::Notarize(my_vote)) = self.my_first_vote.as_ref() { - if &my_vote.block_id == block_id { - return false; // I voted for the same block, no need to send NotarizeFallback - } + if let Some(Vote::Notarize(my_vote)) = self.my_first_vote.as_ref() + && &my_vote.block.block_id == block_id + { + return false; // I voted for the same block, no need to send NotarizeFallback } trace!( "safe_to_notar {block_id:?} skip_ratio={} notarized_ratio={}", - self.skip_total as f64 / self.total_stake as f64, - *stake as f64 / self.total_stake as f64 + self.skip_total as f64 / self.total_stake.get() as f64, + *stake as f64 / self.total_stake.get() as f64 ); // Check if the block fits condition (i) 40% of stake holders voted notarize - let total_stake = NonZeroU64::new(self.total_stake).unwrap(); - let notarized_ratio = Fraction::new(*stake, total_stake); - let notarized_plus_skip_ratio = - Fraction::new(self.skip_total.checked_add(*stake).unwrap(), total_stake); + let notarized_ratio = Fraction::new(*stake, self.total_stake); + let notarized_plus_skip_ratio = Fraction::new( + self.skip_total.checked_add(*stake).unwrap(), + self.total_stake, + ); notarized_ratio >= SAFE_TO_NOTAR_MIN_NOTARIZE_ONLY // Check if the block fits condition (ii) 20% notarized, and 60% notarized or skip || (notarized_ratio >= SAFE_TO_NOTAR_MIN_NOTARIZE_FOR_NOTARIZE_OR_SKIP @@ -140,9 +154,8 @@ impl SlotStakeCounters { let num_stake = self .skip_total .saturating_add(self.notarize_total.saturating_sub(self.top_notarized_stake)); - let total_stake = NonZeroU64::new(self.total_stake).unwrap(); - Fraction::new(num_stake, total_stake) >= SAFE_TO_SKIP_THRESHOLD + Fraction::new(num_stake, self.total_stake) >= SAFE_TO_SKIP_THRESHOLD } else { false } @@ -155,7 +168,7 @@ mod tests { #[test] fn test_safe_to_notar_first_in_leader_window() { - let mut counters = SlotStakeCounters::new(100); + let mut counters = SlotStakeCounters::new(NonZero::new(100).unwrap()); let mut events = vec![]; let mut pending_safe_to_notar = vec![]; @@ -177,7 +190,10 @@ mod tests { // 40% of stake holders voted notarize counters.add_vote( - &Vote::new_notarization_vote(slot, Hash::default()), + &Vote::new_notarization_vote(Block { + slot, + block_id: Hash::default(), + }), 40, false, &mut events, @@ -186,16 +202,23 @@ mod tests { ); // First in leader window goes to events directly assert_eq!(events.len(), 1); - assert!( - matches!(events[0], VotorEvent::SafeToNotar((s, block_id)) if s == slot && block_id == Hash::default()) - ); + match &events[0] { + VotorEvent::SafeToNotar(block) => { + assert_eq!(block.slot, slot); + assert_eq!(block.block_id, Hash::default()); + } + rest => panic!("unexpected: {rest:?}"), + } assert!(pending_safe_to_notar.is_empty()); assert_eq!(stats.event_safe_to_notarize, 1); events.clear(); // Adding more notarizations does not trigger more events counters.add_vote( - &Vote::new_notarization_vote(slot, Hash::default()), + &Vote::new_notarization_vote(Block { + slot, + block_id: Hash::default(), + }), 20, false, &mut events, @@ -207,7 +230,7 @@ mod tests { assert_eq!(stats.event_safe_to_notarize, 1); // Reset counters with slot 4 (first in next leader window) - counters = SlotStakeCounters::new(100); + counters = SlotStakeCounters::new(NonZero::new(100).unwrap()); events.clear(); pending_safe_to_notar.clear(); stats = ConsensusPoolStats::default(); @@ -216,7 +239,10 @@ mod tests { // I voted for notarize b let hash_1 = Hash::new_unique(); counters.add_vote( - &Vote::new_notarization_vote(slot, hash_1), + &Vote::new_notarization_vote(Block { + slot, + block_id: hash_1, + }), 1, true, &mut events, @@ -230,7 +256,10 @@ mod tests { // 25% of stake holders voted notarize b' let hash_2 = Hash::new_unique(); counters.add_vote( - &Vote::new_notarization_vote(slot, hash_2), + &Vote::new_notarization_vote(Block { + slot, + block_id: hash_2, + }), 25, false, &mut events, @@ -251,16 +280,20 @@ mod tests { &mut stats, ); assert_eq!(events.len(), 1); - assert!( - matches!(events[0], VotorEvent::SafeToNotar((s, block_id)) if s == slot && block_id == hash_2) - ); + match &events[0] { + VotorEvent::SafeToNotar(block) => { + assert_eq!(block.slot, slot); + assert_eq!(block.block_id, hash_2); + } + rest => panic!("unexpected: {rest:?}"), + } assert!(pending_safe_to_notar.is_empty()); assert_eq!(stats.event_safe_to_notarize, 1); } #[test] fn test_safe_to_notar_intrawindow_block() { - let mut counters = SlotStakeCounters::new(100); + let mut counters = SlotStakeCounters::new(NonZero::new(100).unwrap()); let mut events = vec![]; let mut pending_safe_to_notar = vec![]; @@ -282,7 +315,7 @@ mod tests { // 40% of stake holders voted notarize let block_id = Hash::new_unique(); counters.add_vote( - &Vote::new_notarization_vote(slot, block_id), + &Vote::new_notarization_vote(Block { slot, block_id }), 40, false, &mut events, @@ -292,14 +325,14 @@ mod tests { // Intrawindow block goes to pending instead of events assert!(events.is_empty()); assert_eq!(pending_safe_to_notar.len(), 1); - assert_eq!(pending_safe_to_notar[0], (slot, block_id)); + assert_eq!(pending_safe_to_notar[0], Block { slot, block_id }); // Stats are not updated for pending assert_eq!(stats.event_safe_to_notarize, 0); } #[test] fn test_safe_to_skip() { - let mut counters = SlotStakeCounters::new(100); + let mut counters = SlotStakeCounters::new(NonZero::new(100).unwrap()); let mut events = vec![]; let mut pending_safe_to_notar = vec![]; @@ -307,7 +340,10 @@ mod tests { let slot = 2; // I voted for notarize b counters.add_vote( - &Vote::new_notarization_vote(slot, Hash::default()), + &Vote::new_notarization_vote(Block { + slot, + block_id: Hash::default(), + }), 10, true, &mut events, @@ -344,7 +380,7 @@ mod tests { assert_eq!(stats.event_safe_to_skip, 1); // Reset counters - counters = SlotStakeCounters::new(100); + counters = SlotStakeCounters::new(NonZero::new(100).unwrap()); events.clear(); pending_safe_to_notar.clear(); stats = ConsensusPoolStats::default(); @@ -352,7 +388,10 @@ mod tests { // I voted for notarize b, 10% of stake holders voted with me let hash_1 = Hash::new_unique(); counters.add_vote( - &Vote::new_notarization_vote(slot, hash_1), + &Vote::new_notarization_vote(Block { + slot, + block_id: hash_1, + }), 10, true, &mut events, @@ -362,7 +401,10 @@ mod tests { // 20% of stake holders voted a different notarization b' let hash_2 = Hash::new_unique(); counters.add_vote( - &Vote::new_notarization_vote(slot, hash_2), + &Vote::new_notarization_vote(Block { + slot, + block_id: hash_2, + }), 20, false, &mut events, @@ -385,7 +427,10 @@ mod tests { // Adding more notarization on b does not trigger more events counters.add_vote( - &Vote::new_notarization_vote(slot, hash_1), + &Vote::new_notarization_vote(Block { + slot, + block_id: hash_1, + }), 10, false, &mut events, diff --git a/votor/src/consensus_pool/stats.rs b/votor/src/consensus_pool/stats.rs index 610b8bd4310..cacd8d6eb45 100644 --- a/votor/src/consensus_pool/stats.rs +++ b/votor/src/consensus_pool/stats.rs @@ -1,5 +1,5 @@ use { - agave_votor_messages::{consensus_message::CertificateType, vote::VoteType}, + agave_votor_messages::{certificate::CertificateType, vote::VoteType}, solana_metrics::datapoint_info, std::time::{Duration, Instant}, }; @@ -21,15 +21,15 @@ impl CertificateStats { fn increment(&mut self, cert_type: &CertificateType) { match cert_type { CertificateType::Finalize(_) => self.finalize = self.finalize.saturating_add(1), - CertificateType::FinalizeFast(_, _) => { + CertificateType::FinalizeFast(_) => { self.finalize_fast = self.finalize_fast.saturating_add(1) } - CertificateType::Notarize(_, _) => self.notarize = self.notarize.saturating_add(1), - CertificateType::NotarizeFallback(_, _) => { + CertificateType::Notarize(_) => self.notarize = self.notarize.saturating_add(1), + CertificateType::NotarizeFallback(_) => { self.notarize_fallback = self.notarize_fallback.saturating_add(1) } CertificateType::Skip(_) => self.skip = self.skip.saturating_add(1), - CertificateType::Genesis(_, _) => self.genesis = self.genesis.saturating_add(1), + CertificateType::Genesis(_) => self.genesis = self.genesis.saturating_add(1), } } diff --git a/votor/src/consensus_pool/vote_pool.rs b/votor/src/consensus_pool/vote_pool.rs index 1a870f62a75..c663a573c82 100644 --- a/votor/src/consensus_pool/vote_pool.rs +++ b/votor/src/consensus_pool/vote_pool.rs @@ -3,7 +3,10 @@ use { agave_votor_messages::consensus_message::VoteMessage, solana_hash::Hash, solana_pubkey::Pubkey, - std::collections::{BTreeMap, BTreeSet}, + std::{ + collections::{BTreeMap, BTreeSet}, + num::NonZero, + }, }; /// There are two types of vote pools: @@ -27,14 +30,14 @@ impl SimpleVotePool { pub(super) fn add_vote( &mut self, validator_vote_key: Pubkey, - validator_stake: Stake, + validator_stake: NonZero, vote: VoteMessage, ) -> Option { if !self.prev_voted_validators.insert(validator_vote_key) { return None; } self.votes.push(vote); - self.total_stake = self.total_stake.saturating_add(validator_stake); + self.total_stake = self.total_stake.saturating_add(validator_stake.get()); Some(self.total_stake) } @@ -75,7 +78,7 @@ impl DuplicateBlockVotePool { pub(super) fn add_vote( &mut self, validator_vote_key: Pubkey, - validator_stake: Stake, + validator_stake: NonZero, vote: VoteMessage, ) -> Option { let block_id = *vote.vote.block_id().unwrap(); @@ -96,7 +99,7 @@ impl DuplicateBlockVotePool { vote_entry.votes.push(vote); vote_entry.total_stake_by_key = vote_entry .total_stake_by_key - .saturating_add(validator_stake); + .saturating_add(validator_stake.get()); Some(vote_entry.total_stake_by_key) } @@ -131,7 +134,10 @@ impl DuplicateBlockVotePool { mod test { use { super::*, - agave_votor_messages::{consensus_message::VoteMessage, vote::Vote}, + agave_votor_messages::{ + consensus_message::{Block, VoteMessage}, + vote::Vote, + }, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, }; @@ -146,16 +152,25 @@ mod test { }; let my_pubkey = Pubkey::new_unique(); - assert_eq!(vote_pool.add_vote(my_pubkey, 10, vote_message), Some(10)); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote_message.clone()), + Some(10) + ); assert_eq!(vote_pool.total_stake(), 10); // Adding the same key again should fail - assert_eq!(vote_pool.add_vote(my_pubkey, 10, vote_message), None); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote_message.clone()), + None + ); assert_eq!(vote_pool.total_stake(), 10); // Adding a different key should succeed let new_pubkey = Pubkey::new_unique(); - assert_eq!(vote_pool.add_vote(new_pubkey, 60, vote_message), Some(70)); + assert_eq!( + vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), vote_message), + Some(70) + ); assert_eq!(vote_pool.total_stake(), 70); } @@ -164,24 +179,36 @@ mod test { let mut vote_pool = DuplicateBlockVotePool::new(1); let my_pubkey = Pubkey::new_unique(); let block_id = Hash::new_unique(); - let vote = Vote::new_notarization_vote(3, block_id); + let vote = Vote::new_notarization_vote(Block { slot: 3, block_id }); let vote = VoteMessage { vote, signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), rank: 1, }; - assert_eq!(vote_pool.add_vote(my_pubkey, 10, vote), Some(10)); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote.clone()), + Some(10) + ); assert_eq!(vote_pool.total_stake_by_block_id(&block_id), 10); // Adding the same key again should fail - assert_eq!(vote_pool.add_vote(my_pubkey, 10, vote), None); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote.clone()), + None + ); // Adding a different bankhash should fail - assert_eq!(vote_pool.add_vote(my_pubkey, 10, vote), None); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote.clone()), + None + ); // Adding a different key should succeed let new_pubkey = Pubkey::new_unique(); - assert_eq!(vote_pool.add_vote(new_pubkey, 60, vote), Some(70)); + assert_eq!( + vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), vote), + Some(70) + ); assert_eq!(vote_pool.total_stake_by_block_id(&block_id), 70); } @@ -193,7 +220,10 @@ mod test { let votes = (0..4) .map(|_| { - let vote = Vote::new_notarization_fallback_vote(7, Hash::new_unique()); + let vote = Vote::new_notarization_fallback_vote(Block { + slot: 7, + block_id: Hash::new_unique(), + }); VoteMessage { vote, signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), @@ -204,14 +234,20 @@ mod test { // Adding the first 3 votes should succeed, but total_stake should remain at 10 for vote in votes.iter().take(3).cloned() { - assert_eq!(vote_pool.add_vote(my_pubkey, 10, vote), Some(10)); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), vote.clone()), + Some(10) + ); assert_eq!( vote_pool.total_stake_by_block_id(vote.vote.block_id().unwrap()), 10 ); } // Adding the 4th vote should fail - assert_eq!(vote_pool.add_vote(my_pubkey, 10, votes[3]), None); + assert_eq!( + vote_pool.add_vote(my_pubkey, NonZero::new(10).unwrap(), votes[3].clone()), + None + ); assert_eq!( vote_pool.total_stake_by_block_id(votes[3].vote.block_id().unwrap()), 0 @@ -220,7 +256,10 @@ mod test { // Adding a different key should succeed let new_pubkey = Pubkey::new_unique(); for vote in votes.iter().skip(1).take(2).cloned() { - assert_eq!(vote_pool.add_vote(new_pubkey, 60, vote), Some(70)); + assert_eq!( + vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), vote.clone()), + Some(70) + ); assert_eq!( vote_pool.total_stake_by_block_id(vote.vote.block_id().unwrap()), 70 @@ -228,13 +267,19 @@ mod test { } // The new key only added 2 votes, so adding block_ids[3] should succeed - assert_eq!(vote_pool.add_vote(new_pubkey, 60, votes[3]), Some(60)); + assert_eq!( + vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), votes[3].clone()), + Some(60) + ); assert_eq!( vote_pool.total_stake_by_block_id(votes[3].vote.block_id().unwrap()), 60 ); // Now if adding the same key again, it should fail - assert_eq!(vote_pool.add_vote(new_pubkey, 60, votes[0]), None); + assert_eq!( + vote_pool.add_vote(new_pubkey, NonZero::new(60).unwrap(), votes[0].clone()), + None + ); } } diff --git a/votor/src/consensus_pool_service.rs b/votor/src/consensus_pool_service.rs index 7c35c442f0b..58748f52621 100644 --- a/votor/src/consensus_pool_service.rs +++ b/votor/src/consensus_pool_service.rs @@ -1,24 +1,31 @@ -//! Service in charge of ingesting new messages into the certificate pool -//! and notifying votor of new events that occur +//! Runs the ingest side of the votor consensus pool. +//! +//! - verified vote and certificate batches from sigverify are sent to the pool; +//! - new `VotorEvent`s are forwarded to the voting/event loop; +//! - newly constructed certificates and standstill refreshes are queued for broadcast; +//! - pending intrawindow `SafeToNotar` blocks are repaired and rechecked; mod stats; use { crate::{ - commitment::{CommitmentAggregationData, CommitmentType, update_commitment_cache}, common::DELTA_STANDSTILL, consensus_pool::{ - AddVoteError, ConsensusPool, parent_ready_tracker::BlockProductionParent, + AddVoteError, ConsensusPool, + parent_ready_tracker::{BlockProductionParent, ParentReady}, }, event::{LeaderWindowInfo, RepairEvent, RepairEventSender, VotorEvent, VotorEventSender}, - generated_cert_types::GeneratedCertTypes, voting_service::BLSOp, }, + agave_bls_sigverify::{ + generated_cert_types::GeneratedCertTypes, sig_verified_messages::SigVerifiedBatch, + }, agave_votor_messages::{ - consensus_message::{Block, Certificate, ConsensusMessage}, + certificate::Certificate, + consensus_message::{Block, ConsensusMessage}, migration::MigrationStatus, }, - crossbeam_channel::{Receiver, Sender, TrySendError, select}, + crossbeam_channel::{Receiver, Sender, TrySendError, select_biased}, solana_clock::Slot, solana_gossip::cluster_info::ClusterInfo, solana_ledger::{blockstore::Blockstore, leader_schedule_cache::LeaderScheduleCache}, @@ -40,7 +47,7 @@ use { }, }; -/// Inputs for the certificate pool thread +/// Inputs for the consensus pool and consensus pool service pub(crate) struct ConsensusPoolContext { pub(crate) exit: Arc, pub(crate) migration_status: Arc, @@ -51,12 +58,13 @@ pub(crate) struct ConsensusPoolContext { pub(crate) blockstore: Arc, pub(crate) sharable_banks: SharableBanks, pub(crate) leader_schedule_cache: Arc, + pub(crate) vote_history_highest_parent_ready: Option<(Slot, Block)>, - pub(crate) consensus_message_receiver: Receiver>, + pub(crate) consensus_message_receiver: Receiver, + pub(crate) own_message_receiver: Receiver, pub(crate) bls_sender: Sender, pub(crate) event_sender: VotorEventSender, - pub(crate) commitment_sender: Sender, pub(crate) repair_event_sender: RepairEventSender, /// Used to communicate the highest finalization cert the pool has observed to the block creation loop. @@ -64,72 +72,131 @@ pub(crate) struct ConsensusPoolContext { } pub(crate) struct ConsensusPoolService { - t_ingest: JoinHandle<()>, + t_consensus_pool_service: JoinHandle<()>, } impl ConsensusPoolService { pub(crate) fn new(ctx: ConsensusPoolContext) -> Self { - let t_ingest = Builder::new() - .name("solCertPoolIngest".to_string()) + let t_consensus_pool_service = Builder::new() + .name("solConPoolService".to_string()) .spawn(move || { - if let Err(e) = Self::consensus_pool_ingest_loop(ctx) { - info!("Certificate pool service exited: {e:?}. Shutting down"); - } + Self::main_loop(ctx); + info!("consensus pool service exited."); }) .unwrap(); - Self { t_ingest } + Self { + t_consensus_pool_service, + } } fn maybe_update_root_and_send_new_certificates( + ctx: &ConsensusPoolContext, consensus_pool: &mut ConsensusPool, - sharable_banks: &SharableBanks, - bls_sender: &Sender, new_finalized_slot: Option, new_certificates_to_send: Vec>, standstill_timer: &mut Instant, stats: &mut ConsensusPoolServiceStats, - highest_finalized: &RwLock>, - ) -> Result<(), AddVoteError> { + ) -> Result<(), ()> { // If we have a new finalized slot, update the root and send new certificates if new_finalized_slot.is_some() { // Reset standstill timer *standstill_timer = Instant::now(); stats.new_finalized_slot += 1; - *highest_finalized.write().unwrap() = consensus_pool.get_highest_finalization_certs(); + *ctx.highest_finalized.write().unwrap() = + consensus_pool.get_highest_finalization_certs(); } - let bank = sharable_banks.root(); + let bank = ctx.sharable_banks.root(); consensus_pool.maybe_prune(bank.slot()); stats.prune_old_state_called += 1; // Send new certificates to peers - Self::send_certificates(bls_sender, new_certificates_to_send, stats) + Self::send_certificates( + ctx, + BLSOp::PushCertificates { + certificates: new_certificates_to_send, + }, + stats, + ) } fn send_certificates( - bls_sender: &Sender, - certificates_to_send: Vec>, + ctx: &ConsensusPoolContext, + op: BLSOp, stats: &mut ConsensusPoolServiceStats, - ) -> Result<(), AddVoteError> { - let num_certs = certificates_to_send.len(); - for (i, certificate) in certificates_to_send.into_iter().enumerate() { - // The buffer should normally be large enough, so we don't handle - // certificate re-send here. - match bls_sender.try_send(BLSOp::PushCertificate { certificate }) { - Ok(_) => { - stats.certificates_sent += 1; - } - Err(TrySendError::Disconnected(_)) => { - return Err(AddVoteError::ChannelDisconnected( - "VotingService".to_string(), - )); - } - Err(TrySendError::Full(_)) => { - stats.certificates_dropped += num_certs.saturating_sub(i); - return Err(AddVoteError::VotingServiceQueueFull); - } + ) -> Result<(), ()> { + let num_certs = Self::num_certificates(&op); + if num_certs == 0 { + return Ok(()); + } + // If we are not a staked identity (hot spare / RPC / new validator / failed VAT) + // we should not send out the certificate. A2A quic only accepts connections + // from staked identities + if !Self::is_current_identity_staked(ctx) { + stats.certificates_skipped_unstaked += num_certs; + return Ok(()); + } + Self::enqueue_certificates(ctx, op, stats) + } + + fn is_current_identity_staked(ctx: &ConsensusPoolContext) -> bool { + ctx.sharable_banks + .root() + .current_epoch_staked_nodes() + .get(&ctx.cluster_info.id()) + .is_some_and(|stake| *stake > 0) + } + + fn num_certificates(op: &BLSOp) -> usize { + match op { + BLSOp::PushCertificates { certificates } + | BLSOp::RefreshCertificates { certificates } => certificates.len(), + _ => unreachable!("expected a certificate BLSOp"), + } + } + + fn enqueue_certificates( + ctx: &ConsensusPoolContext, + op: BLSOp, + stats: &mut ConsensusPoolServiceStats, + ) -> Result<(), ()> { + let num_certs = Self::num_certificates(&op); + + match ctx.bls_sender.try_send(op) { + Ok(()) => { + stats.certificates_sent += num_certs; + Ok(()) + } + Err(TrySendError::Disconnected(_)) => Err(()), + Err(TrySendError::Full(_)) => { + stats.certificates_dropped += num_certs; + warn!( + "{}: channel is full, dropping {num_certs} certs", + ctx.cluster_info.id() + ); + Ok(()) } } + } + + fn process_batch( + ctx: &mut ConsensusPoolContext, + msgs: impl Iterator, + consensus_pool: &mut ConsensusPool, + events: &mut Vec, + standstill_timer: &mut Instant, + stats: &mut ConsensusPoolServiceStats, + ) -> Result<(), ()> { + for msg in msgs { + Self::process_consensus_message( + ctx, + msg, + consensus_pool, + events, + standstill_timer, + stats, + )?; + } Ok(()) } @@ -140,7 +207,7 @@ impl ConsensusPoolService { events: &mut Vec, standstill_timer: &mut Instant, stats: &mut ConsensusPoolServiceStats, - ) -> Result<(), AddVoteError> { + ) -> Result<(), ()> { match message { ConsensusMessage::Certificate(_) => { stats.received_certificates += 1; @@ -150,63 +217,97 @@ impl ConsensusPoolService { } } let root_bank = ctx.sharable_banks.root(); - let (new_finalized_slot, new_certificates_to_send) = - Self::add_message_and_maybe_update_commitment( - &root_bank, - &ctx.cluster_info.id(), - &ctx.my_vote_pubkey, - message, - consensus_pool, - events, - &ctx.commitment_sender, - stats, - )?; + let (new_finalized_slot, new_certificates_to_send) = match Self::add_message( + &root_bank, + &ctx.cluster_info.id(), + &ctx.my_vote_pubkey, + message, + consensus_pool, + events, + stats, + ) { + Ok(res) => res, + Err(err) => { + stats.add_message_failed += 1; + trace!( + "{}, consensus_pool.add_message() failed with {err}", + ctx.cluster_info.id() + ); + return Ok(()); + } + }; Self::maybe_update_root_and_send_new_certificates( + ctx, consensus_pool, - &ctx.sharable_banks, - &ctx.bls_sender, new_finalized_slot, new_certificates_to_send, standstill_timer, stats, - &ctx.highest_finalized, ) } - fn handle_channel_disconnected( - ctx: &mut ConsensusPoolContext, - channel_name: &str, - ) -> Result<(), ()> { + fn handle_channel_disconnected(ctx: &mut ConsensusPoolContext, channel_name: &str) { info!( - "{}: {} disconnected. Exiting", - ctx.cluster_info.id(), - channel_name + "{}: {channel_name} disconnected. Exiting", + ctx.cluster_info.id() ); ctx.exit.store(true, Ordering::Relaxed); - Err(()) } - // Main loop for the certificate pool service, it only exits when any channel is disconnected - fn consensus_pool_ingest_loop(mut ctx: ConsensusPoolContext) -> Result<(), ()> { + /// Finds the initial parent ready that we should use for instantiating the ConsensusPool or kicking off votor + /// The max of genesis block, root block, or the restored parent ready from vote history + fn initial_parent_ready( + genesis_block: Option, + root_block: Block, + vote_history_highest_parent_ready: Option<(Slot, Block)>, + ) -> ParentReady { + let Some(genesis_block) = genesis_block else { + // Alpenglow is not yet enabled, start with just the root + return (root_block.slot.checked_add(1).unwrap(), root_block); + }; + + let initial_block = genesis_block.max(root_block); + let initial_parent_ready = (initial_block.slot.checked_add(1).unwrap(), initial_block); + + if let Some(restored @ (restored_slot, _)) = vote_history_highest_parent_ready + && restored_slot > initial_parent_ready.0 + { + restored + } else { + initial_parent_ready + } + } + + fn root_block(root_bank: &Bank) -> Block { + Block { + slot: root_bank.slot(), + block_id: root_bank + .block_id() + // Once SIMD-0333 is active we can hard unwrap here + .unwrap_or_default(), + } + } + + // Main loop for the consensus pool service + fn main_loop(mut ctx: ConsensusPoolContext) { let mut events = vec![]; let root_bank = ctx.sharable_banks.root(); - // Unlike the other votor threads, consensus pool starts even before alpenglow is enabled - // As it is required to track the Genesis Vote. - let mut consensus_pool = if ctx.migration_status.is_alpenglow_enabled() { - ConsensusPool::new_from_root_bank( - ctx.cluster_info.clone(), - &root_bank, - ctx.generated_cert_types.clone(), - ) - } else { - ConsensusPool::new_from_root_bank_pre_migration( - ctx.cluster_info.clone(), - &root_bank, - ctx.generated_cert_types.clone(), - ctx.migration_status.clone(), - ) - }; + let initial_parent_ready = Self::initial_parent_ready( + ctx.migration_status.genesis_block(), + Self::root_block(&root_bank), + ctx.vote_history_highest_parent_ready, + ); + + // Unlike the other votor threads, consensus pool starts even before Alpenglow is enabled + // because it must track the genesis vote. + let mut consensus_pool = ConsensusPool::new( + ctx.cluster_info.clone(), + &root_bank, + ctx.generated_cert_types.clone(), + ctx.migration_status.clone(), + initial_parent_ready, + ); info!("{}: Certificate pool loop starting", ctx.cluster_info.id()); let mut stats = ConsensusPoolServiceStats::new(); @@ -221,28 +322,23 @@ impl ConsensusPoolService { // Track pending safe-to-notar blocks for intrawindow slots. let mut pending_safe_to_notar = HashSet::new(); - // Ingest votes into certificate pool and notify voting loop of new events + // Ingest votes into consensus pool and notify voting loop of new events while !ctx.exit.load(Ordering::Relaxed) { // Kick off parent ready event, this either happens: // - When we first migrate to alpenglow from TowerBFT - kick off with genesis block // - If we startup post alpenglow migration - kick off with root block + // - If restored vote history is farther ahead, resume from its highest parent-ready if !kick_off_parent_ready && ctx.migration_status.is_alpenglow_enabled() { - let genesis_block = ctx - .migration_status - .genesis_block() - .expect("Alpenglow is enabled"); let root_bank = ctx.sharable_banks.root(); - // can expect once we have block id in snapshots (SIMD-0333) - let root_block = (root_bank.slot(), root_bank.block_id().unwrap_or_default()); - let kick_off_block @ (kick_off_slot, _) = genesis_block.max(root_block); - let start_slot = kick_off_slot.checked_add(1).unwrap(); - - events.push(VotorEvent::ParentReady { - slot: start_slot, - parent_block: kick_off_block, - }); - highest_parent_ready = start_slot; + let (slot, parent_block) = Self::initial_parent_ready( + ctx.migration_status.genesis_block(), + Self::root_block(&root_bank), + ctx.vote_history_highest_parent_ready, + ); + events.push(VotorEvent::ParentReady { slot, parent_block }); kick_off_parent_ready = true; + // Intentionally do not increment `highest_parent_ready` in case + // this is a cluster restart and we are the very first leader. } Self::add_produce_block_event( @@ -259,44 +355,45 @@ impl ConsensusPoolService { // Genesis cert though. if kick_off_parent_ready { events.push(VotorEvent::Standstill( - consensus_pool.highest_finalized_slot().unwrap_or(0), + consensus_pool + .highest_finalized_slot() + .map(|s| s.slot()) + .unwrap_or(ctx.sharable_banks.root().slot()), )); } stats.standstill = true; standstill_timer = Instant::now(); match Self::send_certificates( - &ctx.bls_sender, - consensus_pool.get_certs_for_standstill(), + &ctx, + BLSOp::RefreshCertificates { + certificates: consensus_pool.get_certs_for_standstill(), + }, &mut stats, ) { Ok(()) => (), - Err(AddVoteError::ChannelDisconnected(channel_name)) => { - return Self::handle_channel_disconnected(&mut ctx, channel_name.as_str()); - } - Err(e) => { - trace!( - "{}: unable to push standstill certificates into pool {e}", - ctx.cluster_info.id() - ); + Err(()) => { + return Self::handle_channel_disconnected(&mut ctx, "bls_sender channel"); } } } // Process pending safe-to-notar blocks for intrawindow slots - Self::process_pending_safe_to_notar( + if let Err(()) = Self::process_pending_safe_to_notar( &ctx, &mut consensus_pool, &mut pending_safe_to_notar, &mut events, &mut stats, - )?; + ) { + return Self::handle_channel_disconnected(&mut ctx, "repair_event_sender channel"); + } if events .drain(..) .try_for_each(|event| ctx.event_sender.send(event)) .is_err() { - return Self::handle_channel_disconnected(&mut ctx, "event_sender"); + return Self::handle_channel_disconnected(&mut ctx, "event_sender channel"); } let wait_timeout = if pending_safe_to_notar.is_empty() { @@ -307,57 +404,43 @@ impl ConsensusPoolService { Duration::from_millis(20) }; - let messages: Vec = select! { + let ret = select_biased! { + recv(ctx.own_message_receiver) -> msg => { + let Ok(first) = msg else { + return Self::handle_channel_disconnected(&mut ctx, "own_message_receiver channel"); + }; + Self::handle_own_message(&mut ctx, &mut consensus_pool, &mut events, &mut standstill_timer, first, &mut stats) + } recv(ctx.consensus_message_receiver) -> msg => { let Ok(first) = msg else { - return Self::handle_channel_disconnected(&mut ctx, "consensus_message_receiver"); + return Self::handle_channel_disconnected(&mut ctx, "consensus_message_receiver channel"); }; - std::iter::once(first).chain(ctx.consensus_message_receiver.try_iter()).flatten().collect() + Self::handle_sig_verified_batch(&mut ctx, &mut consensus_pool, &mut events, &mut standstill_timer, first, &mut stats) }, default(wait_timeout) => continue }; - for message in messages { - match Self::process_consensus_message( - &mut ctx, - message, - &mut consensus_pool, - &mut events, - &mut standstill_timer, - &mut stats, - ) { - Ok(()) => {} - Err(AddVoteError::ChannelDisconnected(channel_name)) => { - return Self::handle_channel_disconnected(&mut ctx, channel_name.as_str()); - } - Err(e) => { - // This is a non critical error, a duplicate vote for example - trace!( - "{}: unable to push vote into pool {}", - ctx.cluster_info.id(), - e - ); - stats.add_message_failed += 1; - } + match ret { + Ok(()) => {} + Err(()) => { + return Self::handle_channel_disconnected(&mut ctx, "bls_sender channel"); } } stats.maybe_report(); consensus_pool.maybe_report(); } - Ok(()) } - /// Adds a vote to the certificate pool and updates the commitment cache if necessary + /// Adds a vote to the consensus pool. /// /// If a new finalization slot was recognized, returns the slot - fn add_message_and_maybe_update_commitment( + fn add_message( root_bank: &Bank, my_pubkey: &Pubkey, my_vote_pubkey: &Pubkey, message: ConsensusMessage, consensus_pool: &mut ConsensusPool, votor_events: &mut Vec, - commitment_sender: &Sender, stats: &mut ConsensusPoolServiceStats, ) -> Result<(Option, Vec>), AddVoteError> { let (new_finalized_slot, new_certificates_to_send) = @@ -366,11 +449,7 @@ impl ConsensusPoolService { return Ok((None, new_certificates_to_send)); }; trace!("{my_pubkey}: new finalization certificate for {new_finalized_slot}"); - update_commitment_cache( - CommitmentType::Finalized, - new_finalized_slot, - commitment_sender, - )?; + // RPC-facing finalized commitment is updated after votor selects a root. stats.standstill = false; Ok((Some(new_finalized_slot), new_certificates_to_send)) } @@ -467,13 +546,13 @@ impl ConsensusPoolService { stats: &mut ConsensusPoolServiceStats, ) -> Result<(), ()> { // First, collect new pending blocks from the consensus pool and send them for repair - for block @ (slot, block_id) in consensus_pool.take_pending_safe_to_notar() { + for block in consensus_pool.take_pending_safe_to_notar() { if pending_safe_to_notar.contains(&block) { continue; } match ctx .repair_event_sender - .try_send(RepairEvent::FetchBlock { slot, block_id }) + .try_send(RepairEvent::FetchBlock { block }) { Ok(()) => { stats.pending_safe_to_notar_repair_sent += 1; @@ -490,18 +569,21 @@ impl ConsensusPoolService { } } - let highest_finalized = consensus_pool.highest_finalized_slot().unwrap_or(0); + let highest_finalized = consensus_pool + .highest_finalized_slot() + .map(|s| s.slot()) + .unwrap_or(0); - pending_safe_to_notar.retain(|&(slot, block_id)| { + pending_safe_to_notar.retain(|&block| { // Discard if slot is at or below highest finalized - if slot <= highest_finalized { + if block.slot <= highest_finalized { return false; } // Check if we've received the full block in blockstore let Some(location) = ctx .blockstore - .get_block_location(slot, block_id) + .get_block_location(block.slot, block.block_id) .expect("Blockstore operations must succeed") else { // Block not yet received, keep waiting @@ -511,21 +593,21 @@ impl ConsensusPoolService { // Block has been received, get the parent meta let slot_meta = ctx .blockstore - .meta_from_location(slot, location) + .meta_from_location(block.slot, location) .expect("Blockstore operations must succeed") .expect("SlotMeta must exist if block location is present"); - let parent_block = ( - slot_meta + let parent_block = Block { + slot: slot_meta .parent_slot .expect("parent slot must exist for full blocks"), - slot_meta.parent_block_id, - ); + block_id: slot_meta.parent_block_id, + }; // Check if the parent has a NotarizeFallback certificate (or stronger) if consensus_pool.block_has_notar_fallback_or_stronger(parent_block) { // All conditions met - emit SafeToNotar event - events.push(VotorEvent::SafeToNotar((slot, block_id))); + events.push(VotorEvent::SafeToNotar(block)); stats.pending_safe_to_notar_resolved += 1; return false; } @@ -538,7 +620,52 @@ impl ConsensusPoolService { } pub(crate) fn join(self) -> thread::Result<()> { - self.t_ingest.join() + self.t_consensus_pool_service.join() + } + + fn handle_sig_verified_batch( + ctx: &mut ConsensusPoolContext, + consensus_pool: &mut ConsensusPool, + events: &mut Vec, + standstill_timer: &mut Instant, + first: SigVerifiedBatch, + stats: &mut ConsensusPoolServiceStats, + ) -> Result<(), ()> { + let receiver = ctx.consensus_message_receiver.clone(); + for batch in std::iter::once(first).chain(receiver.try_iter()) { + match batch { + SigVerifiedBatch::Votes(votes) => Self::process_batch( + ctx, + votes.into_iter().map(ConsensusMessage::Vote), + consensus_pool, + events, + standstill_timer, + stats, + ), + SigVerifiedBatch::Certificates(certs) => Self::process_batch( + ctx, + certs.into_iter().map(ConsensusMessage::Certificate), + consensus_pool, + events, + standstill_timer, + stats, + ), + }? + } + Ok(()) + } + + fn handle_own_message( + ctx: &mut ConsensusPoolContext, + consensus_pool: &mut ConsensusPool, + events: &mut Vec, + standstill_timer: &mut Instant, + first: ConsensusMessage, + stats: &mut ConsensusPoolServiceStats, + ) -> Result<(), ()> { + let receiver = ctx.own_message_receiver.clone(); + let msgs = std::iter::once(first).chain(receiver.try_iter()); + Self::process_batch(ctx, msgs, consensus_pool, events, standstill_timer, stats) } } @@ -548,49 +675,41 @@ mod tests { super::*, crate::tests::get_cluster_info, agave_votor_messages::{ - consensus_message::{BLS_KEYPAIR_DERIVE_SEED, CertificateType, VoteMessage}, + certificate::CertificateType, + consensus_message::{BLS_KEYPAIR_DERIVE_SEED, VoteMessage}, vote::Vote, + wire::get_vote_payload_to_sign, }, - crossbeam_channel::unbounded, + crossbeam_channel::{bounded, unbounded}, solana_bls_signatures::{ BLS_SIGNATURE_AFFINE_SIZE, keypair::Keypair as BLSKeypair, signature::Signature as BLSSignature, }, - solana_gossip::cluster_info::ClusterInfo, solana_hash::Hash, + solana_keypair::Keypair, solana_ledger::get_tmp_ledger_path_auto_delete, solana_runtime::{ - bank_forks::{BankForks, SharableBanks}, + bank_forks::BankForks, genesis_utils::{ ValidatorVoteKeypairs, create_genesis_config_with_alpenglow_vote_accounts, }, }, - solana_signer::Signer, std::sync::Arc, }; struct TestContext { consensus_pool: ConsensusPool, - bls_sender: Sender, + ctx: ConsensusPoolContext, bls_receiver: Receiver, - commitment_sender: Sender, - commitment_receiver: Receiver, + _consensus_message_sender: Sender, + event_receiver: Receiver, + _repair_event_receiver: Receiver, validator_keypairs: Vec, - leader_schedule_cache: Arc, - sharable_banks: SharableBanks, - my_pubkey: Pubkey, - my_vote_pubkey: Pubkey, - blockstore: Arc, - exit: Arc, - cluster_info: Arc, - highest_finalized: Arc>>, } impl Default for TestContext { fn default() -> Self { - let (bls_sender, bls_receiver) = crossbeam_channel::unbounded(); - let (commitment_sender, commitment_receiver) = crossbeam_channel::unbounded(); - + let (bls_sender, bls_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new_rand()) @@ -606,7 +725,6 @@ mod tests { stake, ); let my_keypair = validator_keypairs[0].node_keypair.insecure_clone(); - let my_pubkey = my_keypair.pubkey(); let bank0 = Bank::new_for_tests(&genesis.genesis_config); let bank_forks = BankForks::new_rw_arc(bank0); @@ -618,28 +736,52 @@ mod tests { let root_bank = sharable_banks.root(); let cluster_info = get_cluster_info(my_keypair.insecure_clone()); - let consensus_pool = ConsensusPool::new_from_root_bank( + let root_block = Block { + slot: root_bank.slot(), + block_id: root_bank.block_id().unwrap_or_default(), + }; + let initial_parent_ready = (root_bank.slot().checked_add(1).unwrap(), root_block); + let generated_cert_types = Arc::new(GeneratedCertTypes::default()); + let migration_status = Arc::new(MigrationStatus::post_migration_status()); + let consensus_pool = ConsensusPool::new( cluster_info.clone(), &root_bank, - Arc::new(GeneratedCertTypes::default()), + generated_cert_types.clone(), + migration_status.clone(), + initial_parent_ready, ); let my_vote_pubkey = Pubkey::new_unique(); + let (consensus_message_sender, consensus_message_receiver) = unbounded(); + let (_own_message_sender, own_message_receiver) = unbounded(); + let (event_sender, event_receiver) = unbounded(); + let (repair_event_sender, repair_event_receiver) = unbounded(); + + let ctx = ConsensusPoolContext { + exit: Arc::new(AtomicBool::new(false)), + migration_status, + generated_cert_types, + cluster_info, + my_vote_pubkey, + blockstore, + sharable_banks, + leader_schedule_cache, + vote_history_highest_parent_ready: None, + consensus_message_receiver, + own_message_receiver, + bls_sender, + event_sender, + repair_event_sender, + highest_finalized: Arc::new(RwLock::new(None)), + }; TestContext { consensus_pool, - bls_sender, + ctx, bls_receiver, - commitment_sender, - commitment_receiver, + _consensus_message_sender: consensus_message_sender, + event_receiver, + _repair_event_receiver: repair_event_receiver, validator_keypairs, - leader_schedule_cache, - sharable_banks, - my_pubkey, - my_vote_pubkey, - blockstore, - exit: Arc::new(AtomicBool::new(false)), - cluster_info, - highest_finalized: Arc::new(RwLock::new(None)), } } } @@ -647,8 +789,7 @@ mod tests { /// Test the full consensus message flow: /// 1. Validators 0-7 send notarize votes for slot 2. After processing all /// votes, we expect a notarize/finalize certificate to be produced and - /// forwarded via the BLS channel, a finalized event to be emitted, and a - /// commitment update to be sent. + /// forwarded via the BLS channel and a finalized event to be emitted. /// 2. A skip certificate is then sent for slot 3 and we verify it is /// immediately forwarded via the BLS channel. #[test] @@ -659,17 +800,21 @@ mod tests { // validator 0 to 7 send Notarize on slot 2 let block_id = Hash::new_unique(); let target_slot = 2; - let notarize_vote = Vote::new_notarization_vote(target_slot, block_id); + let notarize_vote = Vote::new_notarization_vote(Block { + slot: target_slot, + block_id, + }); let mut events = vec![]; - let root_bank = ctx.sharable_banks.root(); + let root_bank = ctx.ctx.sharable_banks.root(); // Process votes from validators 0-7 for my_rank in 0..8 { let vote_keypair = &ctx.validator_keypairs[my_rank].vote_keypair; let bls_keypair = BLSKeypair::derive_from_signer(vote_keypair, BLS_KEYPAIR_DERIVE_SEED).unwrap(); - let vote_serialized = bincode::serialize(¬arize_vote).unwrap(); + let vote_serialized = + get_vote_payload_to_sign(notarize_vote, ctx.ctx.cluster_info.my_shred_version()); let message = ConsensusMessage::Vote(VoteMessage { vote: notarize_vote, signature: bls_keypair.sign(&vote_serialized).into(), @@ -677,14 +822,13 @@ mod tests { }); let mut stats = ConsensusPoolServiceStats::new(); - let result = ConsensusPoolService::add_message_and_maybe_update_commitment( + let result = ConsensusPoolService::add_message( &root_bank, - &ctx.my_pubkey, - &ctx.my_vote_pubkey, + &ctx.ctx.cluster_info.id(), + &ctx.ctx.my_vote_pubkey, message, &mut ctx.consensus_pool, &mut events, - &ctx.commitment_sender, &mut stats, ); assert!(result.is_ok()); @@ -695,61 +839,43 @@ mod tests { // Send certificates if any were produced if !new_certificates_to_send.is_empty() || new_finalized_slot.is_some() { ConsensusPoolService::maybe_update_root_and_send_new_certificates( + &ctx.ctx, &mut ctx.consensus_pool, - &ctx.sharable_banks, - &ctx.bls_sender, new_finalized_slot, new_certificates_to_send, &mut standstill_timer, &mut stats, - &ctx.highest_finalized, ) .unwrap(); } } // Verify that we received certificates via the bls channel - let mut found_certificate = false; - while let Ok(event) = ctx.bls_receiver.try_recv() { - if let BLSOp::PushCertificate { certificate } = event { - assert_eq!(certificate.cert_type.slot(), target_slot); - assert!( - matches!(certificate.cert_type, CertificateType::Notarize(_, _)) - || matches!(certificate.cert_type, CertificateType::FinalizeFast(_, _)) - || matches!( - certificate.cert_type, - CertificateType::NotarizeFallback(_, _) - ) - ); - found_certificate = true; - } + let BLSOp::PushCertificates { certificates } = ctx.bls_receiver.recv().unwrap() else { + panic!("invalid type"); + }; + assert_eq!(certificates.len(), 2); + for cert in certificates { + assert_eq!(cert.cert_type.slot(), target_slot); + assert!( + matches!(&cert.cert_type, CertificateType::Notarize(_)) + || matches!(&cert.cert_type, CertificateType::FinalizeFast(_)) + || matches!(&cert.cert_type, CertificateType::NotarizeFallback(_,)) + ); } - assert!(found_certificate, "Should have received a certificate"); // Verify that we received a finalized slot event - let finalized_event = events.iter().find(|event| { - matches!( - event, - VotorEvent::Finalized((slot, received_block_id), is_fast_finalized) - if *slot == target_slot && *received_block_id == block_id && *is_fast_finalized - ) + let finalized_event = events.iter().find(|event| match event { + VotorEvent::Finalized(block, is_fast_finalized) => { + block.slot == target_slot && block.block_id == block_id && *is_fast_finalized + } + _ => false, }); assert!( finalized_event.is_some(), "Should have received a finalized event" ); - // Verify that we received a commitment update - let commitment = ctx.commitment_receiver.try_recv(); - assert!(commitment.is_ok()); - let CommitmentAggregationData { - commitment_type, - slot, - .. - } = commitment.unwrap(); - assert_eq!(commitment_type, CommitmentType::Finalized); - assert_eq!(slot, target_slot); - // Now send a Skip certificate on slot 3, should be forwarded immediately let target_slot = 3; let skip_certificate = Certificate { @@ -760,14 +886,13 @@ mod tests { events.clear(); let mut stats = ConsensusPoolServiceStats::new(); - let result = ConsensusPoolService::add_message_and_maybe_update_commitment( + let result = ConsensusPoolService::add_message( &root_bank, - &ctx.my_pubkey, - &ctx.my_vote_pubkey, + &ctx.ctx.cluster_info.id(), + &ctx.ctx.my_vote_pubkey, ConsensusMessage::Certificate(skip_certificate), &mut ctx.consensus_pool, &mut events, - &ctx.commitment_sender, &mut stats, ); assert!(result.is_ok()); @@ -776,22 +901,21 @@ mod tests { let mut standstill_timer = Instant::now(); ConsensusPoolService::maybe_update_root_and_send_new_certificates( + &ctx.ctx, &mut ctx.consensus_pool, - &ctx.sharable_banks, - &ctx.bls_sender, new_finalized_slot, new_certificates_to_send, &mut standstill_timer, &mut stats, - &ctx.highest_finalized, ) .unwrap(); // Verify skip certificate was forwarded let mut found_skip = false; while let Ok(event) = ctx.bls_receiver.try_recv() { - if let BLSOp::PushCertificate { certificate } = event { - if matches!(certificate.cert_type, CertificateType::Skip(slot) if slot == target_slot) + if let BLSOp::PushCertificates { certificates } = event { + assert_eq!(certificates.len(), 1); + if matches!(certificates[0].cert_type, CertificateType::Skip(slot) if slot == target_slot) { found_skip = true; } @@ -803,15 +927,21 @@ mod tests { #[test] fn test_send_produce_block_event() { let mut ctx = TestContext::default(); - let (repair_event_sender, _repair_event_receiver) = unbounded(); // Find when is the next leader slot for me (validator 0) let next_leader_slot = ctx + .ctx .leader_schedule_cache - .next_leader_slot(&ctx.my_pubkey, 0, &ctx.sharable_banks.root(), None, 1000000) + .next_leader_slot( + &ctx.ctx.cluster_info.id(), + 0, + &ctx.ctx.sharable_banks.root(), + None, + 1000000, + ) .expect("Should find a leader slot"); - let root_bank = ctx.sharable_banks.root(); + let root_bank = ctx.ctx.sharable_banks.root(); let mut events = vec![]; // Send skip certificates for all slots up to the next leader slot @@ -823,14 +953,13 @@ mod tests { bitmap: vec![], }; - let result = ConsensusPoolService::add_message_and_maybe_update_commitment( + let result = ConsensusPoolService::add_message( &root_bank, - &ctx.my_pubkey, - &ctx.my_vote_pubkey, + &ctx.ctx.cluster_info.id(), + &ctx.ctx.my_vote_pubkey, ConsensusMessage::Certificate(skip_certificate), &mut ctx.consensus_pool, &mut events, - &ctx.commitment_sender, &mut stats, ); assert!(result.is_ok()); @@ -838,33 +967,20 @@ mod tests { // Now call add_produce_block_event to generate ProduceWindow event let mut highest_parent_ready = root_bank.slot(); - let mut pool_ctx = ConsensusPoolContext { - exit: ctx.exit.clone(), - migration_status: Arc::new(MigrationStatus::post_migration_status()), - generated_cert_types: Arc::new(GeneratedCertTypes::default()), - cluster_info: ctx.cluster_info.clone(), - my_vote_pubkey: ctx.my_vote_pubkey, - blockstore: ctx.blockstore.clone(), - sharable_banks: ctx.sharable_banks.clone(), - leader_schedule_cache: ctx.leader_schedule_cache.clone(), - consensus_message_receiver: crossbeam_channel::unbounded().1, - bls_sender: ctx.bls_sender.clone(), - event_sender: crossbeam_channel::unbounded().0, - commitment_sender: ctx.commitment_sender.clone(), - highest_finalized: ctx.highest_finalized.clone(), - repair_event_sender, - }; // Add a ParentReady event for the slot before our leader slot events.push(VotorEvent::ParentReady { slot: next_leader_slot.0, - parent_block: (next_leader_slot.0 - 1, Hash::new_unique()), + parent_block: Block { + slot: next_leader_slot.0 - 1, + block_id: Hash::new_unique(), + }, }); ConsensusPoolService::add_produce_block_event( &mut highest_parent_ready, &ctx.consensus_pool, - &mut pool_ctx, + &mut ctx.ctx, &mut events, &mut stats, ); @@ -883,6 +999,102 @@ mod tests { ); } + #[test] + fn test_can_produce_window_immediately_on_restart() { + let mut ctx = TestContext::default(); + + let root_bank = ctx.ctx.sharable_banks.root(); + let next_leader_slot = ctx + .ctx + .leader_schedule_cache + .next_leader_slot(&ctx.ctx.cluster_info.id(), 0, &root_bank, None, 1000000) + .expect("Should find a leader slot") + .0; + let restored_parent_ready = ( + next_leader_slot, + Block { + slot: next_leader_slot.checked_sub(1).unwrap(), + block_id: Hash::new_unique(), + }, + ); + ctx.ctx.vote_history_highest_parent_ready = Some(restored_parent_ready); + let exit = ctx.ctx.exit.clone(); + + let handle = thread::spawn(move || ConsensusPoolService::main_loop(ctx.ctx)); + + let deadline = Instant::now() + Duration::from_secs(5); + let mut saw_parent_ready = false; + let mut saw_produce_window = false; + while Instant::now() < deadline && !saw_produce_window { + let timeout = deadline.saturating_duration_since(Instant::now()); + let Ok(event) = ctx.event_receiver.recv_timeout(timeout) else { + break; + }; + + match event { + VotorEvent::ParentReady { slot, parent_block } => { + saw_parent_ready |= (slot, parent_block) == restored_parent_ready; + } + VotorEvent::ProduceWindow(LeaderWindowInfo { start_slot, .. }) => { + saw_produce_window |= start_slot == restored_parent_ready.0; + } + _ => {} + } + } + + exit.store(true, Ordering::Relaxed); + handle.join().unwrap(); + + assert!( + saw_parent_ready, + "Should have received kick-off ParentReady" + ); + assert!( + saw_produce_window, + "Should have received ProduceWindow for kick-off ParentReady" + ); + } + + #[test] + fn test_kick_off_parent_ready_uses_restored_vote_history() { + let genesis_block = Some(Block { + slot: 10, + block_id: Hash::new_unique(), + }); + let root_block = Block { + slot: 12, + block_id: Hash::new_unique(), + }; + assert_eq!( + ConsensusPoolService::initial_parent_ready(genesis_block, root_block, None), + (13, root_block) + ); + + let restored = ( + 16, + Block { + slot: 15, + block_id: Hash::new_unique(), + }, + ); + assert_eq!( + ConsensusPoolService::initial_parent_ready(genesis_block, root_block, Some(restored)), + restored + ); + + let stale = ( + 12, + Block { + slot: 11, + block_id: Hash::new_unique(), + }, + ); + assert_eq!( + ConsensusPoolService::initial_parent_ready(genesis_block, root_block, Some(stale)), + (13, root_block) + ); + } + #[test] fn test_send_certificates() { let ctx = TestContext::default(); @@ -902,25 +1114,91 @@ mod tests { let mut stats = ConsensusPoolServiceStats::new(); let result = ConsensusPoolService::send_certificates( - &ctx.bls_sender, - certificates.clone(), + &ctx.ctx, + BLSOp::PushCertificates { + certificates: certificates.clone(), + }, &mut stats, ); assert!(result.is_ok()); assert_eq!(stats.certificates_sent.0, 2); // Verify certificates were received - let cert1 = ctx.bls_receiver.try_recv(); - assert!(cert1.is_ok()); - if let BLSOp::PushCertificate { certificate } = cert1.unwrap() { - assert!(matches!(certificate.cert_type, CertificateType::Skip(1))); - } + let cert1 = ctx.bls_receiver.try_recv().unwrap(); + let BLSOp::PushCertificates { certificates } = cert1 else { + panic!("invalid type"); + }; + assert_eq!(certificates.len(), 2); + assert!(matches!( + certificates[0].cert_type, + CertificateType::Skip(1) + )); + assert!(matches!( + certificates[1].cert_type, + CertificateType::Skip(2) + )); + } - let cert2 = ctx.bls_receiver.try_recv(); - assert!(cert2.is_ok()); - if let BLSOp::PushCertificate { certificate } = cert2.unwrap() { - assert!(matches!(certificate.cert_type, CertificateType::Skip(2))); - } + #[test] + fn test_send_certificates_refresh() { + let ctx = TestContext::default(); + + let certificates = vec![Arc::new(Certificate { + cert_type: CertificateType::Skip(1), + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + })]; + + let mut stats = ConsensusPoolServiceStats::new(); + ConsensusPoolService::send_certificates( + &ctx.ctx, + BLSOp::RefreshCertificates { + certificates: certificates.clone(), + }, + &mut stats, + ) + .unwrap(); + assert_eq!(stats.certificates_sent.0, 1); + + let BLSOp::RefreshCertificates { certificates } = ctx.bls_receiver.try_recv().unwrap() + else { + panic!("invalid type"); + }; + assert_eq!(certificates.len(), 1); + assert!(matches!( + certificates[0].cert_type, + CertificateType::Skip(1) + )); + } + + #[test] + fn test_send_certificates_skips_unstaked_identity() { + let mut ctx = TestContext::default(); + let certificates = vec![ + Arc::new(Certificate { + cert_type: CertificateType::Skip(1), + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }), + Arc::new(Certificate { + cert_type: CertificateType::Skip(2), + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: vec![], + }), + ]; + let unstaked_identity = Keypair::new(); + let cluster_info = get_cluster_info(unstaked_identity); + ctx.ctx.cluster_info = cluster_info; + let mut stats = ConsensusPoolServiceStats::new(); + ConsensusPoolService::send_certificates( + &ctx.ctx, + BLSOp::PushCertificates { certificates }, + &mut stats, + ) + .unwrap(); + assert_eq!(stats.certificates_sent.0, 0); + assert_eq!(stats.certificates_skipped_unstaked.0, 2); + assert!(ctx.bls_receiver.try_recv().is_err()); } #[test] @@ -935,10 +1213,12 @@ mod tests { })]; let mut stats = ConsensusPoolServiceStats::new(); - let result = - ConsensusPoolService::send_certificates(&ctx.bls_sender, certificates, &mut stats); - - assert!(matches!(result, Err(AddVoteError::ChannelDisconnected(_)))); + let result = ConsensusPoolService::send_certificates( + &ctx.ctx, + BLSOp::PushCertificates { certificates }, + &mut stats, + ); + assert_eq!(result.unwrap_err(), ()); } #[test] @@ -956,14 +1236,12 @@ mod tests { // Test with new_finalized_slot = Some let result = ConsensusPoolService::maybe_update_root_and_send_new_certificates( + &ctx.ctx, &mut ctx.consensus_pool, - &ctx.sharable_banks, - &ctx.bls_sender, Some(5), // new finalized slot certificates, &mut standstill_timer, &mut stats, - &ctx.highest_finalized, ); assert!(result.is_ok()); diff --git a/votor/src/consensus_pool_service/stats.rs b/votor/src/consensus_pool_service/stats.rs index 3bc559815f9..c35e12898cd 100644 --- a/votor/src/consensus_pool_service/stats.rs +++ b/votor/src/consensus_pool_service/stats.rs @@ -13,6 +13,7 @@ pub(super) struct ConsensusPoolServiceStats { pub(super) add_message_failed: Saturating, pub(super) certificates_sent: Saturating, pub(super) certificates_dropped: Saturating, + pub(super) certificates_skipped_unstaked: Saturating, pub(super) new_finalized_slot: Saturating, pub(super) parent_ready_missed_window: Saturating, pub(super) parent_ready_produce_window: Saturating, @@ -31,6 +32,7 @@ impl ConsensusPoolServiceStats { add_message_failed: Saturating(0), certificates_sent: Saturating(0), certificates_dropped: Saturating(0), + certificates_skipped_unstaked: Saturating(0), new_finalized_slot: Saturating(0), parent_ready_missed_window: Saturating(0), parent_ready_produce_window: Saturating(0), @@ -49,6 +51,7 @@ impl ConsensusPoolServiceStats { add_message_failed: Saturating(add_message_failed), certificates_sent: Saturating(certificates_sent), certificates_dropped: Saturating(certificates_dropped), + certificates_skipped_unstaked: Saturating(certificates_skipped_unstaked), new_finalized_slot: Saturating(new_finalized_slot), parent_ready_missed_window: Saturating(parent_ready_missed_window), parent_ready_produce_window: Saturating(parent_ready_produce_window), @@ -65,6 +68,11 @@ impl ConsensusPoolServiceStats { ("add_message_failed", add_message_failed, i64), ("certificates_sent", certificates_sent, i64), ("certificates_dropped", certificates_dropped, i64), + ( + "certificates_skipped_unstaked", + certificates_skipped_unstaked, + i64 + ), ("new_finalized_slot", new_finalized_slot, i64), ( "parent_ready_missed_window", diff --git a/votor/src/consensus_rewards.rs b/votor/src/consensus_rewards.rs deleted file mode 100644 index 30bd6ca10d3..00000000000 --- a/votor/src/consensus_rewards.rs +++ /dev/null @@ -1,247 +0,0 @@ -use { - agave_votor_messages::{ - consensus_message::VoteMessage, - reward_certificate::{ - AddVoteMessage, BuildRewardCertsRequest, BuildRewardCertsRespError, - BuildRewardCertsRespSucc, BuildRewardCertsResponse, NUM_SLOTS_FOR_REWARD, - }, - vote::Vote, - }, - crossbeam_channel::{Receiver, Sender, select_biased}, - entry::Entry, - solana_clock::Slot, - solana_gossip::cluster_info::ClusterInfo, - solana_ledger::leader_schedule_cache::LeaderScheduleCache, - solana_runtime::{bank::Bank, bank_forks::SharableBanks}, - std::{ - collections::BTreeMap, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, - thread::{self, Builder, JoinHandle}, - time::Duration, - }, -}; - -mod entry; - -/// Returns [`false`] if the rewards container is not interested in the [`VoteMessage`]. -/// Returns [`true`] if the rewards container might be interested in the [`VoteMessage`]. -pub fn wants_vote( - cluster_info: &ClusterInfo, - leader_schedule: &LeaderScheduleCache, - root_slot: Slot, - vote: &VoteMessage, -) -> bool { - match vote.vote { - Vote::Notarize(_) | Vote::Skip(_) => (), - Vote::Finalize(_) - | Vote::NotarizeFallback(_) - | Vote::SkipFallback(_) - | Vote::Genesis(_) => return false, - } - let vote_slot = vote.vote.slot(); - if vote_slot.saturating_add(NUM_SLOTS_FOR_REWARD) <= root_slot { - return false; - } - let my_pubkey = cluster_info.id(); - let Some(leader) = - leader_schedule.slot_leader_at(vote_slot.saturating_add(NUM_SLOTS_FOR_REWARD), None) - else { - return false; - }; - if leader.id != my_pubkey { - return false; - } - true -} - -/// Container to store state needed to generate reward certificates. -struct ConsensusRewards { - /// Per [`Slot`], stores skip and notar votes. - votes: BTreeMap, - /// Stores the latest pubkey for the current node. - cluster_info: Arc, - /// Stores the leader schedules. - leader_schedule: Arc, - sharable_banks: SharableBanks, - /// Flag to indicate when the channel receiving loop should exit. - exit: Arc, - /// Channel to receive messages to build reward certificates. - build_reward_certs_receiver: Receiver, - /// Channel send the built reward certificates. - reward_certs_sender: Sender, - /// Channel to receive verified votes. - votes_receiver: Receiver, -} - -impl ConsensusRewards { - /// Constructs a new instance of [`ConsensusRewards`]. - fn new( - cluster_info: Arc, - leader_schedule: Arc, - sharable_banks: SharableBanks, - exit: Arc, - build_reward_certs_receiver: Receiver, - reward_certs_sender: Sender, - votes_receiver: Receiver, - ) -> Self { - Self { - votes: BTreeMap::default(), - cluster_info, - leader_schedule, - sharable_banks, - exit, - build_reward_certs_receiver, - reward_certs_sender, - votes_receiver, - } - } - - /// Runs a loop receiving and handling messages over different channels. - fn run(&mut self) { - while !self.exit.load(Ordering::Relaxed) { - let my_pubkey = self.cluster_info.id(); - // bias messages to build certificates as that is on the critical path - select_biased! { - recv(self.build_reward_certs_receiver) -> msg => { - match msg { - Ok(msg) => { - let resp = BuildRewardCertsResponse { - bank_slot: msg.bank_slot, - result: self.build_certs(msg.bank_slot), - }; - if self.reward_certs_sender.send(resp).is_err() { - error!("{my_pubkey}: cert sender channel is disconnected; exiting."); - break; - } - } - Err(_) => { - error!("{my_pubkey}: build reward certs channel is disconnected; exiting."); - break; - } - } - } - recv(self.votes_receiver) -> msg => { - match msg { - Ok(msg) => { - let bank = self.sharable_banks.root(); - for vote in msg.votes { - self.add_vote(&bank, &vote); - } - } - Err(_) => { - error!("{my_pubkey}: votes receiver channel is disconnected; exiting."); - break; - } - } - } - default(Duration::from_secs(1)) => { - continue; - } - } - } - } - - /// Returns [`true`] if the rewards container is interested in this vote else [`false`]. - fn wants_vote(&self, root_slot: Slot, vote: &VoteMessage) -> bool { - if !wants_vote(&self.cluster_info, &self.leader_schedule, root_slot, vote) { - return false; - } - let Some(entry) = self.votes.get(&vote.vote.slot()) else { - return true; - }; - entry.wants_vote(vote) - } - - /// Adds received [`VoteMessage`] from other validators. - fn add_vote(&mut self, root_bank: &Bank, vote: &VoteMessage) { - let slot = vote.vote.slot(); - let Some(rank_map) = root_bank.get_rank_map(slot) else { - warn!( - "failed to look up rank_map for slot {slot} using bank for slot {}", - root_bank.slot() - ); - return; - }; - let max_validators = rank_map.len(); - let root_slot = root_bank.slot(); - // drop state that is too old based on how the root slot has progressed - // TODO: if this actually purges state, that probably indicates that the leader missed its - // window. We should have a metric for this. - self.votes = self - .votes - .split_off(&root_slot.saturating_sub(NUM_SLOTS_FOR_REWARD)); - - if !self.wants_vote(root_slot, vote) { - return; - } - match self - .votes - .entry(vote.vote.slot()) - .or_insert(Entry::new(max_validators)) - .add_vote(rank_map, vote) - { - Ok(()) => (), - Err(e) => { - warn!("Adding vote {vote:?} failed with {e}"); - } - } - } - - /// Builds reward certificates. - fn build_certs( - &mut self, - bank_slot: Slot, - ) -> Result { - let Some(reward_slot) = bank_slot.checked_sub(NUM_SLOTS_FOR_REWARD) else { - return Ok(BuildRewardCertsRespSucc::default()); - }; - // we assume that the block creation loop will only ever request to build reward certs in a strictly increasing order so we can drop older state - self.votes = self.votes.split_off(&reward_slot); - match self.votes.remove(&reward_slot) { - None => Ok(BuildRewardCertsRespSucc::default()), - Some(entry) => entry.build_certs(reward_slot), - } - } -} - -/// Service to run the consensus reward container in a dedicated thread. -pub struct ConsensusRewardsService { - handle: JoinHandle<()>, -} - -impl ConsensusRewardsService { - /// Creates a new instance of [`ConsensusRewardsService`]. - pub fn new( - cluster_info: Arc, - leader_schedule: Arc, - sharable_banks: SharableBanks, - exit: Arc, - votes_receiver: Receiver, - build_reward_certs_receiver: Receiver, - reward_certs_sender: Sender, - ) -> Self { - let handle = Builder::new() - .name("solConsRew".to_string()) - .spawn(move || { - ConsensusRewards::new( - cluster_info, - leader_schedule, - sharable_banks, - exit, - build_reward_certs_receiver, - reward_certs_sender, - votes_receiver, - ) - .run(); - }) - .unwrap(); - Self { handle } - } - - pub fn join(self) -> thread::Result<()> { - self.handle.join() - } -} diff --git a/votor/src/event.rs b/votor/src/event.rs index 24a120c53ef..ee4f631b7a5 100644 --- a/votor/src/event.rs +++ b/votor/src/event.rs @@ -2,9 +2,11 @@ use { agave_votor_messages::consensus_message::Block, crossbeam_channel::{Receiver, Sender}, solana_clock::Slot, - solana_hash::Hash, solana_runtime::bank::Bank, - std::{sync::Arc, time::Instant}, + std::{ + sync::{Arc, Mutex}, + time::Instant, + }, }; #[derive(Debug, Clone)] @@ -83,10 +85,25 @@ impl VotorEvent { | VotorEvent::SafeToSkip(s) | VotorEvent::TimeoutCrashedLeader(s) | VotorEvent::FirstShred(s) - | VotorEvent::SafeToNotar((s, _)) - | VotorEvent::Finalized((s, _), _) - | VotorEvent::BlockNotarized((s, _)) - | VotorEvent::BlockNotarFallback((s, _)) + | VotorEvent::SafeToNotar(Block { + slot: s, + block_id: _, + }) + | VotorEvent::Finalized( + Block { + slot: s, + block_id: _, + }, + _, + ) + | VotorEvent::BlockNotarized(Block { + slot: s, + block_id: _, + }) + | VotorEvent::BlockNotarFallback(Block { + slot: s, + block_id: _, + }) | VotorEvent::ParentReady { slot: s, parent_block: _, @@ -108,31 +125,54 @@ pub enum RepairEvent { /// - The block has received a NotarizeFallback certificate or stronger /// - An intrawindow block has reached the SafeToNotar threshold, however we need /// to check that the parent has reached notarize-fallback requiring us to fetch this block - FetchBlock { slot: Slot, block_id: Hash }, + FetchBlock { block: Block }, } impl RepairEvent { pub fn slot(&self) -> Slot { match self { - RepairEvent::FetchBlock { slot, .. } => *slot, + RepairEvent::FetchBlock { block } => block.slot, } } } -pub type SwitchBankEventSender = Sender; -pub type SwitchBankEventReceiver = Receiver; - -/// Event sent to replay_stage when a bank needs to be switched as a result of a ParentReady +/// Event sent to replay_stage when a bank needs to be switched as a result of a ParentReady. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SwitchBankEvent { - /// We need to switch any existing banks to this bank including ancestors - Switch { slot: Slot, block_id: Hash }, + /// We need to switch any existing banks to this bank including ancestors. + Switch { block: Block }, } impl SwitchBankEvent { pub fn block(&self) -> Block { match self { - SwitchBankEvent::Switch { slot, block_id } => (*slot, *block_id), + SwitchBankEvent::Switch { block } => *block, + } + } +} + +/// Shared single-cell holding the most recent switch-bank request from votor to replay. +/// +/// Used instead of a channel because replay only ever acts on the latest event — buffering +/// older events doesn't help, and blocking on a full channel could lead to a stall in Votor. +/// Writer (votor) advances monotonically via [`Self::try_advance`]; reader (replay) pulls the +/// current value via [`Self::take`]. +#[derive(Clone, Default)] +pub struct LatestSwitchRequest(Arc>>); + +impl LatestSwitchRequest { + /// Records `event` as the latest pending request, iff it is strictly newer than what's + /// currently held. Returns the previous value (if any) when it was overwritten. + pub fn try_advance(&self, event: SwitchBankEvent) -> Option { + let mut guard = self.0.lock().unwrap(); + match guard.as_ref() { + Some(cur) if event <= *cur => None, + _ => guard.replace(event), } } + + /// Atomically takes the current request, leaving the cell empty. + pub fn take(&self) -> Option { + self.0.lock().unwrap().take() + } } diff --git a/votor/src/event_handler.rs b/votor/src/event_handler.rs index 2a4d8f74bd8..6974af7fc2b 100644 --- a/votor/src/event_handler.rs +++ b/votor/src/event_handler.rs @@ -4,10 +4,8 @@ use { crate::{ commitment::{CommitmentType, update_commitment_cache}, - common::DELTA_FIRST_SLICE, - consensus_metrics::ConsensusMetricsEvent, event::{ - CompletedBlock, RepairEvent, RepairEventSender, SwitchBankEvent, SwitchBankEventSender, + CompletedBlock, LatestSwitchRequest, RepairEvent, RepairEventSender, SwitchBankEvent, VotorEvent, VotorEventReceiver, }, event_handler::stats::EventHandlerStats, @@ -15,10 +13,16 @@ use { timer_manager::TimerManager, vote_history::{VoteHistory, VoteHistoryError}, voting_service::BLSOp, - voting_utils::{VoteError, VotingContext, generate_vote_message}, + voting_utils::{ + VoteError, VotingContext, create_and_send_own_vote_message, + generate_refresh_vote_message, insert_vote_and_create_bls_message, + }, votor::SharedContext, }, - agave_votor_messages::{consensus_message::Block, migration::MigrationStatus, vote::Vote}, + agave_votor_messages::{ + consensus_message::Block, metric_types::ConsensusMetricsEvent, migration::MigrationStatus, + vote::Vote, + }, crossbeam_channel::{RecvError, SendError, TrySendError, select}, parking_lot::RwLock, solana_clock::Slot, @@ -27,7 +31,6 @@ use { solana_pubkey::Pubkey, solana_runtime::{ bank::Bank, - bank_forks_controller::BankForksControllerError, leader_schedule_utils::{ first_of_consecutive_leader_slots, last_of_consecutive_leader_slots, leader_slot_index, }, @@ -78,9 +81,6 @@ enum EventLoopError { #[error("Set identity error")] SetIdentityError(#[from] VoteHistoryError), - - #[error("Bank forks controller error")] - BankForksControllerError(#[from] BankForksControllerError), } pub(crate) struct EventHandler { @@ -157,6 +157,7 @@ impl EventHandler { ); return Err(EventLoopError::SetIdentityError(e)); } + Self::send_vote_history_to_consensus_pool(&local_context.my_pubkey, &mut vctx)?; while !exit.load(Ordering::Relaxed) { let mut receive_event_time = Measure::start("receive_event"); @@ -173,7 +174,7 @@ impl EventHandler { .saturating_add(receive_event_time.as_us() as u32); let root_bank = vctx.sharable_banks.root(); - if event.should_ignore(root_bank.slot()) { + if event.should_ignore(root_bank.slot().max(vctx.vote_history.root())) { local_context.stats.ignored = local_context.stats.ignored.saturating_add(1); continue; } @@ -211,7 +212,7 @@ impl EventHandler { fn handle_parent_ready_event( slot: Slot, - parent_block @ (parent_slot, _): Block, + parent_block: Block, vctx: &mut VotingContext, ctx: &SharedContext, local_context: &mut LocalContext, @@ -222,22 +223,22 @@ impl EventHandler { info!("{my_pubkey}: Parent ready {slot} {parent_block:?}"); // We need to ensure that we've replayed the parent bank. - if parent_slot > vctx.sharable_banks.root().slot() { - request_switch(&ctx.switch_bank_sender, *my_pubkey, parent_block)?; + if parent_block.slot > vctx.sharable_banks.root().slot() { + request_switch(&ctx.latest_switch_request, *my_pubkey, parent_block); } - let should_set_timeouts = vctx.vote_history.add_parent_ready(slot, parent_block); + vctx.vote_history.add_parent_ready(slot, parent_block); Self::check_pending_blocks(my_pubkey, &mut local_context.pending_blocks, vctx, votes)?; - if should_set_timeouts { - // TODO: configure delta_first_slice in bank - let delta_first_slice = DELTA_FIRST_SLICE; - let delta_block = Duration::from_nanos_u128(vctx.sharable_banks.root().ns_per_slot); - timer_manager.write().set_timeouts( - slot, - local_context.standstill_slot, - delta_first_slice, - delta_block, - ); + let root_bank = vctx.sharable_banks.root(); + let delta_block = Duration::from_nanos_u128(root_bank.ns_per_slot_at_slot(slot)); + let delta_first_fec_set = delta_block; + let timeout_inserted = timer_manager.write().set_timeouts( + slot, + local_context.standstill_slot, + delta_first_fec_set, + delta_block, + ); + if timeout_inserted { local_context.stats.timeout_set = local_context.stats.timeout_set.saturating_add(1); } @@ -302,7 +303,7 @@ impl EventHandler { .or_default() .push((block, parent_block)); } - Self::check_rootable_blocks( + Self::check_rootable_blocks_and_bank_hash_mismatches( my_pubkey, ctx, vctx, @@ -311,7 +312,7 @@ impl EventHandler { finalized_blocks, received_shred, stats, - )?; + ); if let Some(parent_block) = Self::add_missing_parent_ready(block, ctx, vctx, local_context) { @@ -392,19 +393,19 @@ impl EventHandler { } // We have observed the safe to notar condition, and can send a notar fallback vote - // TODO: update cert pool to check parent block id for intra window slots - VotorEvent::SafeToNotar(block @ (slot, block_id)) => { + VotorEvent::SafeToNotar(block) => { info!("{my_pubkey}: SafeToNotar {block:?}"); - Self::try_skip_window(my_pubkey, slot, vctx, &mut votes)?; - if vctx.vote_history.its_over(slot) - || vctx.vote_history.voted_notar_fallback(slot, block_id) + Self::try_skip_window(my_pubkey, block.slot, vctx, &mut votes)?; + if vctx.vote_history.its_over(block.slot) + || vctx + .vote_history + .voted_notar_fallback(block.slot, block.block_id) { return Ok(votes); } - info!("{my_pubkey}: Voting notarize-fallback for {slot} {block_id}"); - if let Some(bls_op) = generate_vote_message( - Vote::new_notarization_fallback_vote(slot, block_id), - false, + info!("{my_pubkey}: Voting notarize-fallback for {block:?}"); + if let Some(bls_op) = insert_vote_and_create_bls_message( + Vote::new_notarization_fallback_vote(block), vctx, )? { votes.push(bls_op); @@ -420,7 +421,7 @@ impl EventHandler { } info!("{my_pubkey}: Voting skip-fallback for {slot}"); if let Some(bls_op) = - generate_vote_message(Vote::new_skip_fallback_vote(slot), false, vctx)? + insert_vote_and_create_bls_message(Vote::new_skip_fallback_vote(slot), vctx)? { votes.push(bls_op); } @@ -438,7 +439,7 @@ impl EventHandler { finalized_blocks.insert(block); request_repair(&ctx.repair_event_sender, *my_pubkey, block)?; - Self::check_rootable_blocks( + Self::check_rootable_blocks_and_bank_hash_mismatches( my_pubkey, ctx, vctx, @@ -447,24 +448,24 @@ impl EventHandler { finalized_blocks, received_shred, stats, - )?; + ); - if let Some(slot) = *standstill_slot { - if block.0 > slot { - *standstill_slot = None; - info!( - "{my_pubkey}: Standstill initially detected at slot={slot} has ended \ - at slot={}. Ending timeout extension", - block.0 - ); - } + if let Some(slot) = *standstill_slot + && block.slot > slot + { + *standstill_slot = None; + info!( + "{my_pubkey}: Standstill initially detected at slot={slot} has ended at \ + slot={}. Ending timeout extension", + block.slot + ); } if let Some(parent_block) = Self::add_missing_parent_ready(block, ctx, vctx, local_context) { Self::handle_parent_ready_event( - block.0, + block.slot, parent_block, vctx, ctx, @@ -476,7 +477,7 @@ impl EventHandler { vctx.consensus_metrics_sender .send(( Instant::now(), - vec![ConsensusMetricsEvent::SlotFinalized { slot: block.0 }], + vec![ConsensusMetricsEvent::SlotFinalized { slot: block.slot }], )) .map_err(|_| SendError(()))?; } @@ -520,6 +521,7 @@ impl EventHandler { ); return Err(EventLoopError::SetIdentityError(e)); } + Self::send_vote_history_to_consensus_pool(my_pubkey, vctx)?; } } Ok(votes) @@ -553,7 +555,7 @@ impl EventHandler { vctx: &mut VotingContext, local_context: &mut LocalContext, ) -> Option { - let (slot, block_id) = finalized_block; + let Block { slot, block_id } = finalized_block; let first_slot_of_window = first_of_consecutive_leader_slots(slot); if first_slot_of_window == slot || first_slot_of_window == 0 { // No need to trigger parent ready for the first slot of the window @@ -589,7 +591,10 @@ impl EventHandler { {parent_block_id}", local_context.my_pubkey ); - Some((parent_slot, parent_block_id)) + Some(Block { + slot: parent_slot, + block_id: parent_block_id, + }) } fn handle_set_identity( @@ -605,20 +610,37 @@ impl EventHandler { if *my_pubkey != new_pubkey || vctx.vote_history.node_pubkey != new_pubkey { let my_old_pubkey = vctx.vote_history.node_pubkey; *my_pubkey = new_pubkey; - // The vote history file for the new identity must exist for set-identity to succeed - vctx.vote_history = VoteHistory::restore(ctx.vote_history_storage.as_ref(), my_pubkey)?; + vctx.vote_history = VoteHistory::restore(ctx.vote_history_storage.as_ref(), my_pubkey) + .unwrap_or_else(|_| VoteHistory::new(new_pubkey, 0)); vctx.identity_keypair = new_identity; warn!("set-identity: from {my_old_pubkey} to {my_pubkey}"); } Ok(()) } + fn send_vote_history_to_consensus_pool( + my_pubkey: &Pubkey, + vctx: &mut VotingContext, + ) -> Result<(), VoteError> { + let root = vctx + .vote_history + .root() + .max(vctx.sharable_banks.root().slot()); + let votes = vctx.vote_history.votes_cast_since(root.saturating_sub(1)); + for vote in votes { + info!("{my_pubkey}: Initializing consensus pool with restored vote {vote:?}"); + create_and_send_own_vote_message(vote, vctx, /* respect_wait_to_vote */ false)?; + } + + Ok(()) + } + fn get_block_parent_block(bank: &Bank) -> (Block, Block) { let slot = bank.slot(); - let block = ( + let block = Block { slot, - bank.block_id().expect("Block id must be set upstream"), - ); + block_id: bank.block_id().expect("Block id must be set upstream"), + }; let parent_slot = bank.parent_slot(); let parent_block_id = bank.parent_block_id().unwrap_or_else(|| { // To account for child of genesis and snapshots we insert a @@ -628,7 +650,10 @@ impl EventHandler { trace!("Using default block id for {slot} parent {parent_slot}"); Hash::default() }); - let parent_block = (parent_slot, parent_block_id); + let parent_block = Block { + slot: parent_slot, + block_id: parent_block_id, + }; (block, parent_block) } @@ -641,8 +666,8 @@ impl EventHandler { /// An error returned will cause the voting process to be aborted. fn try_notar( my_pubkey: &Pubkey, - (slot, block_id): Block, - parent_block @ (parent_slot, parent_block_id): Block, + Block { slot, block_id }: Block, + parent_block: Block, pending_blocks: &mut PendingBlocks, voting_context: &mut VotingContext, votes: &mut Vec, @@ -660,20 +685,21 @@ impl EventHandler { return Ok(false); } } else { - if parent_slot.saturating_add(1) != slot { + if parent_block.slot.saturating_add(1) != slot { // Non consecutive return Ok(false); } - if voting_context.vote_history.voted_notar(parent_slot) != Some(parent_block_id) { + if voting_context.vote_history.voted_notar(parent_block.slot) + != Some(parent_block.block_id) + { // Voted skip, or notarize on a different version of the parent return Ok(false); } } info!("{my_pubkey}: Voting notarize for {slot} {block_id}"); - if let Some(bls_op) = generate_vote_message( - Vote::new_notarization_vote(slot, block_id), - false, + if let Some(bls_op) = insert_vote_and_create_bls_message( + Vote::new_notarization_vote(Block { slot, block_id }), voting_context, )? { votes.push(bls_op); @@ -685,7 +711,7 @@ impl EventHandler { )?; pending_blocks.remove(&slot); - Self::try_final(my_pubkey, (slot, block_id), voting_context, votes)?; + Self::try_final(my_pubkey, Block { slot, block_id }, voting_context, votes)?; Ok(true) } @@ -727,29 +753,30 @@ impl EventHandler { /// An error returned will cause the voting process to be aborted. fn try_final( my_pubkey: &Pubkey, - block @ (slot, block_id): Block, + block: Block, voting_context: &mut VotingContext, votes: &mut Vec, ) -> Result { if !voting_context.vote_history.is_block_notarized(&block) - || voting_context.vote_history.its_over(slot) - || voting_context.vote_history.bad_window(slot) + || voting_context.vote_history.its_over(block.slot) + || voting_context.vote_history.bad_window(block.slot) { return Ok(false); } if voting_context .vote_history - .voted_notar(slot) - .is_none_or(|bid| bid != block_id) + .voted_notar(block.slot) + .is_none_or(|bid| bid != block.block_id) { return Ok(false); } - info!("{my_pubkey}: Voting finalize for {slot}"); - if let Some(bls_op) = - generate_vote_message(Vote::new_finalization_vote(slot), false, voting_context)? - { + info!("{my_pubkey}: Voting finalize for {}", block.slot); + if let Some(bls_op) = insert_vote_and_create_bls_message( + Vote::new_finalization_vote(block.slot), + voting_context, + )? { votes.push(bls_op); } Ok(true) @@ -764,10 +791,13 @@ impl EventHandler { // In case we set root in the middle of a leader window, // it's not necessary to vote skip prior to it and we won't // be able to check vote history if we've already voted on it - let root_bank = voting_context.sharable_banks.root(); + let root_slot = voting_context + .vote_history + .root() + .max(voting_context.sharable_banks.root().slot()); // No matter what happens, we should not vote skip for slot 0 let start = first_of_consecutive_leader_slots(slot) - .max(root_bank.slot()) + .max(root_slot) .max(1); for s in start..=last_of_consecutive_leader_slots(slot) { if voting_context.vote_history.voted(s) { @@ -775,7 +805,7 @@ impl EventHandler { } info!("{my_pubkey}: Voting skip for {s}"); if let Some(bls_op) = - generate_vote_message(Vote::new_skip_vote(s), false, voting_context)? + insert_vote_and_create_bls_message(Vote::new_skip_vote(s), voting_context)? { votes.push(bls_op); } @@ -790,15 +820,21 @@ impl EventHandler { voting_context: &mut VotingContext, votes: &mut Vec, ) -> Result<(), VoteError> { + let mut refreshed_votes = Vec::new(); for vote in voting_context .vote_history .votes_cast_since(highest_finalized_slot) { info!("{my_pubkey}: Refreshing vote {vote:?}"); - if let Some(bls_op) = generate_vote_message(vote, true, voting_context)? { - votes.push(bls_op); + if let Some(vote_msg) = generate_refresh_vote_message(vote, voting_context)? { + refreshed_votes.push(Arc::new(vote_msg)); } } + if !refreshed_votes.is_empty() { + votes.push(BLSOp::RefreshVotes { + votes: refreshed_votes, + }); + } Ok(()) } @@ -811,7 +847,10 @@ impl EventHandler { /// - Block has a finalization certificate /// /// If so set root on the highest block that fits these conditions - fn check_rootable_blocks( + /// + /// Additionally check if any of the finalized blocks is frozen with a bank hash mismatch. + /// If so panic as it is unrecoverable. + fn check_rootable_blocks_and_bank_hash_mismatches( my_pubkey: &Pubkey, ctx: &SharedContext, vctx: &mut VotingContext, @@ -820,23 +859,43 @@ impl EventHandler { finalized_blocks: &mut BTreeSet, received_shred: &mut BTreeSet, stats: &mut EventHandlerStats, - ) -> Result<(), BankForksControllerError> { + ) { let bank_forks_r = ctx.bank_forks.read().unwrap(); - let old_root = bank_forks_r.root(); + let old_root = bank_forks_r.root().max(vctx.vote_history.root()); let Some(new_root) = finalized_blocks .iter() - .filter_map(|&(slot, block_id)| { - let bank = bank_forks_r.get(slot)?; - (slot > old_root - && vctx.vote_history.voted(slot) + .filter_map(|&block| { + let bank = bank_forks_r.get(block.slot)?; + + // Check for bank hash mismatch + if bank.is_frozen() + && bank.block_id() == Some(block.block_id) + && let Some(expected_hash) = bank.expected_bank_hash() + && expected_hash != bank.hash() + { + panic!( + "{my_pubkey}: Block {block:?} has been finalized, however we have a bank \ + hash mismatch. The cluster bank hash is {expected_hash} however we \ + computed {}. At this point we will be unable to recover. Ensure that you \ + are running a supported Agave version for this cluster. If this is not \ + operator error,please save a copy of your ledger to share on discord and \ + restart from a snapshot > {}.", + bank.hash(), + block.slot + ); + } + + // Check if this block is rootable + (block.slot > old_root + && vctx.vote_history.voted(block.slot) && bank.is_frozen() - && bank.block_id().is_some_and(|bid| bid == block_id)) - .then_some(slot) + && bank.block_id().is_some_and(|bid| bid == block.block_id)) + .then_some(block.slot) }) .max() else { // No rootable banks - return Ok(()); + return; }; drop(bank_forks_r); root_utils::set_root( @@ -848,9 +907,8 @@ impl EventHandler { pending_blocks, finalized_blocks, received_shred, - )?; + ); stats.set_root(new_root); - Ok(()) } pub(crate) fn join(self) -> thread::Result<()> { @@ -864,14 +922,14 @@ fn request_repair( my_pubkey: Pubkey, block: Block, ) -> Result<(), EventLoopError> { - let (slot, block_id) = block; - let event = RepairEvent::FetchBlock { slot, block_id }; + let event = RepairEvent::FetchBlock { block }; match sender.try_send(event) { Ok(()) => Ok(()), Err(TrySendError::Full(event)) => { error!( "{my_pubkey}: Repair event channel is full, this should not happen. Blocking to \ - send event for slot {slot}" + send event for slot {}", + block.slot ); sender .send(event) @@ -883,28 +941,11 @@ fn request_repair( } } -/// Sends a switch bank event to replay. -fn request_switch( - sender: &SwitchBankEventSender, - my_pubkey: Pubkey, - block: Block, -) -> Result<(), EventLoopError> { - let (slot, block_id) = block; - let event = SwitchBankEvent::Switch { slot, block_id }; - match sender.try_send(event) { - Ok(()) => Ok(()), - Err(TrySendError::Full(event)) => { - error!( - "{my_pubkey}: Switch bank event channel is full, this should not happen. Blocking \ - to send event for slot {slot}" - ); - sender - .send(event) - .map_err(|_| EventLoopError::SenderDisconnected(SendError(()))) - } - Err(TrySendError::Disconnected(_)) => { - Err(EventLoopError::SenderDisconnected(SendError(()))) - } +/// Updates the latest switch-bank request for replay to consume. +fn request_switch(latest: &LatestSwitchRequest, my_pubkey: Pubkey, block: Block) { + let event = SwitchBankEvent::Switch { block }; + if let Some(prev) = latest.try_advance(event) { + trace!("{my_pubkey}: Overwriting previous switch request {prev:?} with ({block:?})"); } } @@ -914,8 +955,7 @@ mod tests { super::*, crate::{ commitment::CommitmentAggregationData, - consensus_metrics::ConsensusMetricsEventReceiver, - event::{LeaderWindowInfo, RepairEventReceiver, SwitchBankEventReceiver}, + event::{LeaderWindowInfo, RepairEventReceiver}, vote_history_storage::{ FileVoteHistoryStorage, SavedVoteHistory, SavedVoteHistoryVersions, VoteHistoryStorage, @@ -924,9 +964,11 @@ mod tests { }, agave_votor_messages::{ consensus_message::{BLS_KEYPAIR_DERIVE_SEED, ConsensusMessage, VoteMessage}, + metric_types::ConsensusMetricsEventReceiver, vote::Vote, + wire::get_vote_payload_to_sign, }, - crossbeam_channel::{Receiver, Sender, TryRecvError, unbounded}, + crossbeam_channel::{Receiver, Sender, TryRecvError, bounded}, parking_lot::RwLock as PlRwLock, solana_bls_signatures::{ keypair::Keypair as BLSKeypair, signature::Signature as BLSSignature, @@ -954,24 +996,23 @@ mod tests { sync::{Arc, RwLock}, time::Instant, }, + tempfile::TempDir, }; struct EventHandlerTestContext { bls_receiver: Receiver, commitment_receiver: Receiver, - own_vote_receiver: Receiver>, + own_vote_receiver: Receiver, bank_forks: Arc>, my_bls_keypair: BLSKeypair, timer_manager: Arc>, leader_window_info_receiver: Receiver, - highest_parent_ready: Arc>, + highest_parent_ready: Arc>, drop_bank_receiver: Receiver>, cluster_info: Arc, consensus_metrics_receiver: ConsensusMetricsEventReceiver, #[allow(dead_code)] // Keep receiver alive to prevent SenderDisconnected errors repair_event_receiver: RepairEventReceiver, - #[allow(dead_code)] - switch_bank_receiver: SwitchBankEventReceiver, shared_context: SharedContext, voting_context: VotingContext, root_context: RootContext, @@ -980,6 +1021,7 @@ mod tests { } struct DirectBankForksController { + my_pubkey: Pubkey, bank_forks: Arc>, blockstore: Arc, leader_schedule_cache: Arc, @@ -991,13 +1033,12 @@ mod tests { Ok(self.bank_forks.write().unwrap().insert(bank)) } - fn set_root( + fn enqueue_set_root( &self, - my_pubkey: Pubkey, parent_slot: Slot, new_root: Slot, highest_super_majority_root: Option, - ) -> Result<(), BankForksControllerError> { + ) { root_utils::check_and_handle_new_root( parent_slot, new_root, @@ -1009,10 +1050,9 @@ mod tests { &self.leader_schedule_cache, &self.bank_forks, None, - &my_pubkey, + &self.my_pubkey, |_| {}, ); - Ok(()) } fn clear_bank(&self, slot: Slot) -> Result<(), BankForksControllerError> { @@ -1027,16 +1067,16 @@ mod tests { } fn setup() -> EventHandlerTestContext { - let (bls_sender, bls_receiver) = unbounded(); - let (commitment_sender, commitment_receiver) = unbounded(); - let (own_vote_sender, own_vote_receiver) = unbounded(); - let (drop_bank_sender, drop_bank_receiver) = unbounded(); + let (bls_sender, bls_receiver) = bounded(1024); + let (commitment_sender, commitment_receiver) = bounded(1024); + let (own_vote_sender, own_vote_receiver) = bounded(1024); + let (drop_bank_sender, drop_bank_receiver) = bounded(1024); let exit = Arc::new(AtomicBool::new(false)); - let (event_sender, _event_receiver) = unbounded(); - let (consensus_metrics_sender, consensus_metrics_receiver) = unbounded(); - let (leader_window_info_sender, leader_window_info_receiver) = unbounded(); - let (repair_event_sender, repair_event_receiver) = unbounded(); - let (switch_bank_sender, switch_bank_receiver) = unbounded(); + let (event_sender, _event_receiver) = bounded(1024); + let (consensus_metrics_sender, consensus_metrics_receiver) = bounded(1024); + let (leader_window_info_sender, leader_window_info_receiver) = bounded(1024); + let (repair_event_sender, repair_event_receiver) = bounded(1024); + let latest_switch_request = LatestSwitchRequest::default(); let timer_manager = Arc::new(PlRwLock::new(TimerManager::new( event_sender, exit, @@ -1080,6 +1120,7 @@ mod tests { &bank_forks.read().unwrap().root_bank(), )); let bank_forks_controller = Arc::new(DirectBankForksController { + my_pubkey: my_node_keypair.pubkey(), bank_forks: bank_forks.clone(), blockstore: blockstore.clone(), leader_schedule_cache, @@ -1095,11 +1136,12 @@ mod tests { blockstore: blockstore.clone(), highest_parent_ready: highest_parent_ready.clone(), repair_event_sender, - switch_bank_sender, + latest_switch_request, }; let vote_history = VoteHistory::new(my_node_keypair.pubkey(), 0); let voting_context = VotingContext { + cluster_info: cluster_info.clone(), identity_keypair: Arc::new(my_node_keypair.insecure_clone()), sharable_banks: bank_forks.read().unwrap().sharable_banks(), vote_history, @@ -1139,7 +1181,6 @@ mod tests { cluster_info, consensus_metrics_receiver, repair_event_receiver, - switch_bank_receiver, highest_parent_ready, shared_context, voting_context, @@ -1337,45 +1378,92 @@ mod tests { fn check_for_votes(&mut self, expected_votes: &[Vote]) { for v in expected_votes { - let expected_vote_serialized = bincode::serialize(v).unwrap(); - let signature: BLSSignature = - self.my_bls_keypair.sign(&expected_vote_serialized).into(); - let expected_message = ConsensusMessage::Vote(VoteMessage { - vote: *v, - rank: 0, - signature, - }); - let prev_length = self.bls_ops.len(); - self.bls_ops.retain(|bls_op| { - !matches!(bls_op, BLSOp::PushVote { message, .. } if **message == expected_message) + let expected_message = self.expected_vote_message(v); + let mut found = false; + self.bls_ops.retain_mut(|bls_op| match bls_op { + BLSOp::PushVote { vote, .. } => { + let keep = vote.as_ref() != &expected_message; + found |= !keep; + keep + } + BLSOp::RefreshVotes { votes } => { + let previous_len = votes.len(); + votes.retain(|vote| vote.as_ref() != &expected_message); + found |= votes.len() != previous_len; + !votes.is_empty() + } + BLSOp::PushCertificates { .. } | BLSOp::RefreshCertificates { .. } => true, }); - assert!( - self.bls_ops.len() < prev_length, - "Did not find expected vote: {expected_message:?}", - ); + assert!(found, "Did not find expected vote: {expected_message:?}"); } } - fn check_for_vote(&mut self, expected_vote: &Vote) { - let expected_vote_serialized = bincode::serialize(expected_vote).unwrap(); - let signature: BLSSignature = - self.my_bls_keypair.sign(&expected_vote_serialized).into(); - let expected_message = ConsensusMessage::Vote(VoteMessage { + fn expected_vote_message(&self, expected_vote: &Vote) -> VoteMessage { + let payload = + get_vote_payload_to_sign(*expected_vote, self.cluster_info.my_shred_version()); + let signature: BLSSignature = self.my_bls_keypair.sign(&payload).into(); + VoteMessage { vote: *expected_vote, rank: 0, signature, + } + } + + fn check_for_vote(&mut self, expected_vote: &Vote) { + let expected_message = self.expected_vote_message(expected_vote); + let mut found = false; + self.bls_ops.retain_mut(|bls_op| match bls_op { + BLSOp::PushVote { vote, .. } => { + let keep = vote.as_ref() != &expected_message; + found |= !keep; + keep + } + BLSOp::RefreshVotes { votes } => { + let previous_len = votes.len(); + votes.retain(|vote| vote.as_ref() != &expected_message); + found |= votes.len() != previous_len; + !votes.is_empty() + } + BLSOp::PushCertificates { .. } | BLSOp::RefreshCertificates { .. } => true, }); - let prev_length = self.bls_ops.len(); - self.bls_ops.retain(|bls_op| { - !matches!(bls_op, BLSOp::PushVote { message, .. } if **message == expected_message) - }); - assert!( - self.bls_ops.len() < prev_length, - "Did not find expected vote: {expected_message:?}", - ); + assert!(found, "Did not find expected vote: {expected_message:?}"); // Also check own_vote_receiver let own_vote = self.own_vote_receiver.try_recv().unwrap(); - assert_eq!(own_vote, vec![expected_message]); + assert_eq!(own_vote, ConsensusMessage::Vote(expected_message)); + } + + fn check_for_own_vote(&self, expected_vote: &Vote) { + let expected_message = self.expected_vote_message(expected_vote); + let own_vote = self.own_vote_receiver.try_recv().unwrap(); + assert_eq!(own_vote, ConsensusMessage::Vote(expected_message)); + } + + fn check_for_own_votes(&self, expected_votes: &[Vote]) { + let mut received_messages = Vec::with_capacity(expected_votes.len()); + for _ in expected_votes { + let ConsensusMessage::Vote(vote) = self.own_vote_receiver.try_recv().unwrap() + else { + panic!("expected own vote"); + }; + received_messages.push(vote); + } + + for expected_vote in expected_votes { + let expected_message = self.expected_vote_message(expected_vote); + let index = received_messages + .iter() + .position(|message| *message == expected_message) + .unwrap_or_else(|| panic!("missing own vote {expected_vote:?}")); + received_messages.remove(index); + } + assert!(received_messages.is_empty()); + } + + fn check_no_own_vote(&self) { + assert_eq!( + self.own_vote_receiver.try_recv().err(), + Some(TryRecvError::Empty) + ); } fn check_for_commitment(&mut self, expected_type: CommitmentType, expected_slot: Slot) { @@ -1444,8 +1532,20 @@ mod tests { // If there is a parent ready for block 1 Notarization is sent out. let slot = 1; let parent_slot = 0; - test_context.send_parent_ready_event(slot, (parent_slot, Hash::default())); - test_context.check_parent_ready_slot((slot, (parent_slot, Hash::default()))); + test_context.send_parent_ready_event( + slot, + Block { + slot: parent_slot, + block_id: Hash::default(), + }, + ); + test_context.check_parent_ready_slot(( + slot, + Block { + slot: parent_slot, + block_id: Hash::default(), + }, + )); let root_bank = test_context .bank_forks .read() @@ -1458,7 +1558,10 @@ mod tests { test_context.check_for_metrics_event(ConsensusMetricsEvent::StartOfSlot { slot }); // We should receive Notarize Vote for block 1 - test_context.check_for_vote(&Vote::new_notarization_vote(slot, block_id_1)); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot, + block_id: block_id_1, + })); test_context.check_for_commitment(CommitmentType::Notarize, slot); // Add block event for 1 again will not trigger another Notarize or commitment test_context.send_block_event(1, bank1.clone()); @@ -1469,7 +1572,10 @@ mod tests { let block_id_2 = bank2.block_id().unwrap(); // Because 2 is middle of window, we should see Notarize vote for block 2 even without parentready - test_context.check_for_vote(&Vote::new_notarization_vote(slot, block_id_2)); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot, + block_id: block_id_2, + })); test_context.check_for_commitment(CommitmentType::Notarize, slot); // Slot 3 somehow links to block 1, should not trigger Notarize vote because it has a wrong parent (not 2) let _ = test_context.create_block_and_send_block_event(3, bank1); @@ -1481,12 +1587,49 @@ mod tests { let block_id_4 = bank4.block_id().unwrap(); // Send parent ready for slot 4 should trigger Notarize vote for slot 4 - test_context.send_parent_ready_event(slot, (2, block_id_2)); - test_context.check_parent_ready_slot((slot, (2, block_id_2))); - test_context.check_for_vote(&Vote::new_notarization_vote(slot, block_id_4)); + test_context.send_parent_ready_event( + slot, + Block { + slot: 2, + block_id: block_id_2, + }, + ); + test_context.check_parent_ready_slot(( + slot, + Block { + slot: 2, + block_id: block_id_2, + }, + )); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot, + block_id: block_id_4, + })); test_context.check_for_commitment(CommitmentType::Notarize, slot); } + #[test] + fn test_restored_parent_ready_sets_timeout() { + let mut test_context = setup(); + let slot = 4; + let parent_block = Block { + slot: 3, + block_id: Hash::new_unique(), + }; + + assert!( + test_context + .voting_context + .vote_history + .add_parent_ready(slot, parent_block) + ); + assert!(!test_context.timer_manager.read().is_timeout_set(slot)); + + test_context.send_parent_ready_event(slot, parent_block); + test_context.check_timeout_set(slot); + assert_eq!(test_context.local_context.stats.timeout_set, 1); + } + #[test] fn test_received_block_notarized_and_timeout() { // Test block notarized event will trigger Finalize vote when all conditions are met @@ -1503,20 +1646,44 @@ mod tests { let block_id_1 = bank1.block_id().unwrap(); // Add parent ready for 0 to trigger notar vote for 1 - test_context.send_parent_ready_event(1, (0, Hash::default())); - test_context.check_parent_ready_slot((1, (0, Hash::default()))); - test_context.check_for_vote(&Vote::new_notarization_vote(1, block_id_1)); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); + test_context.check_parent_ready_slot(( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + )); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + })); test_context.check_for_commitment(CommitmentType::Notarize, 1); // Send block notarized event should trigger Finalize vote - test_context.send_block_notarized_event((1, block_id_1)); + test_context.send_block_notarized_event(Block { + slot: 1, + block_id: block_id_1, + }); test_context.check_for_vote(&Vote::new_finalization_vote(1)); let bank2 = test_context.create_block_and_send_block_event(2, bank1); let block_id_2 = bank2.block_id().unwrap(); // Both Notarize and Finalize votes should trigger for 2 - test_context.check_for_vote(&Vote::new_notarization_vote(2, block_id_2)); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 2, + block_id: block_id_2, + })); test_context.check_for_commitment(CommitmentType::Notarize, 2); - test_context.send_block_notarized_event((2, block_id_2)); + test_context.send_block_notarized_event(Block { + slot: 2, + block_id: block_id_2, + }); test_context.check_for_vote(&Vote::new_finalization_vote(2)); // Create bank3 but do not Notarize, so Finalize vote should not trigger @@ -1526,14 +1693,20 @@ mod tests { // Check no notarization vote for 3 test_context.check_no_vote_or_commitment(); - test_context.send_block_notarized_event((slot, block_id_3)); + test_context.send_block_notarized_event(Block { + slot, + block_id: block_id_3, + }); // Check no Finalize vote for 3 test_context.check_no_vote_or_commitment(); // Now send Block event simulating replay completed for 3 test_context.send_block_event(slot, bank3.clone()); // There should be a notarization vote for 3 - test_context.check_for_vote(&Vote::new_notarization_vote(slot, block_id_3)); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot, + block_id: block_id_3, + })); test_context.check_for_commitment(CommitmentType::Notarize, slot); // Check there is a Finalize vote for 3 test_context.check_for_vote(&Vote::new_finalization_vote(slot)); @@ -1596,33 +1769,63 @@ mod tests { .root(); let bank_1 = test_context.create_block_and_send_block_event(1, root_bank); let block_id_1_old = bank_1.block_id().unwrap(); - test_context.send_parent_ready_event(1, (0, Hash::default())); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); - test_context.check_parent_ready_slot((1, (0, Hash::default()))); - test_context.check_for_vote(&Vote::new_notarization_vote(1, block_id_1_old)); + test_context.check_parent_ready_slot(( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + )); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1_old, + })); test_context.check_for_commitment(CommitmentType::Notarize, 1); // Now we got safe_to_notar event for slot 1 and a different block id let block_id_1_1 = Hash::new_unique(); - test_context.send_safe_to_notar_event((1, block_id_1_1)); + test_context.send_safe_to_notar_event(Block { + slot: 1, + block_id: block_id_1_1, + }); // We should see rest of the window skipped test_context.check_for_vote(&Vote::new_skip_vote(2)); test_context.check_for_vote(&Vote::new_skip_vote(3)); // We should also see notarize fallback for the new block id - test_context.check_for_vote(&Vote::new_notarization_fallback_vote(1, block_id_1_1)); + test_context.check_for_vote(&Vote::new_notarization_fallback_vote(Block { + slot: 1, + block_id: block_id_1_1, + })); // We can trigger safe_to_notar event again for a different block id // In this test you can trigger this any number of times, but the white paper // proved we can only get up to 3 different block ids on a slot, and our // certificate pool implementation checks that. let block_id_1_2 = Hash::new_unique(); - test_context.send_safe_to_notar_event((1, block_id_1_2)); + test_context.send_safe_to_notar_event(Block { + slot: 1, + block_id: block_id_1_2, + }); // No skips this time because we already skipped the rest of the window // We should also see notarize fallback for the new block id - test_context.check_for_vote(&Vote::new_notarization_fallback_vote(1, block_id_1_2)); + test_context.check_for_vote(&Vote::new_notarization_fallback_vote(Block { + slot: 1, + block_id: block_id_1_2, + })); // But getting safe_to_notar for a block id we voted before should be no-op - test_context.send_safe_to_notar_event((1, block_id_1_1)); + test_context.send_safe_to_notar_event(Block { + slot: 1, + block_id: block_id_1_1, + }); test_context.check_no_vote_or_commitment(); } @@ -1639,10 +1842,25 @@ mod tests { .root(); let bank_1 = test_context.create_block_and_send_block_event(1, root_bank); let block_id_1 = bank_1.block_id().unwrap(); - test_context.send_parent_ready_event(1, (0, Hash::default())); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); - test_context.check_parent_ready_slot((1, (0, Hash::default()))); - test_context.check_for_vote(&Vote::new_notarization_vote(1, block_id_1)); + test_context.check_parent_ready_slot(( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + )); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + })); test_context.check_for_commitment(CommitmentType::Notarize, 1); // Now we got safe_to_skip event for slot 1 test_context.send_safe_to_skip_event(1); @@ -1663,7 +1881,14 @@ mod tests { // Produce a full window of blocks // Assume the leader for 1-3 is us, send produce window event - test_context.send_produce_window_event(1, 3, (0, Hash::default())); + test_context.send_produce_window_event( + 1, + 3, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); // Check that leader_window_info is sent via channel let received_leader_window_info = @@ -1672,17 +1897,33 @@ mod tests { assert_eq!(received_leader_window_info.end_slot, 3); assert_eq!( received_leader_window_info.parent_block, - (0, Hash::default()) + Block { + slot: 0, + block_id: Hash::default() + } ); // Suddenly I found out I produced block 1 already, send new produce window event let block_id_1 = Hash::new_unique(); - test_context.send_produce_window_event(2, 3, (1, block_id_1)); + test_context.send_produce_window_event( + 2, + 3, + Block { + slot: 1, + block_id: block_id_1, + }, + ); let received_leader_window_info = test_context.leader_window_info_receiver.try_recv().unwrap(); assert_eq!(received_leader_window_info.start_slot, 2); assert_eq!(received_leader_window_info.end_slot, 3); - assert_eq!(received_leader_window_info.parent_block, (1, block_id_1)); + assert_eq!( + received_leader_window_info.parent_block, + Block { + slot: 1, + block_id: block_id_1 + } + ); } #[test] @@ -1698,13 +1939,34 @@ mod tests { let bank1 = test_context.create_block_and_send_block_event(1, root_bank); let block_id_1 = bank1.block_id().unwrap(); - test_context.send_parent_ready_event(1, (0, Hash::default())); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); - test_context.check_parent_ready_slot((1, (0, Hash::default()))); - test_context.check_for_vote(&Vote::new_notarization_vote(1, block_id_1)); + test_context.check_parent_ready_slot(( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + )); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + })); test_context.check_for_commitment(CommitmentType::Notarize, 1); // Now we got finalized event for slot 1 - test_context.send_finalized_event((1, block_id_1), true); + test_context.send_finalized_event( + Block { + slot: 1, + block_id: block_id_1, + }, + true, + ); // Listen on drop bank receiver, it should get bank 0 let dropped_banks = test_context.drop_bank_receiver.try_recv().unwrap(); assert_eq!(dropped_banks.len(), 1); @@ -1713,6 +1975,47 @@ mod tests { assert_eq!(test_context.bank_forks.read().unwrap().root(), 1); } + #[test] + #[should_panic(expected = "we have a bank hash mismatch")] + fn test_finalized_block_with_bank_hash_mismatch_panics() { + let mut test_context = setup(); + let root_bank = test_context + .bank_forks + .read() + .unwrap() + .sharable_banks() + .root(); + let bank = test_context.create_block_only(1, root_bank); + let block_id = bank.block_id().unwrap(); + let expected_hash = Hash::new_unique(); + bank.set_expected_bank_hash(expected_hash); + + test_context.send_finalized_event(Block { slot: 1, block_id }, true); + } + + #[test] + fn test_finalized_different_block_id_with_bank_hash_mismatch_does_not_panic() { + let mut test_context = setup(); + let root_bank = test_context + .bank_forks + .read() + .unwrap() + .sharable_banks() + .root(); + let bank = test_context.create_block_only(1, root_bank); + let expected_hash = Hash::new_unique(); + bank.set_expected_bank_hash(expected_hash); + let finalized_block_id = Hash::new_unique(); + + test_context.send_finalized_event( + Block { + slot: 1, + block_id: finalized_block_id, + }, + true, + ); + } + #[test] fn test_parent_ready_in_middle_of_window() { let mut test_context = setup(); @@ -1730,21 +2033,45 @@ mod tests { let bank5 = test_context.create_block_and_send_block_event(5, bank4); let block_id_5 = bank5.block_id().unwrap(); - test_context.send_finalized_event((5, block_id_5), true); + test_context.send_finalized_event( + Block { + slot: 5, + block_id: block_id_5, + }, + true, + ); // We should now have parent ready for slot 5 - test_context.check_parent_ready_slot((5, (4, block_id_4))); + test_context.check_parent_ready_slot(( + 5, + Block { + slot: 4, + block_id: block_id_4, + }, + )); // We are partitioned off from rest of the network, and suddenly received finalize for // slot 9 a little before we finished replay slot 9 let bank9 = test_context.create_block_only(9, bank5); let block_id_9 = bank9.block_id().unwrap(); - test_context.send_finalized_event((9, block_id_9), true); + test_context.send_finalized_event( + Block { + slot: 9, + block_id: block_id_9, + }, + true, + ); test_context.send_block_event(9, bank9); // We should now have parent ready for slot 9 - test_context.check_parent_ready_slot((9, (5, block_id_5))); + test_context.check_parent_ready_slot(( + 9, + Block { + slot: 5, + block_id: block_id_5, + }, + )); } #[test] @@ -1760,9 +2087,18 @@ mod tests { .root(); let bank1 = test_context.create_block_and_send_block_event(1, root_bank); let block_id_1 = bank1.block_id().unwrap(); - test_context.send_parent_ready_event(1, (0, Hash::default())); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); - test_context.check_for_vote(&Vote::new_notarization_vote(1, block_id_1)); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + })); test_context.send_timeout_event(2); test_context.check_for_vote(&Vote::new_skip_vote(2)); @@ -1772,13 +2108,22 @@ mod tests { test_context.send_standstill_event(0); test_context.check_for_votes(&[ - Vote::new_notarization_vote(1, block_id_1), + Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + }), Vote::new_skip_vote(2), Vote::new_skip_vote(3), ]); // Finalize block 1, should deactivate standstill - test_context.send_finalized_event((1, block_id_1), true); + test_context.send_finalized_event( + Block { + slot: 1, + block_id: block_id_1, + }, + true, + ); assert!(test_context.local_context.standstill_slot.is_none()); // Send another standstill event with highest finalized at 1, we should refresh votes for 2 and 3 only @@ -1787,6 +2132,40 @@ mod tests { test_context.check_for_votes(&[Vote::new_skip_vote(2), Vote::new_skip_vote(3)]); } + #[test] + fn test_startup_replays_vote_history_to_consensus_pool() { + let mut test_context = setup(); + let notarize_vote = Vote::new_notarization_vote(Block { + slot: 1, + block_id: Hash::new_unique(), + }); + let skip_vote = Vote::new_skip_vote(2); + let fallback_vote = Vote::new_skip_fallback_vote(2); + test_context + .voting_context + .vote_history + .add_vote(notarize_vote); + test_context.voting_context.vote_history.add_vote(skip_vote); + test_context + .voting_context + .vote_history + .add_vote(fallback_vote); + + EventHandler::send_vote_history_to_consensus_pool( + &test_context.local_context.my_pubkey, + &mut test_context.voting_context, + ) + .unwrap(); + + test_context.check_for_own_votes(&[notarize_vote, skip_vote, fallback_vote]); + test_context.check_no_own_vote(); + assert!(test_context.bls_ops.is_empty()); + assert_eq!( + test_context.bls_receiver.try_recv().err(), + Some(TryRecvError::Empty) + ); + } + #[test] fn test_received_set_identity() { let mut test_context = setup(); @@ -1806,7 +2185,13 @@ mod tests { .sharable_banks() .root(); let _ = test_context.create_block_and_send_block_event(1, root_bank.clone()); - test_context.send_parent_ready_event(1, (0, Hash::default())); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); // There should be no votes but we should see commitments for hot spares assert_eq!( @@ -1823,8 +2208,17 @@ mod tests { let slot = 4; let bank4 = test_context.create_block_and_send_block_event(slot, root_bank); let block_id_4 = bank4.block_id().unwrap(); - test_context.send_parent_ready_event(slot, (0, Hash::default())); - test_context.check_for_vote(&Vote::new_notarization_vote(slot, block_id_4)); + test_context.send_parent_ready_event( + slot, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot, + block_id: block_id_4, + })); test_context.check_for_commitment(CommitmentType::Notarize, slot); for file in files_to_remove { @@ -1832,6 +2226,50 @@ mod tests { } } + #[test] + fn test_set_identity_replays_restored_vote_history_to_consensus_pool() { + let mut test_context = setup(); + let old_identity = test_context.cluster_info.keypair().insecure_clone(); + let new_identity = Keypair::new(); + let temp_dir = TempDir::new().unwrap(); + let vote_history_storage = + Arc::new(FileVoteHistoryStorage::new(temp_dir.path().to_path_buf())); + test_context.shared_context.vote_history_storage = vote_history_storage.clone(); + + let new_vote_history = VoteHistory::new(new_identity.pubkey(), 0); + let saved_vote_history = SavedVoteHistory::new(&new_vote_history, &new_identity).unwrap(); + vote_history_storage + .store(&SavedVoteHistoryVersions::from(saved_vote_history)) + .unwrap(); + + let restored_vote = Vote::new_notarization_vote(Block { + slot: 1, + block_id: Hash::new_unique(), + }); + let mut old_vote_history = VoteHistory::new(old_identity.pubkey(), 0); + old_vote_history.add_vote(restored_vote); + let saved_vote_history = SavedVoteHistory::new(&old_vote_history, &old_identity).unwrap(); + vote_history_storage + .store(&SavedVoteHistoryVersions::from(saved_vote_history)) + .unwrap(); + + test_context + .cluster_info + .set_keypair(Arc::new(new_identity.insecure_clone())); + test_context.send_set_identity_event(); + test_context.check_no_own_vote(); + + test_context + .cluster_info + .set_keypair(Arc::new(old_identity.insecure_clone())); + test_context.send_set_identity_event(); + + assert_eq!(test_context.voting_context.vote_history, old_vote_history); + test_context.check_for_own_vote(&restored_vote); + test_context.check_no_own_vote(); + assert!(test_context.bls_ops.is_empty()); + } + #[test] fn test_standstill_slot_tracking() { let mut test_context = setup(); @@ -1848,8 +2286,17 @@ mod tests { .root(); let bank1 = test_context.create_block_and_send_block_event(1, root_bank); let block_id_1 = bank1.block_id().unwrap(); - test_context.send_parent_ready_event(1, (0, Hash::default())); - test_context.check_for_vote(&Vote::new_notarization_vote(1, block_id_1)); + test_context.send_parent_ready_event( + 1, + Block { + slot: 0, + block_id: Hash::default(), + }, + ); + test_context.check_for_vote(&Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + })); // Send standstill event - should record the standstill slot test_context.send_standstill_event(0); @@ -1865,7 +2312,13 @@ mod tests { assert_eq!(test_context.local_context.standstill_slot, Some(0)); // Send a finalized event - should reset standstill_slot - test_context.send_finalized_event((1, block_id_1), false); + test_context.send_finalized_event( + Block { + slot: 1, + block_id: block_id_1, + }, + false, + ); assert!(test_context.local_context.standstill_slot.is_none()); } } diff --git a/votor/src/event_handler/stats.rs b/votor/src/event_handler/stats.rs index 028aa738692..d6e12eedc81 100644 --- a/votor/src/event_handler/stats.rs +++ b/votor/src/event_handler/stats.rs @@ -1,6 +1,6 @@ use { crate::{event::VotorEvent, voting_service::BLSOp}, - agave_votor_messages::{consensus_message::ConsensusMessage, vote::VoteType}, + agave_votor_messages::vote::VoteType, solana_clock::Slot, solana_metrics::datapoint_info, std::{ @@ -154,8 +154,8 @@ impl EventHandlerStats { let entry = self.slot_tracking_map.entry(*slot).or_default(); entry.parent_ready = Some(Instant::now()); } - VotorEvent::Finalized((slot, _), is_fast_finalization) => { - let entry = self.slot_tracking_map.entry(*slot).or_default(); + VotorEvent::Finalized(block, is_fast_finalization) => { + let entry = self.slot_tracking_map.entry(block.slot).or_default(); if entry.finalized.is_none() { entry.finalized = Some((Instant::now(), *is_fast_finalization)); } else if *is_fast_finalization { @@ -185,23 +185,23 @@ impl EventHandlerStats { } pub fn incr_vote(&mut self, bls_op: &BLSOp) { - if let BLSOp::PushVote { message, .. } = bls_op { - let ConsensusMessage::Vote(vote) = **message else { - warn!("Unexpected BLS message type: {message:?}"); - return; - }; - let vote_type = vote.vote.get_type(); - let entry = self.sent_votes.entry(vote_type).or_insert(0); - *entry = entry.saturating_add(1); - if vote_type == VoteType::Notarize { - let entry = self.slot_tracking_map.entry(vote.vote.slot()).or_default(); - entry.vote_notarize = Some(Instant::now()); - } else if vote_type == VoteType::Skip { - let entry = self.slot_tracking_map.entry(vote.vote.slot()).or_default(); - entry.vote_skip = Some(Instant::now()); + match bls_op { + BLSOp::PushVote { vote, .. } => { + let vote_type = vote.vote.get_type(); + let entry = self.sent_votes.entry(vote_type).or_insert(0); + *entry = entry.saturating_add(1); + if vote_type == VoteType::Notarize { + let entry = self.slot_tracking_map.entry(vote.vote.slot()).or_default(); + entry.vote_notarize = Some(Instant::now()); + } else if vote_type == VoteType::Skip { + let entry = self.slot_tracking_map.entry(vote.vote.slot()).or_default(); + entry.vote_skip = Some(Instant::now()); + } + } + BLSOp::RefreshVotes { .. } => (), + _ => { + warn!("Unexpected BLS operation: {bls_op:?}"); } - } else { - warn!("Unexpected BLS operation: {bls_op:?}"); } } diff --git a/votor/src/lib.rs b/votor/src/lib.rs index 5cb854b7c87..7a1161e4bb4 100644 --- a/votor/src/lib.rs +++ b/votor/src/lib.rs @@ -9,10 +9,8 @@ pub mod common; pub mod consensus_metrics; pub mod consensus_pool; mod consensus_pool_service; -pub mod consensus_rewards; pub mod event; mod event_handler; -pub mod generated_cert_types; pub mod root_utils; mod staked_validators_cache; mod timer_manager; diff --git a/votor/src/root_utils.rs b/votor/src/root_utils.rs index 80091ffc84a..f98f468a532 100644 --- a/votor/src/root_utils.rs +++ b/votor/src/root_utils.rs @@ -1,5 +1,10 @@ use { - crate::{event_handler::PendingBlocks, voting_utils::VotingContext, votor::SharedContext}, + crate::{ + commitment::{CommitmentType, update_commitment_cache}, + event_handler::PendingBlocks, + voting_utils::VotingContext, + votor::SharedContext, + }, agave_votor_messages::consensus_message::Block, crossbeam_channel::Sender, solana_clock::Slot, @@ -11,10 +16,8 @@ use { rpc_subscriptions::RpcSubscriptions, }, solana_runtime::{ - bank_forks::BankForks, - bank_forks_controller::{BankForksController, BankForksControllerError}, - installed_scheduler_pool::BankWithScheduler, - snapshot_controller::SnapshotController, + bank_forks::BankForks, bank_forks_controller::BankForksController, + installed_scheduler_pool::BankWithScheduler, snapshot_controller::SnapshotController, }, solana_time_utils::timestamp, std::{ @@ -41,15 +44,18 @@ pub(crate) fn set_root( pending_blocks: &mut PendingBlocks, finalized_blocks: &mut BTreeSet, received_shred: &mut BTreeSet, -) -> Result<(), BankForksControllerError> { +) { info!("{my_pubkey}: setting root {new_root}"); vctx.vote_history.set_root(new_root); *pending_blocks = pending_blocks.split_off(&new_root); - *finalized_blocks = finalized_blocks.split_off(&(new_root, Hash::default())); + *finalized_blocks = finalized_blocks.split_off(&Block { + slot: new_root, + block_id: Hash::default(), + }); *received_shred = received_shred.split_off(&new_root); rctx.bank_forks_controller - .set_root(*my_pubkey, new_root, new_root, Some(new_root))?; + .enqueue_set_root(new_root, new_root, Some(new_root)); // Distinguish between duplicate versions of same slot let hash = ctx.bank_forks.read().unwrap().bank_hash(new_root).unwrap(); @@ -57,10 +63,13 @@ pub(crate) fn set_root( ctx.blockstore .insert_optimistic_slot(new_root, &hash, timestamp().try_into().unwrap()) { - error!( - "failed to record optimistic slot in blockstore: slot={}: {:?}", - new_root, &e - ); + error!("failed to record optimistic slot in blockstore: slot={new_root}: {e:?}"); + } + + if let Err(err) = + update_commitment_cache(CommitmentType::Rooted, new_root, &vctx.commitment_sender) + { + warn!("failed to update Alpenglow rooted commitment for root {new_root}: {err}"); } // It is critical to send the OC notification in order to keep compatibility with @@ -78,7 +87,6 @@ pub(crate) fn set_root( dependency_work, )); } - Ok(()) } /// Sets the new root, additionally performs the callback after setting the bank forks root diff --git a/votor/src/staked_validators_cache.rs b/votor/src/staked_validators_cache.rs index 0912b78690c..a0eb00e3c5c 100644 --- a/votor/src/staked_validators_cache.rs +++ b/votor/src/staked_validators_cache.rs @@ -106,12 +106,7 @@ impl StakedValidatorsCache { let mut nodes: Vec<_> = epoch_staked_nodes .iter() - .filter(|(pubkey, stake)| { - let positive_stake = **stake > 0; - let not_self = pubkey != &&cluster_info.id(); - - positive_stake && (self.include_self || not_self) - }) + .filter(|(pubkey, _stake)| self.include_self || pubkey != &&cluster_info.id()) .filter_map(|(pubkey, stake)| { cluster_info.lookup_contact_info(pubkey, |node| { node.alpenglow().map(|alpenglow_socket| Node { @@ -161,18 +156,16 @@ impl StakedValidatorsCache { ) -> (&[SocketAddr], bool) { // Check if self.alpenglow_port_override has a different last_modified. // Immediately refresh the cache if it does. - if let Some(alpenglow_port_override) = &self.alpenglow_port_override { - if alpenglow_port_override.has_new_override(self.alpenglow_port_override_last_modified) - { - self.alpenglow_port_override_last_modified = - alpenglow_port_override.last_modified(); - trace!( - "refreshing cache entry for epoch {} due to alpenglow port override \ - last_modified change", - self.cur_epoch(slot) - ); - self.refresh_cache_entry(self.cur_epoch(slot), cluster_info, access_time); - } + if let Some(alpenglow_port_override) = &self.alpenglow_port_override + && alpenglow_port_override.has_new_override(self.alpenglow_port_override_last_modified) + { + self.alpenglow_port_override_last_modified = alpenglow_port_override.last_modified(); + trace!( + "refreshing cache entry for epoch {} due to alpenglow port override last_modified \ + change", + self.cur_epoch(slot) + ); + self.refresh_cache_entry(self.cur_epoch(slot), cluster_info, access_time); } self.get_staked_validators_by_epoch(self.cur_epoch(slot), cluster_info, access_time) diff --git a/votor/src/timer_manager.rs b/votor/src/timer_manager.rs index 89a0930e6a8..a0b5e947ff3 100644 --- a/votor/src/timer_manager.rs +++ b/votor/src/timer_manager.rs @@ -61,16 +61,16 @@ impl TimerManager { &self, slot: Slot, standstill_slot: Option, - delta_first_slice: Duration, + delta_first_fec_set: Duration, delta_block: Duration, - ) { + ) -> bool { self.timers.write().set_timeouts( slot, Instant::now(), standstill_slot, - delta_first_slice, + delta_first_fec_set, delta_block, - ); + ) } pub(crate) fn join(self) { @@ -86,28 +86,26 @@ impl TimerManager { #[cfg(test)] mod tests { use { - super::*, - crate::{common::DELTA_FIRST_SLICE, event::VotorEvent}, - crossbeam_channel::unbounded, - solana_clock::DEFAULT_MS_PER_SLOT, - std::time::Duration, + super::*, crate::event::VotorEvent, crossbeam_channel::bounded, + solana_clock::DEFAULT_MS_PER_SLOT, std::time::Duration, }; #[test] fn test_timer_manager() { - let (event_sender, event_receiver) = unbounded(); + let (event_sender, event_receiver) = bounded(1024); let exit = Arc::new(AtomicBool::new(false)); let timer_manager = TimerManager::new( event_sender, exit.clone(), Arc::new(MigrationStatus::post_migration_status()), ); - let delta_first_slice = DELTA_FIRST_SLICE; let delta_block = Duration::from_millis(DEFAULT_MS_PER_SLOT); + let delta_first_fec_set = delta_block; let slot = 52; let start = Instant::now(); - timer_manager.set_timeouts(slot, None, delta_first_slice, delta_block); - // Should see two timeouts at delta_block and DELTA_TIMEOUT + assert!(timer_manager.set_timeouts(slot, None, delta_first_fec_set, delta_block)); + assert!(!timer_manager.set_timeouts(slot, None, delta_first_fec_set, delta_block)); + // Should see the first two timeout events at DELTA_TIMEOUT + delta_block. let mut timeouts_received = 0; while timeouts_received < 2 && Instant::now().duration_since(start) < Duration::from_secs(2) { @@ -125,7 +123,7 @@ mod tests { assert_eq!(s, slot); assert!( Instant::now().duration_since(start) - >= DELTA_TIMEOUT + DELTA_FIRST_SLICE + >= DELTA_TIMEOUT + delta_first_fec_set ); timeouts_received += 1; } diff --git a/votor/src/timer_manager/timers.rs b/votor/src/timer_manager/timers.rs index 7b94b5efabd..fa5a07ac9ce 100644 --- a/votor/src/timer_manager/timers.rs +++ b/votor/src/timer_manager/timers.rs @@ -1,7 +1,8 @@ use { crate::{event::VotorEvent, timer_manager::stats::TimerManagerStats}, crossbeam_channel::Sender, - solana_clock::{NUM_CONSECUTIVE_LEADER_SLOTS, Slot}, + solana_clock::Slot, + solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, solana_runtime::leader_schedule_utils::last_of_consecutive_leader_slots, std::{ cmp::Reverse, @@ -11,7 +12,7 @@ use { }; /// Dynamic skip timeouts can be increased to a maximum of 1hour. -const MAX_TIMEOUT_SECS: f64 = 3600.0; +const MAX_TIMEOUT: Duration = Duration::from_secs(3600); /// Calculate the timeout multiplier based on standstill state. /// Returns 1.0 if not in standstill, or 1.05^n where n is the number of @@ -22,26 +23,48 @@ fn calculate_timeout_multiplier(slot: Slot, standstill_slot: Option) -> f6 Some(standstill_slot) => { // Calculate number of leader windows since standstill let slots_since_standstill = slot.saturating_sub(standstill_slot); - let leader_windows = slots_since_standstill / NUM_CONSECUTIVE_LEADER_SLOTS; + let leader_windows = slots_since_standstill + .checked_div(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot) + .expect("NUM_CONSECUTIVE_LEADER_SLOTS is non-zero"); // Extend timeout by 5% for each leader window 1.05_f64.powi(leader_windows as i32) } } } +#[derive(Clone)] +struct TimeoutConfig { + delta_timeout: Duration, + delta_first_fec_set: Duration, + delta_block: Duration, +} + +impl TimeoutConfig { + fn scaled(&self, slot: Slot, standstill_slot: Option) -> TimeoutConfig { + let multiplier = calculate_timeout_multiplier(slot, standstill_slot); + let delta_timeout = Duration::try_from_secs_f64( + (self.delta_timeout.as_secs_f64() * multiplier).clamp(0.0, MAX_TIMEOUT.as_secs_f64()), + ) + .unwrap_or(MAX_TIMEOUT); + TimeoutConfig { + delta_timeout, + delta_first_fec_set: self.delta_first_fec_set, + delta_block: self.delta_block, + } + } +} + /// Encodes a basic state machine of the different stages involved in handling /// timeouts for a window of slots. enum TimerState { - /// Waiting for initial DELTA_TIMEOUT + DELTA_FIRST_SLICE stage. - WaitForFirstSlice { + /// Waiting for initial DELTA_TIMEOUT + delta_first_fec_set stage. + WaitForFirstFecSet { /// The slots in the window. Must not be empty. window: VecDeque, /// Time when this stage will end. timeout: Instant, - /// The maximum allowed time for producing the first slice. - delta_first_slice: Duration, - /// Protocol slot time, used in [`TimerState::WaitForBlock`]. - delta_block: Duration, + /// Timeout constituent parameters. + timeout_config: TimeoutConfig, }, /// Waiting for DELTA_TIMEOUT + i * DELTA_BLOCK for each block i in window. WaitForBlock { @@ -49,8 +72,8 @@ enum TimerState { window: VecDeque, /// Time when this stage will end. timeout: Instant, - /// Protocol slot time. - delta_block: Duration, + /// Timeout constituent parameters. + timeout_config: TimeoutConfig, }, /// The state machine is done. Done, @@ -60,37 +83,26 @@ impl TimerState { /// Creates a new instance of the state machine. /// /// The `timeout_multiplier` extends only the network-DELTA-derived timeout - /// (`delta_timeout`). `delta_first_slice` and `delta_block` are protocol + /// (`delta_timeout`). `delta_first_fec_set` and `delta_block` are protocol /// pacing, not a function of network delays, so they are not scaled. Also /// returns the next time the timer should fire. - fn new( - slot: Slot, - delta_timeout: Duration, - delta_first_slice: Duration, - delta_block: Duration, - now: Instant, - timeout_multiplier: f64, - ) -> (Self, Instant) { + fn new(slot: Slot, timeout_config: TimeoutConfig, now: Instant) -> (Self, Instant) { let window = (slot..=last_of_consecutive_leader_slots(slot)).collect::>(); assert!(!window.is_empty()); - let scaled_delta_timeout = Duration::from_secs_f64( - (delta_timeout.as_secs_f64() * timeout_multiplier).min(MAX_TIMEOUT_SECS), - ); - // A correct leader may take up to `DELTA_FIRST_SLICE` to send their first slice, + // A correct leader may take up to `delta_first_fec_set` to send their first FEC set, // so the earliest sound point to declare them crashed is - // `DELTA_FIRST_SLICE + delta_timeout` after their window starts. + // `delta_first_fec_set + delta_timeout` after their window starts. let timeout = now - .checked_add(scaled_delta_timeout) + .checked_add(timeout_config.delta_timeout) .unwrap() - .checked_add(delta_first_slice) + .checked_add(timeout_config.delta_first_fec_set) .unwrap(); ( - Self::WaitForFirstSlice { + Self::WaitForFirstFecSet { window, timeout, - delta_first_slice, - delta_block, + timeout_config: timeout_config.clone(), }, timeout, ) @@ -101,11 +113,10 @@ impl TimerState { /// Returns a potentially empty list of events that should be sent. fn progress(&mut self, now: Instant) -> Option { match self { - Self::WaitForFirstSlice { + Self::WaitForFirstFecSet { window, timeout, - delta_first_slice, - delta_block, + timeout_config, } => { assert!(!window.is_empty()); if &now < timeout { @@ -113,23 +124,23 @@ impl TimerState { } let slot = *window.front().unwrap(); // Slot 0's block deadline is `T + delta_block + scaled_delta_timeout`; - // subtract the `delta_first_slice` paid up-front to `WaitForFirstSlice`. + // subtract the `delta_first_fec_set` paid up-front to `WaitForFirstFecSet`. let new_timeout = timeout - .checked_add(*delta_block) + .checked_add(timeout_config.delta_block) .unwrap() - .checked_sub(*delta_first_slice) + .checked_sub(timeout_config.delta_first_fec_set) .unwrap(); *self = Self::WaitForBlock { window: window.to_owned(), timeout: new_timeout, - delta_block: *delta_block, + timeout_config: timeout_config.clone(), }; Some(VotorEvent::TimeoutCrashedLeader(slot)) } Self::WaitForBlock { window, timeout, - delta_block, + timeout_config, } => { assert!(!window.is_empty()); if &now < timeout { @@ -140,7 +151,7 @@ impl TimerState { match window.front() { None => *self = Self::Done, Some(_next_slot) => { - *timeout = timeout.checked_add(*delta_block).unwrap(); + *timeout = timeout.checked_add(timeout_config.delta_block).unwrap(); } } ret @@ -152,7 +163,7 @@ impl TimerState { /// When would this state machine next be able to make progress. fn next_fire(&self) -> Option { match self { - Self::WaitForFirstSlice { timeout, .. } | Self::WaitForBlock { timeout, .. } => { + Self::WaitForFirstFecSet { timeout, .. } | Self::WaitForBlock { timeout, .. } => { Some(*timeout) } Self::Done => None, @@ -192,19 +203,18 @@ impl Timers { slot: Slot, now: Instant, standstill_slot: Option, - delta_first_slice: Duration, + delta_first_fec_set: Duration, delta_block: Duration, - ) { + ) -> bool { + assert!(delta_first_fec_set <= delta_block); assert_eq!(self.heap.len(), self.timers.len()); - let timeout_multiplier = calculate_timeout_multiplier(slot, standstill_slot); - let (timer, next_fire) = TimerState::new( - slot, - self.delta_timeout, - delta_first_slice, + let timeout_config = TimeoutConfig { + delta_timeout: self.delta_timeout, + delta_first_fec_set, delta_block, - now, - timeout_multiplier, - ); + } + .scaled(slot, standstill_slot); + let (timer, next_fire) = TimerState::new(slot, timeout_config, now); // It is possible that this slot already has a timer set e.g. if there // are multiple ParentReady for the same slot. Do not insert new timer then. let mut new_timer_inserted = false; @@ -215,6 +225,7 @@ impl Timers { }); self.stats .incr_timeout_count_with_heap_size(self.heap.len(), new_timer_inserted); + new_timer_inserted } /// Call to make progress on the timer states. If there are still active @@ -264,9 +275,7 @@ impl Timers { #[cfg(test)] mod tests { use { - super::*, - crate::common::{DELTA_FIRST_SLICE, DELTA_TIMEOUT}, - crossbeam_channel::unbounded, + super::*, crate::common::DELTA_TIMEOUT, crossbeam_channel::bounded, solana_clock::DEFAULT_MS_PER_SLOT, }; @@ -275,8 +284,13 @@ mod tests { let one_micro = Duration::from_micros(1); let now = Instant::now(); let slot = 0; - let (mut timer_state, next_fire) = - TimerState::new(slot, one_micro, one_micro, one_micro, now, 1.0); + let timeout_config = TimeoutConfig { + delta_timeout: one_micro, + delta_first_fec_set: one_micro, + delta_block: one_micro, + } + .scaled(slot, None); + let (mut timer_state, next_fire) = TimerState::new(slot, timeout_config, now); assert!(matches!( timer_state.progress(next_fire).unwrap(), @@ -317,18 +331,13 @@ mod tests { fn timers_progress() { let one_micro = Duration::from_micros(1); let mut now = Instant::now(); - let (sender, receiver) = unbounded(); + let (sender, receiver) = bounded(1024); let mut timers = Timers::new(one_micro, sender); assert!(timers.progress(now).is_none()); assert!(receiver.try_recv().unwrap_err().is_empty()); + let delta_block = Duration::from_millis(DEFAULT_MS_PER_SLOT); - timers.set_timeouts( - 0, - now, - None, - DELTA_FIRST_SLICE, - Duration::from_millis(DEFAULT_MS_PER_SLOT), - ); + timers.set_timeouts(0, now, None, delta_block, delta_block); while timers.progress(now).is_some() { now = now.checked_add(one_micro).unwrap(); } @@ -349,28 +358,67 @@ mod tests { assert_eq!(stats.max_heap_size(), 1); } + #[test] + fn default_timeout_schedule_matches_slot_pacing() { + let now = Instant::now(); + let delta_block = Duration::from_millis(DEFAULT_MS_PER_SLOT); + assert_eq!(delta_block, Duration::from_millis(400)); + + let timeout_config = TimeoutConfig { + delta_timeout: DELTA_TIMEOUT, + delta_first_fec_set: delta_block, + delta_block, + } + .scaled(0, None); + let (mut timer_state, next_fire) = TimerState::new(0, timeout_config, now); + + let first_deadline = now + Duration::from_millis(800); + assert_eq!(next_fire, first_deadline); + assert!(matches!( + timer_state.progress(first_deadline).unwrap(), + VotorEvent::TimeoutCrashedLeader(0) + )); + + assert_eq!(timer_state.next_fire().unwrap(), first_deadline); + assert!(matches!( + timer_state.progress(first_deadline).unwrap(), + VotorEvent::Timeout(0) + )); + + let second_deadline = now + Duration::from_millis(1200); + assert_eq!(timer_state.next_fire().unwrap(), second_deadline); + assert!(matches!( + timer_state.progress(second_deadline).unwrap(), + VotorEvent::Timeout(1) + )); + + assert_eq!( + timer_state.next_fire().unwrap(), + now + Duration::from_millis(1600) + ); + } + #[test] fn timer_state_with_multiplier() { // Test that timeout multiplier correctly extends the timeout duration let delta_timeout = Duration::from_millis(100); - let delta_first_slice = Duration::from_millis(10); + let delta_first_fec_set = Duration::from_millis(10); let delta_block = Duration::from_millis(50); let now = Instant::now(); let slot = 0; - let multiplier = 1.5; // 50% longer timeouts - let (mut timer_state, next_fire) = TimerState::new( - slot, + let timeout_config = TimeoutConfig { delta_timeout, - delta_first_slice, + delta_first_fec_set, delta_block, - now, - multiplier, - ); + } + // 1 leader window standstill -> 1.05x multiplier + .scaled(NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot, Some(0)); + let (mut timer_state, next_fire) = TimerState::new(slot, timeout_config, now); - // The first timeout should fire at now + (delta_timeout * 1.5) + delta_first_slice - // = now + 150ms + delta_first_slice - let expected_first_fire = now + Duration::from_millis(150) + delta_first_slice; + // The first timeout should fire at now + (delta_timeout * 1.05) + delta_first_fec_set + // = now + 105ms + delta_first_fec_set + let expected_first_fire = now + Duration::from_millis(105) + delta_first_fec_set; assert!( next_fire >= expected_first_fire - Duration::from_micros(100) && next_fire <= expected_first_fire + Duration::from_micros(100), @@ -384,9 +432,9 @@ mod tests { )); // Slot 0's block deadline is `now + delta_block + scaled_delta_timeout` - // = now + 50ms + 150ms = now + 200ms (unaffected by delta_first_slice). + // = now + 50ms + 105ms = now + 155ms (unaffected by delta_first_fec_set). let next = timer_state.next_fire().unwrap(); - let expected_next = now + Duration::from_millis(200); + let expected_next = now + Duration::from_millis(155); let skew = if next >= expected_next { next - expected_next } else { @@ -428,34 +476,32 @@ mod tests { #[test] fn timer_state_caps_at_max_timeout() { let now = Instant::now(); - // Use a large multiplier that would exceed MAX_TIMEOUT - let multiplier = 1000000.0; let delta_block = Duration::from_millis(DEFAULT_MS_PER_SLOT); + let delta_first_fec_set = delta_block; - let (mut timer_state, next_fire) = TimerState::new( - 100, - DELTA_TIMEOUT, - DELTA_FIRST_SLICE, + let timeout_config = TimeoutConfig { + delta_timeout: DELTA_TIMEOUT, + delta_first_fec_set, delta_block, - now, - multiplier, - ); + } + // ensure a large multiplier that would exceed MAX_TIMEOUT + .scaled(1000, Some(0)); + let (mut timer_state, next_fire) = TimerState::new(1000, timeout_config, now); - // The first timeout should be capped at MAX_TIMEOUT (+ unscaled DELTA_FIRST_SLICE). - let expected_first_fire = - now + Duration::from_secs(MAX_TIMEOUT_SECS as u64) + DELTA_FIRST_SLICE; + // The first timeout should be capped at MAX_TIMEOUT (+ unscaled delta_first_fec_set). + let expected_first_fire = now + MAX_TIMEOUT + delta_first_fec_set; assert_eq!(next_fire, expected_first_fire); // Progress the timer to get TimeoutCrashedLeader assert!(matches!( timer_state.progress(next_fire).unwrap(), - VotorEvent::TimeoutCrashedLeader(100) + VotorEvent::TimeoutCrashedLeader(1000) )); // Slot 0's block deadline is `T + delta_block + scaled_delta_timeout`, so - // the gap from the (DELTA_FIRST_SLICE-shifted) first fire is `delta_block - DELTA_FIRST_SLICE`. + // the gap from the (delta_first_fec_set-shifted) first fire is `delta_block - delta_first_fec_set`. let next = timer_state.next_fire().unwrap(); let actual_delta = next - next_fire; - assert_eq!(actual_delta, delta_block - DELTA_FIRST_SLICE); + assert_eq!(actual_delta, delta_block - delta_first_fec_set); } } diff --git a/votor/src/vote_history.rs b/votor/src/vote_history.rs index 877cc7337ad..bf6199291fd 100644 --- a/votor/src/vote_history.rs +++ b/votor/src/vote_history.rs @@ -2,17 +2,21 @@ use { super::vote_history_storage::{ Result, SavedVoteHistory, SavedVoteHistoryVersions, VoteHistoryStorage, }, - agave_votor_messages::{consensus_message::Block, vote::Vote}, - serde::{Deserialize, Serialize}, + agave_votor_messages::{consensus_message::Block, vote::Vote, wire::VotePayloadToSign}, solana_clock::Slot, solana_hash::Hash, solana_keypair::Keypair, solana_pubkey::Pubkey, std::collections::{HashMap, HashSet, hash_map::Entry}, thiserror::Error, + wincode::{SchemaRead, SchemaWrite}, }; -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +// Dummy shred version used for persisted votes - will be re-signed with the +// correct shred version when performing the refresh +const VOTE_HISTORY_SHRED_VERSION: u16 = 0; + +#[derive(Debug, SchemaRead, SchemaWrite, PartialEq, Clone)] pub enum VoteHistoryVersions { Current(VoteHistory), } @@ -30,10 +34,13 @@ impl VoteHistoryVersions { #[cfg_attr( feature = "frozen-abi", - derive(AbiExample), - frozen_abi(digest = "9dp4rEVqAsT7mfiL5oEgWrxgWCUiEe4Fk8xJoTWwSN1X") + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + abi_digest = "MYpTecggZfULsn6SC1bojefNFK1R5kjBZg7wE8H8dHF", + abi_serializer = "wincode" + ) )] -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Default, SchemaWrite, SchemaRead)] pub struct VoteHistory { /// The validator identity that cast votes pub node_pubkey: Pubkey, @@ -63,7 +70,7 @@ pub struct VoteHistory { its_over: HashSet, /// All votes cast for a `slot`, for use in refresh - votes_cast: HashMap>, + votes_cast: HashMap>, /// Blocks which have a notarization certificate via the certificate pool notarized_blocks: HashSet, @@ -130,7 +137,8 @@ impl VoteHistory { .iter() .filter(|&(&s, _)| s > slot) .flat_map(|(_, votes)| votes.iter()) - .cloned() + .copied() + .map(Vote::from) .collect() } @@ -163,12 +171,15 @@ impl VoteHistory { /// Add a new vote to the voting history pub fn add_vote(&mut self, vote: Vote) { assert!(vote.slot() >= self.root); - // TODO: these assert!s are for my debugging, can consider removing - // in final version + // Asserts here guard against vote equivocation. match vote { Vote::Notarize(vote) => { - assert!(self.voted.insert(vote.slot)); - assert!(self.voted_notar.insert(vote.slot, vote.block_id).is_none()); + assert!(self.voted.insert(vote.block.slot)); + assert!( + self.voted_notar + .insert(vote.block.slot, vote.block.block_id) + .is_none() + ); } Vote::Finalize(vote) => { assert!(!self.skipped(vote.slot)); @@ -179,13 +190,13 @@ impl VoteHistory { self.skipped.insert(vote.slot); } Vote::NotarizeFallback(vote) => { - assert!(self.voted(vote.slot)); - assert!(!self.its_over(vote.slot)); - self.skipped.insert(vote.slot); + assert!(self.voted(vote.block.slot)); + assert!(!self.its_over(vote.block.slot)); + self.skipped.insert(vote.block.slot); self.voted_notar_fallback - .entry(vote.slot) + .entry(vote.block.slot) .or_default() - .insert(vote.block_id); + .insert(vote.block.block_id); } Vote::SkipFallback(vote) => { assert!(self.voted(vote.slot)); @@ -199,12 +210,18 @@ impl VoteHistory { // votor, we do not need to insert anything here. } } - self.votes_cast.entry(vote.slot()).or_default().push(vote); + self.votes_cast + .entry(vote.slot()) + .or_default() + .push(VotePayloadToSign::new_from_vote( + vote, + VOTE_HISTORY_SHRED_VERSION, + )); } /// Add a new notarized block - pub fn add_block_notarized(&mut self, block @ (slot, _): Block) { - if slot < self.root { + pub fn add_block_notarized(&mut self, block: Block) { + if block.slot < self.root { return; } self.notarized_blocks.insert(block); @@ -212,8 +229,7 @@ impl VoteHistory { /// Add a new parent ready slot /// - /// Returns true if the insertion was successful and this was the - /// first parent ready for this slot, indicating we should set timeouts. + /// Returns true if this was the first parent ready for this slot. pub fn add_parent_ready(&mut self, slot: Slot, parent: Block) -> bool { if slot < self.root { return false; @@ -231,7 +247,18 @@ impl VoteHistory { } pub fn highest_parent_ready_slot(&self) -> Option { - self.parent_ready_slots.keys().max().copied() + self.highest_parent_ready().map(|(slot, _)| slot) + } + + pub fn highest_parent_ready(&self) -> Option<(Slot, Block)> { + let (&slot, parents) = self + .parent_ready_slots + .iter() + .max_by_key(|(slot, _)| *slot)?; + + // Choose the parent that maximizes the amount of slots skipped. + // This matches the block production parent selection logic in the `ParentReadyTracker`. + parents.iter().min().copied().map(|parent| (slot, parent)) } /// Sets the new root slot and cleans up outdated slots < `root` @@ -244,7 +271,7 @@ impl VoteHistory { self.skipped.retain(|s| *s >= root); self.its_over.retain(|s| *s >= root); self.votes_cast.retain(|s, _| *s >= root); - self.notarized_blocks.retain(|(s, _)| *s >= root); + self.notarized_blocks.retain(|block| block.slot >= root); self.parent_ready_slots.retain(|s, _| *s >= root); } @@ -274,7 +301,10 @@ pub enum VoteHistoryError { IoError(#[from] std::io::Error), #[error("Serialization Error: {0}")] - SerializeError(#[from] bincode::Error), + SerializeError(#[from] wincode::WriteError), + + #[error("Deserialization Error: {0}")] + DeserializeError(#[from] wincode::ReadError), #[error("The signature on the saved vote history is invalid")] InvalidSignature, @@ -322,7 +352,10 @@ mod test { // Vote Notarize on slot 1 let block_id_1 = Hash::new_unique(); - let vote_notarize_1 = Vote::new_notarization_vote(1, block_id_1); + let vote_notarize_1 = Vote::new_notarization_vote(Block { + slot: 1, + block_id: block_id_1, + }); vote_history.add_vote(vote_notarize_1); assert!(vote_history.voted(1)); assert!(!vote_history.its_over(1)); @@ -356,7 +389,10 @@ mod test { // Now vote NotarizeFallback on slot 2 let block_id_2 = Hash::new_unique(); - let vote_notarize_fallback_2 = Vote::new_notarization_fallback_vote(2, block_id_2); + let vote_notarize_fallback_2 = Vote::new_notarization_fallback_vote(Block { + slot: 2, + block_id: block_id_2, + }); vote_history.add_vote(vote_notarize_fallback_2); assert!(vote_history.voted(2)); assert!(vote_history.skipped(2)); @@ -377,7 +413,10 @@ mod test { // Vote Notarize on slot 3 let block_id_3 = Hash::new_unique(); - let vote_notarize_3 = Vote::new_notarization_vote(3, block_id_3); + let vote_notarize_3 = Vote::new_notarization_vote(Block { + slot: 3, + block_id: block_id_3, + }); vote_history.add_vote(vote_notarize_3); assert!(vote_history.voted(3)); assert!(!vote_history.skipped(3)); @@ -440,12 +479,18 @@ mod test { #[test] fn test_add_notarized_blocks() { let mut vote_history = VoteHistory::new(Pubkey::new_unique(), 0); - let block_1 = (1, Hash::new_unique()); + let block_1 = Block { + slot: 1, + block_id: Hash::new_unique(), + }; assert!(!vote_history.is_block_notarized(&block_1)); vote_history.add_block_notarized(block_1); assert!(vote_history.is_block_notarized(&block_1)); - let block_2 = (2, Hash::new_unique()); + let block_2 = Block { + slot: 2, + block_id: Hash::new_unique(), + }; assert!(!vote_history.is_block_notarized(&block_2)); vote_history.add_block_notarized(block_2); assert!(vote_history.is_block_notarized(&block_2)); @@ -464,38 +509,52 @@ mod test { fn test_add_parent_ready() { let mut vote_history = VoteHistory::new(Pubkey::new_unique(), 0); assert_eq!(vote_history.highest_parent_ready_slot(), None); - let block_id_0 = (0, Hash::new_unique()); - vote_history.add_parent_ready(1, block_id_0); - assert!(vote_history.is_parent_ready(1, &block_id_0)); + let block_0 = Block { + slot: 0, + block_id: Hash::new_unique(), + }; + vote_history.add_parent_ready(1, block_0); + assert!(vote_history.is_parent_ready(1, &block_0)); assert_eq!(vote_history.highest_parent_ready_slot(), Some(1)); vote_history.set_root(1); assert_eq!(vote_history.root(), 1); - assert!(vote_history.is_parent_ready(1, &block_id_0)); + assert!(vote_history.is_parent_ready(1, &block_0)); assert_eq!(vote_history.highest_parent_ready_slot(), Some(1)); // Add parent ready for slot 2 - let block_id_2_0 = (1, Hash::new_unique()); - let block_id_2_1 = (1, Hash::new_unique()); - assert!(vote_history.add_parent_ready(2, block_id_2_0)); - assert!(vote_history.is_parent_ready(2, &block_id_2_0)); + let block_2_0 = Block { + slot: 1, + block_id: Hash::new_unique(), + }; + let block_2_1 = Block { + slot: 1, + block_id: Hash::new_unique(), + }; + assert!(vote_history.add_parent_ready(2, block_2_0)); + assert!(vote_history.is_parent_ready(2, &block_2_0)); assert_eq!(vote_history.highest_parent_ready_slot(), Some(2)); - assert!(!vote_history.add_parent_ready(2, block_id_2_1)); - assert!(vote_history.is_parent_ready(2, &block_id_2_1)); - assert!(!vote_history.add_parent_ready(2, block_id_0)); - assert!(vote_history.is_parent_ready(2, &block_id_0)); + assert!(!vote_history.add_parent_ready(2, block_2_1)); + assert!(vote_history.is_parent_ready(2, &block_2_1)); + assert!(!vote_history.add_parent_ready(2, block_0)); + assert!(vote_history.is_parent_ready(2, &block_0)); // Set root to 2 vote_history.set_root(2); assert_eq!(vote_history.root(), 2); - assert!(!vote_history.is_parent_ready(1, &block_id_0)); - assert!(vote_history.is_parent_ready(2, &block_id_2_0)); - assert!(vote_history.is_parent_ready(2, &block_id_2_1)); - assert!(vote_history.is_parent_ready(2, &block_id_0)); + assert!(!vote_history.is_parent_ready(1, &block_0)); + assert!(vote_history.is_parent_ready(2, &block_2_0)); + assert!(vote_history.is_parent_ready(2, &block_2_1)); + assert!(vote_history.is_parent_ready(2, &block_0)); assert_eq!(vote_history.highest_parent_ready_slot(), Some(2)); + let expected_parent = [block_2_0, block_2_1, block_0].into_iter().min().unwrap(); + assert_eq!( + vote_history.highest_parent_ready(), + Some((2, expected_parent)) + ); // Adding a parent ready for slot before root silently returns false - assert!(!vote_history.add_parent_ready(1, block_id_0)); + assert!(!vote_history.add_parent_ready(1, block_0)); } #[test] @@ -506,7 +565,10 @@ mod test { let vote_history_storage = FileVoteHistoryStorage::new(tmp_dir.path().to_path_buf()); // Add Notarize on 1 and Skip on 2 - let vote_1 = Vote::new_notarization_vote(1, Hash::new_unique()); + let vote_1 = Vote::new_notarization_vote(Block { + slot: 1, + block_id: Hash::new_unique(), + }); let vote_2 = Vote::new_skip_vote(2); vote_history.add_vote(vote_1); vote_history.add_vote(vote_2); diff --git a/votor/src/vote_history_storage.rs b/votor/src/vote_history_storage.rs index 18528bdcbb5..418571efc7f 100644 --- a/votor/src/vote_history_storage.rs +++ b/votor/src/vote_history_storage.rs @@ -7,15 +7,16 @@ use { solana_signer::Signer, std::{ fs::{self, File}, - io::{self, BufReader}, + io::{self, BufReader, BufWriter}, path::PathBuf, }, + wincode::{SchemaRead, SchemaWrite}, }; pub type Result = std::result::Result; #[cfg_attr(feature = "frozen-abi", derive(AbiExample))] -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] pub enum SavedVoteHistoryVersions { Current(SavedVoteHistory), } @@ -30,25 +31,22 @@ impl SavedVoteHistoryVersions { if !t.signature.verify(node_pubkey.as_ref(), &t.data) { return Err(VoteHistoryError::InvalidSignature); } - bincode::deserialize(&t.data).map(VoteHistoryVersions::Current) + wincode::deserialize(&t.data).map(VoteHistoryVersions::Current)? } }; - vote_history - .map_err(|e| e.into()) - .and_then(|vote_history: VoteHistoryVersions| { - let vote_history = vote_history.convert_to_current(); - if vote_history.node_pubkey != *node_pubkey { - return Err(VoteHistoryError::WrongVoteHistory(format!( - "node_pubkey is {:?} but found vote history for {:?}", - node_pubkey, vote_history.node_pubkey - ))); - } - Ok(vote_history) - }) + let vote_history = vote_history.convert_to_current(); + if vote_history.node_pubkey != *node_pubkey { + return Err(VoteHistoryError::WrongVoteHistory(format!( + "node_pubkey is {:?} but found vote history for {:?}", + node_pubkey, vote_history.node_pubkey + ))); + } + Ok(vote_history) } fn serialize_into(&self, file: &mut File) -> Result<()> { - bincode::serialize_into(file, self).map_err(|e| e.into()) + wincode::serialize_into(BufWriter::new(file), self)?; + Ok(()) } fn pubkey(&self) -> Pubkey { @@ -69,12 +67,13 @@ impl From for SavedVoteHistoryVersions { derive(AbiExample), frozen_abi(digest = "J6vB6FWFT8CFEvxndXWes461hroo8Q5L9Wq9cv4FEzaQ") )] -#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq, SchemaWrite, SchemaRead)] pub struct SavedVoteHistory { signature: Signature, #[serde(with = "serde_bytes")] data: Vec, #[serde(skip)] + #[wincode(skip)] node_pubkey: Pubkey, } @@ -88,7 +87,7 @@ impl SavedVoteHistory { ))); } - let data = bincode::serialize(&vote_history)?; + let data = wincode::serialize(&vote_history)?; let signature = keypair.sign_message(&data); Ok(Self { signature, @@ -147,9 +146,8 @@ impl VoteHistoryStorage for FileVoteHistoryStorage { let file = File::open(&filename)?; let mut stream = BufReader::new(file); - bincode::deserialize_from(&mut stream) - .map_err(|e| e.into()) - .and_then(|t: SavedVoteHistoryVersions| t.try_into_vote_history(node_pubkey)) + let saved_vote_history: SavedVoteHistoryVersions = wincode::deserialize_from(&mut stream)?; + saved_vote_history.try_into_vote_history(node_pubkey) } fn store(&self, saved_vote_history: &SavedVoteHistoryVersions) -> Result<()> { @@ -172,7 +170,10 @@ impl VoteHistoryStorage for FileVoteHistoryStorage { #[cfg(test)] mod test { - use {super::*, agave_votor_messages::vote::Vote, solana_keypair::Keypair, tempfile::TempDir}; + use { + super::*, agave_votor_messages::vote::Vote, solana_keypair::Keypair, solana_signer::Signer, + tempfile::TempDir, + }; #[test] fn test_file_vote_history_storage() { @@ -238,4 +239,36 @@ mod test { storage.store(&saved_vote_history_versions).unwrap(); assert!(storage.load(&pubkey).is_err()); } + + #[test] + fn test_load_corrupt_vote_history_storage_returns_deserialize_error() { + let tmp_dir = TempDir::new().unwrap(); + let storage = FileVoteHistoryStorage::new(tmp_dir.path().to_path_buf()); + let pubkey = Pubkey::new_unique(); + + fs::write(storage.filename(&pubkey), [1, 2, 3]).unwrap(); + + let error = storage.load(&pubkey).err().unwrap(); + assert!(matches!(error, VoteHistoryError::DeserializeError(_))); + } + + #[test] + fn test_load_signed_corrupt_vote_history_data_returns_deserialize_error() { + let tmp_dir = TempDir::new().unwrap(); + let storage = FileVoteHistoryStorage::new(tmp_dir.path().to_path_buf()); + let keypair = Keypair::new(); + let pubkey = keypair.pubkey(); + let data = Vec::new(); + let saved_vote_history = SavedVoteHistory { + signature: keypair.sign_message(&data), + data, + node_pubkey: pubkey, + }; + storage + .store(&SavedVoteHistoryVersions::from(saved_vote_history)) + .unwrap(); + + let error = storage.load(&pubkey).err().unwrap(); + assert!(matches!(error, VoteHistoryError::DeserializeError(_))); + } } diff --git a/votor/src/voting_service.rs b/votor/src/voting_service.rs index 35976ce2663..d5eff274e76 100644 --- a/votor/src/voting_service.rs +++ b/votor/src/voting_service.rs @@ -3,19 +3,25 @@ use { staked_validators_cache::StakedValidatorsCache, vote_history_storage::{SavedVoteHistoryVersions, VoteHistoryStorage}, }, - agave_votor_messages::consensus_message::{Certificate, ConsensusMessage}, - crossbeam_channel::Receiver, + agave_votor_messages::{ + certificate::Certificate, consensus_message::VoteMessage, + wire::VersionedWireConsensusMessage, + }, + crossbeam_channel::{Receiver, RecvTimeoutError}, solana_client::connection_cache::ConnectionCache, solana_clock::Slot, solana_connection_cache::client_connection::ClientConnection, solana_gossip::cluster_info::ClusterInfo, solana_measure::measure::Measure, solana_pubkey::Pubkey, - solana_runtime::bank_forks::BankForks, + solana_runtime::{ + bank_forks::BankForks, validated_block_finalization::ValidatedBlockFinalizationCert, + }, solana_transaction_error::TransportError, std::{ - collections::HashMap, + collections::{BTreeMap, HashMap, HashSet}, net::SocketAddr, + ops::Bound::{Excluded, Included, Unbounded}, sync::{Arc, RwLock}, thread::{self, Builder, JoinHandle}, time::{Duration, Instant}, @@ -28,22 +34,165 @@ const STAKED_VALIDATORS_CACHE_TTL_S: u64 = 5; /// before evicting down to this target. const STAKED_VALIDATORS_CACHE_NUM_EPOCH_TARGET: usize = 3; +/// The maximum amount of packets per second we expect from an honest node +pub const VOTOR_RATE_LIMIT_PPS: u64 = 50; + +/// Max new packets per second in steady state: +/// - Notarize + Finalize votes +/// - NotarizeFallback + Notarize + FastFinalize + Finalize certificates +/// +/// 200ms slots, 6 packets * 5 slots per second = 30 PPS +const NEW_PACKETS_PER_SECOND: usize = 30; + +/// The amount of packets we should send per second from the standstill queue +const STANDSTILL_REFRESH_BATCH_SIZE: usize = VOTOR_RATE_LIMIT_PPS as usize - NEW_PACKETS_PER_SECOND; + +/// How often we should refresh messages from the standstill queue +const STANDSTILL_REFRESH_INTERVAL: Duration = Duration::from_secs(1); + #[derive(Debug)] pub enum BLSOp { PushVote { - message: Arc, - slot: Slot, + vote: Arc, saved_vote_history: SavedVoteHistoryVersions, }, - PushCertificate { - certificate: Arc, + PushCertificates { + certificates: Vec>, + }, + RefreshVotes { + votes: Vec>, }, + RefreshCertificates { + certificates: Vec>, + }, +} + +#[derive(Debug)] +/// Maintains a map of messages since the last finalization if we are in standstill. +/// This queue is used to refresh the next `STANDSTILL_REFRESH_BATCH_SIZE` messages +/// every `STANDSTILL_REFRESH_INTERVAL`. +/// +/// When we are not in standstill the queue will be cleared as it's prune by the highest +/// finalized slot. +struct StandstillRefreshQueue { + messages: BTreeMap>, + last_refresh: Instant, + cursor: Slot, +} + +impl Default for StandstillRefreshQueue { + fn default() -> Self { + Self { + messages: BTreeMap::default(), + last_refresh: Instant::now(), + cursor: 0, + } + } +} + +impl StandstillRefreshQueue { + fn insert(&mut self, message: VersionedWireConsensusMessage) { + let slot = message.slot(); + self.messages.entry(slot).or_default().insert(message); + } + + /// Prune any state less than or equal to the highest finalized slot. + fn prune(&mut self, highest_finalized_slot: Slot) { + self.messages + .retain(|slot, _| *slot > highest_finalized_slot); + if self.cursor <= highest_finalized_slot { + self.cursor = highest_finalized_slot; + } + } + + fn is_empty(&self) -> bool { + self.messages.is_empty() + } + + /// Calls `handle_message` for up to the next `limit` messages from the queue, + /// updating the cursor as necessary. + fn for_next_n_messages( + &mut self, + limit: usize, + mut handle_message: impl FnMut(&VersionedWireConsensusMessage), + ) -> usize { + if limit == 0 || self.is_empty() { + return 0; + } + + let mut processed = 0usize; + let starting_cursor = self.cursor; + let mut reached_end = true; + for (slot, slot_messages) in self.messages.range((Excluded(self.cursor), Unbounded)) { + if limit.saturating_sub(processed) < slot_messages.len() { + // We cannot process this batch as it would put us over the limit + if processed == 0 { + // However this is the very first batch! This should never happen + // as the maximum number of possible votes & certificates for a slot is < 20. + // But here we are, to avoid stalling the queue completely allow us to progress + // at the risk of being rate limited + error!( + "First slot batch in the standstill queue exceeds votor rate limit: \ + {slot_messages:?}" + ); + } else { + reached_end = false; + break; + } + } + + processed = processed.saturating_add(slot_messages.len()); + self.cursor = *slot; + for message in slot_messages { + handle_message(message); + } + } + + if reached_end && processed < limit && starting_cursor != 0 { + self.cursor = 0; + for (slot, slot_messages) in self + .messages + .range((Excluded(0), Included(starting_cursor))) + { + if limit.saturating_sub(processed) < slot_messages.len() { + break; + } + + processed = processed.saturating_add(slot_messages.len()); + self.cursor = *slot; + for message in slot_messages { + handle_message(message); + } + } + } + + processed + } + + /// Whether enough time has passed to refresh messages in the queue + fn should_refresh(&self) -> bool { + self.last_refresh.elapsed() >= STANDSTILL_REFRESH_INTERVAL + } + + /// Reset the refresh timer + fn reset_refresh_timer(&mut self) { + self.last_refresh = Instant::now(); + } + + #[cfg(test)] + fn next_messages(&mut self, limit: usize) -> Vec<(Slot, VersionedWireConsensusMessage)> { + let mut messages = Vec::new(); + self.for_next_n_messages(limit, |message| { + messages.push((message.slot(), message.clone())); + }); + messages + } } fn send_message( buf: Vec, socket: &SocketAddr, - connection_cache: &Arc, + connection_cache: &ConnectionCache, ) -> Result<(), TransportError> { let client = connection_cache.get_connection(socket); @@ -122,6 +271,7 @@ impl VotingService { vote_history_storage: Arc, connection_cache: Arc, bank_forks: Arc>, + highest_finalized: Arc>>, test_override: Option, ) -> Self { let (additional_listeners, alpenglow_port_override) = match test_override { @@ -131,6 +281,7 @@ impl VotingService { alpenglow_port_override, }) => (additional_listeners, Some(alpenglow_port_override)), }; + let mut standstill_queue = StandstillRefreshQueue::default(); let thread_hdl = Builder::new() .name("solVotorVoteSvc".to_string()) @@ -144,14 +295,29 @@ impl VotingService { ); info!("AlpenglowVotingService has started"); - while let Ok(bls_op) = bls_receiver.recv() { + loop { + Self::maybe_handle_standstill_queue( + &mut standstill_queue, + highest_finalized.as_ref(), + &cluster_info, + &connection_cache, + &additional_listeners, + &mut staked_validators_cache, + ); + + let bls_op = match bls_receiver.recv_timeout(STANDSTILL_REFRESH_INTERVAL) { + Ok(bls_op) => bls_op, + Err(RecvTimeoutError::Disconnected) => break, + Err(RecvTimeoutError::Timeout) => continue, + }; Self::handle_bls_op( &cluster_info, vote_history_storage.as_ref(), bls_op, - connection_cache.clone(), + &connection_cache, &additional_listeners, &mut staked_validators_cache, + &mut standstill_queue, ); } info!("AlpenglowVotingService has stopped"); @@ -160,11 +326,75 @@ impl VotingService { Self { thread_hdl } } + /// If more than 1 second has passed, prune and send out messages from the queue + fn maybe_handle_standstill_queue( + standstill_queue: &mut StandstillRefreshQueue, + highest_finalized: &RwLock>, + cluster_info: &ClusterInfo, + connection_cache: &ConnectionCache, + additional_listeners: &[SocketAddr], + staked_validators_cache: &mut StakedValidatorsCache, + ) { + if !standstill_queue.should_refresh() { + return; + } + + if standstill_queue.is_empty() { + standstill_queue.reset_refresh_timer(); + return; + } + + let mut num_sent_messages = 0usize; + + let highest_finalized_slot_and_certs = { + let highest_finalized = highest_finalized.read().unwrap(); + highest_finalized.as_ref().map(|highest_finalized| { + ( + highest_finalized.slot(), + highest_finalized.clone_certificates(), + ) + }) + }; + + if let Some((highest_finalized_slot, certificates)) = highest_finalized_slot_and_certs { + standstill_queue.prune(highest_finalized_slot.slot()); + + // Refresh the latest finalization (either Finalize + Notarize or FastFinalize) + for certificate in certificates { + let message = VersionedWireConsensusMessage::new_from_cert( + certificate, + cluster_info.my_shred_version(), + ); + Self::broadcast_consensus_message( + cluster_info, + &message, + connection_cache, + additional_listeners, + staked_validators_cache, + ); + num_sent_messages = num_sent_messages.saturating_add(1); + } + } + + // Refresh the next messages from the queue while adhering to the budget + let remaining_budget = STANDSTILL_REFRESH_BATCH_SIZE.saturating_sub(num_sent_messages); + standstill_queue.for_next_n_messages(remaining_budget, |message| { + Self::broadcast_consensus_message( + cluster_info, + message, + connection_cache, + additional_listeners, + staked_validators_cache, + ); + }); + + standstill_queue.reset_refresh_timer(); + } + fn broadcast_consensus_message( - slot: Slot, cluster_info: &ClusterInfo, - message: &ConsensusMessage, - connection_cache: Arc, + message: &VersionedWireConsensusMessage, + connection_cache: &ConnectionCache, additional_listeners: &[SocketAddr], staked_validators_cache: &mut StakedValidatorsCache, ) { @@ -177,7 +407,7 @@ impl VotingService { }; let (staked_validator_alpenglow_sockets, _) = staked_validators_cache - .get_staked_validators_by_slot(slot, cluster_info, Instant::now()); + .get_staked_validators_by_slot(message.slot(), cluster_info, Instant::now()); let sockets = additional_listeners .iter() .chain(staked_validator_alpenglow_sockets.iter()); @@ -186,7 +416,7 @@ impl VotingService { // will cause a packet spike and overwhelm the network. If we later find out that this is // not an issue, we can optimize this by using multi_targret_send or similar methods. for socket in sockets { - if let Err(e) = send_message(buf.clone(), socket, &connection_cache) { + if let Err(e) = send_message(buf.clone(), socket, connection_cache) { warn!("Failed to send alpenglow message to {socket}: {e:?}"); } } @@ -196,14 +426,14 @@ impl VotingService { cluster_info: &ClusterInfo, vote_history_storage: &dyn VoteHistoryStorage, bls_op: BLSOp, - connection_cache: Arc, + connection_cache: &ConnectionCache, additional_listeners: &[SocketAddr], staked_validators_cache: &mut StakedValidatorsCache, + standstill_queue: &mut StandstillRefreshQueue, ) { match bls_op { BLSOp::PushVote { - message, - slot, + vote, saved_vote_history, } => { let mut measure = Measure::start("alpenglow vote history save"); @@ -213,28 +443,51 @@ impl VotingService { } measure.stop(); trace!("{measure}"); - - Self::broadcast_consensus_message( - slot, - cluster_info, - &message, - connection_cache, - additional_listeners, - staked_validators_cache, + let msg = VersionedWireConsensusMessage::new_from_vote( + Arc::unwrap_or_clone(vote), + cluster_info.my_shred_version(), ); - } - BLSOp::PushCertificate { certificate } => { - let vote_slot = certificate.cert_type.slot(); - let message = ConsensusMessage::Certificate((*certificate).clone()); Self::broadcast_consensus_message( - vote_slot, cluster_info, - &message, + &msg, connection_cache, additional_listeners, staked_validators_cache, ); } + BLSOp::PushCertificates { certificates } => { + for certificate in certificates { + let msg = VersionedWireConsensusMessage::new_from_cert( + Arc::unwrap_or_clone(certificate), + cluster_info.my_shred_version(), + ); + Self::broadcast_consensus_message( + cluster_info, + &msg, + connection_cache, + additional_listeners, + staked_validators_cache, + ); + } + } + BLSOp::RefreshVotes { votes } => { + for vote in votes { + let msg = VersionedWireConsensusMessage::new_from_vote( + Arc::unwrap_or_clone(vote), + cluster_info.my_shred_version(), + ); + standstill_queue.insert(msg); + } + } + BLSOp::RefreshCertificates { certificates } => { + for certificate in certificates { + let message = VersionedWireConsensusMessage::new_from_cert( + Arc::unwrap_or_clone(certificate), + cluster_info.my_shred_version(), + ); + standstill_queue.insert(message); + } + } } } @@ -251,13 +504,17 @@ mod tests { NullVoteHistoryStorage, SavedVoteHistory, SavedVoteHistoryVersions, }, agave_votor_messages::{ - consensus_message::{Certificate, CertificateType, ConsensusMessage, VoteMessage}, + certificate::{Certificate, CertificateType}, + consensus_message::{ConsensusMessage, VoteMessage}, vote::Vote, }, + crossbeam_channel::bounded, + rand::Rng, solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature}, solana_gossip::{cluster_info::ClusterInfo, contact_info::ContactInfo}, solana_keypair::Keypair, solana_net_utils::{SocketAddrSpace, sockets::bind_to_localhost_unique}, + solana_perf::packet::packet_config, solana_runtime::{ bank::Bank, bank_forks::BankForks, @@ -279,10 +536,108 @@ mod tests { tokio_util::sync::CancellationToken, }; + fn test_vote_message( + vote: Vote, + rank: u16, + shred_verion: u16, + ) -> VersionedWireConsensusMessage { + VersionedWireConsensusMessage::new_from_vote( + VoteMessage { + vote, + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + rank, + }, + shred_verion, + ) + } + + fn test_certificate_message( + cert_type: CertificateType, + my_shred_version: u16, + ) -> VersionedWireConsensusMessage { + VersionedWireConsensusMessage::new_from_cert( + Certificate { + cert_type, + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + bitmap: Vec::new(), + }, + my_shred_version, + ) + } + + #[test] + fn test_standstill_refresh_queue_cycles_by_slot() { + let shred_verion = rand::rng().random(); + let mut queue = StandstillRefreshQueue::default(); + let vote_5 = test_vote_message(Vote::new_skip_vote(5), 1, shred_verion); + let vote_6 = test_vote_message(Vote::new_skip_vote(6), 1, shred_verion); + let cert_6 = test_certificate_message(CertificateType::Finalize(6), shred_verion); + + queue.insert(vote_6.clone()); + queue.insert(cert_6.clone()); + queue.insert(vote_5.clone()); + + assert_eq!(queue.next_messages(2), vec![(5, vote_5.clone())]); + let slot_6_messages = queue.next_messages(2); + assert_eq!(slot_6_messages.len(), 2); + assert!(slot_6_messages.contains(&(6, vote_6.clone()))); + assert!(slot_6_messages.contains(&(6, cert_6.clone()))); + assert_eq!(queue.next_messages(2), vec![(5, vote_5)]); + } + + #[test] + fn test_standstill_refresh_queue_deduplicates_identical_messages() { + let shred_verion = rand::rng().random(); + let mut queue = StandstillRefreshQueue::default(); + let message = test_vote_message(Vote::new_skip_vote(8), 1, shred_verion); + + queue.insert(message.clone()); + queue.insert(message.clone()); + + assert_eq!(queue.next_messages(10), vec![(8, message)]); + } + + #[test] + fn test_standstill_refresh_queue_wraps_to_fill_budget() { + let mut queue = StandstillRefreshQueue::default(); + let shred_verion = rand::rng().random(); + let vote_3 = test_vote_message(Vote::new_skip_vote(3), 1, shred_verion); + let vote_4 = test_vote_message(Vote::new_skip_vote(4), 1, shred_verion); + let vote_6 = test_vote_message(Vote::new_skip_vote(6), 1, shred_verion); + + queue.insert(vote_3.clone()); + queue.insert(vote_4.clone()); + queue.insert(vote_6.clone()); + queue.cursor = 5; + + assert_eq!( + queue.next_messages(10), + vec![(6, vote_6), (3, vote_3), (4, vote_4)] + ); + } + + #[test] + fn test_standstill_refresh_queue_prunes_finalized_messages() { + let shred_verion = rand::rng().random(); + let mut queue = StandstillRefreshQueue::default(); + let vote_5 = test_vote_message(Vote::new_skip_vote(5), 1, shred_verion); + let cert_6 = test_certificate_message(CertificateType::Finalize(6), shred_verion); + let vote_6 = test_vote_message(Vote::new_skip_vote(6), 1, shred_verion); + let vote_7 = test_vote_message(Vote::new_skip_vote(7), 1, shred_verion); + + queue.insert(vote_5); + queue.insert(cert_6); + queue.insert(vote_6); + queue.insert(vote_7.clone()); + + queue.prune(6); + assert_eq!(queue.next_messages(10), vec![(7, vote_7)]); + } + fn create_voting_service( bls_receiver: Receiver, listener: SocketAddr, - ) -> (VotingService, Vec) { + ) -> (VotingService, Vec, Arc) { // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new_rand()) @@ -296,58 +651,70 @@ mod tests { let bank_forks = BankForks::new_rw_arc(bank0); let keypair = Keypair::new(); let contact_info = ContactInfo::new_localhost(&keypair.pubkey(), 0); - let cluster_info = ClusterInfo::new( + let cluster_info = Arc::new(ClusterInfo::new( contact_info, Arc::new(keypair), SocketAddrSpace::Unspecified, - ); + )); ( VotingService::new( bls_receiver, - Arc::new(cluster_info), + cluster_info.clone(), Arc::new(NullVoteHistoryStorage::default()), Arc::new(ConnectionCache::new_quic( "TestAlpenglowConnectionCache", 10, )), bank_forks, + Arc::new(RwLock::new(None)), Some(VotingServiceOverride { additional_listeners: vec![listener], alpenglow_port_override: AlpenglowPortOverride::default(), }), ), validator_keypairs, + cluster_info, ) } #[test_case(BLSOp::PushVote { - message: Arc::new(ConsensusMessage::Vote(VoteMessage { + vote: Arc::new(VoteMessage { vote: Vote::new_skip_vote(5), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), rank: 1, - })), - slot: 5, + }), saved_vote_history: SavedVoteHistoryVersions::Current(SavedVoteHistory::default()), }, ConsensusMessage::Vote(VoteMessage { vote: Vote::new_skip_vote(5), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), rank: 1, }))] - #[test_case(BLSOp::PushCertificate { - certificate: Arc::new(Certificate { - cert_type: CertificateType::Skip(5), + #[test_case(BLSOp::PushCertificates { + certificates: vec![Arc::new(Certificate { + cert_type: CertificateType::Skip(5), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: Vec::new(), - }), + })], }, ConsensusMessage::Certificate(Certificate { cert_type: CertificateType::Skip(5), signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), bitmap: Vec::new(), }))] + #[test_case(BLSOp::RefreshVotes { + votes: vec![Arc::new(VoteMessage { + vote: Vote::new_skip_vote(6), + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + rank: 1, + })], + }, ConsensusMessage::Vote(VoteMessage { + vote: Vote::new_skip_vote(6), + signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]), + rank: 1, + }))] fn test_send_message(bls_op: BLSOp, expected_message: ConsensusMessage) { agave_logger::setup(); - let (bls_sender, bls_receiver) = crossbeam_channel::unbounded(); + let (bls_sender, bls_receiver) = bounded(1024); // Create listener thread on a random port we allocated and return SocketAddr to create VotingService // Bind to a random UDP port @@ -355,13 +722,14 @@ mod tests { let listener_addr = socket.local_addr().unwrap(); // Create VotingService with the listener address - let (_, validator_keypairs) = create_voting_service(bls_receiver, listener_addr); + let (_, validator_keypairs, cluster_info) = + create_voting_service(bls_receiver, listener_addr); // Send a BLS message via the VotingService assert!(bls_sender.send(bls_op).is_ok()); // Start a quick streamer to handle quick control packets - let (sender, receiver) = crossbeam_channel::unbounded(); + let (sender, receiver) = bounded(1024); let stakes = validator_keypairs .iter() .map(|x| (x.node_keypair.pubkey(), 100)) @@ -390,16 +758,12 @@ mod tests { let packets = receiver.recv().unwrap(); let packet = packets.first().expect("No packets received"); - let received_message = packet - .deserialize_slice::(..) - .unwrap_or_else(|err| { - panic!( - "Failed to deserialize BLSMessage: {:?} {:?}", - size_of::(), - err - ) - }); - assert_eq!(received_message, expected_message); + let received_message = + wincode::config::deserialize_exact(packet.data(..).unwrap(), packet_config()).unwrap(); + assert_eq!( + VersionedWireConsensusMessage::new(expected_message, cluster_info.my_shred_version()), + received_message + ); cancel.cancel(); quic_server_thread.join().unwrap(); } diff --git a/votor/src/voting_utils.rs b/votor/src/voting_utils.rs index 75f4f47a87d..986e5332929 100644 --- a/votor/src/voting_utils.rs +++ b/votor/src/voting_utils.rs @@ -1,18 +1,20 @@ use { crate::{ commitment::{CommitmentAggregationData, CommitmentError}, - consensus_metrics::ConsensusMetricsEventSender, vote_history::{VoteHistory, VoteHistoryError}, vote_history_storage::{SavedVoteHistory, SavedVoteHistoryVersions}, voting_service::BLSOp, }, agave_votor_messages::{ consensus_message::{BLS_KEYPAIR_DERIVE_SEED, ConsensusMessage, VoteMessage}, + metric_types::ConsensusMetricsEventSender, vote::Vote, + wire::get_vote_payload_to_sign, }, crossbeam_channel::{SendError, Sender}, solana_bls_signatures::{BlsError, keypair::Keypair as BLSKeypair}, solana_clock::{Epoch, Slot}, + solana_gossip::cluster_info::ClusterInfo, solana_keypair::Keypair, solana_pubkey::Pubkey, solana_runtime::{bank::Bank, bank_forks::SharableBanks, epoch_stakes::BLSPubkeyStakeEntry}, @@ -53,8 +55,8 @@ pub enum GenerateVoteTxResult { // The following are the successful cases // Generated a vote transaction Tx(Transaction), - // Generated a ConsensusMessage - ConsensusMessage(ConsensusMessage), + // Generated a VoteMessage + Vote(VoteMessage), } impl GenerateVoteTxResult { @@ -74,7 +76,7 @@ impl GenerateVoteTxResult { | Self::WaitForStartupVerification | Self::WaitToVoteSlot(_) | Self::NoRankFound => false, - Self::Tx(_) | Self::ConsensusMessage(_) => false, + Self::Tx(_) | Self::Vote(_) => false, } } @@ -86,7 +88,7 @@ impl GenerateVoteTxResult { | Self::WaitForStartupVerification | Self::WaitToVoteSlot(_) | Self::NoRankFound => true, - Self::Tx(_) | Self::ConsensusMessage(_) => false, + Self::Tx(_) | Self::Vote(_) => false, } } } @@ -111,13 +113,14 @@ pub enum VoteError { /// Context required to construct vote transactions pub struct VotingContext { + pub cluster_info: Arc, pub vote_history: VoteHistory, pub vote_account_pubkey: Pubkey, pub identity_keypair: Arc, pub authorized_voter_keypairs: Arc>>>, // The BLS keypair should always change with authorized_voter_keypairs. pub derived_bls_keypairs: HashMap>, - pub own_vote_sender: Sender>, + pub own_vote_sender: Sender, pub bls_sender: Sender, pub commitment_sender: Sender, pub wait_to_vote_slot: Option, @@ -147,18 +150,22 @@ pub fn generate_vote_tx( vote: Vote, bank: &Bank, vote_account_pubkey: Pubkey, + shred_version: u16, identity_keypair: &Keypair, authorized_voter_keypairs: &RwLock>>, wait_to_vote_slot: Option, derived_bls_keypairs: &mut HashMap>, ) -> GenerateVoteTxResult { + if authorized_voter_keypairs.read().unwrap().is_empty() { + return GenerateVoteTxResult::NonVoting; + } if bank.get_vote_account(&vote_account_pubkey).is_none() { return GenerateVoteTxResult::VoteAccountNotFound(vote_account_pubkey); } - if let Some(slot) = wait_to_vote_slot { - if vote.slot() < slot { - return GenerateVoteTxResult::WaitToVoteSlot(slot); - } + if let Some(slot) = wait_to_vote_slot + && vote.slot() < slot + { + return GenerateVoteTxResult::WaitToVoteSlot(slot); } let rank_map = bank @@ -212,99 +219,134 @@ pub fn generate_vote_tx( return GenerateVoteTxResult::NonVoting; }; - let vote_serialized = wincode::serialize(&vote).unwrap(); - GenerateVoteTxResult::ConsensusMessage(ConsensusMessage::Vote(VoteMessage { + let vote_payload_to_sign = get_vote_payload_to_sign(vote, shred_version); + GenerateVoteTxResult::Vote(VoteMessage { vote, - signature: bls_keypair.sign(&vote_serialized).into(), + signature: bls_keypair.sign(&vote_payload_to_sign).into(), rank: my_rank, - })) + }) } -/// Send an alpenglow vote as a BLSMessage -/// `bank` will be used for: -/// - startup verification -/// - vote account checks -/// - authorized voter checks -/// -/// We also update the vote history and send the vote to -/// the certificate pool thread for ingestion. -/// -/// Returns false if we are currently a non-voting node -fn insert_vote_and_create_bls_message( +/// Creates a vote message from `vote`, respecting `context.wait_to_vote_slot` only if `respect_wait_to_vote` is true +fn create_vote_message( vote: Vote, - is_refresh: bool, context: &mut VotingContext, -) -> Result { - // Update and save the vote history - if !is_refresh { - context.vote_history.add_vote(vote); - } - + respect_wait_to_vote: bool, +) -> Result { let bank = context.sharable_banks.root(); - let message = match generate_vote_tx( + let wait_to_vote_slot = if respect_wait_to_vote { + context.wait_to_vote_slot + } else { + None + }; + match generate_vote_tx( vote, &bank, context.vote_account_pubkey, + context.cluster_info.my_shred_version(), &context.identity_keypair, &context.authorized_voter_keypairs, - context.wait_to_vote_slot, + wait_to_vote_slot, &mut context.derived_bls_keypairs, ) { - GenerateVoteTxResult::ConsensusMessage(m) => m, + GenerateVoteTxResult::Vote(vote) => Ok(vote), e => { if e.is_transient_error() { - return Err(VoteError::TransientError(Box::new(e))); + Err(VoteError::TransientError(Box::new(e))) } else { - return Err(VoteError::InvalidConfig(Box::new(e))); + Err(VoteError::InvalidConfig(Box::new(e))) } } + } +} + +fn handle_skippable_vote_error(err: VoteError, action: &str) -> Result<(), VoteError> { + match err { + VoteError::InvalidConfig(e) => { + warn!("Failed to {action}: {e:?}"); + // These are not fatal errors, just skip the vote for now. But they are + // misconfigurations that should be warned about. + Ok(()) + } + VoteError::TransientError(e) => { + info!("Failed to {action}: {e:?}"); + // These are transient errors, just skip the vote for now. + Ok(()) + } + e => Err(e), + } +} + +/// Build a normal push-vote BLS op. +/// +/// This updates vote history, sends the vote to the certificate pool thread for ingestion, and +/// saves vote history for persistence. +pub fn insert_vote_and_create_bls_message( + vote: Vote, + context: &mut VotingContext, +) -> Result, VoteError> { + // Update and save the vote history + context.vote_history.add_vote(vote); + + let Some(vote_msg) = + create_and_send_own_vote_message(vote, context, /* respect_wait_to_vote */ true)? + else { + return Ok(None); }; - context - .own_vote_sender - .send(vec![message.clone()]) - .map_err(|_| SendError(()))?; - // TODO: for refresh votes use a different BLSOp so we don't have to rewrite the same vote history to file let saved_vote_history = SavedVoteHistory::new(&context.vote_history, &context.identity_keypair)?; // Return vote for sending - Ok(BLSOp::PushVote { - message: Arc::new(message), - slot: vote.slot(), + Ok(Some(BLSOp::PushVote { + vote: Arc::new(vote_msg), saved_vote_history: SavedVoteHistoryVersions::from(saved_vote_history), - }) + })) } -pub fn generate_vote_message( +pub(crate) fn create_and_send_own_vote_message( vote: Vote, - is_refresh: bool, - vctx: &mut VotingContext, -) -> Result, VoteError> { - let bls_op = match insert_vote_and_create_bls_message(vote, is_refresh, vctx) { - Ok(bls_op) => bls_op, - Err(VoteError::InvalidConfig(e)) => { - warn!("Failed to generate vote and push to votes: {e:?}"); - // These are not fatal errors, just skip the vote for now. But they are misconfigurations - // that should be warned about. - return Ok(None); - } - Err(VoteError::TransientError(e)) => { - info!("Failed to generate vote and push to votes: {e:?}"); - // These are transient errors, just skip the vote for now. + context: &mut VotingContext, + respect_wait_to_vote: bool, +) -> Result, VoteError> { + let vote_msg = match create_vote_message(vote, context, respect_wait_to_vote) { + Ok(vote_msg) => vote_msg, + Err(e) => { + handle_skippable_vote_error(e, "generate vote message")?; return Ok(None); } - Err(e) => return Err(e), }; - Ok(Some(bls_op)) + + context + .own_vote_sender + .send(ConsensusMessage::Vote(vote_msg.clone())) + .map_err(|_| SendError(()))?; + + Ok(Some(vote_msg)) +} + +pub fn generate_refresh_vote_message( + vote: Vote, + vctx: &mut VotingContext, +) -> Result, VoteError> { + match create_vote_message(vote, vctx, /* respect_wait_to_vote */ true) { + Ok(vote_msg) => Ok(Some(vote_msg)), + Err(e) => { + handle_skippable_vote_error(e, "generate refresh vote message")?; + Ok(None) + } + } } #[cfg(test)] mod tests { use { super::*, - crossbeam_channel::unbounded, + agave_votor_messages::consensus_message::Block, + crossbeam_channel::bounded, + solana_gossip::contact_info::ContactInfo, solana_hash::Hash, + solana_net_utils::SocketAddrSpace, solana_runtime::{ bank::{Bank, SlotLeader}, bank_forks::BankForks, @@ -317,20 +359,21 @@ mod tests { }; fn generate_expected_consensus_message( + ctx: &VotingContext, vote: Vote, my_bls_keypair: &BLSKeypair, - ) -> ConsensusMessage { - let vote_serialized = bincode::serialize(&vote).unwrap(); - let signature = my_bls_keypair.sign(&vote_serialized); - ConsensusMessage::Vote(VoteMessage { + ) -> VoteMessage { + let payload = get_vote_payload_to_sign(vote, ctx.cluster_info.my_shred_version()); + let signature = my_bls_keypair.sign(&payload); + VoteMessage { vote, signature: signature.into(), rank: 0, - }) + } } fn setup_voting_context_and_bank_forks( - own_vote_sender: Sender>, + own_vote_sender: Sender, validator_keypairs: &[ValidatorVoteKeypairs], my_index: usize, ) -> VotingContext { @@ -343,7 +386,7 @@ mod tests { } fn setup_voting_context_and_bank_forks_with_forks( - own_vote_sender: Sender>, + own_vote_sender: Sender, validator_keypairs: &[ValidatorVoteKeypairs], my_index: usize, ) -> (VotingContext, Arc>) { @@ -358,11 +401,18 @@ mod tests { let bank_forks = BankForks::new_rw_arc(bank0); let my_keys = &validator_keypairs[my_index]; + let contact_info = ContactInfo::new_localhost(&my_keys.node_keypair.pubkey(), 0); + let cluster_info = Arc::new(ClusterInfo::new( + contact_info, + Arc::new(my_keys.node_keypair.insecure_clone()), + SocketAddrSpace::Unspecified, + )); let sharable_banks = bank_forks.read().unwrap().sharable_banks(); - let bls_sender = unbounded().0; - let commitment_sender = unbounded().0; - let consensus_metrics_sender = unbounded().0; + let bls_sender = bounded(1024).0; + let commitment_sender = bounded(1024).0; + let consensus_metrics_sender = bounded(1024).0; let voting_context = VotingContext { + cluster_info, vote_history: VoteHistory::new(my_keys.node_keypair.pubkey(), 0), vote_account_pubkey: my_keys.vote_keypair.pubkey(), identity_keypair: Arc::new(my_keys.node_keypair.insecure_clone()), @@ -382,7 +432,7 @@ mod tests { #[test] fn test_generate_own_vote_message() { - let (own_vote_sender, own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) @@ -399,23 +449,26 @@ mod tests { // Generate a normal notarization vote and check it's sent out correctly. let block_id = Hash::new_unique(); let vote_slot = 2; - let vote = Vote::new_notarization_vote(vote_slot, block_id); - let result = generate_vote_message(vote, false, &mut voting_context) + let vote = Vote::new_notarization_vote(Block { + slot: vote_slot, + block_id, + }); + let result = insert_vote_and_create_bls_message(vote, &mut voting_context) .ok() .unwrap() .unwrap(); - let expected_message = generate_expected_consensus_message(vote, &my_bls_keypair); + let expected_message = + generate_expected_consensus_message(&voting_context, vote, &my_bls_keypair); if let BLSOp::PushVote { - message, - slot, + vote, saved_vote_history, - } = &result + } = result { - assert_eq!(slot, &vote_slot); - assert_eq!(**message, expected_message); + let msg = Arc::unwrap_or_clone(vote); + assert_eq!(msg, expected_message); assert_eq!( saved_vote_history, - &SavedVoteHistoryVersions::from( + SavedVoteHistoryVersions::from( SavedVoteHistory::new( &voting_context.vote_history, &voting_context.identity_keypair @@ -424,17 +477,32 @@ mod tests { ) ); } else { - panic!("Expected BLSOp::PushVote, got {result:?}"); + panic!("Expected BLSOp::VotePush, got {result:?}"); } // Check that own vote sender receives the vote let received_message = own_vote_receiver.recv().unwrap(); - assert_eq!(received_message, vec![expected_message]); + assert_eq!( + received_message, + ConsensusMessage::Vote(expected_message.clone()) + ); + + let refresh_vote = Vote::new_notarization_vote(Block { + slot: vote_slot, + block_id, + }); + let refresh_result = generate_refresh_vote_message(refresh_vote, &mut voting_context) + .ok() + .unwrap() + .unwrap(); + assert_eq!(refresh_result.vote.slot(), vote_slot); + assert_eq!(refresh_result, expected_message); + assert!(own_vote_receiver.try_recv().is_err()); } #[test] fn test_wait_to_vote_slot() { - let (own_vote_sender, _own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, _own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) @@ -447,7 +515,7 @@ mod tests { voting_context.wait_to_vote_slot = Some(4); let vote = Vote::new_finalization_vote(2); assert!( - generate_vote_message(vote, false, &mut voting_context) + insert_vote_and_create_bls_message(vote, &mut voting_context) .unwrap() .is_none() ); @@ -455,7 +523,7 @@ mod tests { // If we have reached wait_to_vote_slot, we should be able to vote voting_context.wait_to_vote_slot = Some(1); assert!( - generate_vote_message(vote, false, &mut voting_context) + insert_vote_and_create_bls_message(vote, &mut voting_context) .unwrap() .is_some() ); @@ -463,7 +531,7 @@ mod tests { #[test] fn test_non_voting_node() { - let (own_vote_sender, _own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, _own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) @@ -475,19 +543,26 @@ mod tests { // Empty authorized voter keypairs to simulate non voting node voting_context.authorized_voter_keypairs = Arc::new(std::sync::RwLock::new(vec![])); let vote = Vote::new_skip_vote(5); - // For non-voting nodes, we just return Ok(None) - assert!( - generate_vote_message(vote, false, &mut voting_context) - .unwrap() - .is_none() - ); + assert!(matches!( + generate_vote_tx( + vote, + &voting_context.sharable_banks.root(), + voting_context.vote_account_pubkey, + voting_context.cluster_info.my_shred_version(), + &voting_context.identity_keypair, + &voting_context.authorized_voter_keypairs, + voting_context.wait_to_vote_slot, + &mut voting_context.derived_bls_keypairs, + ), + GenerateVoteTxResult::NonVoting + )); // Recover correct value to vote again voting_context.authorized_voter_keypairs = Arc::new(RwLock::new(vec![Arc::new( validator_keypairs[my_index].vote_keypair.insecure_clone(), )])); assert!( - generate_vote_message(vote, false, &mut voting_context) + insert_vote_and_create_bls_message(vote, &mut voting_context) .unwrap() .is_some() ); @@ -495,7 +570,7 @@ mod tests { #[test] fn test_wrong_identity_keypair() { - let (own_vote_sender, _own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, _own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) @@ -504,28 +579,30 @@ mod tests { let mut voting_context = setup_voting_context_and_bank_forks(own_vote_sender, &validator_keypairs, my_index); - // Wrong identity keypair - voting_context.identity_keypair = Arc::new(Keypair::new()); - let vote = Vote::new_notarization_vote(6, Hash::new_unique()); - assert!( - generate_vote_message(vote, true, &mut voting_context) - .unwrap() - .is_none() - ); - - // Recover correct value to vote again - voting_context.identity_keypair = - Arc::new(validator_keypairs[my_index].node_keypair.insecure_clone()); - assert!( - generate_vote_message(vote, true, &mut voting_context) - .unwrap() - .is_some() - ); + // Wrong identity keypair should return HotSpare based on rank_map.node_pubkey. + let wrong_identity_keypair = Arc::new(Keypair::new()); + let vote = Vote::new_notarization_vote(Block { + slot: 6, + block_id: Hash::new_unique(), + }); + assert!(matches!( + generate_vote_tx( + vote, + &voting_context.sharable_banks.root(), + voting_context.vote_account_pubkey, + voting_context.cluster_info.my_shred_version(), + &wrong_identity_keypair, + &voting_context.authorized_voter_keypairs, + voting_context.wait_to_vote_slot, + &mut voting_context.derived_bls_keypairs, + ), + GenerateVoteTxResult::HotSpare + )); } #[test] fn test_wrong_vote_account_pubkey() { - let (own_vote_sender, _own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, _own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) @@ -536,9 +613,12 @@ mod tests { // Wrong vote account pubkey voting_context.vote_account_pubkey = Pubkey::new_unique(); - let vote = Vote::new_notarization_vote(7, Hash::new_unique()); + let vote = Vote::new_notarization_vote(Block { + slot: 7, + block_id: Hash::new_unique(), + }); assert!( - generate_vote_message(vote, true, &mut voting_context) + generate_refresh_vote_message(vote, &mut voting_context) .unwrap() .is_none() ); @@ -546,7 +626,7 @@ mod tests { // Recover correct value to vote again voting_context.vote_account_pubkey = validator_keypairs[my_index].vote_keypair.pubkey(); assert!( - generate_vote_message(vote, true, &mut voting_context) + generate_refresh_vote_message(vote, &mut voting_context) .unwrap() .is_some() ); @@ -556,7 +636,7 @@ mod tests { #[should_panic(expected = "could not find rank map for slot 1000000000")] fn test_panic_on_future_slot() { agave_logger::setup(); - let (own_vote_sender, _own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, _own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) @@ -566,14 +646,17 @@ mod tests { setup_voting_context_and_bank_forks(own_vote_sender, &validator_keypairs, my_index); // If we try to vote for a slot in the future, we should panic - let vote = Vote::new_notarization_vote(1_000_000_000, Hash::new_unique()); - let _ = generate_vote_message(vote, false, &mut voting_context); + let vote = Vote::new_notarization_vote(Block { + slot: 1_000_000_000, + block_id: Hash::new_unique(), + }); + let _ = insert_vote_and_create_bls_message(vote, &mut voting_context); } #[test] fn test_zero_staked_validator_fails_voting() { agave_logger::setup(); - let (own_vote_sender, _own_vote_receiver) = crossbeam_channel::unbounded(); + let (own_vote_sender, _own_vote_receiver) = bounded(1024); // Create 10 node validatorvotekeypairs vec let validator_keypairs = (0..10) .map(|_| ValidatorVoteKeypairs::new(Keypair::new(), Keypair::new(), Keypair::new())) @@ -621,9 +704,12 @@ mod tests { .root() .epoch_schedule() .get_first_slot_in_epoch(1); - let vote = Vote::new_notarization_vote(first_slot_in_epoch_1, Hash::new_unique()); + let vote = Vote::new_notarization_vote(Block { + slot: first_slot_in_epoch_1, + block_id: Hash::new_unique(), + }); assert!( - generate_vote_message(vote, false, &mut voting_context) + insert_vote_and_create_bls_message(vote, &mut voting_context) .unwrap() .is_some() ); @@ -634,9 +720,12 @@ mod tests { .root() .epoch_schedule() .get_first_slot_in_epoch(2); - let vote = Vote::new_notarization_vote(first_slot_in_epoch_2, Hash::new_unique()); + let vote = Vote::new_notarization_vote(Block { + slot: first_slot_in_epoch_2, + block_id: Hash::new_unique(), + }); assert!( - generate_vote_message(vote, false, &mut voting_context) + insert_vote_and_create_bls_message(vote, &mut voting_context) .unwrap() .is_none() ); diff --git a/votor/src/votor.rs b/votor/src/votor.rs index 9d596c4265c..206d8b9d3eb 100644 --- a/votor/src/votor.rs +++ b/votor/src/votor.rs @@ -46,17 +46,13 @@ use { crate::{ commitment::CommitmentAggregationData, - consensus_metrics::{ - ConsensusMetrics, ConsensusMetricsEventReceiver, ConsensusMetricsEventSender, - }, + consensus_metrics::ConsensusMetrics, consensus_pool_service::{ConsensusPoolContext, ConsensusPoolService}, - consensus_rewards::ConsensusRewardsService, event::{ - LeaderWindowInfo, RepairEventSender, SwitchBankEventSender, VotorEventReceiver, + LatestSwitchRequest, LeaderWindowInfo, RepairEventSender, VotorEventReceiver, VotorEventSender, }, event_handler::{EventHandler, EventHandlerContext}, - generated_cert_types::GeneratedCertTypes, root_utils::RootContext, timer_manager::TimerManager, vote_history::VoteHistory, @@ -64,15 +60,17 @@ use { voting_service::BLSOp, voting_utils::VotingContext, }, + agave_bls_sigverify::{ + generated_cert_types::GeneratedCertTypes, sig_verified_messages::SigVerifiedBatch, + }, agave_votor_messages::{ - consensus_message::ConsensusMessage, - reward_certificate::{AddVoteMessage, BuildRewardCertsRequest, BuildRewardCertsResponse}, + consensus_message::{Block, ConsensusMessage}, + metric_types::{ConsensusMetricsEventReceiver, ConsensusMetricsEventSender}, }, crossbeam_channel::{Receiver, Sender}, parking_lot::RwLock as PlRwLock, solana_clock::Slot, solana_gossip::cluster_info::ClusterInfo, - solana_hash::Hash, solana_keypair::Keypair, solana_ledger::{blockstore::Blockstore, leader_schedule_cache::LeaderScheduleCache}, solana_pubkey::Pubkey, @@ -114,19 +112,17 @@ pub struct VotorConfig { pub commitment_sender: Sender, pub bank_notification_sender: Option, pub leader_window_info_sender: Sender, - pub highest_parent_ready: Arc>, + pub highest_parent_ready: Arc>, pub event_sender: VotorEventSender, - pub own_vote_sender: Sender>, - pub reward_certs_sender: Sender, + pub own_vote_sender: Sender, pub repair_event_sender: RepairEventSender, - pub switch_bank_sender: SwitchBankEventSender, + pub latest_switch_request: LatestSwitchRequest, // Receivers pub event_receiver: VotorEventReceiver, - pub consensus_message_receiver: Receiver>, + pub consensus_message_receiver: Receiver, + pub own_message_receiver: Receiver, pub consensus_metrics_receiver: ConsensusMetricsEventReceiver, - pub reward_votes_receiver: Receiver, - pub build_reward_certs_receiver: Receiver, } /// Context shared with block creation, replay, gossip, banking stage etc @@ -135,17 +131,16 @@ pub(crate) struct SharedContext { pub(crate) bank_forks: Arc>, pub(crate) cluster_info: Arc, pub(crate) leader_window_info_sender: Sender, - pub(crate) highest_parent_ready: Arc>, + pub(crate) highest_parent_ready: Arc>, pub(crate) vote_history_storage: Arc, pub(crate) repair_event_sender: RepairEventSender, - pub(crate) switch_bank_sender: SwitchBankEventSender, + pub(crate) latest_switch_request: LatestSwitchRequest, } pub struct Votor { event_handler: EventHandler, consensus_pool_service: ConsensusPoolService, timer_manager: Arc>, - consensus_rewards_service: ConsensusRewardsService, metrics: JoinHandle<()>, } @@ -170,14 +165,12 @@ impl Votor { event_sender, own_vote_sender, repair_event_sender, - switch_bank_sender, + latest_switch_request, event_receiver, consensus_message_receiver, + own_message_receiver, consensus_metrics_sender, consensus_metrics_receiver, - reward_votes_receiver, - build_reward_certs_receiver, - reward_certs_sender, generated_cert_types, highest_finalized, bank_forks_controller, @@ -185,6 +178,7 @@ impl Votor { let migration_status = bank_forks.read().unwrap().migration_status(); let identity_keypair = cluster_info.keypair(); + let vote_history_highest_parent_ready = vote_history.highest_parent_ready(); // Get the sharable root bank let sharable_banks = bank_forks.read().unwrap().sharable_banks(); @@ -197,10 +191,11 @@ impl Votor { leader_window_info_sender, vote_history_storage, repair_event_sender: repair_event_sender.clone(), - switch_bank_sender, + latest_switch_request, }; let voting_context = VotingContext { + cluster_info: cluster_info.clone(), vote_history, vote_account_pubkey: vote_account, identity_keypair, @@ -246,10 +241,11 @@ impl Votor { blockstore, sharable_banks: sharable_banks.clone(), leader_schedule_cache: leader_schedule_cache.clone(), + vote_history_highest_parent_ready, consensus_message_receiver, + own_message_receiver, bls_sender, event_sender, - commitment_sender, repair_event_sender, highest_finalized, }; @@ -261,20 +257,10 @@ impl Votor { ); let event_handler = EventHandler::new(event_handler_context); let consensus_pool_service = ConsensusPoolService::new(consensus_pool_context); - let consensus_rewards_service = ConsensusRewardsService::new( - cluster_info, - leader_schedule_cache, - sharable_banks, - exit, - reward_votes_receiver, - build_reward_certs_receiver, - reward_certs_sender, - ); Self { event_handler, consensus_pool_service, - consensus_rewards_service, timer_manager, metrics, } @@ -282,7 +268,6 @@ impl Votor { pub fn join(self) -> thread::Result<()> { self.consensus_pool_service.join()?; - self.consensus_rewards_service.join()?; // Loop till we manage to unwrap the Arc and then we can join. let mut timer_manager = self.timer_manager; diff --git a/watchtower/Cargo.toml b/watchtower/Cargo.toml index 75775f36272..6a6fbc76aff 100644 --- a/watchtower/Cargo.toml +++ b/watchtower/Cargo.toml @@ -11,11 +11,14 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] [dependencies] +agave-feature-set = { workspace = true } agave-logger = { workspace = true } clap = { workspace = true } humantime = { workspace = true } @@ -23,7 +26,7 @@ log = { workspace = true } solana-clap-utils = { workspace = true } solana-cli-config = { workspace = true } solana-cli-output = { workspace = true } -solana-hash = "=4.3.0" +solana-hash = "=4.4.0" solana-metrics = { workspace = true } solana-native-token = "=3.0.0" solana-notifier = { workspace = true } @@ -31,3 +34,7 @@ solana-pubkey = { version = "=4.2.0", default-features = false } solana-rpc-client = { workspace = true } solana-rpc-client-api = { workspace = true } solana-version = { workspace = true } +solana-vote-interface = { workspace = true } + +[lints] +workspace = true diff --git a/watchtower/README.md b/watchtower/README.md index c94fdf52d91..e6efdbd5895 100644 --- a/watchtower/README.md +++ b/watchtower/README.md @@ -8,6 +8,13 @@ If you only care about the health of several specific validators, the `--validator-identity` command-line argument can be used to restrict failure notifications to issues only affecting that set of validators. +When the Validator Admission Ticket feature is active, watchtower also alerts +when a monitored validator's vote account balance is below the bank-side VAT +balance threshold computed from RPC-visible state. This check only verifies the +vote account balance over public RPC. It does not verify full VAT eligibility, +including BLS pubkey presence, stake, top validator set inclusion, or tie cutoff +behavior. + User can provide either 1 or 3 RPC URLs for the cluster via the `--url` or `--urls` command-line arguments respectively. 2 URLs are not accepted because it's not enough to have redundnacy, and more than 3 URLs are not accepted because there's little diff --git a/watchtower/src/main.rs b/watchtower/src/main.rs index 0a838196be1..93a9ce2a7b4 100644 --- a/watchtower/src/main.rs +++ b/watchtower/src/main.rs @@ -17,6 +17,7 @@ use { solana_pubkey::Pubkey, solana_rpc_client::rpc_client::RpcClient, solana_rpc_client_api::{client_error, response::RpcVoteAccountStatus}, + solana_vote_interface::state::VoteStateV4, std::{ collections::HashMap, error, @@ -25,6 +26,8 @@ use { }, }; +const LEGACY_VAT_TO_BURN_PER_EPOCH: u64 = 1_600_000_000; + struct Config { address_labels: HashMap, ignore_http_bad_gateway: bool, @@ -284,6 +287,88 @@ struct EndpointData { last_recent_blockhash: Hash, } +fn slot_time_reduction_vat_burns() -> [(Pubkey, u64); 4] { + [ + ( + agave_feature_set::reduce_slot_time_to_200ms::id(), + 800_000_000, + ), + ( + agave_feature_set::reduce_slot_time_to_250ms::id(), + 1_000_000_000, + ), + ( + agave_feature_set::reduce_slot_time_to_300ms::id(), + 1_200_000_000, + ), + ( + agave_feature_set::reduce_slot_time_to_350ms::id(), + 1_400_000_000, + ), + ] +} + +fn is_feature_active_at_slot( + rpc_client: &RpcClient, + feature_id: &Pubkey, + slot: u64, +) -> client_error::Result { + Ok(matches!( + rpc_client.get_feature_activation_slot(feature_id)?, + Some(activation_slot) if activation_slot <= slot + )) +} + +fn vat_to_burn_per_epoch(rpc_client: &RpcClient, slot: u64) -> client_error::Result { + let epoch_schedule = rpc_client.get_epoch_schedule()?; + + // Keep this table in sync with runtime/src/slot_params.rs. Slot-time + // feature gates take effect at the first slot of the epoch after activation. + for (feature_id, vat_to_burn_per_epoch) in slot_time_reduction_vat_burns() { + let Some(activation_slot) = rpc_client.get_feature_activation_slot(&feature_id)? else { + continue; + }; + if activation_slot > slot { + continue; + } + + let activation_epoch = epoch_schedule.get_epoch(activation_slot); + let effective_slot = + epoch_schedule.get_first_slot_in_epoch(activation_epoch.saturating_add(1)); + if effective_slot <= slot { + return Ok(vat_to_burn_per_epoch); + } + } + + Ok(LEGACY_VAT_TO_BURN_PER_EPOCH) +} + +fn get_minimum_vat_vote_account_balance( + rpc_client: &RpcClient, +) -> client_error::Result> { + let slot = rpc_client.get_slot()?; + if !is_feature_active_at_slot( + rpc_client, + &agave_feature_set::validator_admission_ticket::id(), + slot, + )? { + return Ok(None); + } + + let vote_account_rent_exempt_minimum = + rpc_client.get_minimum_balance_for_rent_exemption(VoteStateV4::size_of())?; + let vat_to_burn_per_epoch = + if is_feature_active_at_slot(rpc_client, &agave_feature_set::alpenglow::id(), slot)? { + vat_to_burn_per_epoch(rpc_client, slot)? + } else { + 0 + }; + + Ok(Some( + vote_account_rent_exempt_minimum + vat_to_burn_per_epoch, + )) +} + fn query_endpoint( config: &Config, endpoint: &mut EndpointData, @@ -354,29 +439,70 @@ fn query_endpoint( } let mut validator_errors = vec![]; + let minimum_vat_vote_account_balance = if config.validator_identity_pubkeys.is_empty() { + None + } else { + get_minimum_vat_vote_account_balance(&endpoint.rpc_client)? + }; for validator_identity in config.validator_identity_pubkeys.iter() { + let validator_identity_string = validator_identity.to_string(); let formatted_validator_identity = - format_labeled_address(&validator_identity.to_string(), &config.address_labels); + format_labeled_address(&validator_identity_string, &config.address_labels); if vote_accounts .delinquent .iter() - .any(|vai| vai.node_pubkey == *validator_identity.to_string()) + .any(|vai| vai.node_pubkey == validator_identity_string.as_str()) { validator_errors.push(format!("{formatted_validator_identity} delinquent")); } else if !vote_accounts .current .iter() - .any(|vai| vai.node_pubkey == *validator_identity.to_string()) + .any(|vai| vai.node_pubkey == validator_identity_string.as_str()) { validator_errors.push(format!("{formatted_validator_identity} missing")); } - if let Some(balance) = validator_balances.get(validator_identity) { - if *balance < config.minimum_validator_identity_balance { - failures.push(( - "balance", - format!("{} has {}", formatted_validator_identity, Sol(*balance)), - )); + if let Some(balance) = validator_balances.get(validator_identity) + && *balance < config.minimum_validator_identity_balance + { + failures.push(( + "balance", + format!("{} has {}", formatted_validator_identity, Sol(*balance)), + )); + } + + if let Some(minimum_vat_vote_account_balance) = minimum_vat_vote_account_balance { + for vote_account in vote_accounts + .current + .iter() + .chain(vote_accounts.delinquent.iter()) + .filter(|vai| vai.node_pubkey == validator_identity_string.as_str()) + { + let Ok(vote_pubkey) = vote_account.vote_pubkey.parse::() else { + failures.push(( + "vat-vote-account-balance", + format!( + "{} vote account {} is not a valid pubkey", + formatted_validator_identity, vote_account.vote_pubkey + ), + )); + continue; + }; + + let balance = endpoint.rpc_client.get_balance(&vote_pubkey)?; + if balance < minimum_vat_vote_account_balance { + failures.push(( + "vat-vote-account-balance", + format!( + "{} vote account {} has {}, below required VAT balance \ + threshold of {}", + formatted_validator_identity, + vote_pubkey, + Sol(balance), + Sol(minimum_vat_vote_account_balance) + ), + )); + } } } } @@ -392,13 +518,12 @@ fn query_endpoint( Ok(failures.into_iter().next()) // Only report the first failure if any } Err(err) => { - if let client_error::ErrorKind::Reqwest(reqwest_err) = err.kind() { - if let Some(client_error::reqwest::StatusCode::BAD_GATEWAY) = reqwest_err.status() { - if config.ignore_http_bad_gateway { - warn!("Error suppressed: {err}"); - return Ok(None); - } - } + if let client_error::ErrorKind::Reqwest(reqwest_err) = err.kind() + && let Some(client_error::reqwest::StatusCode::BAD_GATEWAY) = reqwest_err.status() + && config.ignore_http_bad_gateway + { + warn!("Error suppressed: {err}"); + return Ok(None); } warn!("rpc-error: {err}"); Err(err) diff --git a/xdp-ebpf/Cargo.toml b/xdp-ebpf/Cargo.toml index 5c751eebfc2..c27982b5e66 100644 --- a/xdp-ebpf/Cargo.toml +++ b/xdp-ebpf/Cargo.toml @@ -9,6 +9,11 @@ license = { workspace = true } edition = { workspace = true } include = ["Cargo.toml", "src/**", "README", "agave-xdp-prog"] +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [[bin]] name = "agave-xdp-prog" path = "src/bin/agave-xdp-prog.rs" diff --git a/xdp/Cargo.toml b/xdp/Cargo.toml index 559cc441777..7ec0c768fd8 100644 --- a/xdp/Cargo.toml +++ b/xdp/Cargo.toml @@ -8,10 +8,16 @@ license = { workspace = true } edition = { workspace = true } publish = true +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] agave-unstable-api = [] [dependencies] +agave-cpu-utils = { workspace = true } arc-swap = { workspace = true } bytes = { workspace = true } crossbeam-channel = { workspace = true } @@ -24,4 +30,30 @@ agave-xdp-ebpf = { workspace = true } arrayvec = { workspace = true } aya = { workspace = true } caps = { workspace = true } -core_affinity = { workspace = true } + +[target.'cfg(target_os = "linux")'.dev-dependencies] +aya = { workspace = true, features = ["test-helpers"] } +nix = { workspace = true, features = ["net", "poll", "socket"] } + +[[test]] +name = "netlink_snapshot" +path = "tests/netlink_snapshot.rs" +required-features = ["agave-unstable-api"] + +[[test]] +name = "route_monitor" +path = "tests/route_monitor.rs" +required-features = ["agave-unstable-api"] + +[[test]] +name = "router_snapshot" +path = "tests/router_snapshot.rs" +required-features = ["agave-unstable-api"] + +[[test]] +name = "transmitter_smoke" +path = "tests/transmitter_smoke.rs" +required-features = ["agave-unstable-api"] + +[lints] +workspace = true diff --git a/xdp/src/lib.rs b/xdp/src/lib.rs index 6f149efa5af..daf78473219 100644 --- a/xdp/src/lib.rs +++ b/xdp/src/lib.rs @@ -31,51 +31,6 @@ pub mod transmitter; pub use program::load_xdp_program; use std::{io, net::Ipv4Addr}; -#[cfg(target_os = "linux")] -pub fn set_cpu_affinity(cpus: impl IntoIterator) -> Result<(), io::Error> { - unsafe { - let mut cpu_set = std::mem::zeroed(); - - for cpu in cpus { - libc::CPU_SET(cpu, &mut cpu_set); - } - - let result = libc::sched_setaffinity( - 0, - std::mem::size_of::(), - &cpu_set as *const libc::cpu_set_t, - ); - if result != 0 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } - } -} - -#[cfg(not(target_os = "linux"))] -pub fn set_cpu_affinity(_cpus: impl IntoIterator) -> Result<(), io::Error> { - unimplemented!() -} - -#[cfg(target_os = "linux")] -pub fn get_cpu() -> Result { - unsafe { - let result = libc::sched_getcpu(); - if result < 0 { - assert_eq!(result, -1); - Err(io::Error::last_os_error()) - } else { - Ok(result as usize) - } - } -} - -#[cfg(not(target_os = "linux"))] -pub fn get_cpu() -> Result { - unimplemented!() -} - /// Returns the IPv4 address of the specified network interface. /// /// If the interface is part of a bonded interface, returns the master's IPv4 address. diff --git a/xdp/src/netlink.rs b/xdp/src/netlink.rs index 450acaf0020..991eece9213 100644 --- a/xdp/src/netlink.rs +++ b/xdp/src/netlink.rs @@ -34,6 +34,10 @@ const IFLA_GRE_TTL: u16 = 8; const IFLA_GRE_TOS: u16 = 9; const IFLA_GRE_PMTUDISC: u16 = 10; +// VLAN nested attributes (from include/uapi/linux/if_link.h) +const IFLA_VLAN_ID: u16 = 1; +const IFLA_VLAN_PROTOCOL: u16 = 5; + #[repr(C)] #[allow(non_camel_case_types)] struct ifinfomsg { @@ -386,11 +390,19 @@ pub struct GreTunnelInfo { pub pmtudisc: u8, } +/// 802.1Q VLAN sub-interface information from netlink (IFLA_LINKINFO kind "vlan"). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VlanLinkInfo { + /// VLAN ID (IFLA_VLAN_ID), 1-4094 in practice. + pub vid: u16, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct InterfaceInfo { pub if_index: u32, pub mtu: u32, pub gre_tunnel: Option, + pub vlan_link: Option, } impl InterfaceInfo { @@ -453,13 +465,35 @@ pub(crate) fn parse_rtm_ifinfomsg(msg: &NetlinkMessage) -> Option // Parse GRE tunnel information if this is a GRE interface let gre_tunnel = parse_gre_tunnel_info_from_linkinfo(&attrs); + // Parse VLAN information if this is an 802.1Q VLAN sub-interface + let vlan_link = parse_vlan_link_info_from_linkinfo(&attrs); Some(InterfaceInfo { if_index: ifi.ifi_index, mtu, gre_tunnel, + vlan_link, }) } +// Parse 802.1Q VLAN information from netlink +fn parse_vlan_link_info_from_linkinfo(attrs: &HashMap) -> Option { + let vlan = parse_linkinfo_data_for_kind(attrs, b"vlan")?; + + // Only 802.1Q is supported; skip 802.1ad (QinQ) sub-interfaces. The protocol attribute is a + // big-endian u16; kernels predating 802.1ad support omit it, which implies 802.1Q. + if let Some(proto) = vlan.get(&IFLA_VLAN_PROTOCOL) { + let proto = u16::from_be_bytes(proto.data.get(..2)?.try_into().ok()?); + if proto != libc::ETH_P_8021Q as u16 { + return None; + } + } + + let vid = vlan + .get(&IFLA_VLAN_ID) + .and_then(|a| u16_from_ne_bytes(a.data))?; + Some(VlanLinkInfo { vid }) +} + // Parse GRE tunnel information from netlink fn parse_gre_tunnel_info_from_linkinfo(attrs: &HashMap) -> Option { let gre = parse_linkinfo_data_for_kind(attrs, b"gre")?; @@ -598,10 +632,10 @@ pub fn parse_rtm_newneigh(msg: &NetlinkMessage, if_index: Option) -> Option return None; } let nd_msg = unsafe { ptr::read_unaligned(msg.data.as_ptr() as *const ndmsg) }; - if let Some(idx) = if_index { - if nd_msg.ndm_ifindex != idx as i32 { - return None; - } + if let Some(idx) = if_index + && nd_msg.ndm_ifindex != idx as i32 + { + return None; } let Ok(attrs) = parse_attrs(&msg.data[mem::size_of::()..]) else { return None; @@ -615,12 +649,12 @@ pub fn parse_rtm_newneigh(msg: &NetlinkMessage, if_index: Option) -> Option if let Some(dst_attr) = attrs.get(&NDA_DST) { neighbor.destination = parse_ip_address(dst_attr.data, nd_msg.ndm_family); } - if let Some(lladdr_attr) = attrs.get(&NDA_LLADDR) { - if lladdr_attr.data.len() >= 6 { - let mut mac = [0u8; 6]; - mac.copy_from_slice(&lladdr_attr.data[0..6]); - neighbor.lladdr = Some(MacAddress(mac)); - } + if let Some(lladdr_attr) = attrs.get(&NDA_LLADDR) + && lladdr_attr.data.len() >= 6 + { + let mut mac = [0u8; 6]; + mac.copy_from_slice(&lladdr_attr.data[0..6]); + neighbor.lladdr = Some(MacAddress(mac)); } Some(neighbor) } @@ -783,3 +817,8 @@ fn u32_from_ne_bytes(data: &[u8]) -> Option { let bytes: [u8; 4] = data.get(..4)?.try_into().ok()?; Some(u32::from_ne_bytes(bytes)) } + +fn u16_from_ne_bytes(data: &[u8]) -> Option { + let bytes: [u8; 2] = data.get(..2)?.try_into().ok()?; + Some(u16::from_ne_bytes(bytes)) +} diff --git a/xdp/src/packet.rs b/xdp/src/packet.rs index dd6a419817a..13c2c2a951c 100644 --- a/xdp/src/packet.rs +++ b/xdp/src/packet.rs @@ -7,9 +7,17 @@ use { }; pub const ETH_HEADER_SIZE: usize = 14; +/// Plain Ethernet header (14B) + 802.1Q tag (4B). +pub const VLAN_ETH_HEADER_SIZE: usize = ETH_HEADER_SIZE + 4; pub const IP_HEADER_SIZE: usize = 20; pub const UDP_HEADER_SIZE: usize = 8; +/// Total header size of an untagged IPv4 UDP frame: Ethernet + IP + UDP. +pub const PACKET_HEADER_SIZE: usize = ETH_HEADER_SIZE + IP_HEADER_SIZE + UDP_HEADER_SIZE; +/// Total header size of an 802.1Q tagged IPv4 UDP frame: tagged Ethernet + IP + UDP. +pub const VLAN_PACKET_HEADER_SIZE: usize = VLAN_ETH_HEADER_SIZE + IP_HEADER_SIZE + UDP_HEADER_SIZE; const IP_DONT_FRAGMENT: u16 = 0x4000; +/// EtherType identifying an 802.1Q tagged frame. +const ETH_P_8021Q: u16 = 0x8100; pub fn write_eth_header(packet: &mut [u8], src_mac: &[u8; 6], dst_mac: &[u8; 6]) { packet[0..6].copy_from_slice(dst_mac); @@ -17,6 +25,116 @@ pub fn write_eth_header(packet: &mut [u8], src_mac: &[u8; 6], dst_mac: &[u8; 6]) packet[12..14].copy_from_slice(&(ETH_P_IP as u16).to_be_bytes()); } +/// Write an 18-byte Ethernet header carrying an 802.1Q VLAN tag. +/// +/// Layout: `dst MAC (6) | src MAC (6) | TPID=0x8100 (2) | TCI (2) | inner EtherType (2)`. +/// The TCI packs `pcp` into the high 3 bits, DEI=0, and `vid` into the low 12 bits. +#[inline] +pub fn write_vlan_eth_header( + packet: &mut [u8], + src_mac: &[u8; 6], + dst_mac: &[u8; 6], + vid: u16, + pcp: u8, +) { + packet[0..6].copy_from_slice(dst_mac); + packet[6..12].copy_from_slice(src_mac); + packet[12..14].copy_from_slice(Ð_P_8021Q.to_be_bytes()); + // TCI: PCP in the top 3 bits, then DEI=0 (frames are not marked drop-eligible, matching what + // the kernel VLAN driver emits by default), then the VID in the low 12 bits. + let tci = (((pcp as u16) & 0x7) << 13) | (vid & 0x0FFF); + packet[14..16].copy_from_slice(&tci.to_be_bytes()); + packet[16..18].copy_from_slice(&(ETH_P_IP as u16).to_be_bytes()); +} + +/// Construct a complete untagged IPv4 UDP frame in `packet`. +/// +/// Layout: `[Eth] [IPv4] [UDP] [payload]`. The caller is responsible for sizing `packet` to at +/// least `PACKET_HEADER_SIZE + payload.len()`; this function returns `false` and writes nothing +/// if the buffer is too small. +#[allow(clippy::too_many_arguments)] +pub fn construct_packet( + packet: &mut [u8], + src_mac: &[u8; 6], + dst_mac: &[u8; 6], + src_ip: &Ipv4Addr, + dst_ip: &Ipv4Addr, + src_port: u16, + dst_port: u16, + payload: &[u8], + ecn: Option, +) -> bool { + let payload_len = payload.len(); + if packet.len() < PACKET_HEADER_SIZE + payload_len { + return false; + } + // write the payload first as it's needed for checksum calculation (if enabled) + packet[PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + payload_len].copy_from_slice(payload); + write_eth_header(packet, src_mac, dst_mac); + write_ip_header_for_udp( + &mut packet[ETH_HEADER_SIZE..], + src_ip, + dst_ip, + ecn, + (UDP_HEADER_SIZE + payload_len) as u16, + ); + write_udp_header( + &mut packet[ETH_HEADER_SIZE + IP_HEADER_SIZE..], + src_ip, + src_port, + dst_ip, + dst_port, + payload_len as u16, + false, + ); + true +} + +/// Construct a complete VLAN-tagged IPv4 UDP frame in `packet`. +/// +/// Layout: `[Eth + 802.1Q] [IPv4] [UDP] [payload]`. The caller is responsible for sizing `packet` +/// to at least `VLAN_PACKET_HEADER_SIZE + payload.len()`; this function returns `false` and +/// writes nothing if the buffer is too small. +#[allow(clippy::too_many_arguments)] +pub fn construct_vlan_packet( + packet: &mut [u8], + src_mac: &[u8; 6], + dst_mac: &[u8; 6], + src_ip: &Ipv4Addr, + dst_ip: &Ipv4Addr, + src_port: u16, + dst_port: u16, + vid: u16, + pcp: u8, + payload: &[u8], + ecn: Option, +) -> bool { + let payload_len = payload.len(); + if packet.len() < VLAN_PACKET_HEADER_SIZE + payload_len { + return false; + } + // write the payload first as it's needed for checksum calculation (if enabled) + packet[VLAN_PACKET_HEADER_SIZE..VLAN_PACKET_HEADER_SIZE + payload_len].copy_from_slice(payload); + write_vlan_eth_header(packet, src_mac, dst_mac, vid, pcp); + write_ip_header_for_udp( + &mut packet[VLAN_ETH_HEADER_SIZE..], + src_ip, + dst_ip, + ecn, + (UDP_HEADER_SIZE + payload_len) as u16, + ); + write_udp_header( + &mut packet[VLAN_ETH_HEADER_SIZE + IP_HEADER_SIZE..], + src_ip, + src_port, + dst_ip, + dst_port, + payload_len as u16, + false, + ); + true +} + pub(crate) fn write_ip_header( packet: &mut [u8], src_ip: &Ipv4Addr, @@ -148,3 +266,168 @@ fn calculate_ip_checksum(header: &[u8]) -> u16 { !(sum as u16) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_write_vlan_eth_header_layout() { + // dst, src, TPID=0x8100, TCI (pcp=0, dei=0, vid=900 -> 0x0384), inner EtherType=0x0800 + let src = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01]; + let dst = [0x01, 0x00, 0x5e, 0x00, 0x00, 0x03]; + let mut buf = [0u8; VLAN_ETH_HEADER_SIZE]; + write_vlan_eth_header(&mut buf, &src, &dst, 900, 0); + assert_eq!(&buf[0..6], &dst); + assert_eq!(&buf[6..12], &src); + assert_eq!(&buf[12..14], &[0x81, 0x00]); + assert_eq!(&buf[14..16], &[0x03, 0x84]); + assert_eq!(&buf[16..18], &[0x08, 0x00]); + } + + #[test] + fn test_write_vlan_eth_header_packs_pcp_and_vid() { + // pcp=5, vid=0x0FFF (max 12-bit value). TCI = (5 << 13) | 0x0FFF = 0xAFFF. + let mut buf = [0u8; VLAN_ETH_HEADER_SIZE]; + write_vlan_eth_header(&mut buf, &[0u8; 6], &[0u8; 6], 0x0FFF, 5); + assert_eq!(&buf[14..16], &[0xAF, 0xFF]); + } + + #[test] + fn test_write_vlan_eth_header_masks_pcp_and_vid_inputs() { + // High bits in pcp and vid must not leak into adjacent fields. + let mut buf = [0u8; VLAN_ETH_HEADER_SIZE]; + write_vlan_eth_header(&mut buf, &[0u8; 6], &[0u8; 6], 0xFFFF, 0xFF); + // pcp masked to 0x7, then shifted into bits 13..16; vid masked to 0x0FFF. + // Expected TCI = (0x7 << 13) | 0x0FFF = 0xEFFF. + assert_eq!(&buf[14..16], &[0xEF, 0xFF]); + } + + #[test] + fn test_construct_packet_layout() { + let src_mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01]; + let dst_mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02]; + let src_ip = Ipv4Addr::new(10, 228, 0, 5); + let dst_ip = Ipv4Addr::new(10, 228, 0, 9); + let payload = b"hello-world"; + let mut buf = vec![0u8; PACKET_HEADER_SIZE + payload.len()]; + + assert!(construct_packet( + &mut buf, &src_mac, &dst_mac, &src_ip, &dst_ip, 7000, 7733, payload, None, + )); + + // Untagged ethernet header + assert_eq!(&buf[0..6], &dst_mac); + assert_eq!(&buf[6..12], &src_mac); + assert_eq!(&buf[12..14], &[0x08, 0x00]); // ethertype IPv4 + + // IPv4 header sanity: version+IHL byte, protocol=UDP, src/dst. + assert_eq!(buf[ETH_HEADER_SIZE], 0x45); + assert_eq!(buf[ETH_HEADER_SIZE + 9], IPPROTO_UDP as u8); + assert_eq!( + &buf[ETH_HEADER_SIZE + 12..ETH_HEADER_SIZE + 16], + &src_ip.octets() + ); + assert_eq!( + &buf[ETH_HEADER_SIZE + 16..ETH_HEADER_SIZE + 20], + &dst_ip.octets() + ); + + // UDP header sanity: src/dst ports, length. + let udp_off = ETH_HEADER_SIZE + IP_HEADER_SIZE; + assert_eq!(&buf[udp_off..udp_off + 2], &7000u16.to_be_bytes()); + assert_eq!(&buf[udp_off + 2..udp_off + 4], &7733u16.to_be_bytes()); + assert_eq!( + &buf[udp_off + 4..udp_off + 6], + &((UDP_HEADER_SIZE + payload.len()) as u16).to_be_bytes() + ); + + // Payload tail. + assert_eq!(&buf[udp_off + UDP_HEADER_SIZE..], payload.as_slice()); + } + + #[test] + fn test_construct_packet_rejects_undersized_buffer() { + let mut tiny = [0u8; 10]; + let ok = construct_packet( + &mut tiny, + &[0; 6], + &[0; 6], + &Ipv4Addr::UNSPECIFIED, + &Ipv4Addr::UNSPECIFIED, + 0, + 0, + b"too big", + None, + ); + assert!(!ok); + // Buffer must not be partially written. + assert!(tiny.iter().all(|&b| b == 0)); + } + + #[test] + fn test_construct_vlan_packet_layout() { + let src_mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01]; + let dst_mac = [0x01, 0x00, 0x5e, 0x00, 0x00, 0x03]; + let src_ip = Ipv4Addr::new(10, 228, 0, 5); + let dst_ip = Ipv4Addr::new(239, 0, 0, 3); + let payload = b"hello-world"; + let mut buf = vec![0u8; VLAN_PACKET_HEADER_SIZE + payload.len()]; + + assert!(construct_vlan_packet( + &mut buf, &src_mac, &dst_mac, &src_ip, &dst_ip, 7000, 7733, 900, 0, payload, None, + )); + + // VLAN-tagged ethernet header + assert_eq!(&buf[0..6], &dst_mac); + assert_eq!(&buf[6..12], &src_mac); + assert_eq!(&buf[12..14], &[0x81, 0x00]); + assert_eq!(&buf[14..16], &[0x03, 0x84]); // vid=900, pcp=0 + assert_eq!(&buf[16..18], &[0x08, 0x00]); // inner ethertype IPv4 + + // IPv4 header sanity: version+IHL byte, protocol=UDP, src/dst. + assert_eq!(buf[VLAN_ETH_HEADER_SIZE], 0x45); + assert_eq!(buf[VLAN_ETH_HEADER_SIZE + 9], IPPROTO_UDP as u8); + assert_eq!( + &buf[VLAN_ETH_HEADER_SIZE + 12..VLAN_ETH_HEADER_SIZE + 16], + &src_ip.octets() + ); + assert_eq!( + &buf[VLAN_ETH_HEADER_SIZE + 16..VLAN_ETH_HEADER_SIZE + 20], + &dst_ip.octets() + ); + + // UDP header sanity: src/dst ports, length. + let udp_off = VLAN_ETH_HEADER_SIZE + IP_HEADER_SIZE; + assert_eq!(&buf[udp_off..udp_off + 2], &7000u16.to_be_bytes()); + assert_eq!(&buf[udp_off + 2..udp_off + 4], &7733u16.to_be_bytes()); + assert_eq!( + &buf[udp_off + 4..udp_off + 6], + &((UDP_HEADER_SIZE + payload.len()) as u16).to_be_bytes() + ); + + // Payload tail. + assert_eq!(&buf[udp_off + UDP_HEADER_SIZE..], payload.as_slice()); + } + + #[test] + fn test_construct_vlan_packet_rejects_undersized_buffer() { + let mut tiny = [0u8; 10]; + let ok = construct_vlan_packet( + &mut tiny, + &[0; 6], + &[0; 6], + &Ipv4Addr::UNSPECIFIED, + &Ipv4Addr::UNSPECIFIED, + 0, + 0, + 1, + 0, + b"too big", + None, + ); + assert!(!ok); + // Buffer must not be partially written. + assert!(tiny.iter().all(|&b| b == 0)); + } +} diff --git a/xdp/src/program.rs b/xdp/src/program.rs index a9ce2e34b98..3775a42751b 100644 --- a/xdp/src/program.rs +++ b/xdp/src/program.rs @@ -2,7 +2,10 @@ use { crate::device::NetworkDevice, - aya::{Ebpf, EbpfLoader, programs::Xdp}, + aya::{ + Ebpf, EbpfLoader, + programs::{Xdp, xdp::XdpMode}, + }, std::io::{Cursor, Write}, }; @@ -44,7 +47,7 @@ pub fn load_xdp_program(dev: &NetworkDevice) -> Result Result, @@ -48,6 +64,7 @@ pub struct NextHop { pub mtu: u32, pub preferred_src_ip: Option, pub gre: Option, + pub vlan: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -406,6 +423,8 @@ pub struct Router { cached_default_route: Option, // cache for gre route info keyed by interface index. This stays tiny in practice. cached_gre_info: Vec, + // cache for vlan encap info keyed by interface index. One entry per VLAN sub-interface. + cached_vlan_info: Vec, } struct RoutingTableDisplay<'a>(&'a [Route]); @@ -441,6 +460,7 @@ impl Router { interfaces: tables.interfaces, cached_default_route: None, cached_gre_info: Vec::new(), + cached_vlan_info: Vec::new(), }; let mut has_gre_interface = false; @@ -451,6 +471,14 @@ impl Router { router.cached_gre_info.push(gre); } } + if let Some(vlan) = interface.vlan_link { + router.cached_vlan_info.push(VlanRouteInfo { + if_index: interface.if_index, + vid: vlan.vid, + // Kernel default egress QoS mapping; a configurable mapping can come later. + pcp: 0, + }); + } } if router.cached_gre_info.is_empty() && has_gre_interface { warn!("GRE cache: GRE interface(s) present but none with valid remote resolved"); @@ -487,6 +515,13 @@ impl Router { self.interface_gre_route_info(interface) } + fn cached_vlan_route_info(&self, if_index: u32) -> Option { + self.cached_vlan_info + .iter() + .find(|vlan| vlan.if_index == if_index) + .copied() + } + fn resolve_next_hop( &self, route_ip: Ipv4Addr, @@ -495,20 +530,23 @@ impl Router { let if_index = route .out_if_index .ok_or(RouteError::MissingOutputInterface)?; - let next_hop_ip = IpAddr::V4(route.gateway.unwrap_or(route_ip)); + let next_hop_v4 = route.gateway.unwrap_or(route_ip); + let next_hop_ip = IpAddr::V4(next_hop_v4); let preferred_src_ip = route.preferred_src; - if let Some(default_route) = &self.cached_default_route { - if default_route.ip_addr == next_hop_ip && default_route.if_index == if_index { - return Ok(NextHop { - ip_addr: next_hop_ip, - if_index, - mtu: default_route.mtu, - mac_addr: default_route.mac_addr, - preferred_src_ip, - gre: default_route.gre.clone(), - }); - } + if let Some(default_route) = &self.cached_default_route + && default_route.ip_addr == next_hop_ip + && default_route.if_index == if_index + { + return Ok(NextHop { + ip_addr: next_hop_ip, + if_index, + mtu: default_route.mtu, + mac_addr: default_route.mac_addr, + preferred_src_ip, + gre: default_route.gre.clone(), + vlan: default_route.vlan, + }); } if let Some(gre) = self.gre_route_info(if_index) { @@ -519,6 +557,7 @@ impl Router { mac_addr: Some(gre.mac_addr), preferred_src_ip, gre: Some(gre), + vlan: None, }); } @@ -528,8 +567,16 @@ impl Router { .find(|interface| interface.if_index == if_index) .map(|interface| interface.mtu) .ok_or(RouteError::UnknownInterfaceIndex(if_index))?; + let vlan = self.cached_vlan_route_info(if_index); - let mac_addr = self.neighbors.lookup(next_hop_ip, if_index).cloned(); + // IPv4 multicast destinations map deterministically to a multicast MAC (RFC 1112 §6.4) + // and never participate in ARP, so there is no neighbor entry to look up. Otherwise + // consult the neighbor cache for the unicast MAC. + let mac_addr = if next_hop_v4.is_multicast() { + Some(ipv4_multicast_mac(next_hop_v4)) + } else { + self.neighbors.lookup(next_hop_ip, if_index).cloned() + }; Ok(NextHop { ip_addr: next_hop_ip, mac_addr, @@ -537,6 +584,7 @@ impl Router { mtu, preferred_src_ip, gre: None, + vlan, }) } @@ -615,6 +663,14 @@ impl Router { } } +/// Map an IPv4 multicast address to its Ethernet destination MAC per RFC 1112 §6.4: +/// the low 23 bits of the address are placed into the low 23 bits of `01:00:5e:00:00:00`. +#[inline] +fn ipv4_multicast_mac(ip: Ipv4Addr) -> MacAddress { + let [_, b1, b2, b3] = ip.octets(); + MacAddress([0x01, 0x00, 0x5e, b1 & 0x7f, b2, b3]) +} + fn format_route(f: &mut fmt::Formatter<'_>, route: &Route) -> fmt::Result { match route.destination { Some(destination) if route.dst_len == 32 => write!(f, "{destination}")?, @@ -642,7 +698,7 @@ fn format_route(f: &mut fmt::Formatter<'_>, route: &Route) -> fmt::Res mod tests { use { super::*, - crate::netlink::{MacAddress, NeighborEntry, RouteEntry}, + crate::netlink::{MacAddress, NeighborEntry, RouteEntry, VlanLinkInfo}, libc::{AF_INET, NUD_REACHABLE}, std::net::{IpAddr, Ipv4Addr}, }; @@ -723,11 +779,13 @@ mod tests { if_index: 1, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, })); assert!(tables.upsert_interface(InterfaceInfo { if_index: 2, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, })); assert!(tables.upsert_neighbor(NeighborEntry { destination: Some(IpAddr::V4(primary_gateway)), @@ -769,6 +827,7 @@ mod tests { if_index: 1, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, })); let gateway = Ipv4Addr::new(10, 255, 255, 1); @@ -852,11 +911,13 @@ mod tests { if_index: 2, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, }, InterfaceInfo { if_index: 3, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, }, ], ); @@ -904,6 +965,7 @@ mod tests { if_index: test_if_index, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, }; // Upsert new interface and check that it was inserted @@ -950,6 +1012,7 @@ mod tests { if_index: 1, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, })); assert!(tables.upsert_neighbor(NeighborEntry { destination: Some(IpAddr::V4(main_gateway)), @@ -990,6 +1053,7 @@ mod tests { if_index: 1, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, })); assert!(tables.upsert_neighbor(NeighborEntry { destination: Some(IpAddr::V4(main_gateway)), @@ -1058,6 +1122,7 @@ mod tests { if_index: if_index_underlay as u32, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, }, InterfaceInfo { if_index: if_index_gre1 as u32, @@ -1069,6 +1134,7 @@ mod tests { tos: 0, pmtudisc: 0, }), + vlan_link: None, }, InterfaceInfo { if_index: if_index_gre2 as u32, @@ -1080,6 +1146,7 @@ mod tests { tos: 0, pmtudisc: 0, }), + vlan_link: None, }, ]; @@ -1140,6 +1207,7 @@ mod tests { if_index: if_index_underlay as u32, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, }, InterfaceInfo { if_index: if_index_gre as u32, @@ -1151,6 +1219,7 @@ mod tests { tos: 0, pmtudisc: 0, }), + vlan_link: None, }, ]; @@ -1189,6 +1258,7 @@ mod tests { if_index: 1, mtu: DEFAULT_MTU_FOR_TESTS, gre_tunnel: None, + vlan_link: None, })); assert!(tables.upsert_neighbor(NeighborEntry { destination: Some(IpAddr::V4(default_gateway)), @@ -1271,4 +1341,216 @@ mod tests { assert_eq!(next_hop.if_index, 2); assert_eq!(next_hop.ip_addr, IpAddr::V4(backup)); } + + #[test] + fn test_vlan_route_resolves_with_vlan_info() { + // A route through a configured VLAN sub-interface attaches VlanRouteInfo to the NextHop; + // the MAC still comes from the neighbor cache and no GRE encap is set. + let peer = Ipv4Addr::new(10, 228, 0, 99); + let vlan_if = 100u32; + let vlan_src = Ipv4Addr::new(10, 228, 0, 5); + let peer_mac = MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x09]); + + let neighbors = vec![NeighborEntry { + destination: Some(IpAddr::V4(peer)), + lladdr: Some(peer_mac), + ifindex: vlan_if as i32, + state: NUD_REACHABLE, + }]; + let interfaces = vec![InterfaceInfo { + if_index: vlan_if, + mtu: DEFAULT_MTU_FOR_TESTS, + gre_tunnel: None, + vlan_link: Some(VlanLinkInfo { vid: 900 }), + }]; + let routes = vec![Route { + destination: Some(Ipv4Addr::new(10, 228, 0, 0)), + gateway: None, + preferred_src: Some(vlan_src), + out_if_index: Some(vlan_if), + priority: None, + type_: 0, + dst_len: 22, + }]; + + let router = router_from_tables(neighbors, routes, interfaces); + let next_hop = router.route_v4(peer).unwrap(); + assert_eq!(next_hop.if_index, vlan_if); + assert_eq!(next_hop.preferred_src_ip, Some(vlan_src)); + assert_eq!(next_hop.mac_addr, Some(peer_mac)); + assert!(next_hop.gre.is_none()); + let vlan = next_hop.vlan.expect("vlan info must be populated"); + assert_eq!(vlan.if_index, vlan_if); + assert_eq!(vlan.vid, 900); + assert_eq!(vlan.pcp, 0); + } + + #[test] + fn test_multiple_vlan_interfaces_resolve_their_own_vid() { + // With more than one VLAN sub-interface, each route is tagged with the vid of the + // interface it egresses through. + let peer_a = Ipv4Addr::new(10, 1, 0, 9); + let peer_b = Ipv4Addr::new(10, 2, 0, 9); + let vlan_if_a = 100u32; + let vlan_if_b = 101u32; + let mac_a = MacAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x0a]); + let mac_b = MacAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x0b]); + + let neighbors = vec![ + NeighborEntry { + destination: Some(IpAddr::V4(peer_a)), + lladdr: Some(mac_a), + ifindex: vlan_if_a as i32, + state: NUD_REACHABLE, + }, + NeighborEntry { + destination: Some(IpAddr::V4(peer_b)), + lladdr: Some(mac_b), + ifindex: vlan_if_b as i32, + state: NUD_REACHABLE, + }, + ]; + let interfaces = vec![ + InterfaceInfo { + if_index: vlan_if_a, + mtu: DEFAULT_MTU_FOR_TESTS, + gre_tunnel: None, + vlan_link: Some(VlanLinkInfo { vid: 900 }), + }, + InterfaceInfo { + if_index: vlan_if_b, + mtu: DEFAULT_MTU_FOR_TESTS, + gre_tunnel: None, + vlan_link: Some(VlanLinkInfo { vid: 901 }), + }, + ]; + let routes = vec![ + test_route(Some(peer_a), vlan_if_a), + test_route(Some(peer_b), vlan_if_b), + ]; + + let router = router_from_tables(neighbors, routes, interfaces); + let hop_a = router.route_v4(peer_a).unwrap(); + assert_eq!(hop_a.vlan.map(|v| v.vid), Some(900)); + assert_eq!(hop_a.mac_addr, Some(mac_a)); + let hop_b = router.route_v4(peer_b).unwrap(); + assert_eq!(hop_b.vlan.map(|v| v.vid), Some(901)); + assert_eq!(hop_b.mac_addr, Some(mac_b)); + } + + #[test] + fn test_gre_wins_when_both_gre_and_vlan_configured() { + // Defensive: if a configured VLAN if_index also happens to be a GRE interface, the resolve + // path picks GRE first (single-encap-per-interface contract). + let remote = Ipv4Addr::new(10, 0, 0, 1); + let gre_dest = Ipv4Addr::new(192, 168, 0, 1); + let if_index_underlay = 1u32; + let if_index_iface = 100u32; + let underlay_mac = MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x01]); + + let neighbors = vec![NeighborEntry { + destination: Some(IpAddr::V4(remote)), + lladdr: Some(underlay_mac), + ifindex: if_index_underlay as i32, + state: NUD_REACHABLE, + }]; + let routes = vec![ + test_route(Some(remote), if_index_underlay), + test_route(Some(gre_dest), if_index_iface), + ]; + let interfaces = vec![ + InterfaceInfo { + if_index: if_index_underlay, + mtu: DEFAULT_MTU_FOR_TESTS, + gre_tunnel: None, + vlan_link: None, + }, + InterfaceInfo { + if_index: if_index_iface, + mtu: DEFAULT_MTU_FOR_TESTS, + gre_tunnel: Some(GreTunnelInfo { + local: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 3)), + remote: IpAddr::V4(remote), + ttl: 0, + tos: 0, + pmtudisc: 0, + }), + vlan_link: Some(VlanLinkInfo { vid: 5 }), + }, + ]; + let router = router_from_tables(neighbors, routes, interfaces); + let next_hop = router.route_v4(gre_dest).unwrap(); + assert!(next_hop.gre.is_some()); + assert!(next_hop.vlan.is_none()); + } + + #[test] + fn test_ipv4_multicast_mac_mapping() { + // RFC 1112: 01:00:5e + (octet1 & 0x7f) + octet2 + octet3 + assert_eq!( + ipv4_multicast_mac(Ipv4Addr::new(224, 0, 0, 1)), + MacAddress([0x01, 0x00, 0x5e, 0x00, 0x00, 0x01]), + ); + assert_eq!( + ipv4_multicast_mac(Ipv4Addr::new(239, 0, 0, 3)), + MacAddress([0x01, 0x00, 0x5e, 0x00, 0x00, 0x03]), + ); + // High bit of the second octet must be cleared (only the low 23 bits map). + assert_eq!( + ipv4_multicast_mac(Ipv4Addr::new(239, 128, 0, 1)), + MacAddress([0x01, 0x00, 0x5e, 0x00, 0x00, 0x01]), + ); + assert_eq!( + ipv4_multicast_mac(Ipv4Addr::new(239, 255, 255, 250)), + MacAddress([0x01, 0x00, 0x5e, 0x7f, 0xff, 0xfa]), + ); + } + + #[test] + fn test_multicast_route_resolves_with_computed_mac_without_arp() { + // Route 239.0.0.0/8 dev iface (no gateway, no ARP entry): resolution must yield the + // RFC-1112 multicast MAC instead of falling through to a missing neighbor. + let multicast_dst = Ipv4Addr::new(239, 0, 0, 3); + let if_index = 100u32; + let routes = vec![Route { + destination: Some(Ipv4Addr::new(239, 0, 0, 0)), + gateway: None, + preferred_src: None, + out_if_index: Some(if_index), + priority: None, + type_: 0, + dst_len: 8, + }]; + let interfaces = vec![InterfaceInfo { + if_index, + mtu: DEFAULT_MTU_FOR_TESTS, + gre_tunnel: None, + vlan_link: None, + }]; + let router = router_from_tables(vec![], routes, interfaces); + let next_hop = router.route_v4(multicast_dst).unwrap(); + assert_eq!( + next_hop.mac_addr, + Some(MacAddress([0x01, 0x00, 0x5e, 0x00, 0x00, 0x03])) + ); + assert_eq!(next_hop.if_index, if_index); + } + + #[test] + fn test_unicast_without_neighbor_returns_no_mac() { + // Regression guard for the unicast branch: with no neighbor entry the MAC stays None so + // the tx loop drops the packet rather than sending it to a bogus address. + let dst = Ipv4Addr::new(10, 1, 2, 3); + let if_index = 5u32; + let routes = vec![test_route(Some(dst), if_index)]; + let interfaces = vec![InterfaceInfo { + if_index, + mtu: DEFAULT_MTU_FOR_TESTS, + gre_tunnel: None, + vlan_link: None, + }]; + let router = router_from_tables(vec![], routes, interfaces); + let next_hop = router.route_v4(dst).unwrap(); + assert!(next_hop.mac_addr.is_none()); + } } diff --git a/xdp/src/socket.rs b/xdp/src/socket.rs index e5f99ad3038..a5beb95ae50 100644 --- a/xdp/src/socket.rs +++ b/xdp/src/socket.rs @@ -44,7 +44,11 @@ impl Socket { unsafe { let fd = socket(AF_XDP, SOCK_RAW, 0); if fd < 0 { - return Err(io::Error::last_os_error()); + return Err(Error::syscall( + "socket(AF_XDP, SOCK_RAW) failed", + io::Error::last_os_error(), + ) + .into()); } let fd = OwnedFd::from_raw_fd(fd); @@ -65,7 +69,11 @@ impl Socket { mem::size_of::() as libc::socklen_t, ) < 0 { - return Err(io::Error::last_os_error()); + return Err(Error::syscall( + "setsockopt(XDP_UMEM_REG) failed", + io::Error::last_os_error(), + ) + .into()); } for (ring, size) in [ @@ -87,7 +95,11 @@ impl Socket { mem::size_of::() as socklen_t, ) < 0 { - return Err(io::Error::last_os_error()); + return Err(Error::syscall( + format!("setsockopt(SOL_XDP, ring={ring}, size={size}) failed",), + io::Error::last_os_error(), + ) + .into()); } } @@ -101,7 +113,11 @@ impl Socket { &mut optlen, ) < 0 { - return Err(io::Error::last_os_error()); + return Err(Error::syscall( + "getsockopt(XDP_MMAP_OFFSETS) failed", + io::Error::last_os_error(), + ) + .into()); } let tx_completion_ring = TxCompletionRing::new( @@ -110,7 +126,8 @@ impl Socket { tx_completion_ring_size.saturating_mul(mem::size_of::()), &offsets.cr, XDP_UMEM_PGOFF_COMPLETION_RING, - )?, + ) + .map_err(|source| Error::syscall("mmap completion ring failed", source))?, tx_completion_ring_size as u32, ); @@ -120,7 +137,8 @@ impl Socket { rx_fill_ring_size.saturating_mul(mem::size_of::()), &offsets.fr, XDP_UMEM_PGOFF_FILL_RING, - )?, + ) + .map_err(|source| Error::syscall("mmap fill ring failed", source))?, rx_fill_ring_size as u32, fd.as_raw_fd(), ); @@ -130,9 +148,15 @@ impl Socket { // pre-populated before calling bind() for _ in 0..rx_fill_ring_size { let Some(frame) = umem.reserve() else { - return Err(io::Error::other("Failed to reserve frame for RX fill ring")); + return Err(Error::InsufficientUmemFrames { + required: rx_fill_ring_size, + available: umem.available(), + } + .into()); }; - rx_fill_ring.write(frame)?; + rx_fill_ring + .write(frame) + .map_err(|source| Error::syscall("RX fill ring write failed", source))?; } rx_fill_ring.commit(); } @@ -143,7 +167,8 @@ impl Socket { tx_ring_size.saturating_mul(mem::size_of::()), &offsets.tx, XDP_PGOFF_TX_RING as u64, - )?, + ) + .map_err(|source| Error::syscall("mmap tx ring failed", source))?, tx_ring_size as u32, fd.as_raw_fd(), )); @@ -155,7 +180,8 @@ impl Socket { rx_ring_size.saturating_mul(mem::size_of::()), &offsets.rx, XDP_PGOFF_RX_RING as u64, - )?, + ) + .map_err(|source| Error::syscall("mmap rx ring failed", source))?, rx_ring_size as u32, fd.as_raw_fd(), )) @@ -178,7 +204,14 @@ impl Socket { mem::size_of::() as socklen_t, ) < 0 { - return Err(io::Error::last_os_error()); + return Err(Error::syscall( + format!( + "bind(AF_XDP, ifindex={}, queue={}, flags=0x{:x}) failed", + sxdp.sxdp_ifindex, sxdp.sxdp_queue_id, sxdp.sxdp_flags + ), + io::Error::last_os_error(), + ) + .into()); } let tx = Tx { @@ -371,3 +404,33 @@ impl RxRing { self.consumer.sync(commit); } } + +#[derive(Debug, thiserror::Error)] +enum Error { + #[error("{message}: {source}")] + Syscall { + message: String, + #[source] + source: io::Error, + }, + #[error( + "insufficient UMEM frames for RX fill ring prefill: required={required}, \ + available={available}" + )] + InsufficientUmemFrames { required: usize, available: usize }, +} + +impl Error { + fn syscall(message: impl Into, source: io::Error) -> Self { + Self::Syscall { + message: message.into(), + source, + } + } +} + +impl From for io::Error { + fn from(error: Error) -> io::Error { + io::Error::other(error) + } +} diff --git a/xdp/src/transmitter.rs b/xdp/src/transmitter.rs index 0dce51881e4..5b51b55e592 100644 --- a/xdp/src/transmitter.rs +++ b/xdp/src/transmitter.rs @@ -16,10 +16,10 @@ use { load_xdp_program, route::{RouteTable, Router, RoutingTables}, route_monitor::RouteMonitor, - set_cpu_affinity, tx_loop::{TxLoop, TxLoopBuilder, TxLoopConfigBuilder, TxPacket}, umem::{OwnedUmem, PageAlignedMemory}, }, + agave_cpu_utils::{CpuId, cpu_affinity, set_cpu_affinity}, arc_swap::ArcSwap, arrayvec::ArrayVec, aya::Ebpf, @@ -35,10 +35,25 @@ use { #[cfg(target_os = "linux")] const ROUTE_MONITOR_UPDATE_INTERVAL: Duration = Duration::from_millis(50); +/// Binding of a single NIC hardware TX queue to a CPU core. +/// +/// Each binding becomes one TX worker thread, pinned to `cpu`, whose AF_XDP +/// socket is bound to hardware queue `queue` on the configured interface. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct QueueCpuBinding { + /// NIC hardware TX queue id the AF_XDP socket binds to. + pub queue: u32, + /// Logical CPU core the worker thread is pinned to. + pub cpu: usize, +} + #[derive(Clone, Debug)] pub struct XdpConfig { pub interface: Option, - pub cpus: Vec, + /// NIC-queue -> CPU-core bindings. One TX worker is created per entry, in + /// order. The queue id is taken explicitly from the binding rather than + /// inferred from position, so callers can target arbitrary hardware queues. + pub queues: Vec, pub zero_copy: bool, // The capacity of the channel that sits between senders and each XDP thread that enqueues // packets to the NIC. @@ -54,7 +69,7 @@ impl Default for XdpConfig { fn default() -> Self { Self { interface: None, - cpus: vec![], + queues: vec![], zero_copy: false, tx_channel_cap: Self::DEFAULT_TX_CHANNEL_CAP, } @@ -62,10 +77,14 @@ impl Default for XdpConfig { } impl XdpConfig { - pub fn new(interface: Option>, cpus: Vec, zero_copy: bool) -> Self { + pub fn new( + interface: Option>, + queues: Vec, + zero_copy: bool, + ) -> Self { Self { interface: interface.map(|s| s.into()), - cpus, + queues, zero_copy, tx_channel_cap: XdpConfig::DEFAULT_TX_CHANNEL_CAP, } @@ -79,6 +98,7 @@ pub struct BytesTxPacket { src_addr: SocketAddrV4, dst_addrs: XdpAddrs, ecn: Option, + allow_mtu_overflow: bool, payload: Bytes, } @@ -97,9 +117,15 @@ impl BytesTxPacket { src_addr, dst_addrs: dst_addrs.into(), ecn, + allow_mtu_overflow: false, payload, } } + + /// Sets whether MTU overflow is possible for this packet. + pub fn set_allow_mtu_overflow(&mut self, allow: bool) { + self.allow_mtu_overflow = allow; + } } #[cfg(not(target_os = "linux"))] @@ -112,6 +138,8 @@ impl BytesTxPacket { ) -> Self { Self } + + pub fn set_allow_mtu_overflow(&mut self, _allow: bool) {} } #[cfg(target_os = "linux")] @@ -134,6 +162,10 @@ impl TxPacket for BytesTxPacket { fn ecn(&self) -> Option { self.ecn } + + fn allow_mtu_overflow(&self) -> bool { + self.allow_mtu_overflow + } } #[derive(Clone)] @@ -219,11 +251,11 @@ impl TransmitterBuilder { use { caps::Capability::{CAP_BPF, CAP_NET_ADMIN, CAP_NET_RAW, CAP_PERFMON}, log::debug, - std::collections::HashSet, + std::{collections::HashSet, io}, }; let XdpConfig { interface: maybe_interface, - cpus, + queues, zero_copy, tx_channel_cap, } = config; @@ -238,32 +270,36 @@ impl TransmitterBuilder { tx_loop_config_builder.zero_copy(zero_copy); let tx_loop_config = tx_loop_config_builder.build_with_src_device(&dev); - let reserved_cores = cpus.iter().cloned().collect::>(); - let available_cores = core_affinity::get_core_ids() - .expect("linux provide affine cores") + let reserved_cores = queues + .iter() + .map(|binding| CpuId::new(binding.cpu)) + .collect::>>()?; + let unreserved_cores = cpu_affinity(None)? .into_iter() - .map(|core_affinity::CoreId { id }| id) - .collect::>(); - let unreserved_cores = available_cores - .difference(&reserved_cores) - .cloned() + .filter(|core| !reserved_cores.contains(core)) .collect::>(); - let tx_loop_builders = cpus - .into_iter() - .zip(std::iter::repeat_with(|| tx_loop_config.clone())) - .enumerate() - .map(|(i, (cpu_id, config))| { - // since we aren't necessarily allocating from the thread that we intend to run on, - // temporarily switch to the target cpu for each TxLoop to ensure that the Umem region - // is allocated to the correct numa node - set_cpu_affinity([cpu_id]).unwrap(); - let tx_loop_builder = TxLoopBuilder::new(cpu_id, QueueId(i as u64), config, &dev); - // migrate main thread back off of the last xdp reserved cpu - set_cpu_affinity(unreserved_cores.clone()).unwrap(); - tx_loop_builder - }) - .collect::>(); + if unreserved_cores.is_empty() { + return Err("all CPUs are reserved; no CPU available for the main thread".into()); + } + + let mut tx_loop_builders = Vec::with_capacity(queues.len()); + for binding in queues { + // since we aren't necessarily allocating from the thread that we intend to run on, + // temporarily switch to the target cpu for each TxLoop to ensure that the Umem region + // is allocated to the correct numa node + let cpu = CpuId::new(binding.cpu)?; + set_cpu_affinity(None, [cpu])?; + let tx_loop_builder = TxLoopBuilder::new( + binding.cpu, + QueueId(binding.queue as u64), + tx_loop_config.clone(), + &dev, + ); + // migrate main thread back off of the last xdp reserved cpu + set_cpu_affinity(None, unreserved_cores.iter().copied())?; + tx_loop_builders.push(tx_loop_builder); + } // switch to higher caps while we setup XDP. We assume that an error in // this function is irrecoverable so we don't try to drop on errors. @@ -285,7 +321,7 @@ impl TransmitterBuilder { let tx_loops = tx_loop_builders .into_iter() .map(|tx_loop_builder| tx_loop_builder.build()) - .collect::>(); + .collect::, io::Error>>()?; let tables_result = RoutingTables::from_netlink(RouteTable::Main); diff --git a/xdp/src/tx_loop.rs b/xdp/src/tx_loop.rs index 1edd31bec37..dfa3c8bae47 100644 --- a/xdp/src/tx_loop.rs +++ b/xdp/src/tx_loop.rs @@ -10,17 +10,18 @@ use { }, netlink::MacAddress, packet::{ - ETH_HEADER_SIZE, IP_HEADER_SIZE, UDP_HEADER_SIZE, write_eth_header, - write_ip_header_for_udp, write_udp_header, + IP_HEADER_SIZE, PACKET_HEADER_SIZE, UDP_HEADER_SIZE, VLAN_PACKET_HEADER_SIZE, + construct_packet, construct_vlan_packet, }, route::NextHop, - set_cpu_affinity, socket::{Socket, Tx, TxRing}, umem::{Frame, OwnedUmem, PageAlignedMemory, Umem}, }, + agave_cpu_utils::set_cpu_affinity, crossbeam_channel::{Receiver, Sender, TryRecvError}, libc::{_SC_PAGESIZE, sysconf}, std::{ + io, net::{IpAddr, SocketAddr, SocketAddrV4}, thread, time::Duration, @@ -81,7 +82,6 @@ pub struct TxLoopConfig { pub struct TxLoopBuilder { cpu_id: usize, - queue_id: QueueId, zero_copy: bool, src_mac: MacAddress, queue: DeviceQueue, @@ -138,7 +138,6 @@ impl TxLoopBuilder> { TxLoopBuilder { cpu_id, - queue_id, zero_copy, src_mac, queue, @@ -147,10 +146,9 @@ impl TxLoopBuilder> { } } - pub fn build(self) -> TxLoop> { + pub fn build(self) -> Result>, io::Error> { let TxLoopBuilder { cpu_id, - queue_id, zero_copy, src_mac, queue, @@ -158,9 +156,15 @@ impl TxLoopBuilder> { umem, } = self; - let Ok((socket, tx)) = Socket::tx(queue, umem, zero_copy, tx_size * 2, tx_size) else { - panic!("failed to create AF_XDP socket on queue {queue_id:?}"); - }; + let queue_id = queue.id(); + let (socket, tx) = + Socket::tx(queue, umem, zero_copy, tx_size * 2, tx_size).map_err(|err| { + log::error!( + "failed to create AF_XDP TX socket for queue {queue_id:?} on CPU {cpu_id}: \ + {err}" + ); + err + })?; let Tx { // this is where we'll queue frames @@ -170,13 +174,13 @@ impl TxLoopBuilder> { } = tx; let ring = ring.unwrap(); - TxLoop { + Ok(TxLoop { cpu_id, src_mac, socket, ring, completion, - } + }) } } @@ -205,6 +209,9 @@ pub trait TxPacket { /// Explicit congestion notification bits to set on the packet. fn ecn(&self) -> Option; + + /// Returns true when this packet is expected to occasionally exceed the route MTU. + fn allow_mtu_overflow(&self) -> bool; } impl TxLoop { @@ -233,7 +240,7 @@ impl TxLoop { } = self; // each queue is bound to its own CPU core - set_cpu_affinity([cpu_id]).unwrap(); + set_cpu_affinity(None, [agave_cpu_utils::CpuId::new(cpu_id).unwrap()]).unwrap(); let umem = socket.umem(); let umem_tx_capacity = umem.available(); @@ -284,6 +291,7 @@ impl TxLoop { let src_ip = src_addr.ip(); let src_port = src_addr.port(); let ecn = item.ecn(); + let can_overflow_mtu = item.allow_mtu_overflow(); for addr in item.dst_addrs().as_ref() { if ring.available() == 0 || umem.available() == 0 { commit_pending(&mut ring, &mut written_uncommitted); @@ -337,14 +345,16 @@ impl TxLoop { if l3_inner_packet_len > gre.mtu as usize || l3_outer_gre_packet_len > next_hop.mtu as usize { - log::warn!( - "dropping packet: GRE payload exceeds MTU for {addr}: L3 inner \ - packet length {l3_inner_packet_len}, L3 outer GRE packet length \ - {l3_outer_gre_packet_len}, MTU: {mtu}, underlay_mtu: \ - {underlay_mtu}.", - mtu = gre.mtu, - underlay_mtu = next_hop.mtu - ); + if !can_overflow_mtu { + log::warn!( + "dropping packet: GRE payload exceeds MTU for {addr}: L3 \ + inner packet length {l3_inner_packet_len}, L3 outer GRE \ + packet length {l3_outer_gre_packet_len}, MTU: {mtu}, \ + underlay_mtu: {underlay_mtu}.", + mtu = gre.mtu, + underlay_mtu = next_hop.mtu + ); + } batched_packets -= 1; umem.release(frame.offset()); continue; @@ -381,7 +391,7 @@ impl TxLoop { umem.release(frame.offset()); continue; } - } else { + } else if let Some(vlan) = &next_hop.vlan { // we need the MAC address to send the packet let Some(dest_mac) = next_hop.mac_addr else { log::warn!( @@ -394,20 +404,86 @@ impl TxLoop { continue; }; + // The 802.1Q tag is added at L2, so the L3 size compared against the MTU + // is the same as the untagged path. let l3_packet_len = IP_HEADER_SIZE + UDP_HEADER_SIZE + len; if l3_packet_len > next_hop.mtu as usize { + if !can_overflow_mtu { + log::warn!( + "dropping packet: packet size {l3_packet_len} exceeds MTU \ + {mtu} for {addr}", + mtu = next_hop.mtu + ); + } + batched_packets -= 1; + umem.release(frame.offset()); + continue; + } + + let packet_len = VLAN_PACKET_HEADER_SIZE + len; + if packet_len > umem_frame_size { log::warn!( - "dropping packet: packet size {l3_packet_len} exceeds MTU {mtu} \ - for {addr}", - mtu = next_hop.mtu + "dropping packet: VLAN packet size {packet_len} exceeds frame \ + size {umem_frame_size} for {addr}" ); batched_packets -= 1; umem.release(frame.offset()); continue; } - const PACKET_HEADER_SIZE: usize = - ETH_HEADER_SIZE + IP_HEADER_SIZE + UDP_HEADER_SIZE; + frame.set_len(packet_len); + let packet = umem.map_frame_mut(&frame); + + // The route's preferred src is the IP assigned to the VLAN sub-interface, + // which is the right inner src for traffic egressing this VLAN. Fall back + // to the device's src IP if the route did not carry one. + let inner_src_ip = next_hop.preferred_src_ip.as_ref().unwrap_or(src_ip); + + if !construct_vlan_packet( + packet, + &src_mac.0, + &dest_mac.0, + inner_src_ip, + &dst_ip, + src_port, + addr.port(), + vlan.vid, + vlan.pcp, + payload, + ecn, + ) { + log::warn!("dropping packet: VLAN frame did not fit in UMEM slot"); + batched_packets -= 1; + umem.release(frame.offset()); + continue; + } + } else { + // we need the MAC address to send the packet + let Some(dest_mac) = next_hop.mac_addr else { + log::warn!( + "dropping packet: peer {addr} must be routed through {} which has \ + no known MAC address", + next_hop.ip_addr + ); + batched_packets -= 1; + umem.release(frame.offset()); + continue; + }; + + let l3_packet_len = IP_HEADER_SIZE + UDP_HEADER_SIZE + len; + if l3_packet_len > next_hop.mtu as usize { + if !can_overflow_mtu { + log::warn!( + "dropping packet: packet size {l3_packet_len} exceeds MTU \ + {mtu} for {addr}", + mtu = next_hop.mtu + ); + } + batched_packets -= 1; + umem.release(frame.offset()); + continue; + } + let packet_len = PACKET_HEADER_SIZE + len; if packet_len > umem_frame_size { log::warn!( @@ -422,29 +498,22 @@ impl TxLoop { frame.set_len(packet_len); let packet = umem.map_frame_mut(&frame); - // write the payload first as it's needed for checksum calculation (if enabled) - packet[PACKET_HEADER_SIZE..][..len].copy_from_slice(payload); - - write_eth_header(packet, &src_mac.0, &dest_mac.0); - - write_ip_header_for_udp( - &mut packet[ETH_HEADER_SIZE..], + if !construct_packet( + packet, + &src_mac.0, + &dest_mac.0, src_ip, &dst_ip, - ecn, - (UDP_HEADER_SIZE + len) as u16, - ); - - write_udp_header( - &mut packet[ETH_HEADER_SIZE + IP_HEADER_SIZE..], - src_ip, src_port, - &dst_ip, addr.port(), - len as u16, - // don't do checksums - false, - ); + payload, + ecn, + ) { + log::warn!("dropping packet: frame did not fit in UMEM slot"); + batched_packets -= 1; + umem.release(frame.offset()); + continue; + } } ring.write(frame, 0) diff --git a/xdp/tests/README.md b/xdp/tests/README.md new file mode 100644 index 00000000000..c46e9f3f9da --- /dev/null +++ b/xdp/tests/README.md @@ -0,0 +1,51 @@ +# XDP Integration Tests + +These tests are marked as ignored so ordinary workspace test jobs do not run them without privileges. They are run through `cargo xtask xdp-test`. + +The tests run directly on the host and require root or equivalent network admin privileges because the harness creates a temporary network namespace, `veth` interfaces, routes, and neighbors. `xtask` builds the test binaries as the current user, then applies `--runner` only when running the compiled test executables: + +```bash +cargo xtask xdp-test --runner "sudo -n -E" +``` + +To run a single test binary locally, use this form: + +```bash +cargo xtask xdp-test --runner "sudo -n -E" --test -- --exact --nocapture +``` + +## Test Topology + +Each integration test runs in a fresh temporary network namespace created with `unshare(CLONE_NEWNET)`. The tests bring `lo` up, create the interfaces needed by that test, and restore the original namespace when the test exits. + +The primary topology is one veth pair inside that namespace: + +```text +temporary test network namespace + + route and neighbor state under test + | + v + axdp0 10.0.0.1/24 02:aa:bb:cc:dd:01 + | + | veth peer + | + axdp1 10.0.0.2/24 02:aa:bb:cc:dd:02 + + neighbor: 10.0.0.2 -> 02:aa:bb:cc:dd:02 dev axdp0 + route example: 203.0.113.0/24 via 10.0.0.2 dev axdp0 +``` + +GRE coverage adds `gxdp0` on top of the veth pair and routes the overlay prefix through that tunnel: + +```text +GRE overlay route: + 192.0.2.0/24 dev gxdp0 src 192.0.2.1 + +GRE tunnel: + gxdp0 + local underlay: 10.0.0.1 (axdp0) + remote underlay: 10.0.0.2 (axdp1) + overlay source: 192.0.2.1/32 + ttl: 64 +``` diff --git a/xdp/tests/common/mod.rs b/xdp/tests/common/mod.rs new file mode 100644 index 00000000000..b91c2429b88 --- /dev/null +++ b/xdp/tests/common/mod.rs @@ -0,0 +1,221 @@ +#![cfg(target_os = "linux")] +#![allow(dead_code)] + +pub use aya::test_helpers::NetNsGuard; +use { + agave_xdp::netlink::MacAddress, + std::{ + ffi::{CString, OsString}, + os::unix::ffi::OsStringExt, + path::{Path, PathBuf}, + process::{Command, Output}, + sync::OnceLock, + thread, + time::{Duration, Instant}, + }, +}; + +pub const LEFT_IFACE: &str = "axdp0"; +pub const RIGHT_IFACE: &str = "axdp1"; +pub const GRE_IFACE: &str = "gxdp0"; + +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub struct TestLinks { + pub left_if_index: u32, + pub right_if_index: u32, + pub left_ip: std::net::Ipv4Addr, + pub right_ip: std::net::Ipv4Addr, + pub left_mac: MacAddress, + pub right_mac: MacAddress, +} + +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub struct TestGreTunnel { + pub if_index: u32, + pub local_ip: std::net::Ipv4Addr, + pub remote_ip: std::net::Ipv4Addr, + pub overlay_ip: std::net::Ipv4Addr, +} + +pub fn setup_veth_pair() -> TestLinks { + let left_ip = std::net::Ipv4Addr::new(10, 0, 0, 1); + let right_ip = std::net::Ipv4Addr::new(10, 0, 0, 2); + let left_mac = MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x01]); + let right_mac = MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x02]); + + run_ip(&[ + "link", + "add", + LEFT_IFACE, + "type", + "veth", + "peer", + "name", + RIGHT_IFACE, + ]); + set_link_mac(LEFT_IFACE, &left_mac.to_string()); + set_link_mac(RIGHT_IFACE, &right_mac.to_string()); + add_ipv4_addr(&format!("{left_ip}/24"), LEFT_IFACE); + add_ipv4_addr(&format!("{right_ip}/24"), RIGHT_IFACE); + set_link_up(LEFT_IFACE); + set_link_up(RIGHT_IFACE); + + TestLinks { + left_if_index: if_index(LEFT_IFACE), + right_if_index: if_index(RIGHT_IFACE), + left_ip, + right_ip, + left_mac, + right_mac, + } +} + +pub fn setup_gre_tunnel(underlay: &TestLinks) -> TestGreTunnel { + let local = underlay.left_ip.to_string(); + let remote = underlay.right_ip.to_string(); + let overlay_ip = std::net::Ipv4Addr::new(192, 0, 2, 1); + + run_ip(&[ + "tunnel", "add", GRE_IFACE, "mode", "gre", "local", &local, "remote", &remote, "ttl", "64", + ]); + add_ipv4_addr(&format!("{overlay_ip}/32"), GRE_IFACE); + set_link_up(GRE_IFACE); + + TestGreTunnel { + if_index: if_index(GRE_IFACE), + local_ip: underlay.left_ip, + remote_ip: underlay.right_ip, + overlay_ip, + } +} + +pub fn add_route(destination: &str, via: std::net::Ipv4Addr, dev: &str) { + let via = via.to_string(); + run_ip(&["route", "replace", destination, "via", &via, "dev", dev]); +} + +pub fn add_route_to_dev(destination: &str, dev: &str) { + run_ip(&["route", "replace", destination, "dev", dev]); +} + +pub fn add_route_to_dev_with_src(destination: &str, dev: &str, src: std::net::Ipv4Addr) { + let src = src.to_string(); + run_ip(&["route", "replace", destination, "dev", dev, "src", &src]); +} + +pub fn delete_link(dev: &str) { + run_ip(&["link", "del", dev]); +} + +#[allow(dead_code)] +pub fn delete_route(destination: &str) { + run_ip(&["route", "del", destination]); +} + +pub fn replace_neighbor(ip: std::net::Ipv4Addr, mac: MacAddress, dev: &str) { + let ip = ip.to_string(); + let mac = mac.to_string(); + run_ip(&[ + "neigh", + "replace", + &ip, + "lladdr", + &mac, + "dev", + dev, + "nud", + "permanent", + ]); +} + +pub fn delete_neighbor(ip: std::net::Ipv4Addr, dev: &str) { + let ip = ip.to_string(); + run_ip(&["neigh", "del", &ip, "dev", dev]); +} + +#[allow(dead_code)] +pub fn wait_until(description: &str, timeout: Duration, mut predicate: F) -> T +where + F: FnMut() -> Option, +{ + let start = Instant::now(); + loop { + if let Some(value) = predicate() { + return value; + } + + if start.elapsed() >= timeout { + panic!("timed out waiting for {description}"); + } + + thread::sleep(Duration::from_millis(10)); + } +} + +fn set_link_mac(dev: &str, mac: &str) { + run_ip(&["link", "set", "dev", dev, "address", mac]); +} + +fn set_link_up(dev: &str) { + run_ip(&["link", "set", "dev", dev, "up"]); +} + +fn add_ipv4_addr(addr: &str, dev: &str) { + run_ip(&["addr", "add", addr, "dev", dev]); +} + +pub fn if_index(dev: &str) -> u32 { + let dev = CString::new(dev).expect("interface name must not contain NUL"); + let index = unsafe { libc::if_nametoindex(dev.as_ptr()) }; + assert_ne!(index, 0, "failed to resolve ifindex for interface"); + index +} + +fn run_ip(args: &[&str]) { + run_command(ip_command(), args); +} + +fn run_command(program: &Path, args: &[&str]) { + let Output { + status, + stdout, + stderr, + } = Command::new(program) + .args(args) + .output() + .unwrap_or_else(|err| panic!("failed to run {program:?} {args:?}: {err}")); + if status.success() { + return; + } + + panic!( + "{program:?} {args:?} failed: {} +stdout: +{} +stderr: +{}", + status, + OsString::from_vec(stdout).display(), + OsString::from_vec(stderr).display(), + ); +} + +fn ip_command() -> &'static PathBuf { + static IP_COMMAND: OnceLock = OnceLock::new(); + IP_COMMAND.get_or_init(|| { + let mut candidates = std::env::var_os("IP") + .into_iter() + .map(PathBuf::from) + .chain([ + PathBuf::from("/usr/sbin/ip"), + PathBuf::from("/sbin/ip"), + PathBuf::from("ip"), + ]); + + candidates + .find(|path| path == Path::new("ip") || path.exists()) + .unwrap_or_else(|| PathBuf::from("ip")) + }) +} diff --git a/xdp/tests/netlink_snapshot.rs b/xdp/tests/netlink_snapshot.rs new file mode 100644 index 00000000000..0aa1d6840da --- /dev/null +++ b/xdp/tests/netlink_snapshot.rs @@ -0,0 +1,73 @@ +#![cfg(target_os = "linux")] + +mod common; + +use { + agave_xdp::{ + netlink::{netlink_get_interfaces, netlink_get_neighbors, netlink_get_routes}, + route::RouteTable, + }, + libc::{AF_INET, NUD_PERMANENT}, + std::net::{IpAddr, Ipv4Addr}, +}; + +#[test] +#[ignore = "requires root and network namespace privileges"] +fn netlink_snapshot_reads_the_prepared_namespace() { + let _netns = common::NetNsGuard::new().expect("create network namespace"); + let links = common::setup_veth_pair(); + + let routed_prefix = "203.0.113.0/24"; + common::replace_neighbor(links.right_ip, links.right_mac, common::LEFT_IFACE); + common::add_route(routed_prefix, links.right_ip, common::LEFT_IFACE); + + let interfaces = netlink_get_interfaces(AF_INET as u8).expect("read interfaces from netlink"); + assert!( + interfaces + .iter() + .any(|interface| interface.if_index == links.left_if_index) + ); + assert!( + interfaces + .iter() + .any(|interface| interface.if_index == links.right_if_index) + ); + + let routes = + netlink_get_routes(AF_INET as u8, u32::from(RouteTable::Main)).expect("read routes"); + assert!(routes.iter().any(|route| { + route.destination == Some(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 0))) + && route.gateway == Some(IpAddr::V4(links.right_ip)) + && route.out_if_index == Some(links.left_if_index as i32) + && route.dst_len == 24 + })); + + let neighbors = + netlink_get_neighbors(None, AF_INET as u8).expect("read neighbor table from netlink"); + assert!(neighbors.iter().any(|neighbor| { + neighbor.destination == Some(IpAddr::V4(links.right_ip)) + && neighbor.lladdr == Some(links.right_mac) + && neighbor.ifindex == links.left_if_index as i32 + && neighbor.state == NUD_PERMANENT + })); +} + +#[test] +#[ignore = "requires root and network namespace privileges"] +fn netlink_snapshot_reads_gre_tunnel_metadata() { + let _netns = common::NetNsGuard::new().expect("create network namespace"); + let links = common::setup_veth_pair(); + let gre = common::setup_gre_tunnel(&links); + + let interfaces = netlink_get_interfaces(AF_INET as u8).expect("read interfaces from netlink"); + let tunnel = interfaces + .iter() + .find(|interface| interface.if_index == gre.if_index) + .and_then(|interface| interface.gre_tunnel.as_ref()) + .expect("read GRE tunnel metadata from netlink"); + + assert_eq!(tunnel.local, IpAddr::V4(gre.local_ip)); + assert_eq!(tunnel.remote, IpAddr::V4(gre.remote_ip)); + assert_eq!(tunnel.ttl, 64); + assert_eq!(tunnel.tos, 0); +} diff --git a/xdp/tests/route_monitor.rs b/xdp/tests/route_monitor.rs new file mode 100644 index 00000000000..fa6fb7a01d6 --- /dev/null +++ b/xdp/tests/route_monitor.rs @@ -0,0 +1,275 @@ +#![cfg(target_os = "linux")] + +mod common; + +use { + agave_xdp::{ + netlink::MacAddress, + route::{RouteError, RouteTable, Router}, + route_monitor::RouteMonitor, + }, + arc_swap::ArcSwap, + std::{ + net::{IpAddr, Ipv4Addr}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + thread::JoinHandle, + time::Duration, + }, +}; + +struct RouteMonitorGuard { + router: Arc>, + exit: Arc, + handle: Option>, +} + +impl Drop for RouteMonitorGuard { + fn drop(&mut self) { + self.exit.store(true, Ordering::Relaxed); + let Some(handle) = self.handle.take() else { + return; + }; + if let Err(err) = handle.join() { + if std::thread::panicking() { + eprintln!("route monitor thread panicked: {err:?}"); + } else { + std::panic::resume_unwind(err); + } + } + } +} + +fn start_route_monitor() -> RouteMonitorGuard { + let router = Router::new().expect("build initial router"); + let router = Arc::new(ArcSwap::from_pointee(router)); + let exit = Arc::new(AtomicBool::new(false)); + let handle = RouteMonitor::start( + Arc::clone(&router), + RouteTable::Main, + Arc::clone(&exit), + Duration::ZERO, + || {}, + ); + RouteMonitorGuard { + router, + exit, + handle: Some(handle), + } +} + +#[test] +#[ignore = "requires root and network namespace privileges"] +fn route_monitor_publishes_live_route_updates() { + let _netns = common::NetNsGuard::new().expect("create network namespace"); + let links = common::setup_veth_pair(); + + let monitor = start_route_monitor(); + + let routed_destination = Ipv4Addr::new(203, 0, 113, 7); + assert!(matches!( + monitor.router.load().route_v4(routed_destination), + Err(RouteError::NoRouteFound(_)) + )); + + common::replace_neighbor(links.right_ip, links.right_mac, common::LEFT_IFACE); + common::add_route("203.0.113.0/24", links.right_ip, common::LEFT_IFACE); + + common::wait_until( + "the route monitor to publish a newly added route", + Duration::from_secs(2), + || { + let router = monitor.router.load(); + match router.route_v4(routed_destination) { + Ok(next_hop) + if next_hop.if_index == links.left_if_index + && next_hop.ip_addr == IpAddr::V4(links.right_ip) + && next_hop.mac_addr == Some(links.right_mac) => + { + Some(()) + } + _ => None, + } + }, + ); + + common::delete_route("203.0.113.0/24"); + common::wait_until( + "the route monitor to publish a removed route", + Duration::from_secs(2), + || { + matches!( + monitor.router.load().route_v4(routed_destination), + Err(RouteError::NoRouteFound(_)) + ) + .then_some(()) + }, + ); +} + +#[test] +#[ignore = "requires root and network namespace privileges"] +fn route_monitor_publishes_live_neighbor_updates() { + let _netns = common::NetNsGuard::new().expect("create network namespace"); + let links = common::setup_veth_pair(); + + let monitor = start_route_monitor(); + let routed_destination = Ipv4Addr::new(203, 0, 113, 7); + + common::add_route("203.0.113.0/24", links.right_ip, common::LEFT_IFACE); + let initial_mac = links.right_mac; + common::replace_neighbor(links.right_ip, initial_mac, common::LEFT_IFACE); + + common::wait_until( + "the route monitor to publish the initial neighbor", + Duration::from_secs(2), + || { + let router = monitor.router.load(); + match router.route_v4(routed_destination) { + Ok(next_hop) + if next_hop.if_index == links.left_if_index + && next_hop.ip_addr == IpAddr::V4(links.right_ip) + && next_hop.mac_addr == Some(initial_mac) => + { + Some(()) + } + _ => None, + } + }, + ); + + let updated_mac = MacAddress([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0x44]); + common::replace_neighbor(links.right_ip, updated_mac, common::LEFT_IFACE); + + common::wait_until( + "the route monitor to publish a replaced neighbor", + Duration::from_secs(2), + || { + let router = monitor.router.load(); + match router.route_v4(routed_destination) { + Ok(next_hop) if next_hop.mac_addr == Some(updated_mac) => Some(()), + _ => None, + } + }, + ); + + common::delete_neighbor(links.right_ip, common::LEFT_IFACE); + common::wait_until( + "the route monitor to publish a removed neighbor", + Duration::from_secs(2), + || { + let router = monitor.router.load(); + match router.route_v4(routed_destination) { + Ok(next_hop) + if next_hop.if_index == links.left_if_index + && next_hop.ip_addr == IpAddr::V4(links.right_ip) + && next_hop.mac_addr.is_none() => + { + Some(()) + } + _ => None, + } + }, + ); +} + +#[test] +#[ignore = "requires root and network namespace privileges"] +fn route_monitor_publishes_link_removals() { + let _netns = common::NetNsGuard::new().expect("create network namespace"); + let links = common::setup_veth_pair(); + + common::replace_neighbor(links.right_ip, links.right_mac, common::LEFT_IFACE); + common::add_route("203.0.113.0/24", links.right_ip, common::LEFT_IFACE); + + let monitor = start_route_monitor(); + let routed_destination = Ipv4Addr::new(203, 0, 113, 7); + common::wait_until( + "the route monitor to publish the initial link-backed route", + Duration::from_secs(2), + || { + let router = monitor.router.load(); + match router.route_v4(routed_destination) { + Ok(next_hop) + if next_hop.if_index == links.left_if_index + && next_hop.ip_addr == IpAddr::V4(links.right_ip) => + { + Some(()) + } + _ => None, + } + }, + ); + + common::delete_link(common::LEFT_IFACE); + common::wait_until( + "the route monitor to publish a removed link", + Duration::from_secs(2), + || { + matches!( + monitor.router.load().route_v4(routed_destination), + Err(RouteError::NoRouteFound(_)) + ) + .then_some(()) + }, + ); +} + +#[test] +#[ignore = "requires root and network namespace privileges"] +fn route_monitor_publishes_live_gre_route_updates() { + let _netns = common::NetNsGuard::new().expect("create network namespace"); + let links = common::setup_veth_pair(); + + let monitor = start_route_monitor(); + let overlay_destination = Ipv4Addr::new(192, 0, 2, 99); + assert!(matches!( + monitor.router.load().route_v4(overlay_destination), + Err(RouteError::NoRouteFound(_)) + )); + + common::replace_neighbor(links.right_ip, links.right_mac, common::LEFT_IFACE); + common::add_route_to_dev(&format!("{}/32", links.right_ip), common::LEFT_IFACE); + let gre = common::setup_gre_tunnel(&links); + common::add_route_to_dev_with_src("192.0.2.0/24", common::GRE_IFACE, gre.overlay_ip); + + common::wait_until( + "the route monitor to publish a GRE overlay route", + Duration::from_secs(2), + || { + let router = monitor.router.load(); + match router.route_v4(overlay_destination) { + Ok(next_hop) + if next_hop.if_index == gre.if_index + && next_hop.ip_addr == IpAddr::V4(overlay_destination) + && next_hop.mac_addr == Some(links.right_mac) + && next_hop.preferred_src_ip == Some(gre.overlay_ip) + && next_hop.gre.as_ref().is_some_and(|gre_route| { + gre_route.if_index == gre.if_index + && gre_route.mac_addr == links.right_mac + && gre_route.tunnel_info.local == IpAddr::V4(gre.local_ip) + && gre_route.tunnel_info.remote == IpAddr::V4(gre.remote_ip) + }) => + { + Some(()) + } + _ => None, + } + }, + ); + + common::delete_link(common::GRE_IFACE); + common::wait_until( + "the route monitor to publish a removed GRE link", + Duration::from_secs(2), + || { + matches!( + monitor.router.load().route_v4(overlay_destination), + Err(RouteError::NoRouteFound(_)) + ) + .then_some(()) + }, + ); +} diff --git a/xdp/tests/router_snapshot.rs b/xdp/tests/router_snapshot.rs new file mode 100644 index 00000000000..0392e0837f3 --- /dev/null +++ b/xdp/tests/router_snapshot.rs @@ -0,0 +1,44 @@ +#![cfg(target_os = "linux")] + +mod common; + +use { + agave_xdp::route::{RouteTable, Router, RoutingTables}, + std::net::{IpAddr, Ipv4Addr}, +}; + +#[test] +#[ignore = "requires root and network namespace privileges"] +fn router_snapshot_resolves_gre_routes_from_netlink() { + let _netns = common::NetNsGuard::new().expect("create network namespace"); + let links = common::setup_veth_pair(); + + common::replace_neighbor(links.right_ip, links.right_mac, common::LEFT_IFACE); + common::add_route_to_dev(&format!("{}/32", links.right_ip), common::LEFT_IFACE); + let gre = common::setup_gre_tunnel(&links); + common::add_route_to_dev_with_src("192.0.2.0/24", common::GRE_IFACE, gre.overlay_ip); + + let router_from_tables = + Router::from_tables(RoutingTables::from_netlink(RouteTable::Main).expect("read tables")) + .expect("build router from snapshot tables"); + let router_from_netlink = Router::new().expect("build router directly from netlink"); + let overlay_destination = Ipv4Addr::new(192, 0, 2, 99); + + for router in [&router_from_tables, &router_from_netlink] { + let next_hop = router + .route_v4(overlay_destination) + .expect("resolve GRE overlay route"); + assert_eq!(next_hop.if_index, gre.if_index); + assert_eq!(next_hop.ip_addr, IpAddr::V4(overlay_destination)); + assert_eq!(next_hop.mac_addr, Some(links.right_mac)); + assert_eq!(next_hop.preferred_src_ip, Some(gre.overlay_ip)); + + let gre_route = next_hop.gre.as_ref().expect("route should use GRE"); + assert_eq!(gre_route.if_index, gre.if_index); + assert_eq!(gre_route.mac_addr, links.right_mac); + assert_eq!(gre_route.tunnel_info.local, IpAddr::V4(gre.local_ip)); + assert_eq!(gre_route.tunnel_info.remote, IpAddr::V4(gre.remote_ip)); + assert_eq!(gre_route.tunnel_info.ttl, 64); + assert_eq!(gre_route.tunnel_info.tos, 0); + } +} diff --git a/xdp/tests/transmitter_smoke.rs b/xdp/tests/transmitter_smoke.rs new file mode 100644 index 00000000000..df933c598b7 --- /dev/null +++ b/xdp/tests/transmitter_smoke.rs @@ -0,0 +1,447 @@ +#![cfg(target_os = "linux")] + +mod common; + +use { + agave_cpu_utils::cpu_affinity, + agave_xdp::{ + gre::packet::GRE_HEADER_BASE_SIZE, + netlink::MacAddress, + packet::{ETH_HEADER_SIZE, IP_HEADER_SIZE, UDP_HEADER_SIZE}, + transmitter::{ + BytesTxPacket, QueueCpuBinding, Transmitter, TransmitterBuilder, XdpConfig, XdpSender, + }, + }, + bytes::Bytes, + nix::{ + errno::Errno, + poll::{PollFd, PollFlags, PollTimeout, poll}, + sys::socket::{ + AddressFamily, MsgFlags, SockFlag, SockProtocol, SockType, SockaddrLike, + SockaddrStorage, bind, recv, socket, + }, + }, + std::{ + io, mem, + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + os::fd::{AsFd, AsRawFd, OwnedFd}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + time::{Duration, Instant}, + }, +}; + +fn transmitter_cpu() -> usize { + let cores = cpu_affinity(None).expect("linux provides affine cores"); + assert!( + cores.len() >= 2, + "transmitter smoke test requires at least 2 affine CPU cores, found {}", + cores.len(), + ); + **cores.first().expect("at least two affine cores") +} + +struct PacketSocket { + fd: OwnedFd, +} + +struct TransmitterGuard { + transmitter: Option, + sender: Option, + exit: Arc, +} + +impl TransmitterGuard { + fn new(transmitter: Transmitter, sender: XdpSender, exit: Arc) -> Self { + Self { + transmitter: Some(transmitter), + sender: Some(sender), + exit, + } + } + + fn sender(&self) -> &XdpSender { + self.sender.as_ref().expect("sender is live") + } +} + +impl Drop for TransmitterGuard { + fn drop(&mut self) { + self.exit.store(true, Ordering::Relaxed); + drop(self.sender.take()); + let Some(transmitter) = self.transmitter.take() else { + return; + }; + if let Err(err) = transmitter.join() { + if std::thread::panicking() { + eprintln!("transmitter thread panicked: {err:?}"); + } else { + std::panic::resume_unwind(err); + } + } + } +} + +impl PacketSocket { + fn bind(if_index: u32) -> io::Result { + let fd = socket( + AddressFamily::Packet, + SockType::Raw, + SockFlag::SOCK_CLOEXEC, + SockProtocol::EthAll, + ) + .map_err(io::Error::from)?; + let addr = libc::sockaddr_ll { + sll_family: libc::AF_PACKET as u16, + sll_protocol: (libc::ETH_P_ALL as u16).to_be(), + sll_ifindex: if_index as i32, + sll_hatype: 0, + sll_pkttype: 0, + sll_halen: 0, + sll_addr: [0; 8], + }; + let addr = unsafe { + SockaddrStorage::from_raw( + (&addr as *const libc::sockaddr_ll).cast(), + Some(mem::size_of::() as libc::socklen_t), + ) + } + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid packet address"))?; + bind(fd.as_raw_fd(), &addr).map_err(io::Error::from)?; + Ok(Self { fd }) + } + + fn recv_matching_udp( + &self, + expected: &ExpectedUdpPacket<'_>, + timeout: Duration, + ) -> io::Result> { + self.recv_matching_payload("matching UDP frame", timeout, |frame| { + matching_udp_payload(frame, expected) + }) + } + + fn recv_matching_gre_udp( + &self, + expected: &ExpectedGreUdpPacket<'_>, + timeout: Duration, + ) -> io::Result> { + self.recv_matching_payload("matching GRE UDP frame", timeout, |frame| { + matching_gre_udp_payload(frame, expected) + }) + } + + fn recv_matching_payload( + &self, + description: &str, + timeout: Duration, + mut matcher: F, + ) -> io::Result> + where + F: for<'a> FnMut(&'a [u8]) -> Option<&'a [u8]>, + { + let deadline = Instant::now().checked_add(timeout).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "timeout overflows instant") + })?; + let mut frame = [0u8; 2048]; + loop { + let now = Instant::now(); + if now >= deadline { + return Err(io::Error::new( + io::ErrorKind::TimedOut, + format!("timed out waiting for {description}"), + )); + } + let remaining = deadline.saturating_duration_since(now); + let mut pfd = [PollFd::new(self.fd.as_fd(), PollFlags::POLLIN)]; + let timeout = PollTimeout::try_from(remaining).unwrap_or(PollTimeout::MAX); + match poll(&mut pfd, timeout) { + Ok(0) => continue, + Ok(_) => {} + Err(Errno::EINTR) => continue, + Err(err) => return Err(io::Error::from(err)), + } + + let len = match recv(self.fd.as_raw_fd(), &mut frame, MsgFlags::empty()) { + Ok(len) => len, + Err(Errno::EINTR) => continue, + Err(err) => return Err(io::Error::from(err)), + }; + let frame = &frame[..len]; + if let Some(payload) = matcher(frame) { + return Ok(payload.to_vec()); + } + } + } +} + +struct ExpectedUdpPacket<'a> { + src_mac: MacAddress, + dst_mac: MacAddress, + src_ip: Ipv4Addr, + dst_ip: Ipv4Addr, + src_port: u16, + dst_port: u16, + payload: &'a [u8], +} + +struct ExpectedGreUdpPacket<'a> { + outer_src_mac: MacAddress, + outer_dst_mac: MacAddress, + outer_src_ip: Ipv4Addr, + outer_dst_ip: Ipv4Addr, + inner_src_ip: Ipv4Addr, + inner_dst_ip: Ipv4Addr, + src_port: u16, + dst_port: u16, + payload: &'a [u8], +} + +struct ExpectedUdpDatagram<'a> { + src_ip: Ipv4Addr, + dst_ip: Ipv4Addr, + src_port: u16, + dst_port: u16, + payload: &'a [u8], +} + +fn matching_udp_payload<'a>(frame: &'a [u8], expected: &ExpectedUdpPacket<'_>) -> Option<&'a [u8]> { + if frame.len() < ETH_HEADER_SIZE { + return None; + } + if frame[0..6] != expected.dst_mac.0 || frame[6..12] != expected.src_mac.0 { + return None; + } + if u16::from_be_bytes([frame[12], frame[13]]) != libc::ETH_P_IP as u16 { + return None; + } + + matching_ipv4_udp_payload( + &frame[ETH_HEADER_SIZE..], + &ExpectedUdpDatagram { + src_ip: expected.src_ip, + dst_ip: expected.dst_ip, + src_port: expected.src_port, + dst_port: expected.dst_port, + payload: expected.payload, + }, + ) +} + +fn matching_gre_udp_payload<'a>( + frame: &'a [u8], + expected: &ExpectedGreUdpPacket<'_>, +) -> Option<&'a [u8]> { + const GRE_FLAGS_VERSION_BASIC: u16 = 0x0000; + + if frame.len() < ETH_HEADER_SIZE.checked_add(IP_HEADER_SIZE)? { + return None; + } + if frame[0..6] != expected.outer_dst_mac.0 || frame[6..12] != expected.outer_src_mac.0 { + return None; + } + if u16::from_be_bytes([frame[12], frame[13]]) != libc::ETH_P_IP as u16 { + return None; + } + + let outer_ip = &frame[ETH_HEADER_SIZE..]; + let outer_ihl = usize::from(outer_ip[0] & 0x0f).checked_mul(4)?; + let gre_offset = ETH_HEADER_SIZE.checked_add(outer_ihl)?; + let min_frame_len = gre_offset + .checked_add(GRE_HEADER_BASE_SIZE)? + .checked_add(IP_HEADER_SIZE)?; + if outer_ihl < IP_HEADER_SIZE || frame.len() < min_frame_len { + return None; + } + if outer_ip[9] != libc::IPPROTO_GRE as u8 { + return None; + } + if outer_ip[12..16] != expected.outer_src_ip.octets() + || outer_ip[16..20] != expected.outer_dst_ip.octets() + { + return None; + } + + let gre = &frame[gre_offset..]; + if u16::from_be_bytes([gre[0], gre[1]]) != GRE_FLAGS_VERSION_BASIC { + return None; + } + if u16::from_be_bytes([gre[2], gre[3]]) != libc::ETH_P_IP as u16 { + return None; + } + + let inner_offset = gre_offset.checked_add(GRE_HEADER_BASE_SIZE)?; + matching_ipv4_udp_payload( + frame.get(inner_offset..)?, + &ExpectedUdpDatagram { + src_ip: expected.inner_src_ip, + dst_ip: expected.inner_dst_ip, + src_port: expected.src_port, + dst_port: expected.dst_port, + payload: expected.payload, + }, + ) +} + +fn matching_ipv4_udp_payload<'a>( + ip: &'a [u8], + expected: &ExpectedUdpDatagram<'_>, +) -> Option<&'a [u8]> { + let min_udp_len = IP_HEADER_SIZE.checked_add(UDP_HEADER_SIZE)?; + if ip.len() < min_udp_len { + return None; + } + + let ihl = usize::from(ip[0] & 0x0f).checked_mul(4)?; + let min_packet_len = ihl.checked_add(UDP_HEADER_SIZE)?; + if ihl < IP_HEADER_SIZE || ip.len() < min_packet_len { + return None; + } + if ip[9] != libc::IPPROTO_UDP as u8 { + return None; + } + if ip[12..16] != expected.src_ip.octets() || ip[16..20] != expected.dst_ip.octets() { + return None; + } + + let udp = &ip[ihl..]; + if u16::from_be_bytes([udp[0], udp[1]]) != expected.src_port + || u16::from_be_bytes([udp[2], udp[3]]) != expected.dst_port + { + return None; + } + let udp_len = usize::from(u16::from_be_bytes([udp[4], udp[5]])); + if udp_len < UDP_HEADER_SIZE || udp.len() < udp_len { + return None; + } + let payload = &udp[UDP_HEADER_SIZE..udp_len]; + (payload == expected.payload).then_some(payload) +} + +#[test] +#[ignore = "requires root and network namespace privileges"] +fn transmitter_sends_udp_payload_over_veth_in_copy_mode() { + let cpu_id = transmitter_cpu(); + + let _netns = common::NetNsGuard::new().expect("create network namespace"); + let links = common::setup_veth_pair(); + common::replace_neighbor(links.right_ip, links.right_mac, common::LEFT_IFACE); + + let receiver = PacketSocket::bind(links.right_if_index).expect("bind raw packet receiver"); + let dst_port = 45_678; + let src_port = 12_345; + let destination = SocketAddr::V4(SocketAddrV4::new(links.right_ip, dst_port)); + let payload = Bytes::from_static(b"agave-xdp-transmitter-smoke"); + + let exit = Arc::new(AtomicBool::new(false)); + let mut config = XdpConfig::new( + Some(common::LEFT_IFACE.to_string()), + vec![QueueCpuBinding { + queue: 0, + cpu: cpu_id, + }], + false, + ); + config.tx_channel_cap = 16; + + let (transmitter, sender) = TransmitterBuilder::new(config, Arc::clone(&exit)) + .expect("build copy-mode transmitter") + .build(); + let transmitter = TransmitterGuard::new(transmitter, sender, exit); + + let packet = BytesTxPacket::new( + SocketAddrV4::new(links.left_ip, src_port), + destination, + None, + payload.clone(), + ); + transmitter + .sender() + .try_send(0, packet) + .expect("queue packet through XdpSender::try_send"); + + let received = receiver + .recv_matching_udp( + &ExpectedUdpPacket { + src_mac: links.left_mac, + dst_mac: links.right_mac, + src_ip: links.left_ip, + dst_ip: links.right_ip, + src_port, + dst_port, + payload: payload.as_ref(), + }, + Duration::from_secs(3), + ) + .expect("receive UDP frame from AF_XDP transmitter"); + assert_eq!(received, payload.as_ref()); +} + +#[test] +#[ignore = "requires root and network namespace privileges"] +fn transmitter_sends_udp_payload_over_gre_tunnel_in_copy_mode() { + let cpu_id = transmitter_cpu(); + + let _netns = common::NetNsGuard::new().expect("create network namespace"); + let links = common::setup_veth_pair(); + common::replace_neighbor(links.right_ip, links.right_mac, common::LEFT_IFACE); + common::add_route_to_dev(&format!("{}/32", links.right_ip), common::LEFT_IFACE); + let gre = common::setup_gre_tunnel(&links); + common::add_route_to_dev_with_src("192.0.2.0/24", common::GRE_IFACE, gre.overlay_ip); + + // Sending to the overlay destination exercises route lookup plus GRE encapsulation. + // The raw receiver observes the outer packet on the underlay veth peer. + let receiver = PacketSocket::bind(links.right_if_index).expect("bind raw packet receiver"); + let dst_port = 45_679; + let src_port = 12_346; + let overlay_destination = Ipv4Addr::new(192, 0, 2, 99); + let destination = SocketAddr::V4(SocketAddrV4::new(overlay_destination, dst_port)); + let payload = Bytes::from_static(b"agave-xdp-transmitter-gre-smoke"); + + let exit = Arc::new(AtomicBool::new(false)); + let mut config = XdpConfig::new( + Some(common::LEFT_IFACE.to_string()), + vec![QueueCpuBinding { + queue: 0, + cpu: cpu_id, + }], + false, + ); + config.tx_channel_cap = 16; + + let (transmitter, sender) = TransmitterBuilder::new(config, Arc::clone(&exit)) + .expect("build copy-mode transmitter") + .build(); + let transmitter = TransmitterGuard::new(transmitter, sender, exit); + + let packet = BytesTxPacket::new( + SocketAddrV4::new(links.left_ip, src_port), + destination, + None, + payload.clone(), + ); + transmitter + .sender() + .try_send(0, packet) + .expect("queue packet through XdpSender::try_send"); + + let received = receiver + .recv_matching_gre_udp( + &ExpectedGreUdpPacket { + outer_src_mac: links.left_mac, + outer_dst_mac: links.right_mac, + outer_src_ip: gre.local_ip, + outer_dst_ip: gre.remote_ip, + inner_src_ip: gre.overlay_ip, + inner_dst_ip: overlay_destination, + src_port, + dst_port, + payload: payload.as_ref(), + }, + Duration::from_secs(3), + ) + .expect("receive GRE-encapsulated UDP frame from AF_XDP transmitter"); + assert_eq!(received, payload.as_ref()); +}